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