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