329 std::span<const RoomObject> before_objects,
330 std::span<const RoomObject> after_objects,
334 const auto before_stamps = CanonicalRailStamps(before_objects);
335 const auto after_stamps = CanonicalRailStamps(after_objects);
336 std::vector<CanonicalRailStamp> changed;
337 std::set_symmetric_difference(before_stamps.begin(), before_stamps.end(),
338 after_stamps.begin(), after_stamps.end(),
339 std::back_inserter(changed));
340 if (changed.empty()) {
344 std::bitset<kGridSize * kGridSize> touched;
345 for (
const auto& stamp : changed) {
346 if (stamp.value != 0xB0 && stamp.value != 0xB1) {
347 return absl::FailedPreconditionError(
348 "Automatic rail collision supports straight pieces only; use the "
349 "rail behavior tools for corners and crossings");
351 if (!HasValidRailFootprint(stamp)) {
352 return absl::FailedPreconditionError(
353 "Automatic rail collision requires upper-layer 16 pixel pieces "
354 "fully inside the room");
356 for (
int dy = 0; dy < 2; ++dy) {
357 for (
int dx = 0; dx < 2; ++dx) {
358 touched.set((stamp.y + dy) * kGridSize + stamp.x + dx);
362 const auto before_grid = BuildCanonicalRailGrid(before_stamps);
363 const auto after_grid = BuildCanonicalRailGrid(after_stamps);
364 for (
size_t offset = 0; offset < before_collision.
tiles.size(); ++offset) {
365 if (!touched.test(offset)) {
368 if (before_grid.conflicts.test(offset) ||
369 after_grid.conflicts.test(offset)) {
370 return absl::FailedPreconditionError(absl::StrFormat(
371 "Conflicting rail shapes overlap collision cell %zu", offset));
373 if (before_grid.values[offset] == after_grid.values[offset]) {
377 const uint8_t desired = after_grid.values[offset];
378 auto owner = result.ownership.find(offset);
379 if (owner != result.ownership.end()) {
380 if (before_collision.
tiles[offset] != owner->second.generated_value ||
383 result.ownership.erase(owner);
384 }
else if (desired == 0) {
385 result.collision.tiles[offset] = owner->second.baseline_value;
386 result.collision.overrides.set(offset, owner->second.baseline_override);
387 result.ownership.erase(owner);
390 result.collision.SetOverride(offset, desired);
391 owner->second.generated_value = desired;
398 const uint8_t current = before_collision.
tiles[offset];
399 if (IsProtectedRailBehavior(current)) {
402 if (before_collision.
HasOverride(offset) && current != 0 &&
403 !IsOrdinaryRail(current)) {
404 return absl::FailedPreconditionError(absl::StrFormat(
405 "Other collision occupies rail cell %zu; edit collision explicitly",
408 if (current == desired && before_collision.
HasOverride(offset)) {
411 result.ownership[offset] = {before_collision.
overrides.test(offset),
413 result.collision.SetOverride(offset, desired);
421 return absl::InvalidArgumentError(
"Room pointer is null");
436 std::array<bool, kGridSize * kGridSize> occupied{};
443 const int subtype = obj.size_ & 0x1F;
449 ResolveTrackObjectDimensions(obj, options, dimension_service);
450 int base_x = obj.x_ + dims.offset_x_tiles;
451 int base_y = obj.y_ + dims.offset_y_tiles;
452 int w = std::max(1, dims.width_tiles);
453 int h = std::max(1, dims.height_tiles);
455 for (
int dy = 0; dy < h; ++dy) {
456 for (
int dx = 0; dx < w; ++dx) {
457 int gx = base_x + dx;
458 int gy = base_y + dy;
459 if (gx >= 0 && gx < kGridSize && gy >= 0 && gy < kGridSize) {
460 occupied[gy * kGridSize + gx] =
true;
467 for (
int y = 0; y < kGridSize; ++y) {
468 for (
int x = 0; x < kGridSize; ++x) {
469 if (!occupied[y * kGridSize + x])
472 bool up = (y > 0) && occupied[(y - 1) * kGridSize + x];
473 bool down = (y < kGridSize - 1) && occupied[(y + 1) * kGridSize + x];
474 bool left = (x > 0) && occupied[y * kGridSize + (x - 1)];
475 bool right = (x < kGridSize - 1) && occupied[y * kGridSize + (x + 1)];
477 uint8_t tile = ClassifyTile(up, down, left, right);
481 if (tile >= 0xB7 && tile <= 0xBA)
483 if (tile >= 0xB2 && tile <= 0xB5)
490 if (sx < 0 || sx >= kGridSize || sy < 0 || sy >= kGridSize)
492 size_t idx = sy * kGridSize + sx;
494 if (IsCornerTile(tile)) {
504 if (ox < 0 || ox >= kGridSize || oy < 0 || oy >= kGridSize)
506 size_t idx = oy * kGridSize + ox;
521 return absl::InvalidArgumentError(
"ROM not loaded");
524 return absl::OutOfRangeError(
"Room ID out of range");
527 const auto& data = rom->
vector();
529 return absl::FailedPreconditionError(
"ROM vector is empty");
534 static_cast<int>(data.size())) {
535 return absl::FailedPreconditionError(
536 "Custom collision pointer table not present in this ROM");
539 return absl::FailedPreconditionError(
540 "Custom collision data region not present in this ROM");
543 return absl::FailedPreconditionError(
544 "Custom collision data region truncated (ROM too small)");
553 "CustomCollisionPointers"));
557 "CustomCollisionData"));
562 std::vector<uint8_t> encoded;
563 encoded.push_back(0xF0);
564 encoded.push_back(0xF0);
566 for (
int y = 0; y < kGridSize; ++y) {
567 for (
int x = 0; x < kGridSize; ++x) {
568 uint8_t tile = map.
tiles[y * kGridSize + x];
571 uint16_t offset =
static_cast<uint16_t
>(y * kGridSize + x);
572 encoded.push_back(offset & 0xFF);
573 encoded.push_back(offset >> 8);
574 encoded.push_back(tile);
577 encoded.push_back(0xFF);
578 encoded.push_back(0xFF);
582 const size_t safe_end =
583 std::min(
static_cast<size_t>(data.size()),
586 std::vector<CollisionBlob> blobs;
590 if (ptr_offset + 2 >=
static_cast<int>(data.size()))
593 uint32_t snes_ptr = data[ptr_offset] | (data[ptr_offset + 1] << 8) |
594 (data[ptr_offset + 2] << 16);
600 return absl::FailedPreconditionError(
601 absl::StrFormat(
"Custom collision pointer for room 0x%02X points "
602 "before data region (pc=0x%06X)",
606 return absl::FailedPreconditionError(
607 absl::StrFormat(
"Custom collision pointer for room 0x%02X overlaps "
608 "WaterFill reserved region (pc=0x%06X)",
611 if (pc >= data.size()) {
612 return absl::OutOfRangeError(
"Custom collision pointer out of ROM range");
615 FindCollisionBlobEnd(data, pc, safe_end, r));
616 blobs.push_back(CollisionBlob{r, pc, end_pc});
617 if (end_pc > max_used_pc) {
618 max_used_pc = end_pc;
625 uint32_t write_pos = max_used_pc;
626 const auto target = std::find_if(
627 blobs.begin(), blobs.end(),
628 [room_id](
const CollisionBlob& blob) { return blob.room_id == room_id; });
629 if (target != blobs.end() && encoded.size() <= target->end - target->start) {
630 const bool overlaps_other = std::any_of(
631 blobs.begin(), blobs.end(), [&](
const CollisionBlob& other) {
632 return other.room_id != room_id && BlobsOverlap(*target, other);
634 if (!overlaps_other) {
635 write_pos = target->start;
641 return absl::ResourceExhaustedError(absl::StrFormat(
642 "Not enough collision data space. Need %d bytes at 0x%06X, "
643 "region ends at 0x%06X",
647 if (write_pos + encoded.size() > data.size()) {
648 return absl::OutOfRangeError(
649 absl::StrFormat(
"ROM too small for custom collision write (need "
650 "end=0x%06X, size=0x%06X)",
651 write_pos + encoded.size(), data.size()));
654 rom->
WriteVector(
static_cast<int>(write_pos), std::move(encoded)));
657 uint32_t snes_addr =
PcToSnes(write_pos);
663 return absl::OkStatus();
668 int min_x = kGridSize, max_x = 0, min_y = kGridSize, max_y = 0;
669 for (
int y = 0; y < kGridSize; ++y) {
670 for (
int x = 0; x < kGridSize; ++x) {
671 if (map.
tiles[y * kGridSize + x] != 0) {
672 min_x = std::min(min_x, x);
673 max_x = std::max(max_x, x);
674 min_y = std::min(min_y, y);
675 max_y = std::max(max_y, y);
684 min_x = std::max(0, min_x - 1);
685 min_y = std::max(0, min_y - 1);
686 max_x = std::min(kGridSize - 1, max_x + 1);
687 max_y = std::min(kGridSize - 1, max_y + 1);
689 std::stringstream ss;
692 for (
int x = min_x; x <= max_x; ++x) {
693 ss << absl::StrFormat(
"%X", x % 16);
697 for (
int y = min_y; y <= max_y; ++y) {
698 ss << absl::StrFormat(
"%02X: ", y);
699 for (
int x = min_x; x <= max_x; ++x) {
700 uint8_t tile = map.
tiles[y * kGridSize + x];
701 ss << TileToChar(tile);