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