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 <cstdint>
6#include <sstream>
7#include <string>
8#include <utility>
9
10#include "absl/status/status.h"
11#include "absl/strings/str_format.h"
12#include "rom/snes.h"
13#include "rom/write_fence.h"
14#include "util/macro.h"
18
19namespace yaze {
20namespace zelda3 {
21
22namespace {
23
24constexpr int kGridSize = 64;
25constexpr uint16_t kCollisionSingleTileMarker = 0xF0F0;
26constexpr uint16_t kCollisionEndMarker = 0xFFFF;
29
30// Map corner type to its switch equivalent for promotion.
32 switch (corner) {
41 default:
42 return corner;
43 }
44}
45
46bool IsCornerTile(uint8_t tile) {
47 return tile >= 0xB2 && tile <= 0xB5;
48}
49
50// Classify a tile based on its 4-neighbor connectivity.
51//
52// The algorithm: for each occupied tile in the grid, check which of the
53// 4 cardinal neighbors are also occupied. The pattern of neighbors uniquely
54// determines the tile type:
55// - 1 neighbor (endpoint) → stop tile, direction based on which neighbor
56// - 2 neighbors (line or corner) → straight or corner
57// - 3 neighbors → T-junction
58// - 4 neighbors → intersection
59uint8_t ClassifyTile(bool up, bool down, bool left, bool right) {
60 int count = (up ? 1 : 0) + (down ? 1 : 0) + (left ? 1 : 0) + (right ? 1 : 0);
61
62 if (count == 0) {
63 // Isolated tile — treat as intersection (shouldn't happen in practice)
64 return static_cast<uint8_t>(TrackTileType::Intersection);
65 }
66
67 if (count == 1) {
68 // Endpoint → stop tile. Direction is where the neighbor IS, because
69 // the cart arrives from that direction and will depart back that way.
70 if (down)
71 return static_cast<uint8_t>(TrackTileType::StopNorth);
72 if (up)
73 return static_cast<uint8_t>(TrackTileType::StopSouth);
74 if (right)
75 return static_cast<uint8_t>(TrackTileType::StopWest);
76 if (left)
77 return static_cast<uint8_t>(TrackTileType::StopEast);
78 }
79
80 if (count == 2) {
81 // Two neighbors — either a straight line or a corner
82 if (left && right)
83 return static_cast<uint8_t>(TrackTileType::HorizStraight);
84 if (up && down)
85 return static_cast<uint8_t>(TrackTileType::VertStraight);
86 if (down && right)
87 return static_cast<uint8_t>(TrackTileType::CornerTL);
88 if (up && right)
89 return static_cast<uint8_t>(TrackTileType::CornerBL);
90 if (down && left)
91 return static_cast<uint8_t>(TrackTileType::CornerTR);
92 if (up && left)
93 return static_cast<uint8_t>(TrackTileType::CornerBR);
94 }
95
96 if (count == 3) {
97 // T-junction — named for the direction WITHOUT a neighbor
98 if (!up)
99 return static_cast<uint8_t>(TrackTileType::TJuncSouth);
100 if (!down)
101 return static_cast<uint8_t>(TrackTileType::TJuncNorth);
102 if (!left)
103 return static_cast<uint8_t>(TrackTileType::TJuncEast);
104 if (!right)
105 return static_cast<uint8_t>(TrackTileType::TJuncWest);
106 }
107
108 // count == 4: full intersection
109 return static_cast<uint8_t>(TrackTileType::Intersection);
110}
111
112// Get the tile label character for ASCII visualization.
113char TileToChar(uint8_t tile) {
114 switch (tile) {
115 case 0xB0:
116 return '-'; // horiz straight
117 case 0xB1:
118 return '|'; // vert straight
119 case 0xB2:
120 return '/'; // corner TL (down+right)
121 case 0xB3:
122 return '\\'; // corner BL (up+right)
123 case 0xB4:
124 return '\\'; // corner TR (down+left)
125 case 0xB5:
126 return '/'; // corner BR (up+left)
127 case 0xB6:
128 return '+'; // intersection
129 case 0xB7:
130 return 'N'; // stop north
131 case 0xB8:
132 return 'S'; // stop south
133 case 0xB9:
134 return 'W'; // stop west
135 case 0xBA:
136 return 'E'; // stop east
137 case 0xBB:
138 return 'T'; // T-junc north
139 case 0xBC:
140 return 'T'; // T-junc south
141 case 0xBD:
142 return 'T'; // T-junc east
143 case 0xBE:
144 return 'T'; // T-junc west
145 case 0xD0:
146 return '@'; // switch TL
147 case 0xD1:
148 return '@'; // switch BL
149 case 0xD2:
150 return '@'; // switch TR
151 case 0xD3:
152 return '@'; // switch BR
153 default:
154 return '.';
155 }
156}
157
159 const RoomObject& obj, const GeneratorOptions& options,
160 const DimensionService& dimension_service) {
161 auto dims = dimension_service.GetDimensions(obj);
162
163 // In CLI-only contexts, custom object feature flags are often disabled.
164 // Object 0x31 may then map through DrawNothing and collapse to a 1x1
165 // footprint. Track assets are authored as 2x2 pieces, so recover a stable
166 // fallback here to avoid sparse/gapped collision generation.
167 if (obj.id_ == static_cast<int16_t>(options.track_object_id) &&
168 dims.offset_x_tiles == 0 && dims.offset_y_tiles == 0 &&
169 dims.width_tiles == 1 && dims.height_tiles == 1) {
171 dims.height_tiles = kFallbackTrackFootprintHeightTiles;
172 }
173
174 return dims;
175}
176
177} // namespace
178
179absl::StatusOr<TrackCollisionResult> GenerateTrackCollision(
180 Room* room, const GeneratorOptions& options) {
181 if (!room) {
182 return absl::InvalidArgumentError("Room pointer is null");
183 }
184
185 // Ensure objects are loaded
186 if (room->GetTileObjects().empty()) {
187 room->LoadObjects();
188 }
189
191 result.room_id = room->id();
192 result.collision_map.tiles.fill(0);
193
194 // Step 1: Build occupancy grid from rail objects.
195 // Rail objects (ID 0x31) have coordinates in the room's tile space.
196 // RoomObject x_ and y_ are in tile coordinates (each tile = 8 pixels).
197 // The collision grid is 64x64 (covering 512x512 pixels = full room).
198 std::array<bool, kGridSize * kGridSize> occupied{};
199 auto& dimension_service = DimensionService::Get();
200
201 for (const auto& obj : room->GetTileObjects()) {
202 if (obj.id_ != static_cast<int16_t>(options.track_object_id)) {
203 continue;
204 }
205
206 const auto dims =
207 ResolveTrackObjectDimensions(obj, options, dimension_service);
208 int base_x = obj.x_ + dims.offset_x_tiles;
209 int base_y = obj.y_ + dims.offset_y_tiles;
210 int w = std::max(1, dims.width_tiles);
211 int h = std::max(1, dims.height_tiles);
212
213 for (int dy = 0; dy < h; ++dy) {
214 for (int dx = 0; dx < w; ++dx) {
215 int gx = base_x + dx;
216 int gy = base_y + dy;
217 if (gx >= 0 && gx < kGridSize && gy >= 0 && gy < kGridSize) {
218 occupied[gy * kGridSize + gx] = true;
219 }
220 }
221 }
222 }
223
224 // Step 2: Classify each occupied tile by neighbor connectivity.
225 for (int y = 0; y < kGridSize; ++y) {
226 for (int x = 0; x < kGridSize; ++x) {
227 if (!occupied[y * kGridSize + x])
228 continue;
229
230 bool up = (y > 0) && occupied[(y - 1) * kGridSize + x];
231 bool down = (y < kGridSize - 1) && occupied[(y + 1) * kGridSize + x];
232 bool left = (x > 0) && occupied[y * kGridSize + (x - 1)];
233 bool right = (x < kGridSize - 1) && occupied[y * kGridSize + (x + 1)];
234
235 uint8_t tile = ClassifyTile(up, down, left, right);
236 result.collision_map.tiles[y * kGridSize + x] = tile;
237 result.tiles_generated++;
238
239 if (tile >= 0xB7 && tile <= 0xBA)
240 result.stop_count++;
241 if (tile >= 0xB2 && tile <= 0xB5)
242 result.corner_count++;
243 }
244 }
245
246 // Step 3: Apply switch promotions.
247 for (const auto& [sx, sy] : options.switch_promotions) {
248 if (sx < 0 || sx >= kGridSize || sy < 0 || sy >= kGridSize)
249 continue;
250 size_t idx = sy * kGridSize + sx;
251 uint8_t tile = result.collision_map.tiles[idx];
252 if (IsCornerTile(tile)) {
253 result.collision_map.tiles[idx] = static_cast<uint8_t>(
254 PromoteCornerToSwitch(static_cast<TrackTileType>(tile)));
255 result.corner_count--;
256 result.switch_count++;
257 }
258 }
259
260 // Step 4: Apply manual stop overrides.
261 for (const auto& [ox, oy, otype] : options.stop_overrides) {
262 if (ox < 0 || ox >= kGridSize || oy < 0 || oy >= kGridSize)
263 continue;
264 size_t idx = oy * kGridSize + ox;
265 if (result.collision_map.tiles[idx] != 0) {
266 result.collision_map.tiles[idx] = static_cast<uint8_t>(otype);
267 }
268 }
269
270 result.collision_map.has_data = (result.tiles_generated > 0);
272
273 return result;
274}
275
276absl::Status WriteTrackCollision(Rom* rom, int room_id,
277 const CustomCollisionMap& map) {
278 if (!rom || !rom->is_loaded()) {
279 return absl::InvalidArgumentError("ROM not loaded");
280 }
281 if (room_id < 0 || room_id >= kNumberOfRooms) {
282 return absl::OutOfRangeError("Room ID out of range");
283 }
284
285 const auto& data = rom->vector();
286 if (data.empty()) {
287 return absl::FailedPreconditionError("ROM vector is empty");
288 }
289
290 const int ptrs_size = kNumberOfRooms * 3;
291 if (kCustomCollisionRoomPointers + ptrs_size >
292 static_cast<int>(data.size())) {
293 return absl::FailedPreconditionError(
294 "Custom collision pointer table not present in this ROM");
295 }
296 if (kCustomCollisionDataPosition >= static_cast<int>(data.size())) {
297 return absl::FailedPreconditionError(
298 "Custom collision data region not present in this ROM");
299 }
300 if (kCustomCollisionDataSoftEnd > static_cast<int>(data.size())) {
301 return absl::FailedPreconditionError(
302 "Custom collision data region truncated (ROM too small)");
303 }
304
305 // Save-time guardrails: only allow writes to the collision pointer table and
306 // collision data bank (excluding the reserved WaterFill tail region).
309 static_cast<uint32_t>(kCustomCollisionRoomPointers),
310 static_cast<uint32_t>(kCustomCollisionRoomPointers + ptrs_size),
311 "CustomCollisionPointers"));
313 fence.Allow(static_cast<uint32_t>(kCustomCollisionDataPosition),
314 static_cast<uint32_t>(kCustomCollisionDataSoftEnd),
315 "CustomCollisionData"));
316 yaze::rom::ScopedWriteFence scope(rom, &fence);
317
318 // Encode collision data in single-tile format.
319 // Format: [F0 F0] [offset_lo offset_hi tile] ... [FF FF]
320 std::vector<uint8_t> encoded;
321 encoded.push_back(0xF0);
322 encoded.push_back(0xF0);
323
324 for (int y = 0; y < kGridSize; ++y) {
325 for (int x = 0; x < kGridSize; ++x) {
326 uint8_t tile = map.tiles[y * kGridSize + x];
327 if (tile == 0)
328 continue;
329 uint16_t offset = static_cast<uint16_t>(y * kGridSize + x);
330 encoded.push_back(offset & 0xFF);
331 encoded.push_back(offset >> 8);
332 encoded.push_back(tile);
333 }
334 }
335 encoded.push_back(0xFF);
336 encoded.push_back(0xFF);
337
338 // Find the end of existing collision data by scanning all room pointers
339 // to determine the highest used offset.
340 const size_t safe_end =
341 std::min(static_cast<size_t>(data.size()),
342 static_cast<size_t>(kCustomCollisionDataSoftEnd));
343 uint32_t max_used_pc = static_cast<uint32_t>(kCustomCollisionDataPosition);
344 for (int r = 0; r < kNumberOfRooms; ++r) {
345 int ptr_offset = kCustomCollisionRoomPointers + (r * 3);
346 if (ptr_offset + 2 >= static_cast<int>(data.size()))
347 continue;
348
349 uint32_t snes_ptr = data[ptr_offset] | (data[ptr_offset + 1] << 8) |
350 (data[ptr_offset + 2] << 16);
351 if (snes_ptr == 0)
352 continue;
353
354 uint32_t pc = SnesToPc(snes_ptr);
355 if (pc < static_cast<uint32_t>(kCustomCollisionDataPosition)) {
356 return absl::FailedPreconditionError(
357 absl::StrFormat("Custom collision pointer for room 0x%02X points "
358 "before data region (pc=0x%06X)",
359 r, pc));
360 }
361 if (pc >= static_cast<uint32_t>(kCustomCollisionDataSoftEnd)) {
362 return absl::FailedPreconditionError(
363 absl::StrFormat("Custom collision pointer for room 0x%02X overlaps "
364 "WaterFill reserved region (pc=0x%06X)",
365 r, pc));
366 }
367 if (pc >= data.size()) {
368 return absl::OutOfRangeError("Custom collision pointer out of ROM range");
369 }
370 // Walk past this room's data to find its end
371 size_t cursor = pc;
372 bool single_mode = false;
373 bool found_end_marker = false;
374 while (cursor + 1 < safe_end) {
375 uint16_t val = data[cursor] | (data[cursor + 1] << 8);
376 cursor += 2;
377 if (val == kCollisionEndMarker) {
378 found_end_marker = true;
379 break;
380 }
381 if (val == kCollisionSingleTileMarker) {
382 single_mode = true;
383 continue;
384 }
385 if (!single_mode) {
386 // Rectangle mode: skip width, height, then width*height bytes
387 if (cursor + 1 >= safe_end)
388 break;
389 uint8_t w = data[cursor];
390 uint8_t h = data[cursor + 1];
391 cursor += 2;
392 cursor += w * h;
393 } else {
394 // Single tile mode: skip 1 byte (tile value)
395 cursor += 1;
396 }
397 }
398 // If we hit the reserved region without an end marker, treat as corruption.
399 if (!found_end_marker && cursor + 1 >= safe_end) {
400 return absl::FailedPreconditionError(
401 absl::StrFormat("Custom collision data for room 0x%02X is "
402 "unterminated before WaterFill reserved region",
403 r));
404 }
405 if (cursor > max_used_pc) {
406 max_used_pc = static_cast<uint32_t>(cursor);
407 }
408 }
409
410 // Check if there's enough space
411 uint32_t write_pos = max_used_pc;
412 if (write_pos + encoded.size() > kCustomCollisionDataSoftEnd) {
413 return absl::ResourceExhaustedError(absl::StrFormat(
414 "Not enough collision data space. Need %d bytes at 0x%06X, "
415 "region ends at 0x%06X",
416 encoded.size(), write_pos, kCustomCollisionDataSoftEnd));
417 }
418
419 if (write_pos + encoded.size() > data.size()) {
420 return absl::OutOfRangeError(
421 absl::StrFormat("ROM too small for custom collision write (need "
422 "end=0x%06X, size=0x%06X)",
423 write_pos + encoded.size(), data.size()));
424 }
426 rom->WriteVector(static_cast<int>(write_pos), std::move(encoded)));
427
428 // Update pointer table: 3-byte SNES address
429 uint32_t snes_addr = PcToSnes(write_pos);
430 int ptr_offset = kCustomCollisionRoomPointers + (room_id * 3);
431 RETURN_IF_ERROR(rom->WriteByte(ptr_offset, snes_addr & 0xFF));
432 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 1, (snes_addr >> 8) & 0xFF));
433 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 2, (snes_addr >> 16) & 0xFF));
434
435 return absl::OkStatus();
436}
437
439 // Find bounding box of non-zero tiles to avoid printing the entire 64x64.
440 int min_x = kGridSize, max_x = 0, min_y = kGridSize, max_y = 0;
441 for (int y = 0; y < kGridSize; ++y) {
442 for (int x = 0; x < kGridSize; ++x) {
443 if (map.tiles[y * kGridSize + x] != 0) {
444 min_x = std::min(min_x, x);
445 max_x = std::max(max_x, x);
446 min_y = std::min(min_y, y);
447 max_y = std::max(max_y, y);
448 }
449 }
450 }
451
452 if (min_x > max_x)
453 return "(empty)\n";
454
455 // Add 1-tile padding
456 min_x = std::max(0, min_x - 1);
457 min_y = std::max(0, min_y - 1);
458 max_x = std::min(kGridSize - 1, max_x + 1);
459 max_y = std::min(kGridSize - 1, max_y + 1);
460
461 std::stringstream ss;
462 // Column header
463 ss << " ";
464 for (int x = min_x; x <= max_x; ++x) {
465 ss << absl::StrFormat("%X", x % 16);
466 }
467 ss << "\n";
468
469 for (int y = min_y; y <= max_y; ++y) {
470 ss << absl::StrFormat("%02X: ", y);
471 for (int x = min_x; x <= max_x; ++x) {
472 uint8_t tile = map.tiles[y * kGridSize + x];
473 ss << TileToChar(tile);
474 }
475 ss << "\n";
476 }
477
478 return ss.str();
479}
480
481} // namespace zelda3
482} // 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:28
absl::Status WriteByte(int addr, uint8_t value)
Definition rom.cc:476
const auto & vector() const
Definition rom.h:143
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
Definition rom.cc:548
bool is_loaded() const
Definition rom.h:132
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:314
void LoadObjects()
Definition room.cc:1292
int id() const
Definition room.h:566
uint8_t ClassifyTile(bool up, bool down, bool left, bool right)
DimensionService::DimensionResult ResolveTrackObjectDimensions(const RoomObject &obj, const GeneratorOptions &options, const DimensionService &dimension_service)
absl::Status WriteTrackCollision(Rom *rom, int room_id, const CustomCollisionMap &map)
constexpr int kCustomCollisionDataSoftEnd
std::string VisualizeCollisionMap(const CustomCollisionMap &map)
constexpr int kCustomCollisionDataPosition
constexpr int kNumberOfRooms
constexpr int kCustomCollisionRoomPointers
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
std::vector< std::pair< int, int > > switch_promotions
std::vector< std::tuple< int, int, TrackTileType > > stop_overrides