yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
track_collision_generator.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <bitset>
6#include <cstdint>
7#include <iterator>
8#include <sstream>
9#include <string>
10#include <utility>
11
12#include "absl/status/status.h"
13#include "absl/strings/str_format.h"
14#include "core/features.h"
15#include "rom/snes.h"
16#include "rom/write_fence.h"
17#include "util/macro.h"
21
22namespace yaze {
23namespace zelda3 {
24
25namespace {
26
27constexpr int kGridSize = 64;
28constexpr uint16_t kCollisionSingleTileMarker = 0xF0F0;
29constexpr uint16_t kCollisionEndMarker = 0xFFFF;
32
34 int room_id = 0;
35 uint32_t start = 0;
36 uint32_t end = 0;
37};
38
39absl::StatusOr<uint32_t> FindCollisionBlobEnd(const std::vector<uint8_t>& data,
40 uint32_t start, size_t safe_end,
41 int room_id) {
42 size_t cursor = start;
43 bool single_mode = false;
44
45 while (cursor + 1 < safe_end) {
46 const uint16_t value = data[cursor] | (data[cursor + 1] << 8);
47 cursor += 2;
48 if (value == kCollisionEndMarker) {
49 return static_cast<uint32_t>(cursor);
50 }
51 if (value == kCollisionSingleTileMarker) {
52 single_mode = true;
53 continue;
54 }
55
56 if (single_mode) {
57 if (cursor >= safe_end) {
58 break;
59 }
60 ++cursor;
61 continue;
62 }
63
64 if (cursor + 1 >= safe_end) {
65 break;
66 }
67 const size_t width = data[cursor];
68 const size_t height = data[cursor + 1];
69 cursor += 2;
70 const size_t payload_size = width * height;
71 if (payload_size > safe_end - cursor) {
72 break;
73 }
74 cursor += payload_size;
75 }
76
77 return absl::FailedPreconditionError(absl::StrFormat(
78 "Custom collision data for room 0x%02X is unterminated before "
79 "WaterFill reserved region",
80 room_id));
81}
82
83bool BlobsOverlap(const CollisionBlob& lhs, const CollisionBlob& rhs) {
84 return lhs.start < rhs.end && rhs.start < lhs.end;
85}
86
87// Map corner type to its switch equivalent for promotion.
89 switch (corner) {
98 default:
99 return corner;
100 }
101}
102
103bool IsCornerTile(uint8_t tile) {
104 return tile >= 0xB2 && tile <= 0xB5;
105}
106
107// Classify a tile based on its 4-neighbor connectivity.
108//
109// The algorithm: for each occupied tile in the grid, check which of the
110// 4 cardinal neighbors are also occupied. The pattern of neighbors uniquely
111// determines the tile type:
112// - 1 neighbor (endpoint) → stop tile, direction based on which neighbor
113// - 2 neighbors (line or corner) → straight or corner
114// - 3 neighbors → T-junction
115// - 4 neighbors → intersection
116uint8_t ClassifyTile(bool up, bool down, bool left, bool right) {
117 int count = (up ? 1 : 0) + (down ? 1 : 0) + (left ? 1 : 0) + (right ? 1 : 0);
118
119 if (count == 0) {
120 // Isolated tile — treat as intersection (shouldn't happen in practice)
121 return static_cast<uint8_t>(TrackTileType::Intersection);
122 }
123
124 if (count == 1) {
125 // Endpoint → stop tile. Direction is where the neighbor IS, because
126 // the cart arrives from that direction and will depart back that way.
127 if (down)
128 return static_cast<uint8_t>(TrackTileType::StopNorth);
129 if (up)
130 return static_cast<uint8_t>(TrackTileType::StopSouth);
131 if (right)
132 return static_cast<uint8_t>(TrackTileType::StopWest);
133 if (left)
134 return static_cast<uint8_t>(TrackTileType::StopEast);
135 }
136
137 if (count == 2) {
138 // Two neighbors — either a straight line or a corner
139 if (left && right)
140 return static_cast<uint8_t>(TrackTileType::HorizStraight);
141 if (up && down)
142 return static_cast<uint8_t>(TrackTileType::VertStraight);
143 if (down && right)
144 return static_cast<uint8_t>(TrackTileType::CornerTL);
145 if (up && right)
146 return static_cast<uint8_t>(TrackTileType::CornerBL);
147 if (down && left)
148 return static_cast<uint8_t>(TrackTileType::CornerTR);
149 if (up && left)
150 return static_cast<uint8_t>(TrackTileType::CornerBR);
151 }
152
153 if (count == 3) {
154 // Oracle names T junctions for the CLOSED side. Its minecart input and
155 // default-direction tables turn away from that edge (e.g. $BB blocks north).
156 if (!up)
157 return static_cast<uint8_t>(TrackTileType::TJuncNorth);
158 if (!down)
159 return static_cast<uint8_t>(TrackTileType::TJuncSouth);
160 if (!left)
161 return static_cast<uint8_t>(TrackTileType::TJuncWest);
162 if (!right)
163 return static_cast<uint8_t>(TrackTileType::TJuncEast);
164 }
165
166 // count == 4: full intersection
167 return static_cast<uint8_t>(TrackTileType::Intersection);
168}
169
170// Get the tile label character for ASCII visualization.
171char TileToChar(uint8_t tile) {
172 switch (tile) {
173 case 0xB0:
174 return '-'; // horiz straight
175 case 0xB1:
176 return '|'; // vert straight
177 case 0xB2:
178 return '/'; // corner TL (down+right)
179 case 0xB3:
180 return '\\'; // corner BL (up+right)
181 case 0xB4:
182 return '\\'; // corner TR (down+left)
183 case 0xB5:
184 return '/'; // corner BR (up+left)
185 case 0xB6:
186 return '+'; // intersection
187 case 0xB7:
188 return 'N'; // stop north
189 case 0xB8:
190 return 'S'; // stop south
191 case 0xB9:
192 return 'W'; // stop west
193 case 0xBA:
194 return 'E'; // stop east
195 case 0xBB:
196 return 'T'; // T-junc north
197 case 0xBC:
198 return 'T'; // T-junc south
199 case 0xBD:
200 return 'T'; // T-junc east
201 case 0xBE:
202 return 'T'; // T-junc west
203 case 0xD0:
204 return '@'; // switch TL
205 case 0xD1:
206 return '@'; // switch BL
207 case 0xD2:
208 return '@'; // switch TR
209 case 0xD3:
210 return '@'; // switch BR
211 default:
212 return '.';
213 }
214}
215
217 const RoomObject& obj, const GeneratorOptions& options,
218 const DimensionService& dimension_service) {
219 auto dims = dimension_service.GetDimensions(obj);
220
221 // Object 0x31's size byte selects a custom-object subtype; it is not a
222 // vanilla width/height nibble. Without project custom-object geometry,
223 // DimensionService falls through to that vanilla interpretation and gives
224 // non-zero subtypes incorrect variable-width footprints. Oracle's track
225 // assets are all authored as 2x2 pieces, so use that canonical footprint in
226 // the project-free CLI path. Configured non-0x31 track IDs continue to use
227 // DimensionService normally.
228 const bool is_canonical_oos_track =
229 obj.id_ == 0x31 &&
230 obj.id_ == static_cast<int16_t>(options.track_object_id);
231 if (is_canonical_oos_track &&
233 dims.offset_x_tiles = 0;
234 dims.offset_y_tiles = 0;
235 dims.width_tiles = kFallbackTrackFootprintWidthTiles;
236 dims.height_tiles = kFallbackTrackFootprintHeightTiles;
237 return dims;
238 }
239
240 // Also recover when custom objects are enabled but a configured subtype has
241 // no loaded payload and therefore measures as the 1x1 fallback.
242 if (is_canonical_oos_track && dims.offset_x_tiles == 0 &&
243 dims.offset_y_tiles == 0 && dims.width_tiles == 1 &&
244 dims.height_tiles == 1) {
245 dims.width_tiles = kFallbackTrackFootprintWidthTiles;
246 dims.height_tiles = kFallbackTrackFootprintHeightTiles;
247 }
248
249 return dims;
250}
251
253 int x;
254 int y;
255 int layer;
256 uint8_t value;
257
258 bool operator==(const CanonicalRailStamp&) const = default;
259 bool operator<(const CanonicalRailStamp& other) const {
260 return std::tie(x, y, layer, value) <
261 std::tie(other.x, other.y, other.layer, other.value);
262 }
263};
264
265std::vector<CanonicalRailStamp> CanonicalRailStamps(
266 std::span<const RoomObject> objects) {
267 // Semantic identity only: corner/crossing values identify changed geometry
268 // for explicit refusal below; they do not establish a four-cell stencil.
269 // Oracle artwork variants share behavior; subtype is not a size nibble.
270 static constexpr std::array<uint8_t, 16> kBehavior = {
271 0xB0, 0xB1, 0xB2, 0xB4, 0xB3, 0xB5, 0xB1, 0xB0,
272 0xB2, 0xB4, 0xB3, 0xB5, 0xB6, 0x00, 0xB6, 0x00};
273 std::vector<CanonicalRailStamp> stamps;
274 for (const auto& object : objects) {
275 const size_t subtype = object.size_ & 0x1F;
276 if (object.id_ != 0x31 || subtype >= kBehavior.size() ||
277 kBehavior[subtype] == 0) {
278 continue;
279 }
280 stamps.push_back(
281 {object.x_, object.y_, object.GetLayerValue(), kBehavior[subtype]});
282 }
283 std::sort(stamps.begin(), stamps.end());
284 return stamps;
285}
286
288 return stamp.layer == 0 && stamp.x >= 0 && stamp.y >= 0 &&
289 stamp.x <= kGridSize - 2 && stamp.y <= kGridSize - 2;
290}
291
293 std::array<uint8_t, kGridSize * kGridSize> values{};
294 std::bitset<kGridSize * kGridSize> conflicts;
295};
296
298 const std::vector<CanonicalRailStamp>& stamps) {
300 for (const auto& stamp : stamps) {
301 // Unchanged unsupported geometry does not block unrelated object edits.
302 if (!HasValidRailFootprint(stamp)) {
303 continue;
304 }
305 for (int dy = 0; dy < 2; ++dy) {
306 for (int dx = 0; dx < 2; ++dx) {
307 const size_t offset = (stamp.y + dy) * kGridSize + stamp.x + dx;
308 if (grid.values[offset] != 0 && grid.values[offset] != stamp.value) {
309 grid.conflicts.set(offset);
310 }
311 grid.values[offset] = stamp.value;
312 }
313 }
314 }
315 return grid;
316}
317
318bool IsOrdinaryRail(uint8_t value) {
319 return value >= 0xB0 && value <= 0xB6;
320}
321
322bool IsProtectedRailBehavior(uint8_t value) {
323 return (value >= 0xB7 && value <= 0xBE) || (value >= 0xD0 && value <= 0xD3);
324}
325
326} // namespace
327
328absl::StatusOr<MinecartTrackCollisionEdit> SynchronizeMinecartTrackCollision(
329 std::span<const RoomObject> before_objects,
330 std::span<const RoomObject> after_objects,
331 const CustomCollisionMap& before_collision,
332 const MinecartTrackCollisionOwnership& before_ownership) {
333 MinecartTrackCollisionEdit result{before_collision, before_ownership};
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()) {
341 return result;
342 }
343
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");
350 }
351 if (!HasValidRailFootprint(stamp)) {
352 return absl::FailedPreconditionError(
353 "Automatic rail collision requires upper-layer 16 pixel pieces "
354 "fully inside the room");
355 }
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);
359 }
360 }
361 }
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)) {
366 continue;
367 }
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));
372 }
373 if (before_grid.values[offset] == after_grid.values[offset]) {
374 continue;
375 }
376
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 ||
381 !before_collision.HasOverride(offset)) {
382 // A manual edit invalidates ownership; never restore over that edit.
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);
388 continue;
389 } else {
390 result.collision.SetOverride(offset, desired);
391 owner->second.generated_value = desired;
392 continue;
393 }
394 }
395 if (desired == 0) {
396 continue; // Existing authored rails have no session ownership.
397 }
398 const uint8_t current = before_collision.tiles[offset];
399 if (IsProtectedRailBehavior(current)) {
400 continue;
401 }
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",
406 offset));
407 }
408 if (current == desired && before_collision.HasOverride(offset)) {
409 continue; // Do not adopt a matching authored rail.
410 }
411 result.ownership[offset] = {before_collision.overrides.test(offset),
412 current, desired};
413 result.collision.SetOverride(offset, desired);
414 }
415 return result;
416}
417
418absl::StatusOr<TrackCollisionResult> GenerateTrackCollision(
419 Room* room, const GeneratorOptions& options) {
420 if (!room) {
421 return absl::InvalidArgumentError("Room pointer is null");
422 }
423
424 // An empty object list can be a valid unsaved edit. The explicit loaded flag
425 // is the only safe authority for deciding whether ROM data must be parsed.
426 room->EnsureObjectsLoaded();
427
429 result.room_id = room->id();
430 result.collision_map.tiles.fill(0);
431
432 // Step 1: Build occupancy grid from rail objects.
433 // Rail objects (ID 0x31) have coordinates in the room's tile space.
434 // RoomObject x_ and y_ are in tile coordinates (each tile = 8 pixels).
435 // The collision grid is 64x64 (covering 512x512 pixels = full room).
436 std::array<bool, kGridSize * kGridSize> occupied{};
437 auto& dimension_service = DimensionService::Get();
438
439 for (const auto& obj : room->GetTileObjects()) {
440 if (obj.id_ != static_cast<int16_t>(options.track_object_id)) {
441 continue;
442 }
443 const int subtype = obj.size_ & 0x1F;
444 if (!IsMinecartTrackGraphicsSubtype(options.track_object_id, subtype)) {
445 continue;
446 }
447
448 const auto dims =
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);
454
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;
461 }
462 }
463 }
464 }
465
466 // Step 2: Classify each occupied tile by neighbor connectivity.
467 for (int y = 0; y < kGridSize; ++y) {
468 for (int x = 0; x < kGridSize; ++x) {
469 if (!occupied[y * kGridSize + x])
470 continue;
471
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)];
476
477 uint8_t tile = ClassifyTile(up, down, left, right);
478 result.collision_map.tiles[y * kGridSize + x] = tile;
479 result.tiles_generated++;
480
481 if (tile >= 0xB7 && tile <= 0xBA)
482 result.stop_count++;
483 if (tile >= 0xB2 && tile <= 0xB5)
484 result.corner_count++;
485 }
486 }
487
488 // Step 3: Apply switch promotions.
489 for (const auto& [sx, sy] : options.switch_promotions) {
490 if (sx < 0 || sx >= kGridSize || sy < 0 || sy >= kGridSize)
491 continue;
492 size_t idx = sy * kGridSize + sx;
493 uint8_t tile = result.collision_map.tiles[idx];
494 if (IsCornerTile(tile)) {
495 result.collision_map.tiles[idx] = static_cast<uint8_t>(
496 PromoteCornerToSwitch(static_cast<TrackTileType>(tile)));
497 result.corner_count--;
498 result.switch_count++;
499 }
500 }
501
502 // Step 4: Apply manual stop overrides.
503 for (const auto& [ox, oy, otype] : options.stop_overrides) {
504 if (ox < 0 || ox >= kGridSize || oy < 0 || oy >= kGridSize)
505 continue;
506 size_t idx = oy * kGridSize + ox;
507 if (result.collision_map.tiles[idx] != 0) {
508 result.collision_map.tiles[idx] = static_cast<uint8_t>(otype);
509 }
510 }
511
512 result.collision_map.has_data = (result.tiles_generated > 0);
514
515 return result;
516}
517
518absl::Status WriteTrackCollision(Rom* rom, int room_id,
519 const CustomCollisionMap& map) {
520 if (!rom || !rom->is_loaded()) {
521 return absl::InvalidArgumentError("ROM not loaded");
522 }
523 if (room_id < 0 || room_id >= kNumberOfRooms) {
524 return absl::OutOfRangeError("Room ID out of range");
525 }
526
527 const auto& data = rom->vector();
528 if (data.empty()) {
529 return absl::FailedPreconditionError("ROM vector is empty");
530 }
531
532 const int ptrs_size = kNumberOfRooms * 3;
533 if (kCustomCollisionRoomPointers + ptrs_size >
534 static_cast<int>(data.size())) {
535 return absl::FailedPreconditionError(
536 "Custom collision pointer table not present in this ROM");
537 }
538 if (kCustomCollisionDataPosition >= static_cast<int>(data.size())) {
539 return absl::FailedPreconditionError(
540 "Custom collision data region not present in this ROM");
541 }
542 if (kCustomCollisionDataSoftEnd > static_cast<int>(data.size())) {
543 return absl::FailedPreconditionError(
544 "Custom collision data region truncated (ROM too small)");
545 }
546
547 // Save-time guardrails: only allow writes to the collision pointer table and
548 // collision data bank (excluding the reserved WaterFill tail region).
551 static_cast<uint32_t>(kCustomCollisionRoomPointers),
552 static_cast<uint32_t>(kCustomCollisionRoomPointers + ptrs_size),
553 "CustomCollisionPointers"));
555 fence.Allow(static_cast<uint32_t>(kCustomCollisionDataPosition),
556 static_cast<uint32_t>(kCustomCollisionDataSoftEnd),
557 "CustomCollisionData"));
558 yaze::rom::ScopedWriteFence scope(rom, &fence);
559
560 // Encode collision data in single-tile format.
561 // Format: [F0 F0] [offset_lo offset_hi tile] ... [FF FF]
562 std::vector<uint8_t> encoded;
563 encoded.push_back(0xF0);
564 encoded.push_back(0xF0);
565
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];
569 if (!map.HasOverride(y * kGridSize + x))
570 continue;
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);
575 }
576 }
577 encoded.push_back(0xFF);
578 encoded.push_back(0xFF);
579
580 // Find the end of existing collision data by scanning all room pointers
581 // to determine the highest used offset.
582 const size_t safe_end =
583 std::min(static_cast<size_t>(data.size()),
584 static_cast<size_t>(kCustomCollisionDataSoftEnd));
585 uint32_t max_used_pc = static_cast<uint32_t>(kCustomCollisionDataPosition);
586 std::vector<CollisionBlob> blobs;
587 blobs.reserve(kNumberOfRooms);
588 for (int r = 0; r < kNumberOfRooms; ++r) {
589 int ptr_offset = kCustomCollisionRoomPointers + (r * 3);
590 if (ptr_offset + 2 >= static_cast<int>(data.size()))
591 continue;
592
593 uint32_t snes_ptr = data[ptr_offset] | (data[ptr_offset + 1] << 8) |
594 (data[ptr_offset + 2] << 16);
595 if (snes_ptr == 0)
596 continue;
597
598 uint32_t pc = SnesToPc(snes_ptr);
599 if (pc < static_cast<uint32_t>(kCustomCollisionDataPosition)) {
600 return absl::FailedPreconditionError(
601 absl::StrFormat("Custom collision pointer for room 0x%02X points "
602 "before data region (pc=0x%06X)",
603 r, pc));
604 }
605 if (pc >= static_cast<uint32_t>(kCustomCollisionDataSoftEnd)) {
606 return absl::FailedPreconditionError(
607 absl::StrFormat("Custom collision pointer for room 0x%02X overlaps "
608 "WaterFill reserved region (pc=0x%06X)",
609 r, pc));
610 }
611 if (pc >= data.size()) {
612 return absl::OutOfRangeError("Custom collision pointer out of ROM range");
613 }
614 ASSIGN_OR_RETURN(const uint32_t end_pc,
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;
619 }
620 }
621
622 // Reuse the selected room's current blob only when its entire physical span
623 // is uniquely owned and the replacement fits. Aliased or overlapping blobs
624 // remain copy-on-write so editing one room cannot mutate another room.
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);
633 });
634 if (!overlaps_other) {
635 write_pos = target->start;
636 }
637 }
638
639 // Append when there is no safe reusable span, then check available space.
640 if (write_pos + encoded.size() > kCustomCollisionDataSoftEnd) {
641 return absl::ResourceExhaustedError(absl::StrFormat(
642 "Not enough collision data space. Need %d bytes at 0x%06X, "
643 "region ends at 0x%06X",
644 encoded.size(), write_pos, kCustomCollisionDataSoftEnd));
645 }
646
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()));
652 }
654 rom->WriteVector(static_cast<int>(write_pos), std::move(encoded)));
655
656 // Update pointer table: 3-byte SNES address
657 uint32_t snes_addr = PcToSnes(write_pos);
658 int ptr_offset = kCustomCollisionRoomPointers + (room_id * 3);
659 RETURN_IF_ERROR(rom->WriteByte(ptr_offset, snes_addr & 0xFF));
660 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 1, (snes_addr >> 8) & 0xFF));
661 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 2, (snes_addr >> 16) & 0xFF));
662
663 return absl::OkStatus();
664}
665
667 // Find bounding box of non-zero tiles to avoid printing the entire 64x64.
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);
676 }
677 }
678 }
679
680 if (min_x > max_x)
681 return "(empty)\n";
682
683 // Add 1-tile padding
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);
688
689 std::stringstream ss;
690 // Column header
691 ss << " ";
692 for (int x = min_x; x <= max_x; ++x) {
693 ss << absl::StrFormat("%X", x % 16);
694 }
695 ss << "\n";
696
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);
702 }
703 ss << "\n";
704 }
705
706 return ss.str();
707}
708
709} // namespace zelda3
710} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:52
absl::Status WriteByte(int addr, uint8_t value)
Definition rom.cc:1104
const auto & vector() const
Definition rom.h:217
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
Definition rom.cc:1188
bool is_loaded() const
Definition rom.h:199
static Flags & get()
Definition features.h:122
absl::Status Allow(uint32_t start, uint32_t end, std::string_view label)
Definition write_fence.h:32
Unified dimension lookup for dungeon room objects.
static DimensionService & Get()
DimensionResult GetDimensions(const RoomObject &obj) const
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:445
void EnsureObjectsLoaded()
Definition room.cc:1026
int id() const
Definition room.h:1012
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
bool BlobsOverlap(const CollisionBlob &lhs, const CollisionBlob &rhs)
uint8_t ClassifyTile(bool up, bool down, bool left, bool right)
std::vector< CanonicalRailStamp > CanonicalRailStamps(std::span< const RoomObject > objects)
absl::StatusOr< uint32_t > FindCollisionBlobEnd(const std::vector< uint8_t > &data, uint32_t start, size_t safe_end, int room_id)
DimensionService::DimensionResult ResolveTrackObjectDimensions(const RoomObject &obj, const GeneratorOptions &options, const DimensionService &dimension_service)
CanonicalRailGrid BuildCanonicalRailGrid(const std::vector< CanonicalRailStamp > &stamps)
absl::Status WriteTrackCollision(Rom *rom, int room_id, const CustomCollisionMap &map)
constexpr int kCustomCollisionDataSoftEnd
std::map< size_t, MinecartTrackCollisionCellOwner > MinecartTrackCollisionOwnership
std::string VisualizeCollisionMap(const CustomCollisionMap &map)
absl::StatusOr< MinecartTrackCollisionEdit > SynchronizeMinecartTrackCollision(std::span< const RoomObject > before_objects, std::span< const RoomObject > after_objects, const CustomCollisionMap &before_collision, const MinecartTrackCollisionOwnership &before_ownership)
constexpr int kCustomCollisionDataPosition
constexpr int kNumberOfRooms
constexpr int kCustomCollisionRoomPointers
bool IsMinecartTrackGraphicsSubtype(int object_id, int subtype)
absl::StatusOr< TrackCollisionResult > GenerateTrackCollision(Room *room, const GeneratorOptions &options)
uint32_t PcToSnes(uint32_t addr)
Definition snes.h:17
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::array< uint8_t, 64 *64 > tiles
bool HasOverride(size_t offset) const
std::vector< std::pair< int, int > > switch_promotions
std::vector< std::tuple< int, int, TrackTileType > > stop_overrides