yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
object_parser.cc
Go to the documentation of this file.
1#include "object_parser.h"
2
3#include <cstring>
4
5#include "absl/strings/str_format.h"
6#include "util/log.h"
9
10// ROM addresses for object data are defined in room_object.h:
11// - kRoomObjectSubtype1 = 0x8000 (SNES $01:8000)
12// - kRoomObjectSubtype2 = 0x83F0 (SNES $01:83F0)
13// - kRoomObjectSubtype3 = 0x84F0 (SNES $01:84F0)
14// - kRoomObjectTileAddress = 0x1B52 (SNES $00:9B52)
15
16// Subtype 1 tile count lookup table.
17//
18// Each entry specifies how many tiles the corresponding `RoomDraw_*`
19// routine reads from the per-object tile stream. Index directly by
20// `(object_id & 0xFF)` for subtype 1 objects.
21//
22// Audit history:
23// - Initial table sourced from ZScream's `DungeonObjectData.cs`.
24// - 2026-04-25 audit (zelda3-hacking-expert): IDs `0x47` and `0x48`
25// stored as 0 (fallback to 8) but routine bodies in
26// `special_routines.cc::DrawWaterfall47` and `DrawWaterfall48`
27// deterministically index tile slots 0..14 and 0..8 respectively
28// (three 1x5 / 1x3 columns). The under-fetched 8-tile vector is
29// not a bounds bug — `TileAtWrapped` wraps `index % tiles.size()`
30// — but the wrap quietly substitutes the wrong tile for the right
31// column (e.g. Waterfall47 column 3 indexes tile 12, which wraps
32// to tile 4 with size 8). Updated to the real counts so the wrap
33// never fires for vanilla data.
34// - IDs `0xD3..0xD6` are routed to `DrawNothing` (logic-only;
35// `CheckIfWallIsMoved` flag tests). Their tile-count entry is `0`
36// because those routines have no tile payload. Other uncataloged zero
37// entries still use the conservative 8-tile fallback.
38// - IDs `0xCD` and `0xCE` consume 24 words: top corner (9), vertical
39// pattern (6), and bottom corner (9). ZScream's count of 28 crosses into
40// the next object's data block, so yaze uses the routine-proven count.
41// - IDs `0x3C` and `0x4C` consume 8 and 12 words respectively. Their
42// registry routines index the complete 4x2 and 4x3 payloads, while
43// ZScream's counts of 4 and 9 under-fetch the final stamp/column and make
44// ObjectDrawer fall back to a single tile.
45// - 2026-04-25 ZScream parity diff: full byte-for-byte comparison
46// against ZScream's `subtype1Lengths` (`DungeonObjectData.cs:184`)
47// remains pinned, with routine-body-proven corrections allowlisted.
48// The pin lives in `object_parser_test.cc::Subtype1TileLengths`
49// `MatchesZScreamReferenceExceptAtKnownDivergences` and detects
50// future drift in either direction.
51// clang-format off
52static constexpr uint8_t kSubtype1TileLengths[0xF8] = {
53 4, 8, 8, 8, 8, 8, 8, 4, 4, 5, 5, 5, 5, 5, 5, 5, // 0x00-0x0F
54 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, // 0x10-0x1F
55 5, 9, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 6, // 0x20-0x2F
56 6, 1, 1, 16, 1, 1, 16, 16, 6, 8, 12, 12, 8, 8, 4, 3, // 0x30-0x3F (0x3C=Doubled2x2:8)
57 3, 3, 3, 3, 3, 3, 3, 15, 9, 8, 8, 4, 12, 16, 16, 16, // 0x40-0x4F (0x47=Waterfall47:15, 0x48=Waterfall48:9, 0x4C=Bar4x3:12)
58 1, 18, 18, 4, 1, 8, 8, 1, 1, 1, 1, 18, 18, 15, 4, 3, // 0x50-0x5F
59 4, 8, 8, 8, 8, 8, 8, 4, 4, 3, 1, 1, 6, 6, 1, 1, // 0x60-0x6F
60 16, 1, 1, 16, 16, 8, 16, 16, 4, 1, 1, 4, 1, 4, 1, 8, // 0x70-0x7F
61 8, 12, 12, 12, 12, 18, 18, 8, 12, 4, 3, 3, 3, 1, 1, 6, // 0x80-0x8F
62 8, 8, 4, 4, 16, 4, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x90-0x9F
63 1, 1, 1, 1, 24, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0xA0-0xAF
64 1, 1, 16, 3, 3, 8, 8, 8, 4, 4, 16, 4, 4, 4, 1, 1, // 0xB0-0xBF
65 1, 68, 1, 1, 8, 8, 8, 8, 8, 8, 8, 1, 1, 24, 24, 1, // 0xC0-0xCF (0xCD/0xCE use 24-word platform payloads)
66 1, 8, 8, 0, 0, 0, 0, 1, 8, 8, 8, 8, 21, 16, 4, 8, // 0xD0-0xDF (0xD3..0xD6: DrawNothing logic; tiles unused)
67 8, 8, 8, 8, 8, 8, 8, 8, 8, 1, 1, 1, 1, 1, 1, 1, // 0xE0-0xEF
68 1, 1, 1, 1, 1, 1, 1, 1 // 0xF0-0xF7
69};
70// clang-format on
71
72static inline bool IsSubtype1LogicOnlyZeroTileObject(int object_id) {
73 int index = object_id & 0xFF;
74 return index >= 0xD3 && index <= 0xD6;
75}
76
77// Helper function to get tile count for Subtype 1 objects.
78static inline int GetSubtype1TileCount(int object_id) {
79 int index = object_id & 0xFF;
80 if (index < 0xF8) {
81 int count = kSubtype1TileLengths[index];
82 if (count > 0) {
83 return count;
84 }
85 if (IsSubtype1LogicOnlyZeroTileObject(index)) {
86 return 0;
87 }
88 return 8; // Default to 8 if an uncataloged table entry has 0.
89 }
90 return 8; // Default for IDs >= 0xF8
91}
92
93namespace yaze {
94namespace zelda3 {
95
96absl::StatusOr<std::vector<gfx::TileInfo>> ObjectParser::ParseObject(
97 int16_t object_id) {
98 if (rom_ == nullptr) {
99 return absl::InvalidArgumentError("ROM is null");
100 }
101
102 // Validate object ID is non-negative
103 if (object_id < 0) {
104 return absl::InvalidArgumentError(
105 absl::StrFormat("Invalid object ID: %d", object_id));
106 }
107
108 int subtype = DetermineSubtype(object_id);
109
110 switch (subtype) {
111 case 1:
112 return ParseSubtype1(object_id);
113 case 2:
114 return ParseSubtype2(object_id);
115 case 3:
116 return ParseSubtype3(object_id);
117 default:
118 return absl::InvalidArgumentError(
119 absl::StrFormat("Invalid object subtype for ID: %#04x", object_id));
120 }
121}
122
123absl::StatusOr<ObjectRoutineInfo> ObjectParser::ParseObjectRoutine(
124 int16_t object_id) {
125 if (rom_ == nullptr) {
126 return absl::InvalidArgumentError("ROM is null");
127 }
128
129 auto subtype_info = GetObjectSubtype(object_id);
130 if (!subtype_info.ok()) {
131 return subtype_info.status();
132 }
133
134 ObjectRoutineInfo routine_info;
135 routine_info.routine_ptr = subtype_info->routine_ptr;
136 routine_info.tile_ptr = subtype_info->subtype_ptr;
137 routine_info.tile_count = subtype_info->max_tile_count;
138 routine_info.is_repeatable = true;
139 routine_info.is_orientation_dependent = true;
140
141 return routine_info;
142}
143
144absl::StatusOr<ObjectSubtypeInfo> ObjectParser::GetObjectSubtype(
145 int16_t object_id) {
147 info.subtype = DetermineSubtype(object_id);
148
149 switch (info.subtype) {
150 case 1: {
151 int index = object_id & 0xFF;
152 info.subtype_ptr = kRoomObjectSubtype1 + (index * 2);
153 info.routine_ptr = kRoomObjectSubtype1 + 0x200 + (index * 2);
154 break;
155 }
156 case 2: {
157 // Type 2 objects: 0x100-0x13F (64 objects only)
158 // Index mask 0x3F ensures we stay within 64-entry table bounds
159 int index = (object_id - 0x100) & 0x3F;
160 info.subtype_ptr = kRoomObjectSubtype2 + (index * 2);
161 // Routine table starts 128 bytes (64 entries * 2 bytes) after data table
162 info.routine_ptr = kRoomObjectSubtype2 + 0x80 + (index * 2);
163 break;
164 }
165 case 3: {
166 // Type 3 object IDs are 0xF80-0xFFF (128 objects)
167 // Table index should be 0-127
168 int index = (object_id - 0xF80) & 0x7F;
169 info.subtype_ptr = kRoomObjectSubtype3 + (index * 2);
170 info.routine_ptr = kRoomObjectSubtype3 + 0x100 + (index * 2);
171 break;
172 }
173 default:
174 return absl::InvalidArgumentError(
175 absl::StrFormat("Invalid object subtype for ID: %#04x", object_id));
176 }
177
179
180 return info;
181}
182
183absl::StatusOr<ObjectSizeInfo> ObjectParser::ParseObjectSize(
184 int16_t object_id, uint8_t size_byte) {
185 ObjectSizeInfo info;
186
187 // Extract size bits (0-3 for X, 4-7 for Y)
188 int size_x = size_byte & 0x03;
189 int size_y = (size_byte >> 2) & 0x03;
190
191 info.width_tiles = (size_x + 1) * 2; // Convert to tile count
192 info.height_tiles = (size_y + 1) * 2;
193
194 // Determine orientation based on object ID and size
195 // This is a heuristic based on the object naming patterns
196 if (object_id >= 0x80 && object_id <= 0xFF) {
197 // Objects 0x80-0xFF are typically vertical
198 info.is_horizontal = false;
199 } else {
200 // Objects 0x00-0x7F are typically horizontal
201 info.is_horizontal = true;
202 }
203
204 // Determine if object is repeatable
205 info.is_repeatable = (size_byte != 0);
206 info.repeat_count = size_byte == 0 ? 32 : size_byte;
207
208 return info;
209}
210
211absl::StatusOr<std::vector<gfx::TileInfo>> ObjectParser::ParseSubtype1(
212 int16_t object_id) {
213 int index = object_id & 0xFF;
214 int tile_ptr = kRoomObjectSubtype1 + (index * 2);
215
216 if (tile_ptr + 1 >= (int)rom_->size()) {
217 return absl::OutOfRangeError(
218 absl::StrFormat("Tile pointer out of range: %#06x", tile_ptr));
219 }
220
221 // Read tile data pointer (16-bit little-endian offset)
222 uint8_t low = rom_->data()[tile_ptr];
223 uint8_t high = rom_->data()[tile_ptr + 1];
224 int16_t offset = (int16_t)((high << 8) | low); // Signed offset!
225 int tile_data_ptr = kRoomObjectTileAddress + offset;
226
227 // DEBUG: Log wall objects 0x61/0x62 and ceiling 0xC0 to verify tile data
228 bool is_debug_object = (object_id == 0x61 || object_id == 0x62 ||
229 object_id == 0xC0 || object_id == 0xC2);
230 static int debug_count = 0;
231 if (debug_count < 10 || is_debug_object) {
232 LOG_DEBUG("ObjectParser",
233 "ParseSubtype1: obj=0x%02X%s tile_ptr=0x%04X (SNES $01:%04X)",
234 object_id, is_debug_object ? " (DEBUG)" : "", tile_ptr, tile_ptr);
235 LOG_DEBUG("ObjectParser",
236 " ROM[0x%04X..0x%04X]=0x%02X 0x%02X offset=%d (0x%04X)",
237 tile_ptr, tile_ptr + 1, low, high, offset, (uint16_t)offset);
238 LOG_DEBUG("ObjectParser",
239 " tile_data_ptr=0x%04X+0x%04X=0x%04X (SNES $00:%04X)",
240 kRoomObjectTileAddress, (uint16_t)offset, tile_data_ptr,
241 tile_data_ptr + 0x8000);
242
243 // Fix: Check for negative tile_data_ptr to prevent SIGSEGV on corrupted ROMs
244 if (tile_data_ptr >= 0 && tile_data_ptr + 8 < (int)rom_->size()) {
245 uint16_t tw0 =
246 rom_->data()[tile_data_ptr] | (rom_->data()[tile_data_ptr + 1] << 8);
247 uint16_t tw1 = rom_->data()[tile_data_ptr + 2] |
248 (rom_->data()[tile_data_ptr + 3] << 8);
249 LOG_DEBUG("ObjectParser", " First 2 tiles: $%04X(id=%d) $%04X(id=%d)",
250 tw0, tw0 & 0x3FF, tw1, tw1 & 0x3FF);
251 } else {
252 LOG_DEBUG("ObjectParser", " Tile data at 0x%04X: <OUT OF BOUNDS>",
253 tile_data_ptr);
254 }
255 if (!is_debug_object)
256 debug_count++;
257 }
258
259 // Use lookup table for correct tile count per object ID
260 int tile_count = GetSubtype1TileCount(object_id);
261 return ReadTileData(tile_data_ptr, tile_count);
262}
263
264absl::StatusOr<std::vector<gfx::TileInfo>> ObjectParser::ParseSubtype2(
265 int16_t object_id) {
266 constexpr int kDamFloodGateOpenTileOffset = 0x13E8;
267 constexpr int kDamFloodGateTileCount = 40;
268
269 // Type 2 objects: 0x100-0x13F (64 objects only)
270 int index = (object_id - 0x100) & 0x3F;
271 int tile_ptr = kRoomObjectSubtype2 + (index * 2);
272
273 if (tile_ptr + 1 >= (int)rom_->size()) {
274 return absl::OutOfRangeError(
275 absl::StrFormat("Tile pointer out of range: %#06x", tile_ptr));
276 }
277
278 // Read tile data pointer
279 uint8_t low = rom_->data()[tile_ptr];
280 uint8_t high = rom_->data()[tile_ptr + 1];
281 int tile_data_ptr = kRoomObjectTileAddress + ((high << 8) | low);
282
283 // Determine tile count based on object ID
284 int tile_count = GetSubtype2TileCount(object_id);
285
286 // DEBUG: Log corner tile loading (0x100-0x103)
287 bool is_corner = (object_id >= 0x100 && object_id <= 0x103);
288 if (is_corner) {
289 LOG_DEBUG("ObjectParser",
290 "ParseSubtype2: CORNER obj=0x%03X index=%d tile_ptr=0x%04X",
291 object_id, index, tile_ptr);
292 LOG_DEBUG("ObjectParser",
293 " ROM[0x%04X..0x%04X]=0x%02X 0x%02X offset=0x%04X", tile_ptr,
294 tile_ptr + 1, low, high, (high << 8) | low);
295 LOG_DEBUG(
296 "ObjectParser", " tile_data_ptr=0x%04X+0x%04X=0x%04X (tile_count=%d)",
297 kRoomObjectTileAddress, (high << 8) | low, tile_data_ptr, tile_count);
298
299 // Show first 2 tile words
300 if (tile_data_ptr >= 0 && tile_data_ptr + 4 < (int)rom_->size()) {
301 uint16_t tw0 =
302 rom_->data()[tile_data_ptr] | (rom_->data()[tile_data_ptr + 1] << 8);
303 uint16_t tw1 = rom_->data()[tile_data_ptr + 2] |
304 (rom_->data()[tile_data_ptr + 3] << 8);
305 LOG_DEBUG("ObjectParser", " First 2 tiles: $%04X(id=%d) $%04X(id=%d)",
306 tw0, tw0 & 0x3FF, tw1, tw1 & 0x3FF);
307 }
308 }
309
310 if (object_id == 0x137) {
311 auto closed_tiles = ReadTileData(tile_data_ptr, kDamFloodGateTileCount);
312 if (!closed_tiles.ok()) {
313 return closed_tiles.status();
314 }
315 auto open_tiles =
316 ReadTileData(kRoomObjectTileAddress + kDamFloodGateOpenTileOffset,
317 kDamFloodGateTileCount);
318 if (!open_tiles.ok()) {
319 return open_tiles.status();
320 }
321 closed_tiles->insert(closed_tiles->end(), open_tiles->begin(),
322 open_tiles->end());
323 return closed_tiles;
324 }
325
326 return ReadTileData(tile_data_ptr, tile_count);
327}
328
329absl::StatusOr<std::vector<gfx::TileInfo>> ObjectParser::ParseSubtype3(
330 int16_t object_id) {
331 constexpr int kBombableFloorOpenTileOffset = 0x05BA;
332 constexpr int kBombableFloorStateTileCount = 16;
333 constexpr int kPrisonCellTileOffset = 0x1488;
334 constexpr int kPrisonCellTileCount = 6;
335 constexpr int kBigKeyLockTileOffset = 0x1494;
336 constexpr int kClosedChestTileOffset = 0x149C;
337 constexpr int kOpenChestTileOffset = 0x14A4;
338 constexpr int kChestStateTileCount = 4;
339 constexpr int kClosedBigChestTileOffset = 0x14AC;
340 constexpr int kOpenBigChestTileOffset = 0x14C4;
341 constexpr int kBigChestStateTileCount = 12;
342
343 // Type 3 object IDs are 0xF80-0xFFF (128 objects)
344 // Table index should be 0-127, calculated by subtracting base offset 0xF80
345 int index = (object_id - 0xF80) & 0x7F;
346 int tile_ptr = kRoomObjectSubtype3 + (index * 2);
347
348 if (tile_ptr + 1 >= (int)rom_->size()) {
349 return absl::OutOfRangeError(
350 absl::StrFormat("Tile pointer out of range: %#06x", tile_ptr));
351 }
352
353 // Read tile data pointer
354 uint8_t low = rom_->data()[tile_ptr];
355 uint8_t high = rom_->data()[tile_ptr + 1];
356 int tile_data_ptr = kRoomObjectTileAddress + ((high << 8) | low);
357
358 // Determine tile count based on object ID
359 int tile_count = GetSubtype3TileCount(object_id);
360
361 // BombableFloor's intact 4x4 block starts at the object's pointer, while
362 // USDASM's replacement 4x4 block lives separately at obj05BA. Keep the two
363 // 16-word states adjacent in the parsed payload for stateful rendering.
364 if (object_id == 0xFC7) {
365 auto intact_tiles =
366 ReadTileData(tile_data_ptr, kBombableFloorStateTileCount);
367 if (!intact_tiles.ok()) {
368 return intact_tiles.status();
369 }
370 auto open_tiles =
371 ReadTileData(kRoomObjectTileAddress + kBombableFloorOpenTileOffset,
372 kBombableFloorStateTileCount);
373 if (!open_tiles.ok()) {
374 return open_tiles.status();
375 }
376 intact_tiles->insert(intact_tiles->end(), open_tiles->begin(),
377 open_tiles->end());
378 return intact_tiles;
379 }
380
381 // Both PrisonCell IDs dispatch to RoomDraw_PrisonCell, which ignores the
382 // subtype table pointer and loads literal obj1488. F8D's vanilla table entry
383 // does not point there, so following the table would render the two aliases
384 // differently even though the game does not.
385 if (object_id == 0xF8D || object_id == 0xF97) {
386 return ReadTileData(kRoomObjectTileAddress + kPrisonCellTileOffset,
387 kPrisonCellTileCount);
388 }
389
390 // BigKeyLock overwrites the table-derived X register with obj1494 before
391 // drawing. Follow the runtime literal so a relocated F98 table entry does
392 // not change the editor while the game continues using obj1494.
393 if (object_id == 0xF98) {
394 return ReadTileData(kRoomObjectTileAddress + kBigKeyLockTileOffset,
395 kChestStateTileCount);
396 }
397
398 // Chest overwrites X with obj149C/obj14A4 for its closed/open branches.
399 if (object_id == 0xF99) {
400 auto closed_tiles = ReadTileData(
401 kRoomObjectTileAddress + kClosedChestTileOffset, kChestStateTileCount);
402 if (!closed_tiles.ok()) {
403 return closed_tiles.status();
404 }
405 auto open_tiles = ReadTileData(
406 kRoomObjectTileAddress + kOpenChestTileOffset, kChestStateTileCount);
407 if (!open_tiles.ok()) {
408 return open_tiles.status();
409 }
410 closed_tiles->insert(closed_tiles->end(), open_tiles->begin(),
411 open_tiles->end());
412 return closed_tiles;
413 }
414
415 // BigChest likewise overwrites X with obj14AC/obj14C4. F9A/FB2 remain
416 // direct-open routines and legitimately use their table-derived pointers.
417 if (object_id == 0xFB1) {
418 auto closed_tiles =
419 ReadTileData(kRoomObjectTileAddress + kClosedBigChestTileOffset,
420 kBigChestStateTileCount);
421 if (!closed_tiles.ok()) {
422 return closed_tiles.status();
423 }
424 auto open_tiles =
425 ReadTileData(kRoomObjectTileAddress + kOpenBigChestTileOffset,
426 kBigChestStateTileCount);
427 if (!open_tiles.ok()) {
428 return open_tiles.status();
429 }
430 closed_tiles->insert(closed_tiles->end(), open_tiles->begin(),
431 open_tiles->end());
432 return closed_tiles;
433 }
434
435 return ReadTileData(tile_data_ptr, tile_count);
436}
437
438absl::StatusOr<std::vector<gfx::TileInfo>> ObjectParser::ReadTileData(
439 int address, int tile_count) {
440 // Each tile is stored as a 16-bit word (2 bytes), not 8 bytes!
441 // ZScream: tiles.Add(new Tile(ROM.DATA[pos + ((i * 2))], ROM.DATA[pos + ((i *
442 // 2)) + 1]));
443 if (tile_count < 0) {
444 return absl::InvalidArgumentError(
445 absl::StrFormat("Invalid tile count: %d", tile_count));
446 }
447 if (tile_count == 0) {
448 return std::vector<gfx::TileInfo>{};
449 }
450 if (address < 0 || address + (tile_count * 2) >= (int)rom_->size()) {
451 return absl::OutOfRangeError(
452 absl::StrFormat("Tile data address out of range: %#06x", address));
453 }
454
455 std::vector<gfx::TileInfo> tiles;
456 tiles.reserve(tile_count);
457
458 // DEBUG: Log first tile read
459 static int debug_read_count = 0;
460 bool should_log = (debug_read_count < 3);
461
462 for (int i = 0; i < tile_count; i++) {
463 int tile_offset = address + (i * 2); // 2 bytes per tile word
464
465 // Read 1 word (2 bytes) per tile - this is the SNES tile format
466 uint16_t tile_word =
467 rom_->data()[tile_offset] | (rom_->data()[tile_offset + 1] << 8);
468
469 auto tile_info = gfx::WordToTileInfo(tile_word);
470 tiles.push_back(tile_info);
471
472 // DEBUG: Log first few tiles
473 if (should_log && i < 4) {
474 LOG_DEBUG("ObjectParser",
475 "ReadTile[%d]: addr=0x%06X word=0x%04X id=0x%03X pal=%d "
476 "mirror=(h:%d,v:%d)",
477 i, tile_offset, tile_word, tile_info.id_, tile_info.palette_,
478 tile_info.horizontal_mirror_, tile_info.vertical_mirror_);
479 }
480 }
481
482 if (should_log) {
483 LOG_DEBUG("ObjectParser",
484 "ReadTileData: addr=0x%06X count=%d loaded %zu tiles", address,
485 tile_count, tiles.size());
486 debug_read_count++;
487 }
488
489 return tiles;
490}
491
492int ObjectParser::GetSubtype2TileCount(int16_t object_id) const {
493 // 4x4 corners (0x100-0x10F): 16 tiles (32 bytes)
494 // These are RoomDraw_4x4 and RoomDraw_4x4Corner_BothBG routines
495 if (object_id >= 0x100 && object_id <= 0x10F) {
496 return 16;
497 }
498 // Weird corners (0x110-0x117): 12 tiles (24 bytes)
499 // These are RoomDraw_WeirdCornerBottom_BothBG and RoomDraw_WeirdCornerTop_BothBG
500 if (object_id >= 0x110 && object_id <= 0x117) {
501 return 12;
502 }
503 // 4x4 fixed patterns (stairs/altars/walls)
504 if (object_id == 0x11C || object_id == 0x124 || object_id == 0x125 ||
505 object_id == 0x129 || (object_id >= 0x12D && object_id <= 0x133) ||
506 object_id == 0x13C || object_id == 0x13F) {
507 return 16;
508 }
509 // Water hop stairs use a fixed 4x2 pattern.
510 if (object_id == 0x135 || object_id == 0x136) {
511 return 8;
512 }
513 // Dam floodgate preserves both closed and water-open 10x4 tile blocks.
514 if (object_id == 0x137) {
515 return 80;
516 }
517 // Beds (4x5)
518 if (object_id == 0x122 || object_id == 0x128) {
519 return 20;
520 }
521 // Tables (4x3)
522 if (object_id == 0x123 || object_id == 0x13D) {
523 return 12;
524 }
525 // 3x6 and 6x3 utility patterns
526 if (object_id == 0x12C || object_id == 0x13E) {
527 return 18;
528 }
529 // Spiral stairs (4x3)
530 if (object_id >= 0x138 && object_id <= 0x13B) {
531 return 12;
532 }
533 // Default: 8 tiles for other subtype 2 objects
534 return 8;
535}
536
537int ObjectParser::GetSubtype3TileCount(int16_t object_id) const {
538 // Water face variants:
539 // - 0xF80 (Empty) may branch to the 0xF81 tile block at runtime, so we load
540 // both spans to preserve parity for state-dependent rendering.
541 // - 0xF81 (Spitting): 4x5
542 // - 0xF82 (Drenching): 4x7
543 if (object_id == 0xF80) {
544 return 32;
545 }
546 if (object_id == 0xF81) {
547 return 20;
548 }
549 if (object_id == 0xF82) {
550 return 28;
551 }
552
553 // RoomDraw_PrisonCell loads literal obj1488 and consumes its six words for
554 // both object aliases.
555 if (object_id == 0xF8D || object_id == 0xF97) {
556 return 6;
557 }
558
559 // RupeeFloor (0xF92 = ASM 0x212) reads the two words at obj1DD6 and
560 // repeats them at fixed positions.
561 if (object_id == 0xF92) {
562 return 2;
563 }
564
565 // HammerPegSingle (0xF96 = ASM 0x216) jumps to
566 // RoomDraw_Rightwards2x2 and consumes one fixed 2x2 tile block.
567 if (object_id == 0xF96) {
568 return 4;
569 }
570
571 // BigKeyLock (0xF98 = ASM 0x218) loads literal obj1494 and consumes exactly
572 // one fixed 2x2 block. The following four words belong to obj149C (Chest),
573 // not to an alternate lock state.
574 if (object_id == 0xF98) {
575 return 4;
576 }
577
578 // Chest (F99) carries both closed/open 2x2 states. OpenChest (F9A) is
579 // already the fixed open state and consumes one 2x2 payload.
580 if (object_id == 0xF99) {
581 return 8;
582 }
583 if (object_id == 0xF9A) {
584 return 4;
585 }
586
587 // BombableFloor (0xFC7 = ASM 0x247) has two non-contiguous 4x4 states:
588 // 16 intact words from the object pointer and 16 replacement words at
589 // obj05BA. ParseSubtype3 loads and concatenates both spans.
590 if (object_id == 0xFC7) {
591 return 32;
592 }
593
594 // Somaria path pieces (ASM objects 0x203-0x20C, 0x20E, and 0x20F) each
595 // point to one tile word. RoomDraw_SomariaLine writes that word once.
596 if ((object_id >= 0xF83 && object_id <= 0xF8C) || object_id == 0xF8E ||
597 object_id == 0xF8F) {
598 return 1;
599 }
600
601 // BigChest (FB1) carries closed/open 4x3 states. OpenBigChest (FB2) is the
602 // fixed open state. Both draw through RoomDraw_1x3N_rightwards with N=4.
603 if (object_id == 0xFB1) {
604 return 24;
605 }
606 if (object_id == 0xFB2) {
607 return 12;
608 }
609 // TableRock4x3 variants (0xF94, 0xFCE, 0xFE7-0xFE8, 0xFEC-0xFED): 12 tiles
610 if (object_id == 0xF94 || object_id == 0xFCE ||
611 (object_id >= 0xFE7 && object_id <= 0xFE8) ||
612 (object_id >= 0xFEC && object_id <= 0xFED)) {
613 return 12;
614 }
615 // 4x4 pattern objects: 16 tiles
616 // (0xFC8 = 0x248, 0xFE6 = 0x266, 0xFEB = 0x26B, 0xFFA = 0x27A)
617 if (object_id == 0xFC8 || object_id == 0xFE6 || object_id == 0xFEB ||
618 object_id == 0xFFA) {
619 return 16;
620 }
621 // Boss shells (4x4)
622 if (object_id == 0xF95 || object_id == 0xFF2 || object_id == 0xFFB) {
623 return 16;
624 }
625 // Auto stairs (4x4)
626 if ((object_id >= 0xF9B && object_id <= 0xF9D) || object_id == 0xFB3) {
627 return 16;
628 }
629 // Straight inter-room stairs (4x4)
630 if ((object_id >= 0xF9E && object_id <= 0xFA1) ||
631 (object_id >= 0xFA6 && object_id <= 0xFA9)) {
632 return 16;
633 }
634 // 4x4 single-pattern objects
635 if (object_id == 0xFAA || object_id == 0xFAD || object_id == 0xFAE ||
636 (object_id >= 0xFB4 && object_id <= 0xFB9) || object_id == 0xFCB ||
637 object_id == 0xFCC || object_id == 0xFD4 || object_id == 0xFE2 ||
638 object_id == 0xFF6 || object_id == 0xFF7) {
639 return 16;
640 }
641 // Turtle Rock pipes: 24 tiles (proven from routine bodies, not just
642 // dimensions table). DrawVerticalTurtleRockPipe (0xFBA, 0xFBB at
643 // special_routines.cc:430-437) draws two stacked 4x3 sections, reading
644 // tiles[0..11] then tiles[12..23]. DrawHorizontalTurtleRockPipe
645 // (0xFBC, 0xFBD at special_routines.cc:439-441) calls DrawNx4 with
646 // columns=6, which reads a 6x4 column-major block (tiles[0..23]).
647 // Without this case the parser fell through to the default 8 and
648 // TileAtWrapped substituted tiles[0..7] for indices 8..23 -- same
649 // failure mode Waterfall47/48 had before e9938002.
650 if (object_id >= 0xFBA && object_id <= 0xFBD) {
651 return 24;
652 }
653 // Utility 6x3 (18 tiles)
654 if (object_id == 0xFCD || object_id == 0xFDD) {
655 return 18;
656 }
657 // Utility 3x5 (15 tiles)
658 if (object_id == 0xFD5 || object_id == 0xFDB) {
659 return 15;
660 }
661 // Archery game target door (3x6, 18 tiles)
662 if (object_id >= 0xFE0 && object_id <= 0xFE1) {
663 return 18;
664 }
665 // Solid wall decor 3x4 (12 tiles)
666 if (object_id == 0xFE9 || object_id == 0xFEA || object_id == 0xFEE ||
667 object_id == 0xFEF) {
668 return 12;
669 }
670 // LightBeamOnFloor (0xFF0 / ASM 0x270) reads two unique 4x4 blocks:
671 // obj2376 is reused for the top and overlapping middle stamps, then
672 // obj2396 supplies the bottom stamp.
673 if (object_id == 0xFF0) {
674 return 32;
675 }
676 // BigLightBeamOnFloor (0xFF1 / ASM 0x271) draws four unique 4x4 blocks.
677 if (object_id == 0xFF1) {
678 return 64;
679 }
680 // FloorLight (0xFF4 / ASM 0x274) unconditionally draws four unique 4x4
681 // blocks in an 8x8 grid.
682 if (object_id == 0xFF4) {
683 return 64;
684 }
685 // Ganon Triforce floor decor (two 4x4 blocks -> 32 tiles)
686 if (object_id == 0xFF8) {
687 return 32;
688 }
689 // Table rock 4x3
690 if (object_id == 0xFF9) {
691 return 12;
692 }
693 // Default: 8 tiles for most subtype 3 objects
694 return 8;
695}
696
697int ObjectParser::DetermineSubtype(int16_t object_id) const {
698 // Type 3 IDs from decoding are 0xF80-0xFFF (b3 0xF8-0xFF shifted).
699 if (object_id >= 0xF80) {
700 return 3;
701 } else if (object_id >= 0x100) {
702 return 2;
703 } else {
704 return 1;
705 }
706}
707
708int ObjectParser::ResolveTileCountForObject(int16_t object_id) const {
709 if (object_id < 0) {
710 return 1;
711 }
712
713 switch (DetermineSubtype(object_id)) {
714 case 1:
715 return GetSubtype1TileCount(object_id);
716 case 2:
717 return GetSubtype2TileCount(object_id);
718 case 3:
719 return GetSubtype3TileCount(object_id);
720 default:
721 return 1;
722 }
723}
724
726 ObjectDrawInfo info;
727
728 // Keep draw-info tile counts sourced from subtype tables (ROM-facing data),
729 // while routine selection comes from DrawRoutineRegistry (renderer-facing
730 // canonical mapping).
731 info.tile_count = ResolveTileCountForObject(object_id);
732 const bool is_vertical_band = (object_id >= 0x60 && object_id <= 0x6F);
733 info.is_horizontal = !is_vertical_band;
734 info.is_vertical = is_vertical_band;
735
737 info.routine_name = "DefaultSolid";
738 info.both_layers = false;
739
740 auto& registry = DrawRoutineRegistry::Get();
741 // Feature flags (custom objects) can be toggled at runtime by project
742 // context, so keep parser metadata aligned with current registry mappings.
743 registry.RefreshFeatureFlagMappings();
744
745 int registry_routine = registry.GetRoutineIdForObject(object_id);
746 if (registry_routine < 0) {
747 return info;
748 }
749
750 info.draw_routine_id = registry_routine;
751 if (const DrawRoutineInfo* routine_info =
752 registry.GetRoutineInfo(registry_routine);
753 routine_info != nullptr) {
754 info.routine_name = routine_info->name;
755 info.both_layers = routine_info->draws_to_both_bgs;
756
757 switch (routine_info->category) {
759 info.is_horizontal = false;
760 info.is_vertical = true;
761 break;
763 info.is_horizontal = false;
764 info.is_vertical = false;
765 break;
766 default:
767 info.is_horizontal = true;
768 info.is_vertical = false;
769 break;
770 }
771 }
772
773 return info;
774}
775
776} // namespace zelda3
777} // namespace yaze
auto data() const
Definition rom.h:151
auto size() const
Definition rom.h:150
static DrawRoutineRegistry & Get()
absl::StatusOr< ObjectSubtypeInfo > GetObjectSubtype(int16_t object_id)
Get object subtype information.
absl::StatusOr< ObjectRoutineInfo > ParseObjectRoutine(int16_t object_id)
Parse object routine data.
absl::StatusOr< ObjectSizeInfo > ParseObjectSize(int16_t object_id, uint8_t size_byte)
Parse object size and orientation.
int GetSubtype3TileCount(int16_t object_id) const
Get tile count for subtype 3 objects.
absl::StatusOr< std::vector< gfx::TileInfo > > ParseSubtype2(int16_t object_id)
Parse subtype 2 objects (0x100-0x1FF)
absl::StatusOr< std::vector< gfx::TileInfo > > ParseObject(int16_t object_id)
Parse object data directly from ROM.
int GetSubtype2TileCount(int16_t object_id) const
Get tile count for subtype 2 objects.
absl::StatusOr< std::vector< gfx::TileInfo > > ParseSubtype1(int16_t object_id)
Parse subtype 1 objects (0x00-0xFF)
absl::StatusOr< std::vector< gfx::TileInfo > > ParseSubtype3(int16_t object_id)
Parse subtype 3 objects (0x200+)
absl::StatusOr< std::vector< gfx::TileInfo > > ReadTileData(int address, int tile_count)
Read tile data from ROM.
ObjectDrawInfo GetObjectDrawInfo(int16_t object_id) const
Get draw routine information for an object.
int ResolveTileCountForObject(int16_t object_id) const
int DetermineSubtype(int16_t object_id) const
Determine object subtype from ID.
#define LOG_DEBUG(category, format,...)
Definition log.h:103
TileInfo WordToTileInfo(uint16_t word)
Definition snes_tile.cc:378
constexpr int kRoomObjectSubtype3
Definition room_object.h:46
constexpr int kRoomObjectSubtype1
Definition room_object.h:44
constexpr int kRoomObjectSubtype2
Definition room_object.h:45
constexpr int kRoomObjectTileAddress
Definition room_object.h:47
Metadata about a draw routine.
Draw routine information for object rendering.
Object routine information.
Object size and orientation information.
Object subtype information.