yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
special_routines.cc
Go to the documentation of this file.
1#include "special_routines.h"
2
3#include <algorithm>
4#include <array>
5
6#include "util/log.h"
13
14namespace yaze {
15namespace zelda3 {
16namespace draw_routines {
17
18namespace {
19
20constexpr int kThievesTownEastAtticRoomId = 0x65;
21
22// RoomDraw_BombableFloor opens only in the room named by `CMP.w #$0065` at
23// $01:B3E3. ROM hacks patch that operand (Oracle of Secrets: 0xAD), so read
24// it; fall back to vanilla when the instruction is not there.
25int BombableFloorRoomId(const Rom* rom) {
26 constexpr int kCmpPc = 0xB3E3; // $01:B3E3
27 if (rom != nullptr) {
28 const auto opcode = rom->ReadByte(kCmpPc);
29 const auto operand = rom->ReadWord(kCmpPc + 1);
30 if (opcode.ok() && *opcode == 0xC9 && operand.ok()) {
31 return *operand;
32 }
33 }
34 return kThievesTownEastAtticRoomId;
35}
36
37// RoomDraw_BigLightBeamOnFloor reads `LDA.l $7EF0CA` at $01:A7D3: the save
38// word of the room whose floor was bombed ($7EF000 + 2 * room).
39int LightBeamFloorRoomId(const Rom* rom) {
40 constexpr int kLdaPc = 0xA7D3; // $01:A7D3
41 if (rom != nullptr) {
42 const auto opcode = rom->ReadByte(kLdaPc);
43 const auto address = rom->ReadWord(kLdaPc + 1);
44 const auto bank = rom->ReadByte(kLdaPc + 3);
45 if (opcode.ok() && *opcode == 0xAF && address.ok() && bank.ok() &&
46 *bank == 0x7E && *address >= 0xF000 && *address < 0xF250 &&
47 (*address & 1) == 0) {
48 return (*address - 0xF000) / 2;
49 }
50 }
51 return kThievesTownEastAtticRoomId;
52}
53
54const gfx::TileInfo& TileAtWrapped(std::span<const gfx::TileInfo> tiles,
55 size_t index) {
56 return tiles[index % tiles.size()];
57}
58
59void DrawColumnMajor(gfx::BackgroundBuffer& bg, int base_x, int base_y, int w,
60 int h, std::span<const gfx::TileInfo> tiles,
61 size_t start_index = 0) {
62 if (tiles.empty())
63 return;
64
65 size_t index = start_index;
66 for (int x = 0; x < w; ++x) {
67 for (int y = 0; y < h; ++y) {
68 DrawRoutineUtils::WriteTile8(bg, base_x + x, base_y + y,
69 TileAtWrapped(tiles, index++));
70 }
71 }
72}
73
74void DrawRowMajor(gfx::BackgroundBuffer& bg, int base_x, int base_y, int w,
75 int h, std::span<const gfx::TileInfo> tiles,
76 size_t start_index = 0) {
77 if (tiles.empty())
78 return;
79
80 size_t index = start_index;
81 for (int y = 0; y < h; ++y) {
82 for (int x = 0; x < w; ++x) {
83 DrawRoutineUtils::WriteTile8(bg, base_x + x, base_y + y,
84 TileAtWrapped(tiles, index++));
85 }
86 }
87}
88
89void Draw1x3NRightwards(const DrawContext& ctx, int columns, size_t start_index,
90 int y_offset = 0) {
91 DrawColumnMajor(ctx.target_bg, ctx.object.x_, ctx.object.y_ + y_offset,
92 columns, 3, ctx.tiles, start_index);
93}
94
95void DrawNx4(const DrawContext& ctx, int columns, size_t start_index) {
96 DrawColumnMajor(ctx.target_bg, ctx.object.x_, ctx.object.y_, columns, 4,
97 ctx.tiles, start_index);
98}
99
100void Draw1x5Column(const DrawContext& ctx, int x_offset, size_t start_index) {
101 DrawColumnMajor(ctx.target_bg, ctx.object.x_ + x_offset, ctx.object.y_, 1, 5,
102 ctx.tiles, start_index);
103}
104
105void Draw4x4ColumnMajor(const DrawContext& ctx, int x_offset, int y_offset,
106 size_t start_index) {
107 DrawColumnMajor(ctx.target_bg, ctx.object.x_ + x_offset,
108 ctx.object.y_ + y_offset, 4, 4, ctx.tiles, start_index);
109}
110
112 if (ctx.tiles.size() < 64)
113 return;
114
115 Draw4x4ColumnMajor(ctx, /*x_offset=*/0, /*y_offset=*/0, /*start_index=*/0);
116 Draw4x4ColumnMajor(ctx, /*x_offset=*/4, /*y_offset=*/0, /*start_index=*/16);
117 Draw4x4ColumnMajor(ctx, /*x_offset=*/0, /*y_offset=*/4, /*start_index=*/32);
118 Draw4x4ColumnMajor(ctx, /*x_offset=*/4, /*y_offset=*/4, /*start_index=*/48);
119}
120
122 std::span<const gfx::TileInfo> tiles) {
123 DrawColumnMajor(bg, object.x_, object.y_, 4, 4, tiles);
124}
125
126void DrawMany32x32Block(gfx::BackgroundBuffer& bg, int base_x, int base_y,
127 std::span<const gfx::TileInfo> tiles) {
128 // USDASM RoomDraw_A_Many32x32Blocks ($01:8A44) writes tiles 0..3 through
129 // row-0 tilemap pointers and tiles 4..7 through row-1 pointers, then repeats
130 // the same 4x2 stamp two tile rows lower.
131 DrawRowMajor(bg, base_x, base_y, /*w=*/4, /*h=*/2, tiles);
132 DrawRowMajor(bg, base_x, base_y + 2, /*w=*/4, /*h=*/2, tiles);
133}
134
136 // ASM: RoomDraw_WaterHopStairs_A ($019B1E) falls through PortraitOfMario's
137 // single-pass Downwards4x2VariableSpacing path, which is a fixed 4x2
138 // ROW-MAJOR stamp on the active layer.
139 if (ctx.tiles.size() < 8)
140 return;
141
142 DrawRowMajor(ctx.target_bg, ctx.object.x_, ctx.object.y_, 4, 2, ctx.tiles);
143}
144
146 // ASM: RoomDraw_WaterHopStairs_B ($01A3AE) writes the same 4x2 ROW-MAJOR
147 // footprint to both BG tilemaps. The outer draw framework handles the dual
148 // pass for routines marked draws_to_both_bgs.
150}
151
152// Reads 8 RoomDrawObjectData words at `object_data_offset` (usdasm objXXXX).
153std::array<gfx::TileInfo, 16> LoadRoomDrawObjectTiles(const DrawContext& ctx,
154 int object_data_offset,
155 int count) {
156 std::array<gfx::TileInfo, 16> tiles{};
157 for (int i = 0; i < count && i < 16; ++i) {
158 const auto word = ctx.rom->ReadWord(kRoomObjectTileAddress +
159 object_data_offset + (i * 2));
160 tiles[i] = gfx::WordToTileInfo(word.ok() ? *word : 0);
161 }
162 return tiles;
163}
164
165// RoomTag_OperateWaterFlooring over WaterOverlayData ($04:F1CD): each 3-byte
166// entry is a type-1 object header (x, y, size); every 4x4 block is two 4x2
167// obj0110 stamps written to $7E4000 (yaze BG2).
169 constexpr int kWaterOverlayData = 0x271CD;
170 constexpr int kFloorTiles = 0x0110;
171 const auto tiles = LoadRoomDrawObjectTiles(ctx, kFloorTiles, 8);
172 for (int entry = kWaterOverlayData;; entry += 3) {
173 const auto b0 = ctx.rom->ReadByte(entry);
174 const auto b1 = ctx.rom->ReadByte(entry + 1);
175 if (!b0.ok() || !b1.ok() || (*b0 == 0xFF && *b1 == 0xFF)) {
176 return;
177 }
178 const int x = *b0 >> 2;
179 const int y = *b1 >> 2;
180 const int blocks_x = (*b0 & 0x03) + 1;
181 const int blocks_y = (*b1 & 0x03) + 1;
182 for (int by = 0; by < blocks_y; ++by) {
183 for (int bx = 0; bx < blocks_x; ++bx) {
184 for (int row = 0; row < 4; ++row) {
185 for (int col = 0; col < 4; ++col) {
186 DrawRoutineUtils::WriteTile8(bg2, x + (bx * 4) + col,
187 y + (by * 4) + row,
188 tiles[((row & 1) * 4) + col]);
189 }
190 }
191 }
192 }
193 }
194}
195
197 // ASM: RoomDraw_DamFloodGate ($019BF8) stamps a 10x4 column-major tile block
198 // from the closed tile span by default, and swaps to the alternate water-open
199 // tile span when the overworld event is active.
200 constexpr size_t kSpanTiles = 40;
201 const bool use_open_tiles = ctx.state != nullptr &&
202 ctx.state->IsDamFloodgateOpen(ctx.room_id) &&
203 ctx.tiles.size() >= kSpanTiles * 2;
204 const size_t start_index = use_open_tiles ? kSpanTiles : 0;
205 if (ctx.tiles.size() < start_index + kSpanTiles)
206 return;
207
208 DrawColumnMajor(ctx.target_bg, ctx.object.x_, ctx.object.y_, 10, 4, ctx.tiles,
209 start_index);
210 if (use_open_tiles && ctx.rom != nullptr && ctx.secondary_bg != nullptr) {
212 }
213}
214
215void WritePlatformTile(const DrawContext& ctx, int dx, int dy, size_t index) {
216 if (index >= ctx.tiles.size())
217 return;
218
220 ctx.object.y_ + dy, ctx.tiles[index]);
221}
222
223void DrawPlatform1x3Rightwards(const DrawContext& ctx, int dx, int dy,
224 size_t start_index, int columns) {
225 for (int x = 0; x < columns; ++x) {
226 for (int y = 0; y < 3; ++y) {
227 WritePlatformTile(ctx, dx + x, dy + y, start_index + x * 3 + y);
228 }
229 }
230}
231
232void DrawPlatform2x2(const DrawContext& ctx, int dx, int dy,
233 size_t start_index) {
234 WritePlatformTile(ctx, dx + 0, dy + 0, start_index + 0);
235 WritePlatformTile(ctx, dx + 0, dy + 1, start_index + 1);
236 WritePlatformTile(ctx, dx + 1, dy + 0, start_index + 2);
237 WritePlatformTile(ctx, dx + 1, dy + 1, start_index + 3);
238}
239
240void DrawPlatform3x2(const DrawContext& ctx, int dx, int dy,
241 size_t start_index) {
242 WritePlatformTile(ctx, dx + 0, dy + 0, start_index + 0);
243 WritePlatformTile(ctx, dx + 1, dy + 0, start_index + 1);
244 WritePlatformTile(ctx, dx + 2, dy + 0, start_index + 2);
245 WritePlatformTile(ctx, dx + 0, dy + 1, start_index + 3);
246 WritePlatformTile(ctx, dx + 1, dy + 1, start_index + 4);
247 WritePlatformTile(ctx, dx + 2, dy + 1, start_index + 5);
248}
249
251constexpr std::array<uint16_t, 3> kMovingWallFallbackFillWords = {
252 0x3C15, 0x3C15, 0x3C15};
253
254std::array<gfx::TileInfo, 3> LoadMovingWallFillTiles(const DrawContext& ctx) {
255 std::array<gfx::TileInfo, 3> fill_tiles;
256 for (size_t i = 0; i < fill_tiles.size(); ++i) {
257 uint16_t word = kMovingWallFallbackFillWords[i];
258 if (ctx.rom != nullptr) {
259 const auto word_or = ctx.rom->ReadWord(kMovingWallFillDataOffset +
260 static_cast<int>(i * 2));
261 if (word_or.ok()) {
262 word = *word_or;
263 }
264 }
265 fill_tiles[i] = gfx::WordToTileInfo(word);
266 }
267 return fill_tiles;
268}
269
270void DrawMovingWallFill(const DrawContext& ctx, int start_x, int column_count,
271 int height) {
272 const auto fill_tiles = LoadMovingWallFillTiles(ctx);
273 for (int x = 0; x < column_count; ++x) {
274 DrawRoutineUtils::WriteTile8(ctx.target_bg, start_x + x, ctx.object.y_,
275 fill_tiles[0]);
276 for (int y = 1; y < height - 1; ++y) {
278 ctx.object.y_ + y, fill_tiles[1]);
279 }
281 ctx.object.y_ + height - 1, fill_tiles[2]);
282 }
283}
284
285void DrawMovingWallPlatform(const DrawContext& ctx, int direction) {
286 DrawPlatform1x3Rightwards(ctx, /*dx=*/0, /*dy=*/0, /*start_index=*/0,
287 /*columns=*/3);
288 for (int i = 0; i < direction; ++i) {
289 DrawPlatform3x2(ctx, /*dx=*/0, /*dy=*/3 + i * 2,
290 /*start_index=*/9);
291 }
292 // The ASM advances six words past the vertical pattern before stamping the
293 // second 3x3 corner (payload slots 15..23).
294 DrawPlatform1x3Rightwards(ctx, /*dx=*/0, /*dy=*/3 + direction * 2,
295 /*start_index=*/15, /*columns=*/3);
296}
297
299 size_t start_index, int fill_width) {
300 WritePlatformTile(ctx, /*dx=*/0, dy, start_index + 0);
301
302 for (int i = 0; i < fill_width; ++i) {
303 WritePlatformTile(ctx, /*dx=*/1 + i, dy, start_index + 3);
304 }
305
306 const int left_cap_x = 1 + fill_width;
307 WritePlatformTile(ctx, left_cap_x, dy, start_index + 6);
308
309 for (int i = 0; i < 4; ++i) {
310 WritePlatformTile(ctx, left_cap_x + 1 + i, dy, start_index + 9);
311 }
312
313 const int right_cap_x = left_cap_x + 5;
314 WritePlatformTile(ctx, right_cap_x, dy, start_index + 12);
315
316 for (int i = 0; i < fill_width; ++i) {
317 WritePlatformTile(ctx, right_cap_x + 1 + i, dy, start_index + 15);
318 }
319
320 WritePlatformTile(ctx, right_cap_x + 1 + fill_width, dy, start_index + 18);
321}
322
323} // namespace
324
325void DrawChest(const DrawContext& ctx, int chest_index) {
326 // Routine 39 is used only by stateful Chest (F99) and fixed OpenChest
327 // (F9A). BigChest uses its separate 4x3 routine.
328 bool is_open = ctx.object.id_ == 0xF9A;
329 if (!is_open && ctx.state) {
330 is_open = ctx.state->IsChestOpen(ctx.room_id, chest_index);
331 }
332
333 // Preserve the legacy subtype-1 0xF8-0xFD family handled by this registry
334 // routine. The subtype-3 chest opcodes below are always 2x2.
335 if (ctx.object.id_ < 0xF80 && ctx.tiles.size() >= 16) {
336 size_t start_index = is_open && ctx.tiles.size() >= 32 ? 16 : 0;
337 for (int x = 0; x < 4; ++x) {
338 for (int y = 0; y < 4; ++y) {
340 ctx.target_bg, ctx.object.x_ + x, ctx.object.y_ + y,
341 ctx.tiles[start_index + static_cast<size_t>(x * 4 + y)]);
342 }
343 }
344 return;
345 }
346
347 size_t start_index = 0;
348 if (is_open && ctx.object.id_ == 0xF99 && ctx.tiles.size() >= 8) {
349 start_index = 4;
350 }
351 if (ctx.tiles.size() < start_index + 4) {
352 return;
353 }
354
356 ctx.tiles[start_index]);
358 ctx.tiles[start_index + 1]);
360 ctx.tiles[start_index + 2]);
362 ctx.object.y_ + 1, ctx.tiles[start_index + 3]);
363}
364
365void DrawNothing([[maybe_unused]] const DrawContext& ctx) {
366 // Intentionally empty - represents invisible logic objects or placeholders
367 // ASM: RoomDraw_Nothing_A ($0190F2), RoomDraw_Nothing_B ($01932E), etc.
368 // These routines typically just RTS.
369}
370
371void CustomDraw(const DrawContext& ctx) {
372 // Pattern: Custom draw routine for fixed Oracle runtime families.
373 // When custom objects are enabled, load tile data from external binary files
374 // managed by CustomObjectManager. Each binary encodes SNES tilemap entries
375 // with relative x/y positions computed from the buffer stride layout.
376
377 const bool enabled = ctx.custom_objects != nullptr
378 ? ctx.custom_objects->enabled()
380 if (!enabled) {
381 // Feature disabled: fall back to vanilla 1x1 draw from ROM tile span
382 if (ctx.tiles.size() >= 1) {
384 ctx.tiles[0]);
385 }
386 return;
387 }
388
389 // Look up the custom object by ID and subtype.
390 // ctx.object.id_ is a registered fixed Oracle family (0x31, 0x32, or 0x54);
391 // ctx.object.size_ encodes the subtype.
392 absl::StatusOr<std::shared_ptr<const CustomObject>> result =
393 ctx.custom_objects != nullptr
395 : absl::StatusOr<std::shared_ptr<const CustomObject>>(
397 ctx.object.size_));
398
399 if (!result.ok() || !result.value()) {
400 // Missing or unreadable custom source: fall back to a visible 1x1 tile.
401 if (ctx.tiles.size() >= 1) {
403 ctx.tiles[0]);
404 }
405 return;
406 }
407
408 const auto& custom_obj = *result.value();
409 if (custom_obj.IsEmpty()) {
410 // A terminator-only asset is an intentional draw-nothing override.
411 return;
412 }
413
414 for (const auto& entry : custom_obj.tiles) {
415 // Oracle treats a zero payload word as a no-op. The entry still advances
416 // the decoded cursor, but must preserve any room tile already underneath.
417 if (entry.tile_data == 0) {
418 continue;
419 }
420
421 const uint16_t runtime_word =
422 CustomObjectRuntimeTileWord(ctx.object.id_, entry.tile_data);
423 gfx::TileInfo tile_info = gfx::WordToTileInfo(runtime_word);
424
425 // rel_x/rel_y are already decoded as object-relative coordinates from the
426 // binary stream's buffer position arithmetic; preserve those offsets.
427 int draw_x = ctx.object.x_ + entry.rel_x;
428 int draw_y = ctx.object.y_ + entry.rel_y;
429
430 DrawRoutineUtils::WriteTile8(ctx.target_bg, draw_x, draw_y, tile_info);
431 }
432}
433
435 // Pattern: Door switcher (object 0x35)
436 // Special door logic
437 // Check state to decide graphic
438 int tile_index = 0;
439 if (ctx.state && ctx.state->IsDoorSwitchActive(ctx.room_id)) {
440 // Use active tile if available (assuming 2nd tile is active state)
441 if (ctx.tiles.size() >= 2) {
442 tile_index = 1;
443 }
444 }
445
446 if (ctx.tiles.size() > static_cast<size_t>(tile_index)) {
448 ctx.tiles[tile_index]);
449 }
450}
451
452void DrawSomariaLine(const DrawContext& ctx) {
453 // USDASM RoomDraw_SomariaLine writes one object-data word at the object's
454 // anchor. The apparent path is assembled from separate subtype-3 objects;
455 // the object's size bits do not extend an individual piece.
456 if (ctx.tiles.empty()) {
457 return;
458 }
459
461 ctx.tiles.front());
462}
463
464void DrawWaterFace(const DrawContext& ctx) {
465 // Legacy 2x2 helper used by older corner-family routines. The real subtype-3
466 // water-face objects are handled by DrawEmptyWaterFace /
467 // DrawSpittingWaterFace / DrawDrenchingWaterFace below.
468 if (ctx.tiles.size() >= 4) {
470 ctx.tiles[0]); // col 0, row 0
472 ctx.object.y_ + 1,
473 ctx.tiles[1]); // col 0, row 1
475 ctx.object.y_,
476 ctx.tiles[2]); // col 1, row 0
478 ctx.object.y_ + 1,
479 ctx.tiles[3]); // col 1, row 1
480 }
481}
482
483void DrawLargeCanvasObject(const DrawContext& ctx, int width, int height) {
484 // Generic large object drawer
485 if (ctx.tiles.size() >= static_cast<size_t>(width * height)) {
486 for (int y = 0; y < height; ++y) {
487 for (int x = 0; x < width; ++x) {
489 ctx.object.y_ + y,
490 ctx.tiles[y * width + x]);
491 }
492 }
493 }
494}
495
496void DrawBed4x5(const DrawContext& ctx) {
497 // ASM: RoomDraw_Bed4x5 ($019AEE) writes 4 columns per row, 5 rows total.
498 DrawRowMajor(ctx.target_bg, ctx.object.x_, ctx.object.y_, 4, 5, ctx.tiles);
499}
500
502 // ASM: RoomDraw_DrawRightwards3x6 ($019B50) is RoomDraw_1x3N_rightwards with
503 // A=6 -> 6 columns x 3 rows, column-major.
504 Draw1x3NRightwards(ctx, /*columns=*/6, /*start_index=*/0);
505}
506
507void DrawUtility6x3(const DrawContext& ctx) {
508 // ASM: RoomDraw_Utility6x3 ($019A0C) is also 1x3N_rightwards with A=6.
509 Draw1x3NRightwards(ctx, /*columns=*/6, /*start_index=*/0);
510}
511
513 // ASM: RoomDraw_BigWallDecor ($019A06) calls
514 // RoomDraw_1x3N_rightwards with A=8. The encoded size is not consulted.
515 Draw1x3NRightwards(ctx, /*columns=*/8, /*start_index=*/0);
516}
517
518void DrawTableBowl(const DrawContext& ctx) {
519 // ASM: RoomDraw_TableBowl ($019D6C) loads A=2 and falls through to
520 // RoomDraw_WaterHoldingObject, which writes four tiles per row.
521 if (ctx.tiles.size() < 8)
522 return;
523
524 DrawRowMajor(ctx.target_bg, ctx.object.x_, ctx.object.y_, 4, 2, ctx.tiles);
525}
526
528 // ASM: RoomDraw_SmithyFurnace ($019A66) loads A=6 before jumping to
529 // RoomDraw_SomeBigDecors, which writes six tiles per row for eight rows.
530 if (ctx.tiles.size() < 48)
531 return;
532
533 DrawRowMajor(ctx.target_bg, ctx.object.x_, ctx.object.y_, 6, 8, ctx.tiles);
534}
535
536void DrawBigGrayRock(const DrawContext& ctx) {
537 // ASM: RoomDraw_BigGrayRock ($01B310) draws four 2x2 quadrants through
538 // RoomDraw_Rightwards2x2. Visually that is two 4x2 column-major bands:
539 // 0 2 4 6
540 // 1 3 5 7
541 // 8 10 12 14
542 // 9 11 13 15
543 // The liftable-rock bookkeeping in DrawBigGraySegment is runtime-only and
544 // intentionally not modeled by this visual draw routine.
545 if (ctx.tiles.size() < 16)
546 return;
547
548 DrawColumnMajor(ctx.target_bg, ctx.object.x_, ctx.object.y_, 4, 2, ctx.tiles,
549 /*start_index=*/0);
550 DrawColumnMajor(ctx.target_bg, ctx.object.x_, ctx.object.y_ + 2, 4, 2,
551 ctx.tiles, /*start_index=*/8);
552}
553
555 // ASM: RoomDraw_AgahnimsAltar ($019E30) reads six 14-tile columns from
556 // obj1B4A, duplicates the second column, then mirrors the source columns
557 // across a 14x14 destination. Columns 7..12 toggle the source H-flip with
558 // EOR #$4000; column 13 forces it with ORA #$4000.
559 if (ctx.tiles.size() < 84)
560 return;
561
562 constexpr std::array<int, 14> kSourceColumns = {
563 0, 1, 1, 2, 3, 4, 5, 5, 4, 3, 2, 1, 1, 0,
564 };
565 for (int y = 0; y < 14; ++y) {
566 for (int x = 0; x < 14; ++x) {
567 gfx::TileInfo tile = ctx.tiles[kSourceColumns[x] * 14 + y];
568 if (x >= 7 && x <= 12) {
570 } else if (x == 13) {
571 tile.horizontal_mirror_ = true;
572 }
574 ctx.object.y_ + y, tile);
575 }
576 }
577}
578
580 // ASM: RoomDraw_FortuneTellerRoom ($01A095) reads all 26 words from
581 // obj202E and writes 116 tiles into a sparse, fixed 14x14 BG1 footprint.
582 if (ctx.tiles.size() < 26)
583 return;
584
585 auto write = [&](int dx, int dy, size_t tile_index,
586 bool force_horizontal_mirror = false,
587 bool toggle_horizontal_mirror = false) {
588 gfx::TileInfo tile = ctx.tiles[tile_index];
589 if (force_horizontal_mirror) {
590 tile.horizontal_mirror_ = true;
591 } else if (toggle_horizontal_mirror) {
593 }
595 ctx.object.y_ + dy, tile);
596 };
597
598 // Six repeated two-column roof sections across rows 0..2.
599 for (int section = 0; section < 6; ++section) {
600 const int x = section * 2;
601 write(x + 1, 0, 0);
602 write(x + 2, 0, 0);
603 write(x + 1, 1, 0);
604 write(x + 2, 1, 0);
605 write(x + 1, 2, 1);
606 write(x + 2, 2, 1, /*force_horizontal_mirror=*/true);
607 }
608
609 // Three complete mirrored rows. The left/right wall tile advances through
610 // words 2..4 while the center tile advances through words 5..7.
611 for (int row = 0; row < 3; ++row) {
612 const size_t wall_tile = static_cast<size_t>(2 + row);
613 const size_t center_tile = static_cast<size_t>(5 + row);
614 const int y = 3 + row;
615
616 for (int x : {0, 2, 10, 12}) {
617 write(x, y, wall_tile);
618 }
619 for (int x : {1, 3, 11, 13}) {
620 write(x, y, wall_tile, /*force_horizontal_mirror=*/true);
621 }
622 for (int x : {4, 6, 8}) {
623 write(x, y, center_tile);
624 }
625 for (int x : {5, 7, 9}) {
626 write(x, y, center_tile, /*force_horizontal_mirror=*/true);
627 }
628 }
629
630 // Outer caps around the roof.
631 write(0, 0, 8);
632 write(0, 1, 8);
633 write(13, 0, 8, /*force_horizontal_mirror=*/true);
634 write(13, 1, 8, /*force_horizontal_mirror=*/true);
635 write(0, 2, 9);
636 write(13, 2, 9, /*force_horizontal_mirror=*/true);
637
638 // Four sparse rows at the bottom. Each source word is mirrored with EOR
639 // #$4000, so an existing H-flip must be toggled rather than forced on.
640 for (int row = 0; row < 4; ++row) {
641 const int y = 10 + row;
642 write(3, y, static_cast<size_t>(10 + row));
643 write(10, y, static_cast<size_t>(10 + row),
644 /*force_horizontal_mirror=*/false,
645 /*toggle_horizontal_mirror=*/true);
646 write(4, y, static_cast<size_t>(14 + row));
647 write(9, y, static_cast<size_t>(14 + row),
648 /*force_horizontal_mirror=*/false,
649 /*toggle_horizontal_mirror=*/true);
650 write(5, y, static_cast<size_t>(18 + row));
651 write(8, y, static_cast<size_t>(18 + row),
652 /*force_horizontal_mirror=*/false,
653 /*toggle_horizontal_mirror=*/true);
654 write(6, y, static_cast<size_t>(22 + row));
655 write(7, y, static_cast<size_t>(22 + row),
656 /*force_horizontal_mirror=*/false,
657 /*toggle_horizontal_mirror=*/true);
658 }
659}
660
662 // USDASM's final subtype-2 entry is object 0x13F (table index 0x3F), not a
663 // subtype-3 0x2xx object. RoomDraw_MagicBatAltar ($019A12) advances through
664 // eight source columns, reading seven words down each column from obj2086.
665 if (ctx.tiles.size() < 56)
666 return;
667
668 DrawColumnMajor(ctx.target_bg, ctx.object.x_, ctx.object.y_, 8, 7, ctx.tiles);
669}
670
671void DrawUtility3x5(const DrawContext& ctx) {
672 // ASM: RoomDraw_Utility3x5 ($01A194) uses:
673 // - top row: tiles 0..2
674 // - middle 3 rows: tiles 3..5 repeated per row
675 // - bottom row: tiles 6..8
676 if (ctx.tiles.empty())
677 return;
678
679 // Top row.
680 for (int x = 0; x < 3; ++x) {
682 ctx.target_bg, ctx.object.x_ + x, ctx.object.y_,
683 TileAtWrapped(ctx.tiles, static_cast<size_t>(x)));
684 }
685
686 // Middle rows (rows 1..3) reuse tiles 3..5.
687 for (int y = 1; y <= 3; ++y) {
688 for (int x = 0; x < 3; ++x) {
690 ctx.target_bg, ctx.object.x_ + x, ctx.object.y_ + y,
691 TileAtWrapped(ctx.tiles, static_cast<size_t>(3 + x)));
692 }
693 }
694
695 // Bottom row.
696 for (int x = 0; x < 3; ++x) {
698 ctx.target_bg, ctx.object.x_ + x, ctx.object.y_ + 4,
699 TileAtWrapped(ctx.tiles, static_cast<size_t>(6 + x)));
700 }
701}
702
704 // ASM: RoomDraw_VerticalTurtleRockPipe ($019A90)
705 // Two stacked 4x3 sections: first uses tiles 0..11, second 12..23.
706 if (ctx.tiles.empty())
707 return;
708 Draw1x3NRightwards(ctx, /*columns=*/4, /*start_index=*/0, /*y_offset=*/0);
709 Draw1x3NRightwards(ctx, /*columns=*/4, /*start_index=*/12, /*y_offset=*/3);
710}
711
713 // ASM: RoomDraw_HorizontalTurtleRockPipe ($019AA3) -> RoomDraw_Nx4 with A=6.
714 DrawNx4(ctx, /*columns=*/6, /*start_index=*/0);
715}
716
718 // ASM: RoomDraw_LightBeamOnFloor ($01A7B6)
719 // Draw three 4x4 blocks at y offsets 0, +2, +6. The middle block resets X
720 // to obj2376 and reuses tiles 0..15; the bottom block uses obj2396 at
721 // tiles 16..31.
722 if (ctx.tiles.size() < 32)
723 return;
724 Draw4x4ColumnMajor(ctx, /*x_offset=*/0, /*y_offset=*/0, /*start_index=*/0);
725 Draw4x4ColumnMajor(ctx, /*x_offset=*/0, /*y_offset=*/2, /*start_index=*/0);
726 Draw4x4ColumnMajor(ctx, /*x_offset=*/0, /*y_offset=*/6, /*start_index=*/16);
727}
728
730 // ASM: RoomDraw_BigLightBeamOnFloor ($01A7D3) reads the persisted
731 // bombed-floor flag of a fixed room (vanilla 0x065: $7EF0CA & $0100), then
732 // falls through to RoomDraw_FloorLight when it is active. Do not use
733 // ctx.room_id. Null state is reserved for object/geometry previews, which
734 // keep the beam visible and measurable.
735 if (ctx.state != nullptr &&
736 !ctx.state->IsFloorBombable(LightBeamFloorRoomId(ctx.rom))) {
737 return;
738 }
739 DrawFloorLightGrid(ctx);
740}
741
742void DrawFloorLight(const DrawContext& ctx) {
743 // ASM: RoomDraw_FloorLight ($01A7DC) is unconditional for object 0x274.
744 DrawFloorLightGrid(ctx);
745}
746
748 // ASM-mapped objects route through RoomDraw_4x4.
749 Draw4x4ColumnMajor(ctx, /*x_offset=*/0, /*y_offset=*/0, /*start_index=*/0);
750}
751
753 // USDASM RoomDraw_VitreousGooDamage ($01A809) invokes
754 // RoomDraw_A_Many32x32Blocks twice with A=5, moving the second call four
755 // tile rows down. The routine reads neither object size nor room state and
756 // writes through the currently selected layer's tilemap pointers.
757 if (ctx.tiles.size() < 8)
758 return;
759
760 for (int block_y = 0; block_y < 2; ++block_y) {
761 for (int block_x = 0; block_x < 5; ++block_x) {
762 DrawMany32x32Block(ctx.target_bg, ctx.object.x_ + block_x * 4,
763 ctx.object.y_ + block_y * 4, ctx.tiles);
764 }
765 }
766}
767
769 // ASM: RoomDraw_SolidWallDecor3x4 ($0199EC) -> RoomDraw_Nx4 with A=3.
770 DrawNx4(ctx, /*columns=*/3, /*start_index=*/0);
771}
772
774 // ASM: RoomDraw_ArcheryGameTargetDoor ($01A7A3)
775 // Two 3x3 sections stacked vertically.
776 if (ctx.tiles.empty())
777 return;
778 Draw1x3NRightwards(ctx, /*columns=*/3, /*start_index=*/0, /*y_offset=*/0);
779 Draw1x3NRightwards(ctx, /*columns=*/3, /*start_index=*/9, /*y_offset=*/3);
780}
781
783 // ASM: RoomDraw_GanonTriforceFloorDecor ($01A7F0)
784 // The top 4x4 (words 0..15) is at the anchor. The two bottom 4x4 blocks
785 // (words 16..31, PHX/PLX) are at $08+$01FC and $08+$0204: four rows down,
786 // two columns left and two columns right.
787 if (ctx.tiles.empty())
788 return;
789
790 Draw4x4ColumnMajor(ctx, /*x_offset=*/0, /*y_offset=*/0, /*start_index=*/0);
791 Draw4x4ColumnMajor(ctx, /*x_offset=*/-2, /*y_offset=*/4, /*start_index=*/16);
792 Draw4x4ColumnMajor(ctx, /*x_offset=*/2, /*y_offset=*/4, /*start_index=*/16);
793}
794
795void DrawSingle2x2(const DrawContext& ctx) {
796 // ASM: RoomDraw_Single2x2 ($019A8D) -> RoomDraw_Downwards2x2
797 // Single 2x2 in column-major order.
798 if (ctx.tiles.size() < 4)
799 return;
800
802 ctx.tiles[0]);
804 ctx.tiles[1]);
806 ctx.tiles[2]);
808 ctx.object.y_ + 1, ctx.tiles[3]);
809}
810
811void DrawSingle4x4(const DrawContext& ctx) {
812 // ASM: RoomDraw_4x4 ($0197ED), column-major 4x4 block (16 tiles).
813 if (ctx.tiles.size() < 16)
814 return;
815
816 int tid = 0;
817 for (int x = 0; x < 4; ++x) {
818 for (int y = 0; y < 4; ++y) {
820 ctx.object.y_ + y, ctx.tiles[tid++]);
821 }
822 }
823}
824
825void DrawSingle4x3(const DrawContext& ctx) {
826 // ASM: RoomDraw_TableRock4x3 ($0199E6), column-major 4x3 block (12 tiles).
827 if (ctx.tiles.size() < 12)
828 return;
829
830 int tid = 0;
831 for (int x = 0; x < 4; ++x) {
832 for (int y = 0; y < 3; ++y) {
834 ctx.object.y_ + y, ctx.tiles[tid++]);
835 }
836 }
837}
838
839void DrawRupeeFloor(const DrawContext& ctx) {
840 // USDASM RoomDraw_RupeeFloor ($01:9AA9-$01:9AED) exits when the
841 // current-room $0402 & $1000 event is set. State-free selector/geometry
842 // previews remain visible.
843 if ((ctx.state != nullptr && ctx.state->IsRupeeFloorCleared(ctx.room_id)) ||
844 ctx.tiles.size() < 2) {
845 return;
846 }
847
848 constexpr std::array<int, 3> kTopRows = {0, 3, 6};
849 constexpr std::array<int, 3> kBottomRows = {1, 4, 7};
850 for (int col = 0; col < 3; ++col) {
851 const int x = ctx.object.x_ + col * 2;
852 for (int row : kTopRows) {
854 ctx.tiles[0]);
855 }
856 for (int row : kBottomRows) {
858 ctx.tiles[1]);
859 }
860 }
861}
862
863void DrawActual4x4(const DrawContext& ctx) {
864 // ASM: RoomDraw_4x4 ($0197ED), used for true 4x4 tile8 objects.
865 DrawSingle4x4(ctx);
866}
867
868void DrawWaterfall47(const DrawContext& ctx) {
869 // ASM: RoomDraw_Waterfall47 ($019466)
870 // First 1x5 column from 0..4, middle columns from 5..9, final from 10..14.
871 if (ctx.tiles.empty())
872 return;
873
874 const int size = ctx.object.size_ & 0x0F;
875 const int middle_columns = (size + 1) * 2; // ASL $B2
876
877 Draw1x5Column(ctx, /*x_offset=*/0, /*start_index=*/0);
878 for (int i = 0; i < middle_columns; ++i) {
879 Draw1x5Column(ctx, /*x_offset=*/1 + i, /*start_index=*/5);
880 }
881 Draw1x5Column(ctx, /*x_offset=*/1 + middle_columns, /*start_index=*/10);
882}
883
884void DrawWaterfall48(const DrawContext& ctx) {
885 // ASM: RoomDraw_Waterfall48 ($019488)
886 // First 1x3 column from 0..2, middle columns from 3..5, final from 6..8.
887 if (ctx.tiles.empty())
888 return;
889
890 const int size = ctx.object.size_ & 0x0F;
891 const int middle_columns = (size + 1) * 2; // ASL $B2
892
893 DrawColumnMajor(ctx.target_bg, ctx.object.x_, ctx.object.y_, /*w=*/1, /*h=*/3,
894 ctx.tiles, /*start_index=*/0);
895 for (int i = 0; i < middle_columns; ++i) {
896 DrawColumnMajor(ctx.target_bg, ctx.object.x_ + 1 + i, ctx.object.y_,
897 /*w=*/1, /*h=*/3, ctx.tiles, /*start_index=*/3);
898 }
899 DrawColumnMajor(ctx.target_bg, ctx.object.x_ + 1 + middle_columns,
900 ctx.object.y_, /*w=*/1, /*h=*/3, ctx.tiles,
901 /*start_index=*/6);
902}
903
904// ============================================================================
905// SuperSquare Routines (Phase 4)
906// ============================================================================
907// A "SuperSquare" is a 16x16 tile area (4 rows of 4 tiles each).
908// These routines draw floor patterns that repeat in super square units.
909
911 // ASM: RoomDraw_4x4BlocksIn4x4SuperSquare ($018B94)
912 // Draws solid 4x4 blocks using a single tile, repeated in a grid pattern.
913 // Size determines number of super squares in X and Y directions.
914 int size_x = ((ctx.object.size_ >> 2) & 0x03) + 1;
915 int size_y = (ctx.object.size_ & 0x03) + 1;
916
917 LOG_DEBUG("DrawRoutines",
918 "Draw4x4BlocksIn4x4SuperSquare: obj=0x%03X pos=(%d,%d) size=0x%02X "
919 "tiles=%zu size_x=%d size_y=%d",
920 ctx.object.id_, ctx.object.x_, ctx.object.y_, ctx.object.size_,
921 ctx.tiles.size(), size_x, size_y);
922
923 if (ctx.tiles.empty()) {
924 LOG_DEBUG("DrawRoutines",
925 "Draw4x4BlocksIn4x4SuperSquare: SKIPPING - no tiles loaded!");
926 return;
927 }
928
929 // Use first tile for all blocks
930 const auto& tile = ctx.tiles[0];
931 LOG_DEBUG("DrawRoutines",
932 "Draw4x4BlocksIn4x4SuperSquare: tile[0] id=%d palette=%d", tile.id_,
933 tile.palette_);
934
935 for (int sy = 0; sy < size_y; ++sy) {
936 for (int sx = 0; sx < size_x; ++sx) {
937 // Each super square is 4x4 tiles
938 int base_x = ctx.object.x_ + (sx * 4);
939 int base_y = ctx.object.y_ + (sy * 4);
940
941 // Draw 4x4 block with same tile
942 for (int y = 0; y < 4; ++y) {
943 for (int x = 0; x < 4; ++x) {
944 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + y,
945 tile);
946 }
947 }
948 }
949 }
950}
951
953 // ASM: RoomDraw_3x3FloorIn4x4SuperSquare ($018D8A)
954 // Draws 3x3 floor patterns within super square units.
955 int size_x = ((ctx.object.size_ >> 2) & 0x03) + 1;
956 int size_y = (ctx.object.size_ & 0x03) + 1;
957
958 if (ctx.tiles.empty())
959 return;
960
961 const auto& tile = ctx.tiles[0];
962 for (int sy = 0; sy < size_y; ++sy) {
963 for (int sx = 0; sx < size_x; ++sx) {
964 int base_x = ctx.object.x_ + (sx * 3);
965 int base_y = ctx.object.y_ + (sy * 3);
966
967 for (int y = 0; y < 3; ++y) {
968 for (int x = 0; x < 3; ++x) {
969 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + y,
970 tile);
971 }
972 }
973 }
974 }
975}
976
978 // ASM: RoomDraw_4x4FloorIn4x4SuperSquare ($018FA5)
979 // Most common floor pattern - draws 4x4 floor tiles in super square units.
980 // Uses RoomDraw_A_Many32x32Blocks internally in assembly.
981 int size_x = ((ctx.object.size_ >> 2) & 0x03) + 1;
982 int size_y = (ctx.object.size_ & 0x03) + 1;
983
984 if (ctx.tiles.empty())
985 return;
986 if (ctx.tiles.size() < 8) {
987 // Keep abbreviated preview payloads visible with a solid fill. This
988 // fallback is editor behavior, not the vanilla eight-word floor stamp.
990 return;
991 }
992
993 for (int sy = 0; sy < size_y; ++sy) {
994 for (int sx = 0; sx < size_x; ++sx) {
995 int base_x = ctx.object.x_ + (sx * 4);
996 int base_y = ctx.object.y_ + (sy * 4);
997
998 DrawMany32x32Block(ctx.target_bg, base_x, base_y, ctx.tiles);
999 }
1000 }
1001}
1002
1004 // $018FA2 loads $046A then falls into $018FA5. ObjectDrawer resolves the
1005 // room's Floor 1 pattern into ctx.tiles before dispatching this wrapper.
1007}
1008
1010 // $018F9D loads $0490 then branches to $018FA5. ObjectDrawer resolves the
1011 // room's Floor 2 pattern into ctx.tiles before dispatching this wrapper.
1013}
1014
1016 // ASM: Object 0xA4 - Big hole pattern
1017 // Draws a rectangular hole with border tiles using Size as the expansion.
1018 int size = ctx.object.size_ & 0x0F;
1019
1020 if (ctx.tiles.size() < 24)
1021 return;
1022
1023 int base_x = ctx.object.x_;
1024 int base_y = ctx.object.y_;
1025 int max = size + 3; // Bottom/right edge offset
1026
1027 // Corners
1028 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x, base_y, ctx.tiles[8]);
1029 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + max, base_y,
1030 ctx.tiles[14]);
1031 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x, base_y + max,
1032 ctx.tiles[17]);
1033 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + max, base_y + max,
1034 ctx.tiles[23]);
1035
1036 // Edges and interior
1037 for (int xx = 1; xx < max; ++xx) {
1038 for (int yy = 1; yy < max; ++yy) {
1039 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + xx, base_y + yy,
1040 ctx.tiles[0]);
1041 }
1042 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + xx, base_y,
1043 ctx.tiles[10]);
1044 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + xx, base_y + max,
1045 ctx.tiles[19]);
1046 }
1047
1048 for (int yy = 1; yy < max; ++yy) {
1049 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x, base_y + yy,
1050 ctx.tiles[9]);
1051 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + max, base_y + yy,
1052 ctx.tiles[15]);
1053 }
1054}
1055
1057 // ASM: RoomDraw_Spike2x2In4x4SuperSquare ($019708)
1058 // Draws 2x2 spike patterns in a tiled grid
1059 int size_x = ((ctx.object.size_ >> 2) & 0x03) + 1;
1060 int size_y = (ctx.object.size_ & 0x03) + 1;
1061
1062 if (ctx.tiles.size() < 4)
1063 return;
1064
1065 for (int sy = 0; sy < size_y; ++sy) {
1066 for (int sx = 0; sx < size_x; ++sx) {
1067 int base_x = ctx.object.x_ + (sx * 2);
1068 int base_y = ctx.object.y_ + (sy * 2);
1069
1070 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x, base_y, ctx.tiles[0]);
1071 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 1, base_y,
1072 ctx.tiles[2]);
1073 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x, base_y + 1,
1074 ctx.tiles[1]);
1075 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 1, base_y + 1,
1076 ctx.tiles[3]);
1077 }
1078 }
1079}
1080
1082 // USDASM RoomDraw_TableRock4x4_1to16 ($01:93DC), object 0xDD. The payload
1083 // is four row-major rows of [left, middle A, middle B, right]. Each drawn
1084 // row is left, (A, B) repeated size_x+1 times, right. Rows: payload row 0
1085 // once, payload row 1 2*size_y+1 times, then rows 2 and 3 once each (the
1086 // final JSR falls through .draw_rock_segment_with_advance a second time).
1087 const int size_x = (ctx.object.size_ >> 2) & 0x03;
1088 const int size_y = ctx.object.size_ & 0x03;
1089 if (ctx.tiles.size() < 16) {
1090 return;
1091 }
1092
1093 auto draw_row = [&](int y, int payload_row) {
1094 const int first = payload_row * 4;
1096 ctx.tiles[first]);
1097 for (int pair = 0; pair <= size_x; ++pair) {
1098 const int x = ctx.object.x_ + 1 + pair * 2;
1099 DrawRoutineUtils::WriteTile8(ctx.target_bg, x, y, ctx.tiles[first + 1]);
1101 ctx.tiles[first + 2]);
1102 }
1103 DrawRoutineUtils::WriteTile8(ctx.target_bg, ctx.object.x_ + 3 + size_x * 2,
1104 y, ctx.tiles[first + 3]);
1105 };
1106
1107 int y = ctx.object.y_;
1108 draw_row(y++, 0);
1109 for (int row = 0; row < 2 * size_y + 1; ++row) {
1110 draw_row(y++, 1);
1111 }
1112 draw_row(y++, 2);
1113 draw_row(y, 3);
1114}
1115
1117 // ASM: RoomDraw_WaterOverlayA8x8_1to16 ($0195D6) / RoomDraw_WaterOverlayB8x8
1118 // These are stateful routines: they configure HDMA geometry and also stamp
1119 // state-dependent tile patterns. The editor currently models one visible
1120 // stamp from the available object payload; runtime water state and layer-mode
1121 // side effects still require independent emulator coverage.
1122
1123 int size_x = ((ctx.object.size_ >> 2) & 0x03);
1124 int size_y = (ctx.object.size_ & 0x03);
1125
1126 int count_x = size_x + 2;
1127 int count_y = size_y + 2;
1128
1129 if (ctx.tiles.empty())
1130 return;
1131 if (ctx.tiles.size() < 8) {
1132 // Fallback for abbreviated tile payloads: still stamp a visible overlay.
1133 for (int yy = 0; yy < count_y; ++yy) {
1134 for (int xx = 0; xx < count_x; ++xx) {
1135 int base_x = ctx.object.x_ + (xx * 4);
1136 int base_y = ctx.object.y_ + (yy * 4);
1137 const auto& tile =
1138 ctx.tiles[static_cast<size_t>((xx + yy) % ctx.tiles.size())];
1139 for (int y = 0; y < 4; ++y) {
1140 for (int x = 0; x < 4; ++x) {
1141 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + y,
1142 tile);
1143 }
1144 }
1145 }
1146 }
1147 return;
1148 }
1149
1150 if (ctx.object.id_ == 0xD8 && ctx.state != nullptr && ctx.rom != nullptr &&
1151 ctx.secondary_bg != nullptr &&
1152 ctx.state->IsDamFloodgateOpen(ctx.room_id)) {
1153 // RoomDraw_WaterOverlayA8x8_1to16 with the room's 's' bit set skips
1154 // RoomDraw_NoWater and writes one 4x4 of obj1438 (row-major) to $7E2000
1155 // (yaze BG1) at (x + 2*(size_x+1), y + 2*(size_y+1)).
1156 constexpr int kWaterOnTiles = 0x1438;
1157 const auto tiles = LoadRoomDrawObjectTiles(ctx, kWaterOnTiles, 16);
1158 const int base_x = ctx.object.x_ + (2 * (size_x + 1));
1159 const int base_y = ctx.object.y_ + (2 * (size_y + 1));
1160 for (int row = 0; row < 4; ++row) {
1161 for (int col = 0; col < 4; ++col) {
1162 DrawRoutineUtils::WriteTile8(*ctx.secondary_bg, base_x + col,
1163 base_y + row, tiles[(row * 4) + col]);
1164 }
1165 }
1166 return;
1167 }
1168
1169 if (ctx.object.id_ == 0xDA) {
1170 // RoomDraw_WaterOverlayB8x8_1to16: WaterOverlayObjectCount gives
1171 // 2*size_y+3 two-row chunks (3, 5, 7 or 9), so the overlay is 4*size_y+6
1172 // rows tall, not 4*(size_y+2). Both the drained and filled branches reach
1173 // the same drawing loop. Confirmed against game tilemaps of rooms 0x035
1174 // and 0x037.
1175 const int chunks = 2 * size_y + 3;
1176 for (int chunk = 0; chunk < chunks; ++chunk) {
1177 for (int xx = 0; xx < count_x; ++xx) {
1178 const int base_x = ctx.object.x_ + (xx * 4);
1179 const int base_y = ctx.object.y_ + (chunk * 2);
1180 for (int x = 0; x < 4; ++x) {
1181 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y,
1182 ctx.tiles[x]);
1183 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 1,
1184 ctx.tiles[4 + x]);
1185 }
1186 }
1187 }
1188 return;
1189 }
1190
1191 for (int yy = 0; yy < count_y; ++yy) {
1192 for (int xx = 0; xx < count_x; ++xx) {
1193 int base_x = ctx.object.x_ + (xx * 4);
1194 int base_y = ctx.object.y_ + (yy * 4);
1195
1196 for (int x = 0; x < 4; ++x) {
1197 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y,
1198 ctx.tiles[x]);
1199 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 2,
1200 ctx.tiles[x]);
1201 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 1,
1202 ctx.tiles[4 + x]);
1203 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 3,
1204 ctx.tiles[4 + x]);
1205 }
1206 }
1207 }
1208}
1209
1210// ============================================================================
1211// Stair Routines
1212// ============================================================================
1213
1215 // ASM: RoomDraw_InterRoomFatStairsUp ($01A41B)
1216 // Uses RoomDraw_4x4, which consumes a 4x4 tile block in COLUMN-MAJOR order.
1217 // In original game, registers position in $06B0 for transition handling
1218 // For editor display, we just draw the visual representation
1219
1220 if (ctx.tiles.size() < 16)
1221 return;
1222
1223 Draw4x4ColumnMajorTo(ctx.target_bg, ctx.object, ctx.tiles);
1224}
1225
1227 // ASM: RoomDraw_InterRoomFatStairsDownA ($01A458)
1228 // Uses tile data at obj10A8
1229 DrawInterRoomFatStairsUp(ctx); // Same visual structure
1230}
1231
1233 // ASM: RoomDraw_InterRoomFatStairsDownB ($01A486)
1234 // Uses tile data at obj10A8
1235 DrawInterRoomFatStairsUp(ctx); // Same visual structure
1236}
1237
1238void DrawSpiralStairs(const DrawContext& ctx, bool going_up, bool is_upper) {
1239 // ASM: RoomDraw_SpiralStairsGoingUpUpper, etc.
1240 // Calls RoomDraw_1x3N_rightwards with A=4 -> 4 columns x 3 rows = 12 tiles
1241 // Tile order is COLUMN-MAJOR (down first, then right).
1242 // The active object-list pointer chooses the raster target. Upper/lower only
1243 // selects transition metadata and the fixed tilemap that receives the two
1244 // priority-bit flank mutations; ObjectDrawer applies those after this draw.
1245 (void)going_up;
1246 (void)is_upper;
1247
1248 if (ctx.tiles.size() < 12)
1249 return;
1250
1251 DrawColumnMajor(ctx.target_bg, ctx.object.x_, ctx.object.y_, 4, 3, ctx.tiles);
1252}
1253
1254void DrawAutoStairs(const DrawContext& ctx) {
1255 // ASM: RoomDraw_AutoStairs* routines
1256 // The multi-layer variants stamp both BG tilemaps; merged/single-layer
1257 // variants stay on the active target layer.
1258 if (ctx.tiles.size() < 16)
1259 return;
1260
1261 Draw4x4ColumnMajorTo(ctx.target_bg, ctx.object, ctx.tiles);
1262
1263 if (ctx.secondary_bg != nullptr &&
1265 Draw4x4ColumnMajorTo(*ctx.secondary_bg, ctx.object, ctx.tiles);
1266 }
1267}
1268
1270 // ASM: RoomDraw_StraightInterroomStairs* routines
1271 // Upper variants render on BG1 only. Lower variants render the 4x4 block on
1272 // BG2 and expose the front edge on BG1 as well.
1273 if (ctx.tiles.size() < 16)
1274 return;
1275
1277 ctx.object.id_) ||
1278 ctx.secondary_bg == nullptr ||
1280 Draw4x4ColumnMajorTo(ctx.target_bg, ctx.object, ctx.tiles);
1281 return;
1282 }
1283
1284 const bool south_lower =
1286 ctx.object.id_);
1287 size_t tile_index = 0;
1288 for (int col = 0; col < 4; ++col) {
1289 for (int row = 0; row < 4; ++row) {
1290 const auto& tile = TileAtWrapped(ctx.tiles, tile_index++);
1292 ctx.object.y_ + row, tile);
1293 if ((!south_lower && row == 0) || (south_lower && row == 3)) {
1295 ctx.object.y_ + row, tile);
1296 }
1297 }
1298 }
1299}
1300
1301// ============================================================================
1302// Interactive Object Routines
1303// ============================================================================
1304
1305void DrawPrisonCell(const DrawContext& ctx) {
1306 // ASM: RoomDraw_PrisonCell ($019C44)
1307 // The routine selects the current object-list tilemap through $BF, then
1308 // writes one sparse 16x4 cell. Offsets below are the tile-coordinate form of
1309 // the long writes at $019C5A-$019CC5. In particular, columns 7 and 8 remain
1310 // open on rows 1-3 so objects drawn earlier (room 0x080's big-key lock) show
1311 // through the cell opening.
1312 if (ctx.tiles.size() < 6) {
1313 return;
1314 }
1315
1316 const int base_x = ctx.object.x_;
1317 const int base_y = ctx.object.y_;
1318
1319 auto with_horizontal_mirror = [](gfx::TileInfo tile) {
1320 // USDASM uses ORA #$4000 for these writes: force H-flip on rather than
1321 // toggling whatever bit a modified ROM supplied in obj1488.
1322 tile.horizontal_mirror_ = true;
1323 return tile;
1324 };
1325
1326 for (int col = 0; col < 5; ++col) {
1327 const int left_x = base_x + 2 + col;
1328 const int right_x = base_x + 9 + col;
1329 DrawRoutineUtils::WriteTile8(ctx.target_bg, left_x, base_y, ctx.tiles[1]);
1330 DrawRoutineUtils::WriteTile8(ctx.target_bg, right_x, base_y, ctx.tiles[1]);
1331 DrawRoutineUtils::WriteTile8(ctx.target_bg, left_x, base_y + 1,
1332 ctx.tiles[2]);
1333 DrawRoutineUtils::WriteTile8(ctx.target_bg, right_x, base_y + 1,
1334 with_horizontal_mirror(ctx.tiles[2]));
1335 DrawRoutineUtils::WriteTile8(ctx.target_bg, left_x, base_y + 2,
1336 ctx.tiles[4]);
1337 DrawRoutineUtils::WriteTile8(ctx.target_bg, right_x, base_y + 2,
1338 with_horizontal_mirror(ctx.tiles[4]));
1339 DrawRoutineUtils::WriteTile8(ctx.target_bg, left_x, base_y + 3,
1340 ctx.tiles[5]);
1341 DrawRoutineUtils::WriteTile8(ctx.target_bg, right_x, base_y + 3,
1342 with_horizontal_mirror(ctx.tiles[5]));
1343 }
1344
1345 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x, base_y, ctx.tiles[0]);
1346 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 15, base_y,
1347 with_horizontal_mirror(ctx.tiles[0]));
1348 for (int x_offset : {1, 7, 8, 14}) {
1349 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x_offset, base_y,
1350 ctx.tiles[1]);
1351 }
1352 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 1, base_y + 2,
1353 ctx.tiles[3]);
1354 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 14, base_y + 2,
1355 with_horizontal_mirror(ctx.tiles[3]));
1356}
1357
1358void DrawBigKeyLock(const DrawContext& ctx) {
1359 // ASM: RoomDraw_BigKeyLock ($0198AE)
1360 // The opened-state room-event check and shared chest/lock slot advancement
1361 // live in ObjectDrawer, which owns stream ordering. The ROM routine's closed
1362 // branch jumps to RoomDraw_Rightwards2x2 and consumes obj1494 in column-major
1363 // order; there is no second tile state.
1364 DrawSingle2x2(ctx);
1365}
1366
1368 // USDASM RoomDraw_BombableFloor ($01:B3E1) draws four 2x2 quadrants.
1369 // ParseSubtype3 stores the intact obj0220 block at tiles[0..15] and the
1370 // non-contiguous obj05BA replacement block at tiles[16..31]. Each quadrant
1371 // is ordered upper-left, lower-left, upper-right, lower-right.
1372 if (ctx.tiles.size() < 16) {
1373 return;
1374 }
1375
1376 // Only the room named by the routine's CMP operand opens (vanilla 0x065);
1377 // every other room draws the intact floor even with its flag set.
1378 const bool is_open =
1379 ctx.state != nullptr && ctx.room_id == BombableFloorRoomId(ctx.rom) &&
1380 ctx.state->IsFloorBombable(ctx.room_id) && ctx.tiles.size() >= 32;
1381 const size_t state_offset = is_open ? 16 : 0;
1382
1383 for (int block_y = 0; block_y < 2; ++block_y) {
1384 for (int block_x = 0; block_x < 2; ++block_x) {
1385 const size_t block_offset =
1386 state_offset + static_cast<size_t>((block_y * 2 + block_x) * 4);
1387 for (int x = 0; x < 2; ++x) {
1388 for (int y = 0; y < 2; ++y) {
1390 ctx.target_bg, ctx.object.x_ + block_x * 2 + x,
1391 ctx.object.y_ + block_y * 2 + y,
1392 ctx.tiles[block_offset + static_cast<size_t>(x * 2 + y)]);
1393 }
1394 }
1395 }
1396 }
1397}
1398
1400 // USDASM RoomDraw_MovingWallWest ($01:9190): moved walls emit no tiles.
1401 if ((ctx.state != nullptr && ctx.state->IsWallMoved(ctx.room_id)) ||
1402 ctx.tiles.size() < 24) {
1403 return;
1404 }
1405
1406 const int direction = moving_wall::DirectionForSize(ctx.object.size_);
1407 const int column_count = moving_wall::ObjectCountForSize(ctx.object.size_);
1408 const int height = direction * 2 + 6;
1409
1410 // West writes the obj03D8 fill first, growing left from the object anchor,
1411 // then stamps the 3x3 corner and repeated 3x2 vertical wall at the anchor.
1412 DrawMovingWallFill(ctx, ctx.object.x_ - column_count, column_count, height);
1413 DrawMovingWallPlatform(ctx, direction);
1414}
1415
1417 // USDASM RoomDraw_MovingWallEast ($01:921C): moved walls emit no tiles.
1418 if ((ctx.state != nullptr && ctx.state->IsWallMoved(ctx.room_id)) ||
1419 ctx.tiles.size() < 24) {
1420 return;
1421 }
1422
1423 const int direction = moving_wall::DirectionForSize(ctx.object.size_);
1424 const int column_count = moving_wall::ObjectCountForSize(ctx.object.size_);
1425 const int height = direction * 2 + 6;
1426
1427 // East stamps the platform first, then grows the obj03D8 fill right from
1428 // the first column beyond the three-tile-wide platform.
1429 DrawMovingWallPlatform(ctx, direction);
1430 DrawMovingWallFill(ctx, ctx.object.x_ + 3, column_count, height);
1431}
1432
1433// ============================================================================
1434// Water Face Variants
1435// ============================================================================
1436
1437namespace {
1438constexpr int kWaterFaceWidthTiles = 4;
1439
1440void DrawWaterFaceRows(const DrawContext& ctx, int row_count, int tile_offset) {
1441 if (row_count <= 0 || tile_offset < 0 || ctx.tiles.empty()) {
1442 return;
1443 }
1444
1445 if (tile_offset >= static_cast<int>(ctx.tiles.size())) {
1446 return;
1447 }
1448
1449 const int available_tiles = static_cast<int>(ctx.tiles.size()) - tile_offset;
1450 const int available_rows = available_tiles / kWaterFaceWidthTiles;
1451 const int rows_to_draw = std::min(row_count, available_rows);
1452
1453 for (int row = 0; row < rows_to_draw; ++row) {
1454 const int row_base = tile_offset + (row * kWaterFaceWidthTiles);
1455 for (int col = 0; col < kWaterFaceWidthTiles; ++col) {
1457 ctx.object.y_ + row,
1458 ctx.tiles[row_base + col]);
1459 }
1460 }
1461}
1462} // namespace
1463
1465 // ASM: RoomDraw_EmptyWaterFace ($019D29)
1466 //
1467 // usdasm behavior:
1468 // - Base state draws a 4x3 face using data at offset 0x1614.
1469 // - "Water active" branch draws a 4x5 variant using data at offset 0x162C.
1470 //
1471 // IMPORTANT: this uses dedicated water-face state, not door state. Tying
1472 // this branch to IsDoorOpen created cross-feature rendering regressions.
1473 // RoomDraw_EmptyWaterFace only switches to the spitting face for tag2
1474 // 0x1B (room word bit $0100) or 0x19 ('s' bit); any other room keeps the
1475 // empty face. Previews without a room (room_tag2 < 0) follow the state.
1476 const bool tag_allows_water =
1477 ctx.room_tag2 < 0 || ctx.room_tag2 == 0x1B || ctx.room_tag2 == 0x19;
1478 const bool water_active = tag_allows_water && (ctx.state != nullptr) &&
1480
1481 const int row_count = water_active ? 5 : 3;
1482 const int tile_offset = water_active ? 12 : 0; // 0x162C - 0x1614 = 24 bytes
1483 DrawWaterFaceRows(ctx, row_count, tile_offset);
1484}
1485
1487 // ASM: RoomDraw_SpittingWaterFace ($019D64)
1488 // Draws a 4x5 face/spout shape.
1489 DrawWaterFaceRows(ctx, /*row_count=*/5, /*tile_offset=*/0);
1490}
1491
1493 // ASM: RoomDraw_DrenchingWaterFace ($019D83)
1494 // Draws a 4x7 continuous stream.
1495 DrawWaterFaceRows(ctx, /*row_count=*/7, /*tile_offset=*/0);
1496}
1497
1498// ============================================================================
1499// Chest Platform Multi-Part Routines
1500// ============================================================================
1501
1503 // ASM: RoomDraw_ClosedChestPlatform ($018CC7)
1504 // Size fields are subtype-1 packed 2-bit values: $B2=size_x, $B4=size_y.
1505 // The routine expands them to width_blocks=size_x+4 and
1506 // height_blocks=size_y+1 before drawing top, center, bottom, and the center
1507 // 2x2 overlay.
1508
1509 if (ctx.tiles.size() < 68)
1510 return;
1511
1512 const int size_x = (ctx.object.size_ >> 2) & 0x03;
1513 const int size_y = ctx.object.size_ & 0x03;
1514 const int width_blocks = size_x + 4;
1515 const int height_blocks = size_y + 1;
1516
1517 // Top 3 rows: left cap, repeated 2-column middle, right cap.
1518 DrawPlatform1x3Rightwards(ctx, /*dx=*/0, /*dy=*/0, /*start_index=*/0,
1519 /*columns=*/3);
1520 for (int x = 0; x < width_blocks; ++x) {
1521 DrawPlatform1x3Rightwards(ctx, /*dx=*/3 + x * 2, /*dy=*/0,
1522 /*start_index=*/9, /*columns=*/2);
1523 }
1524 DrawPlatform1x3Rightwards(ctx, /*dx=*/3 + width_blocks * 2, /*dy=*/0,
1525 /*start_index=*/15, /*columns=*/3);
1526
1527 // Center rows: left wall, repeated 2x2 carpet, right wall.
1528 for (int y = 0; y < height_blocks; ++y) {
1529 const int dy = 3 + y * 2;
1530 DrawPlatform3x2(ctx, /*dx=*/0, dy, /*start_index=*/24);
1531 for (int x = 0; x < width_blocks; ++x) {
1532 DrawPlatform2x2(ctx, /*dx=*/3 + x * 2, dy, /*start_index=*/30);
1533 }
1534 DrawPlatform3x2(ctx, /*dx=*/3 + width_blocks * 2, dy,
1535 /*start_index=*/34);
1536 }
1537
1538 // Bottom 3 rows.
1539 const int bottom_y = 3 + height_blocks * 2;
1540 DrawPlatform1x3Rightwards(ctx, /*dx=*/0, bottom_y, /*start_index=*/40,
1541 /*columns=*/3);
1542 for (int x = 0; x < width_blocks; ++x) {
1543 DrawPlatform1x3Rightwards(ctx, /*dx=*/3 + x * 2, bottom_y,
1544 /*start_index=*/49, /*columns=*/2);
1545 }
1546 DrawPlatform1x3Rightwards(ctx, /*dx=*/3 + width_blocks * 2, bottom_y,
1547 /*start_index=*/55, /*columns=*/3);
1548
1549 // Center 2x2 overlay, matching the final SrcPtr(0x590) draw in the native
1550 // C++ port and ZScream's tile indices 64,66/65,67.
1551 DrawPlatform2x2(ctx, /*dx=*/width_blocks + 2,
1552 /*dy=*/bottom_y - (height_blocks + 1),
1553 /*start_index=*/64);
1554}
1555
1557 // ASM: RoomDraw_OpenChestPlatform ($019733). The helper draws one row of a
1558 // 10+2*size_x tile platform, repeated (2*size_y+5) times, then two closing
1559 // rows from src+1/src+2.
1560 if (ctx.tiles.size() < 21)
1561 return;
1562
1563 const int size_x = (ctx.object.size_ >> 2) & 0x03;
1564 const int size_y = ctx.object.size_ & 0x03;
1565 const int fill_width = size_x + 1;
1566 const int body_rows = size_y * 2 + 5;
1567
1568 for (int row = 0; row < body_rows; ++row) {
1569 DrawOpenChestPlatformSegment(ctx, row, /*start_index=*/0, fill_width);
1570 }
1571 DrawOpenChestPlatformSegment(ctx, body_rows, /*start_index=*/1, fill_width);
1572 DrawOpenChestPlatformSegment(ctx, body_rows + 1, /*start_index=*/2,
1573 fill_width);
1574}
1575
1577 // ASM: RoomDraw_ChestPlatformHorizontalWallWithCorners ($018D0D)
1578 int width = (ctx.object.size_ & 0x0F) + 1;
1579
1580 if (ctx.tiles.size() < 3)
1581 return;
1582
1583 for (int x = 0; x < width; ++x) {
1584 size_t tile_idx = (x == 0) ? 0 : ((x == width - 1) ? 2 : 1);
1586 ctx.object.y_, ctx.tiles[tile_idx]);
1587 }
1588}
1589
1591 // ASM: RoomDraw_ChestPlatformVerticalWall ($019E70)
1592 int height = (ctx.object.size_ & 0x0F) + 1;
1593
1594 if (ctx.tiles.empty())
1595 return;
1596
1597 for (int y = 0; y < height; ++y) {
1599 ctx.object.y_ + y, ctx.tiles[0]);
1600 }
1601}
1602
1603void RegisterSpecialRoutines(std::vector<DrawRoutineInfo>& registry) {
1604 // Note: Routine IDs are assigned based on the assembly routine table
1605 // These special routines handle chests, doors, and other non-standard objects
1606
1607 // Chest routine - uses a wrapper since it needs chest_index
1608 registry.push_back(DrawRoutineInfo{
1609 .id = 39, // DrawChest (special index)
1610 .name = "Chest",
1611 .function =
1612 [](const DrawContext& ctx) {
1613 // Default chest index 0 - actual index tracked externally
1614 DrawChest(ctx, 0);
1615 },
1616 .draws_to_both_bgs = false,
1617 .base_width = 2,
1618 .base_height = 2,
1619 .min_tiles = 4, // 2x2 block
1621 });
1622
1623 registry.push_back(DrawRoutineInfo{
1624 .id = 38, // DrawNothing
1625 .name = "Nothing",
1626 .function = DrawNothing,
1627 .draws_to_both_bgs = false,
1628 .base_width = 0,
1629 .base_height = 0,
1631 });
1632
1633 registry.push_back(DrawRoutineInfo{
1634 .id = 26, // DrawDoorSwitcherer
1635 .name = "DoorSwitcherer",
1636 .function = DrawDoorSwitcherer,
1637 .draws_to_both_bgs = false,
1638 .base_width = 1,
1639 .base_height = 1,
1640 .min_tiles = 1, // at least one tile for door graphic
1642 });
1643
1644 registry.push_back(DrawRoutineInfo{
1645 .id = 33, // DrawSomariaLine
1646 .name = "SomariaLine",
1647 .function = DrawSomariaLine,
1648 .draws_to_both_bgs = false,
1649 .base_width = 1,
1650 .base_height = 1,
1651 .min_tiles = 1,
1653 });
1654
1655 registry.push_back(DrawRoutineInfo{
1656 .id = 34, // DrawWaterFace
1657 .name = "WaterFace",
1658 .function = DrawWaterFace,
1659 .draws_to_both_bgs = false,
1660 .base_width = 2,
1661 .base_height = 2,
1662 .min_tiles = 4, // 2x2 block
1664 });
1665
1666 // ============================================================================
1667 // SuperSquare Routines (Phase 4) - IDs 56-64
1668 // ============================================================================
1669
1670 registry.push_back(DrawRoutineInfo{
1671 .id = 56, // Draw4x4BlocksIn4x4SuperSquare
1672 .name = "4x4BlocksIn4x4SuperSquare",
1674 .draws_to_both_bgs = false,
1675 .base_width = 0, // Variable: width = (((size >> 2) & 3) + 1) * 4
1676 .base_height = 0, // Variable: height = ((size & 3) + 1) * 4
1678 });
1679
1680 registry.push_back(DrawRoutineInfo{
1681 .id = 57, // Draw3x3FloorIn4x4SuperSquare
1682 .name = "3x3FloorIn4x4SuperSquare",
1683 .function = Draw3x3FloorIn4x4SuperSquare,
1684 .draws_to_both_bgs = false,
1685 .base_width = 0, // Variable
1686 .base_height = 0, // Variable
1688 });
1689
1690 registry.push_back(DrawRoutineInfo{
1691 .id = 58, // Draw4x4FloorIn4x4SuperSquare
1692 .name = "4x4FloorIn4x4SuperSquare",
1693 .function = Draw4x4FloorIn4x4SuperSquare,
1694 .draws_to_both_bgs = false,
1695 .base_width = 0, // Variable
1696 .base_height = 0, // Variable
1698 });
1699
1700 registry.push_back(DrawRoutineInfo{
1701 .id = 59, // Draw4x4FloorOneIn4x4SuperSquare
1702 .name = "4x4FloorOneIn4x4SuperSquare",
1704 .draws_to_both_bgs = false,
1705 .base_width = 0, // Variable
1706 .base_height = 0, // Variable
1708 });
1709
1710 registry.push_back(DrawRoutineInfo{
1711 .id = 60, // Draw4x4FloorTwoIn4x4SuperSquare
1712 .name = "4x4FloorTwoIn4x4SuperSquare",
1714 .draws_to_both_bgs = false,
1715 .base_width = 0, // Variable
1716 .base_height = 0, // Variable
1718 });
1719
1720 registry.push_back(DrawRoutineInfo{
1721 .id = 61, // DrawBigHole4x4_1to16
1722 .name = "BigHole4x4_1to16",
1723 .function = DrawBigHole4x4_1to16,
1724 .draws_to_both_bgs = false,
1725 .base_width = 0, // Variable
1726 .base_height = 0, // Variable
1728 });
1729
1730 registry.push_back(DrawRoutineInfo{
1731 .id = 62, // DrawSpike2x2In4x4SuperSquare
1732 .name = "Spike2x2In4x4SuperSquare",
1733 .function = DrawSpike2x2In4x4SuperSquare,
1734 .draws_to_both_bgs = false,
1735 .base_width = 0, // Variable
1736 .base_height = 0, // Variable
1738 });
1739
1740 registry.push_back(DrawRoutineInfo{
1741 .id = 63, // DrawTableRock4x4_1to16
1742 .name = "TableRock4x4_1to16",
1743 .function = DrawTableRock4x4_1to16,
1744 .draws_to_both_bgs = false,
1745 .base_width = 0, // Variable
1746 .base_height = 0, // Variable
1748 });
1749
1750 registry.push_back(DrawRoutineInfo{
1751 .id = 64, // DrawWaterOverlay8x8_1to16
1752 .name = "WaterOverlay8x8_1to16",
1753 .function = DrawWaterOverlay8x8_1to16,
1754 .draws_to_both_bgs = false,
1755 .base_width = 0, // Variable
1756 .base_height = 0, // Variable
1758 });
1759
1760 // Stair routines (IDs 83-88)
1761 registry.push_back(DrawRoutineInfo{
1762 .id = 83, // DrawInterRoomFatStairsUp
1763 .name = "InterRoomFatStairsUp",
1764 .function = DrawInterRoomFatStairsUp,
1765 .draws_to_both_bgs = false,
1766 .base_width = 4,
1767 .base_height = 4,
1768 .min_tiles = 16, // 4x4 stair pattern
1770 });
1771
1772 registry.push_back(DrawRoutineInfo{
1773 .id = 84, // DrawInterRoomFatStairsDownA
1774 .name = "InterRoomFatStairsDownA",
1775 .function = DrawInterRoomFatStairsDownA,
1776 .draws_to_both_bgs = false,
1777 .base_width = 4,
1778 .base_height = 4,
1779 .min_tiles = 16, // 4x4 stair pattern
1781 });
1782
1783 registry.push_back(DrawRoutineInfo{
1784 .id = 85, // DrawInterRoomFatStairsDownB
1785 .name = "InterRoomFatStairsDownB",
1786 .function = DrawInterRoomFatStairsDownB,
1787 .draws_to_both_bgs = false,
1788 .base_width = 4,
1789 .base_height = 4,
1790 .min_tiles = 16, // 4x4 stair pattern
1792 });
1793
1794 registry.push_back(DrawRoutineInfo{
1795 .id = 86, // DrawAutoStairs
1796 .name = "AutoStairs",
1797 .function = DrawAutoStairs,
1798 .draws_to_both_bgs = false,
1799 .base_width = 4,
1800 .base_height = 4,
1801 .min_tiles = 16, // 4x4 stair pattern
1803 });
1804
1805 registry.push_back(DrawRoutineInfo{
1806 .id = 87, // DrawStraightInterRoomStairs
1807 .name = "StraightInterRoomStairs",
1808 .function = DrawStraightInterRoomStairs,
1809 .draws_to_both_bgs = false,
1810 .base_width = 4,
1811 .base_height = 4,
1812 .min_tiles = 16, // 4x4 stair pattern
1814 });
1815
1816 // Spiral stairs variants (IDs 88-91)
1817 registry.push_back(DrawRoutineInfo{
1818 .id = 88, // DrawSpiralStairsGoingUpUpper
1819 .name = "SpiralStairsGoingUpUpper",
1820 .function =
1821 [](const DrawContext& ctx) { DrawSpiralStairs(ctx, true, true); },
1822 .draws_to_both_bgs = false,
1823 .base_width = 4,
1824 .base_height = 3, // 4x3 pattern
1825 .min_tiles = 12, // 4x3 block
1827 });
1828
1829 registry.push_back(DrawRoutineInfo{
1830 .id = 89, // DrawSpiralStairsGoingDownUpper
1831 .name = "SpiralStairsGoingDownUpper",
1832 .function =
1833 [](const DrawContext& ctx) { DrawSpiralStairs(ctx, false, true); },
1834 .draws_to_both_bgs = false,
1835 .base_width = 4,
1836 .base_height = 3, // 4x3 pattern
1837 .min_tiles = 12, // 4x3 block
1839 });
1840
1841 registry.push_back(DrawRoutineInfo{
1842 .id = 90, // DrawSpiralStairsGoingUpLower
1843 .name = "SpiralStairsGoingUpLower",
1844 .function =
1845 [](const DrawContext& ctx) { DrawSpiralStairs(ctx, true, false); },
1846 .draws_to_both_bgs = false,
1847 .base_width = 4,
1848 .base_height = 3, // 4x3 pattern
1849 .min_tiles = 12, // 4x3 block
1851 });
1852
1853 registry.push_back(DrawRoutineInfo{
1854 .id = 91, // DrawSpiralStairsGoingDownLower
1855 .name = "SpiralStairsGoingDownLower",
1856 .function =
1857 [](const DrawContext& ctx) { DrawSpiralStairs(ctx, false, false); },
1858 .draws_to_both_bgs = false,
1859 .base_width = 4,
1860 .base_height = 3, // 4x3 pattern
1861 .min_tiles = 12, // 4x3 block
1863 });
1864
1865 registry.push_back(DrawRoutineInfo{
1867 .name = "WaterHopStairsA",
1868 .function = DrawWaterHopStairsA,
1869 .draws_to_both_bgs = false,
1870 .base_width = 4,
1871 .base_height = 2,
1872 .min_tiles = 8,
1874 });
1875
1876 registry.push_back(DrawRoutineInfo{
1878 .name = "WaterHopStairsB",
1879 .function = DrawWaterHopStairsB,
1880 .draws_to_both_bgs = true,
1881 .base_width = 4,
1882 .base_height = 2,
1883 .min_tiles = 8,
1885 });
1886
1887 registry.push_back(DrawRoutineInfo{
1889 .name = "DamFloodGate",
1890 .function = DrawDamFloodGate,
1891 .draws_to_both_bgs = false,
1892 .base_width = 10,
1893 .base_height = 4,
1894 .min_tiles = 40,
1896 });
1897
1898 // Interactive object routines (IDs 92-95)
1899 registry.push_back(DrawRoutineInfo{
1900 .id = 92, // DrawBigKeyLock
1901 .name = "BigKeyLock",
1902 .function = DrawBigKeyLock,
1903 .draws_to_both_bgs = false,
1904 .base_width = 2,
1905 .base_height = 2,
1906 .min_tiles = 4, // 2x2 block
1908 });
1909
1910 registry.push_back(DrawRoutineInfo{
1911 .id = 93, // DrawBombableFloor
1912 .name = "BombableFloor",
1913 .function = DrawBombableFloor,
1914 .draws_to_both_bgs = false,
1915 .base_width = 4,
1916 .base_height = 4,
1917 .min_tiles = 16, // One complete 4x4 state
1919 });
1920
1921 // Water face variants (IDs 94-96)
1922 registry.push_back(DrawRoutineInfo{
1923 .id = 94, // DrawEmptyWaterFace
1924 .name = "EmptyWaterFace",
1925 .function = DrawEmptyWaterFace,
1926 .draws_to_both_bgs = false,
1927 .base_width = 4,
1928 .base_height = 3,
1929 .min_tiles = 12, // base 4x3 variant
1931 });
1932
1933 registry.push_back(DrawRoutineInfo{
1934 .id = 95, // DrawSpittingWaterFace
1935 .name = "SpittingWaterFace",
1936 .function = DrawSpittingWaterFace,
1937 .draws_to_both_bgs = false,
1938 .base_width = 4,
1939 .base_height = 5,
1940 .min_tiles = 20, // 4x5 variant
1942 });
1943
1944 registry.push_back(DrawRoutineInfo{
1945 .id = 96, // DrawDrenchingWaterFace
1946 .name = "DrenchingWaterFace",
1947 .function = DrawDrenchingWaterFace,
1948 .draws_to_both_bgs = false,
1949 .base_width = 4,
1950 .base_height = 7,
1951 .min_tiles = 28, // 4x7 variant
1953 });
1954
1955 // Prison cell (Type 3 objects 0x20D, 0x217) writes only to the tilemap
1956 // selected by the current object stream ($BF in USDASM).
1957 registry.push_back(DrawRoutineInfo{
1958 .id = 97, // DrawPrisonCell
1959 .name = "PrisonCell",
1960 .function = DrawPrisonCell,
1961 .draws_to_both_bgs = false,
1962 .base_width = 16,
1963 .base_height = 4,
1964 .min_tiles = 6,
1966 });
1967
1968 registry.push_back(DrawRoutineInfo{
1969 .id = DrawRoutineIds::kBed4x5, // 98
1970 .name = "Bed4x5",
1971 .function = DrawBed4x5,
1972 .draws_to_both_bgs = false,
1973 .base_width = 4,
1974 .base_height = 5,
1976 });
1977
1978 registry.push_back(DrawRoutineInfo{
1980 .name = "Rightwards3x6",
1981 .function = DrawRightwards3x6,
1982 .draws_to_both_bgs = false,
1983 .base_width = 6,
1984 .base_height = 3,
1986 });
1987
1988 registry.push_back(DrawRoutineInfo{
1990 .name = "Utility6x3",
1991 .function = DrawUtility6x3,
1992 .draws_to_both_bgs = false,
1993 .base_width = 6,
1994 .base_height = 3,
1996 });
1997
1998 registry.push_back(DrawRoutineInfo{
2000 .name = "Utility3x5",
2001 .function = DrawUtility3x5,
2002 .draws_to_both_bgs = false,
2003 .base_width = 3,
2004 .base_height = 5,
2006 });
2007
2008 registry.push_back(DrawRoutineInfo{
2010 .name = "VerticalTurtleRockPipe",
2011 .function = DrawVerticalTurtleRockPipe,
2012 .draws_to_both_bgs = false,
2013 .base_width = 4,
2014 .base_height = 6,
2016 });
2017
2018 registry.push_back(DrawRoutineInfo{
2020 .name = "HorizontalTurtleRockPipe",
2021 .function = DrawHorizontalTurtleRockPipe,
2022 .draws_to_both_bgs = false,
2023 .base_width = 6,
2024 .base_height = 4,
2026 });
2027
2028 registry.push_back(DrawRoutineInfo{
2030 .name = "LightBeam",
2031 .function = DrawLightBeamOnFloor,
2032 .draws_to_both_bgs = false,
2033 .base_width = 4,
2034 .base_height = 10,
2035 .min_tiles = 32,
2037 });
2038
2039 registry.push_back(DrawRoutineInfo{
2041 .name = "BigLightBeam",
2042 .function = DrawBigLightBeamOnFloor,
2043 .draws_to_both_bgs = false,
2044 .base_width = 8,
2045 .base_height = 8,
2046 .min_tiles = 64,
2048 });
2049
2050 registry.push_back(DrawRoutineInfo{
2052 .name = "FloorLight",
2053 .function = DrawFloorLight,
2054 .draws_to_both_bgs = false,
2055 .base_width = 8,
2056 .base_height = 8,
2057 .min_tiles = 64,
2059 });
2060
2061 registry.push_back(DrawRoutineInfo{
2063 .name = "BossShell4x4",
2064 .function = DrawBossShell4x4,
2065 .draws_to_both_bgs = false,
2066 .base_width = 4,
2067 .base_height = 4,
2069 });
2070
2071 registry.push_back(DrawRoutineInfo{
2073 .name = "VitreousGooDamage",
2074 .function = DrawVitreousGooDamage,
2075 .draws_to_both_bgs = false,
2076 .base_width = 20,
2077 .base_height = 8,
2078 .min_tiles = 8,
2080 });
2081
2082 registry.push_back(DrawRoutineInfo{
2084 .name = "SolidWallDecor3x4",
2085 .function = DrawSolidWallDecor3x4,
2086 .draws_to_both_bgs = false,
2087 .base_width = 3,
2088 .base_height = 4,
2090 });
2091
2092 registry.push_back(DrawRoutineInfo{
2094 .name = "ArcheryGameTargetDoor",
2095 .function = DrawArcheryGameTargetDoor,
2096 .draws_to_both_bgs = false,
2097 .base_width = 3,
2098 .base_height = 6,
2100 });
2101
2102 registry.push_back(DrawRoutineInfo{
2104 .name = "GanonTriforceFloorDecor",
2105 .function = DrawGanonTriforceFloorDecor,
2106 .draws_to_both_bgs = false,
2107 .base_width = 8,
2108 .base_height = 8,
2110 });
2111
2112 registry.push_back(DrawRoutineInfo{
2114 .name = "Single2x2",
2115 .function = DrawSingle2x2,
2116 .draws_to_both_bgs = false,
2117 .base_width = 2,
2118 .base_height = 2,
2119 .min_tiles = 4,
2121 });
2122
2123 registry.push_back(DrawRoutineInfo{
2125 .name = "Single4x4",
2126 .function = DrawSingle4x4,
2127 .draws_to_both_bgs = false,
2128 .base_width = 4,
2129 .base_height = 4,
2130 .min_tiles = 16,
2132 });
2133
2134 registry.push_back(DrawRoutineInfo{
2136 .name = "Single4x3",
2137 .function = DrawSingle4x3,
2138 .draws_to_both_bgs = false,
2139 .base_width = 4,
2140 .base_height = 3,
2141 .min_tiles = 12,
2143 });
2144
2145 registry.push_back(DrawRoutineInfo{
2147 .name = "RupeeFloor",
2148 .function = DrawRupeeFloor,
2149 .draws_to_both_bgs = false,
2150 .base_width = 5,
2151 .base_height = 8,
2152 .min_tiles = 2,
2154 });
2155
2156 registry.push_back(DrawRoutineInfo{
2158 .name = "Actual4x4",
2159 .function = DrawActual4x4,
2160 .draws_to_both_bgs = false,
2161 .base_width = 4,
2162 .base_height = 4,
2163 .min_tiles = 16,
2165 });
2166
2167 registry.push_back(DrawRoutineInfo{
2169 .name = "BigWallDecor",
2170 .function = DrawBigWallDecor,
2171 .draws_to_both_bgs = false,
2172 .base_width = 8,
2173 .base_height = 3,
2174 .min_tiles = 24,
2176 });
2177
2178 registry.push_back(DrawRoutineInfo{
2180 .name = "TableBowl",
2181 .function = DrawTableBowl,
2182 .draws_to_both_bgs = false,
2183 .base_width = 4,
2184 .base_height = 2,
2185 .min_tiles = 8,
2187 });
2188
2189 registry.push_back(DrawRoutineInfo{
2191 .name = "SmithyFurnace",
2192 .function = DrawSmithyFurnace,
2193 .draws_to_both_bgs = false,
2194 .base_width = 6,
2195 .base_height = 8,
2196 .min_tiles = 48,
2198 });
2199
2200 registry.push_back(DrawRoutineInfo{
2202 .name = "BigGrayRock",
2203 .function = DrawBigGrayRock,
2204 .draws_to_both_bgs = false,
2205 .base_width = 4,
2206 .base_height = 4,
2207 .min_tiles = 16,
2209 });
2210
2211 registry.push_back(DrawRoutineInfo{
2213 .name = "AgahnimsAltar",
2214 .function = DrawAgahnimsAltar,
2215 .draws_to_both_bgs = false,
2216 .base_width = 14,
2217 .base_height = 14,
2218 .min_tiles = 84,
2220 });
2221
2222 registry.push_back(DrawRoutineInfo{
2224 .name = "FortuneTellerRoom",
2225 .function = DrawFortuneTellerRoom,
2226 .draws_to_both_bgs = false,
2227 .base_width = 14,
2228 .base_height = 14,
2229 .min_tiles = 26,
2231 });
2232
2233 registry.push_back(DrawRoutineInfo{
2235 .name = "MagicBatAltar",
2236 .function = DrawMagicBatAltar,
2237 .draws_to_both_bgs = false,
2238 .base_width = 8,
2239 .base_height = 7,
2240 .min_tiles = 56,
2242 });
2243
2244 registry.push_back(DrawRoutineInfo{
2246 .name = "Waterfall47",
2247 .function = DrawWaterfall47,
2248 .draws_to_both_bgs = false,
2249 .base_width = 0, // Variable with size
2250 .base_height = 5,
2252 });
2253
2254 registry.push_back(DrawRoutineInfo{
2256 .name = "Waterfall48",
2257 .function = DrawWaterfall48,
2258 .draws_to_both_bgs = false,
2259 .base_width = 0, // Variable with size
2260 .base_height = 3,
2262 });
2263
2264 // Chest platform routines - use canonical IDs from DrawRoutineIds
2265 registry.push_back(DrawRoutineInfo{
2267 .name = "ClosedChestPlatform",
2268 .function = DrawClosedChestPlatform,
2269 .draws_to_both_bgs = false,
2270 .base_width = 0, // Variable: width = (size & 0x0F) + 4
2271 .base_height = 0, // Variable: height = ((size >> 4) & 0x0F) + 1
2273 });
2274
2275 // Moving wall routines
2276 registry.push_back(DrawRoutineInfo{
2278 .name = "MovingWallWest",
2279 .function = DrawMovingWallWest,
2280 .draws_to_both_bgs = false,
2281 .base_width = 0, // Variable: count selector + 3 platform columns
2282 .base_height = 0, // Variable: 2 * direction selector + 6
2283 .min_tiles = 24,
2285 });
2286
2287 registry.push_back(DrawRoutineInfo{
2289 .name = "MovingWallEast",
2290 .function = DrawMovingWallEast,
2291 .draws_to_both_bgs = false,
2292 .base_width = 0, // Variable: count selector + 3 platform columns
2293 .base_height = 0, // Variable: 2 * direction selector + 6
2294 .min_tiles = 24,
2296 });
2297
2298 registry.push_back(DrawRoutineInfo{
2300 .name = "OpenChestPlatform",
2301 .function = DrawOpenChestPlatform,
2302 .draws_to_both_bgs = false,
2303 .base_width = 0, // Variable
2304 .base_height = 0, // Variable
2306 });
2307
2308 // Long vertical rail with CORNER+MIDDLE+END pattern (ID 117) - object 0x8A
2309 // Matches horizontal rail 0x22 but in vertical orientation
2310 registry.push_back(DrawRoutineInfo{
2312 .name = "DownwardsHasEdge1x1_1to16_plus23",
2313 .function =
2314 [](const DrawContext& ctx) {
2315 // CORNER+MIDDLE+END pattern vertically
2316 int size = ctx.object.size_ & 0x0F;
2317 int count = size + 21;
2318 if (ctx.tiles.size() < 3)
2319 return;
2320
2321 int tile_y = ctx.object.y_;
2322 // USDASM $01:8EC9-$01:8ED4 suppresses the leading corner when the
2323 // current slot already contains the small vertical rail corner.
2324 if (!DrawRoutineUtils::ExistingTileMatchesAny(ctx, ctx.object.x_,
2325 tile_y, {0x00E3})) {
2326 DrawRoutineUtils::WriteTile8(ctx.target_bg, ctx.object.x_, tile_y,
2327 ctx.tiles[0]);
2328 }
2329 tile_y++;
2330 // Middle tiles
2331 for (int s = 0; s < count; s++) {
2332 DrawRoutineUtils::WriteTile8(ctx.target_bg, ctx.object.x_, tile_y,
2333 ctx.tiles[1]);
2334 tile_y++;
2335 }
2336 // End tile
2337 DrawRoutineUtils::WriteTile8(ctx.target_bg, ctx.object.x_, tile_y,
2338 ctx.tiles[2]);
2339 },
2340 .draws_to_both_bgs = false,
2341 .base_width = 1,
2342 .base_height = 23, // size + 23
2343 .min_tiles = 3, // corner + middle + end tiles
2345 });
2346
2347 // Custom Object routine (ID 130) - fixed Oracle custom-object families
2348 // These use external binary files instead of ROM tile data.
2349 // CustomDraw() handles feature-flag gating and binary file lookup.
2350 registry.push_back(DrawRoutineInfo{
2352 .name = "CustomObject",
2353 .function = CustomDraw,
2354 .draws_to_both_bgs = false,
2355 .base_width = 0, // Variable: depends on binary file content
2356 .base_height = 0, // Variable: depends on binary file content
2358 });
2359}
2360
2361} // namespace draw_routines
2362} // namespace zelda3
2363} // 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:49
absl::StatusOr< uint8_t > ReadByte(int offset) const
Definition rom.cc:785
absl::StatusOr< uint16_t > ReadWord(int offset) const
Definition rom.cc:793
SNES 16-bit tile metadata container.
Definition snes_tile.h:52
static CustomObjectManager & Get()
absl::StatusOr< std::shared_ptr< CustomObject > > GetObjectInternal(int object_id, int subtype)
absl::StatusOr< std::shared_ptr< const CustomObject > > GetObject(int object_id, int subtype) const
static DrawRoutineRegistry & Get()
virtual bool IsWaterFaceActive(int room_id) const
virtual bool IsFloorBombable(int room_id) const =0
virtual bool IsWallMoved(int room_id) const =0
virtual bool IsChestOpen(int room_id, int chest_index) const =0
virtual bool IsDoorSwitchActive(int room_id) const =0
virtual bool IsDamFloodgateOpen(int room_id) const
virtual bool IsRupeeFloorCleared(int room_id) const =0
#define LOG_DEBUG(category, format,...)
Definition log.h:133
TileInfo WordToTileInfo(uint16_t word)
Definition snes_tile.cc:379
bool ExistingTileMatchesAny(const gfx::BackgroundBuffer &bg, int tile_x, int tile_y, std::initializer_list< uint16_t > tile_ids)
void WriteTile8(gfx::BackgroundBuffer &bg, int tile_x, int tile_y, const gfx::TileInfo &tile_info)
Write an 8x8 tile to the background buffer.
void DrawNx4(const DrawContext &ctx, int columns, size_t start_index)
std::array< gfx::TileInfo, 3 > LoadMovingWallFillTiles(const DrawContext &ctx)
void DrawMovingWallFill(const DrawContext &ctx, int start_x, int column_count, int height)
void DrawRowMajor(gfx::BackgroundBuffer &bg, int base_x, int base_y, int w, int h, std::span< const gfx::TileInfo > tiles, size_t start_index=0)
void DrawOpenChestPlatformSegment(const DrawContext &ctx, int dy, size_t start_index, int fill_width)
void Draw4x4ColumnMajor(const DrawContext &ctx, int x_offset, int y_offset, size_t start_index)
void DrawWaterFaceRows(const DrawContext &ctx, int row_count, int tile_offset)
void Draw1x3NRightwards(const DrawContext &ctx, int columns, size_t start_index, int y_offset=0)
void DrawPlatform3x2(const DrawContext &ctx, int dx, int dy, size_t start_index)
const gfx::TileInfo & TileAtWrapped(std::span< const gfx::TileInfo > tiles, size_t index)
void Draw4x4ColumnMajorTo(gfx::BackgroundBuffer &bg, const RoomObject &object, std::span< const gfx::TileInfo > tiles)
void Draw1x5Column(const DrawContext &ctx, int x_offset, size_t start_index)
void DrawMany32x32Block(gfx::BackgroundBuffer &bg, int base_x, int base_y, std::span< const gfx::TileInfo > tiles)
void DrawPlatform2x2(const DrawContext &ctx, int dx, int dy, size_t start_index)
void WritePlatformTile(const DrawContext &ctx, int dx, int dy, size_t index)
std::array< gfx::TileInfo, 16 > LoadRoomDrawObjectTiles(const DrawContext &ctx, int object_data_offset, int count)
void DrawColumnMajor(gfx::BackgroundBuffer &bg, int base_x, int base_y, int w, int h, std::span< const gfx::TileInfo > tiles, size_t start_index=0)
void DrawPlatform1x3Rightwards(const DrawContext &ctx, int dx, int dy, size_t start_index, int columns)
void DrawDamWaterFlooring(const DrawContext &ctx, gfx::BackgroundBuffer &bg2)
void DrawTableRock4x4_1to16(const DrawContext &ctx)
Draw 4x4 table rock pattern.
void DrawInterRoomFatStairsDownA(const DrawContext &ctx)
Draw inter-room fat stairs going down A (Type 2 object 0x12E)
void DrawSingle2x2(const DrawContext &ctx)
Draw a single 2x2 block (column-major order)
void DrawWaterfall47(const DrawContext &ctx)
Draw waterfall object 0x47 pattern.
void DrawHorizontalTurtleRockPipe(const DrawContext &ctx)
Draw horizontal Turtle Rock pipe (6x4)
void DrawAutoStairs(const DrawContext &ctx)
Draw auto stairs (Type 2/3 objects 0x130-0x133, 0x21B-0x21D, 0x233)
void DrawSpittingWaterFace(const DrawContext &ctx)
Draw spitting water face (Type 3 object 0x201)
void DrawAgahnimsAltar(const DrawContext &ctx)
Draw the fixed 14x14 Agahnim altar with mirrored right half.
void DrawLargeCanvasObject(const DrawContext &ctx, int width, int height)
Draw large canvas object with arbitrary dimensions.
void DrawMovingWallEast(const DrawContext &ctx)
Draw the east-moving wall (Type 1 object 0xCE)
void DrawBigGrayRock(const DrawContext &ctx)
Draw the fixed 4x4 big gray rock as two column-major 4x2 bands.
void DrawChest(const DrawContext &ctx, int chest_index)
Draw a small chest with open/closed state support.
void DrawMovingWallWest(const DrawContext &ctx)
Draw the west-moving wall (Type 1 object 0xCD)
void DrawBed4x5(const DrawContext &ctx)
Draw a 4x5 bed pattern (row-major)
void DrawSingle4x4(const DrawContext &ctx)
Draw a single 4x4 block (column-major order)
void DrawUtility3x5(const DrawContext &ctx)
Draw utility 3x5 pattern (special row pattern)
void DrawDoorSwitcherer(const DrawContext &ctx)
Draw door switcher object with state-based graphics.
void Draw4x4BlocksIn4x4SuperSquare(const DrawContext &ctx)
Draw 4x4 solid blocks in a super square grid.
void DrawBigHole4x4_1to16(const DrawContext &ctx)
Draw 4x4 big hole pattern.
void DrawFloorLight(const DrawContext &ctx)
Draw unconditional floor light (8x8 footprint)
void Draw4x4FloorTwoIn4x4SuperSquare(const DrawContext &ctx)
Draw two 4x4 floor pattern variant.
void DrawSolidWallDecor3x4(const DrawContext &ctx)
Draw solid wall decor 3x4.
void DrawLightBeamOnFloor(const DrawContext &ctx)
Draw floor light beam composed of three 4x4 blocks.
void DrawWaterFace(const DrawContext &ctx)
Draw a generic 2x2 water-face helper pattern.
void DrawTableBowl(const DrawContext &ctx)
Draw a fixed 4x2 table bowl in row-major order.
void DrawChestPlatformVerticalWall(const DrawContext &ctx)
Draw chest platform vertical wall section.
void DrawClosedChestPlatform(const DrawContext &ctx)
Draw closed chest platform (Type 1 object 0xC1)
void DrawSmithyFurnace(const DrawContext &ctx)
Draw the fixed 6x8 smithy furnace in row-major order.
void DrawVitreousGooDamage(const DrawContext &ctx)
Draw the fixed 5x2 grid of repeated 4x4 Vitreous goo stamps.
void DrawSpike2x2In4x4SuperSquare(const DrawContext &ctx)
Draw 2x2 spike pattern in super square units.
void DrawSingle4x3(const DrawContext &ctx)
Draw a single 4x3 block (column-major order)
void DrawBigLightBeamOnFloor(const DrawContext &ctx)
Draw state-gated big floor light beam active variant (8x8 footprint)
void DrawOpenChestPlatform(const DrawContext &ctx)
Draw open chest platform (Type 1 object 0xDC)
void DrawSpiralStairs(const DrawContext &ctx, bool going_up, bool is_upper)
Draw spiral stairs (Type 2 objects 0x138-0x13B)
void DrawFortuneTellerRoom(const DrawContext &ctx)
Draw the fixed 14x14 fortune teller room on BG1.
void DrawSomariaLine(const DrawContext &ctx)
Draw one Somaria path tile at the object anchor.
void DrawWaterfall48(const DrawContext &ctx)
Draw waterfall object 0x48 pattern.
void DrawVerticalTurtleRockPipe(const DrawContext &ctx)
Draw vertical Turtle Rock pipe (two stacked 4x3 sections)
void DrawBombableFloor(const DrawContext &ctx)
Draw bombable floor (Type 3 object 0xFC7 / ASM object 0x247)
void DrawDrenchingWaterFace(const DrawContext &ctx)
Draw drenching water face (Type 3 object 0x202)
void DrawEmptyWaterFace(const DrawContext &ctx)
Draw empty water face (Type 3 object 0x200)
void DrawBossShell4x4(const DrawContext &ctx)
Draw boss shell 4x4.
void CustomDraw(const DrawContext &ctx)
Custom draw routine for special objects.
void DrawBigWallDecor(const DrawContext &ctx)
Draw fixed 8x3 big wall decor in column-major order.
void Draw4x4FloorIn4x4SuperSquare(const DrawContext &ctx)
Draw 4x4 floor pattern in super square units.
void DrawInterRoomFatStairsUp(const DrawContext &ctx)
Draw inter-room fat stairs going up (Type 2 object 0x12D)
void Draw3x3FloorIn4x4SuperSquare(const DrawContext &ctx)
Draw 3x3 floor pattern in super square units.
void DrawArcheryGameTargetDoor(const DrawContext &ctx)
Draw archery game target door (two 3x3 sections)
void DrawStraightInterRoomStairs(const DrawContext &ctx)
Draw straight inter-room stairs (Type 3 objects 0x21E-0x229)
void DrawGanonTriforceFloorDecor(const DrawContext &ctx)
Draw Ganon triforce floor decor (three 4x4 sections)
void RegisterSpecialRoutines(std::vector< DrawRoutineInfo > &registry)
Register all special/miscellaneous draw routines to the registry.
void DrawInterRoomFatStairsDownB(const DrawContext &ctx)
Draw inter-room fat stairs going down B (Type 2 object 0x12F)
void DrawUtility6x3(const DrawContext &ctx)
Draw utility 6x3 pattern via RoomDraw_1x3N_rightwards.
void DrawRightwards3x6(const DrawContext &ctx)
Draw a 6x3 pattern via RoomDraw_1x3N_rightwards semantics.
void DrawMagicBatAltar(const DrawContext &ctx)
Draw the fixed 8x7 Magic Bat altar in column-major order.
void DrawChestPlatformHorizontalWall(const DrawContext &ctx)
Draw chest platform horizontal wall section.
void DrawWaterOverlay8x8_1to16(const DrawContext &ctx)
Draw water overlay 8x8 pattern.
void DrawRupeeFloor(const DrawContext &ctx)
Draw the blue rupee floor pattern (5x8 bounds with gaps)
void Draw4x4FloorOneIn4x4SuperSquare(const DrawContext &ctx)
Draw single 4x4 floor pattern variant.
void DrawNothing(const DrawContext &ctx)
Draw nothing - represents invisible logic objects or placeholders.
void DrawActual4x4(const DrawContext &ctx)
Draw an actual 4x4 tile8 pattern (column-major order)
void DrawBigKeyLock(const DrawContext &ctx)
Draw a closed big key lock (Yaze 0xF98 / ASM object 0x218)
void DrawPrisonCell(const DrawContext &ctx)
Draw prison cell with bars (Type 3 objects 0x20D, 0x217)
constexpr int DirectionForSize(int size)
constexpr int ObjectCountForSize(int size)
constexpr bool IsFullBothAutoStairsObject(int object_id)
constexpr bool IsSouthMixedStraightInterroomObject(int object_id)
constexpr bool IsMixedStraightInterroomObject(int object_id)
constexpr bool IsFixedBg1StraightInterroomObject(int object_id)
uint16_t CustomObjectRuntimeTileWord(int object_id, uint16_t source_word)
constexpr int kRoomObjectTileAddress
Definition room_object.h:48
Context passed to draw routines containing all necessary state.
std::span< const gfx::TileInfo > tiles
gfx::BackgroundBuffer & target_bg
const CustomObjectRenderSnapshot * custom_objects
gfx::BackgroundBuffer * secondary_bg
const DungeonState * state
Metadata about a draw routine.