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
434void DrawUtility3x5(const DrawContext& ctx) {
435 // ASM: RoomDraw_Utility3x5 ($01A194) uses:
436 // - top row: tiles 0..2
437 // - middle 3 rows: tiles 3..5 repeated per row
438 // - bottom row: tiles 6..8
439 if (ctx.tiles.empty())
440 return;
441
442 // Top row.
443 for (int x = 0; x < 3; ++x) {
445 ctx.target_bg, ctx.object.x_ + x, ctx.object.y_,
446 TileAtWrapped(ctx.tiles, static_cast<size_t>(x)));
447 }
448
449 // Middle rows (rows 1..3) reuse tiles 3..5.
450 for (int y = 1; y <= 3; ++y) {
451 for (int x = 0; x < 3; ++x) {
453 ctx.target_bg, ctx.object.x_ + x, ctx.object.y_ + y,
454 TileAtWrapped(ctx.tiles, static_cast<size_t>(3 + x)));
455 }
456 }
457
458 // Bottom row.
459 for (int x = 0; x < 3; ++x) {
461 ctx.target_bg, ctx.object.x_ + x, ctx.object.y_ + 4,
462 TileAtWrapped(ctx.tiles, static_cast<size_t>(6 + x)));
463 }
464}
465
467 // ASM: RoomDraw_VerticalTurtleRockPipe ($019A90)
468 // Two stacked 4x3 sections: first uses tiles 0..11, second 12..23.
469 if (ctx.tiles.empty())
470 return;
471 Draw1x3NRightwards(ctx, /*columns=*/4, /*start_index=*/0, /*y_offset=*/0);
472 Draw1x3NRightwards(ctx, /*columns=*/4, /*start_index=*/12, /*y_offset=*/3);
473}
474
476 // ASM: RoomDraw_HorizontalTurtleRockPipe ($019AA3) -> RoomDraw_Nx4 with A=6.
477 DrawNx4(ctx, /*columns=*/6, /*start_index=*/0);
478}
479
481 // ASM: RoomDraw_LightBeamOnFloor ($01A7B6)
482 // Draw three 4x4 blocks at y offsets 0, +2, +6. The middle block resets X
483 // to obj2376 and reuses tiles 0..15; the bottom block uses obj2396 at
484 // tiles 16..31.
485 if (ctx.tiles.size() < 32)
486 return;
487 Draw4x4ColumnMajor(ctx, /*x_offset=*/0, /*y_offset=*/0, /*start_index=*/0);
488 Draw4x4ColumnMajor(ctx, /*x_offset=*/0, /*y_offset=*/2, /*start_index=*/0);
489 Draw4x4ColumnMajor(ctx, /*x_offset=*/0, /*y_offset=*/6, /*start_index=*/16);
490}
491
493 // ASM: RoomDraw_BigLightBeamOnFloor ($01A7D3) reads the persisted
494 // bombed-floor flag for fixed room 0x065 ($7EF0CA & $0100), then falls
495 // through to RoomDraw_FloorLight when it is active. Do not use ctx.room_id.
496 // Null state is reserved for object/geometry previews, which keep the beam
497 // visible and measurable.
498 if (ctx.state != nullptr &&
499 !ctx.state->IsFloorBombable(kThievesTownEastAtticRoomId)) {
500 return;
501 }
502 DrawFloorLightGrid(ctx);
503}
504
505void DrawFloorLight(const DrawContext& ctx) {
506 // ASM: RoomDraw_FloorLight ($01A7DC) is unconditional for object 0x274.
507 DrawFloorLightGrid(ctx);
508}
509
511 // ASM-mapped objects route through RoomDraw_4x4.
512 Draw4x4ColumnMajor(ctx, /*x_offset=*/0, /*y_offset=*/0, /*start_index=*/0);
513}
514
516 // ASM: RoomDraw_SolidWallDecor3x4 ($0199EC) -> RoomDraw_Nx4 with A=3.
517 DrawNx4(ctx, /*columns=*/3, /*start_index=*/0);
518}
519
521 // ASM: RoomDraw_ArcheryGameTargetDoor ($01A7A3)
522 // Two 3x3 sections stacked vertically.
523 if (ctx.tiles.empty())
524 return;
525 Draw1x3NRightwards(ctx, /*columns=*/3, /*start_index=*/0, /*y_offset=*/0);
526 Draw1x3NRightwards(ctx, /*columns=*/3, /*start_index=*/9, /*y_offset=*/3);
527}
528
530 // ASM: RoomDraw_GanonTriforceFloorDecor ($01A7F0)
531 // Top block uses 0..15 at +2 X, then two bottom 4x4 blocks use 16..31.
532 if (ctx.tiles.empty())
533 return;
534
535 Draw4x4ColumnMajor(ctx, /*x_offset=*/2, /*y_offset=*/0, /*start_index=*/0);
536 Draw4x4ColumnMajor(ctx, /*x_offset=*/0, /*y_offset=*/4, /*start_index=*/16);
537 Draw4x4ColumnMajor(ctx, /*x_offset=*/4, /*y_offset=*/4, /*start_index=*/16);
538}
539
540void DrawSingle2x2(const DrawContext& ctx) {
541 // ASM: RoomDraw_Single2x2 ($019A8D) -> RoomDraw_Downwards2x2
542 // Single 2x2 in column-major order.
543 if (ctx.tiles.size() < 4)
544 return;
545
547 ctx.tiles[0]);
549 ctx.tiles[1]);
551 ctx.tiles[2]);
553 ctx.object.y_ + 1, ctx.tiles[3]);
554}
555
556void DrawSingle4x4(const DrawContext& ctx) {
557 // ASM: RoomDraw_4x4 ($0197ED), column-major 4x4 block (16 tiles).
558 if (ctx.tiles.size() < 16)
559 return;
560
561 int tid = 0;
562 for (int x = 0; x < 4; ++x) {
563 for (int y = 0; y < 4; ++y) {
565 ctx.object.y_ + y, ctx.tiles[tid++]);
566 }
567 }
568}
569
570void DrawSingle4x3(const DrawContext& ctx) {
571 // ASM: RoomDraw_TableRock4x3 ($0199E6), column-major 4x3 block (12 tiles).
572 if (ctx.tiles.size() < 12)
573 return;
574
575 int tid = 0;
576 for (int x = 0; x < 4; ++x) {
577 for (int y = 0; y < 3; ++y) {
579 ctx.object.y_ + y, ctx.tiles[tid++]);
580 }
581 }
582}
583
584void DrawRupeeFloor(const DrawContext& ctx) {
585 // USDASM RoomDraw_RupeeFloor ($01:9AA9-$01:9AED) exits when the
586 // current-room $0402 & $1000 event is set. State-free selector/geometry
587 // previews remain visible.
588 if ((ctx.state != nullptr && ctx.state->IsRupeeFloorCleared(ctx.room_id)) ||
589 ctx.tiles.size() < 2) {
590 return;
591 }
592
593 constexpr std::array<int, 3> kTopRows = {0, 3, 6};
594 constexpr std::array<int, 3> kBottomRows = {1, 4, 7};
595 for (int col = 0; col < 3; ++col) {
596 const int x = ctx.object.x_ + col * 2;
597 for (int row : kTopRows) {
599 ctx.tiles[0]);
600 }
601 for (int row : kBottomRows) {
603 ctx.tiles[1]);
604 }
605 }
606}
607
608void DrawActual4x4(const DrawContext& ctx) {
609 // ASM: RoomDraw_4x4 ($0197ED), used for true 4x4 tile8 objects.
610 DrawSingle4x4(ctx);
611}
612
613void DrawWaterfall47(const DrawContext& ctx) {
614 // ASM: RoomDraw_Waterfall47 ($019466)
615 // First 1x5 column from 0..4, middle columns from 5..9, final from 10..14.
616 if (ctx.tiles.empty())
617 return;
618
619 const int size = ctx.object.size_ & 0x0F;
620 const int middle_columns = (size + 1) * 2; // ASL $B2
621
622 Draw1x5Column(ctx, /*x_offset=*/0, /*start_index=*/0);
623 for (int i = 0; i < middle_columns; ++i) {
624 Draw1x5Column(ctx, /*x_offset=*/1 + i, /*start_index=*/5);
625 }
626 Draw1x5Column(ctx, /*x_offset=*/1 + middle_columns, /*start_index=*/10);
627}
628
629void DrawWaterfall48(const DrawContext& ctx) {
630 // ASM: RoomDraw_Waterfall48 ($019488)
631 // First 1x3 column from 0..2, middle columns from 3..5, final from 6..8.
632 if (ctx.tiles.empty())
633 return;
634
635 const int size = ctx.object.size_ & 0x0F;
636 const int middle_columns = (size + 1) * 2; // ASL $B2
637
638 DrawColumnMajor(ctx.target_bg, ctx.object.x_, ctx.object.y_, /*w=*/1, /*h=*/3,
639 ctx.tiles, /*start_index=*/0);
640 for (int i = 0; i < middle_columns; ++i) {
641 DrawColumnMajor(ctx.target_bg, ctx.object.x_ + 1 + i, ctx.object.y_,
642 /*w=*/1, /*h=*/3, ctx.tiles, /*start_index=*/3);
643 }
644 DrawColumnMajor(ctx.target_bg, ctx.object.x_ + 1 + middle_columns,
645 ctx.object.y_, /*w=*/1, /*h=*/3, ctx.tiles,
646 /*start_index=*/6);
647}
648
649// ============================================================================
650// SuperSquare Routines (Phase 4)
651// ============================================================================
652// A "SuperSquare" is a 16x16 tile area (4 rows of 4 tiles each).
653// These routines draw floor patterns that repeat in super square units.
654
656 // ASM: RoomDraw_4x4BlocksIn4x4SuperSquare ($018B94)
657 // Draws solid 4x4 blocks using a single tile, repeated in a grid pattern.
658 // Size determines number of super squares in X and Y directions.
659 int size_x = ((ctx.object.size_ >> 2) & 0x03) + 1;
660 int size_y = (ctx.object.size_ & 0x03) + 1;
661
662 LOG_DEBUG("DrawRoutines",
663 "Draw4x4BlocksIn4x4SuperSquare: obj=0x%03X pos=(%d,%d) size=0x%02X "
664 "tiles=%zu size_x=%d size_y=%d",
665 ctx.object.id_, ctx.object.x_, ctx.object.y_, ctx.object.size_,
666 ctx.tiles.size(), size_x, size_y);
667
668 if (ctx.tiles.empty()) {
669 LOG_DEBUG("DrawRoutines",
670 "Draw4x4BlocksIn4x4SuperSquare: SKIPPING - no tiles loaded!");
671 return;
672 }
673
674 // Use first tile for all blocks
675 const auto& tile = ctx.tiles[0];
676 LOG_DEBUG("DrawRoutines",
677 "Draw4x4BlocksIn4x4SuperSquare: tile[0] id=%d palette=%d", tile.id_,
678 tile.palette_);
679
680 for (int sy = 0; sy < size_y; ++sy) {
681 for (int sx = 0; sx < size_x; ++sx) {
682 // Each super square is 4x4 tiles
683 int base_x = ctx.object.x_ + (sx * 4);
684 int base_y = ctx.object.y_ + (sy * 4);
685
686 // Draw 4x4 block with same tile
687 for (int y = 0; y < 4; ++y) {
688 for (int x = 0; x < 4; ++x) {
689 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + y,
690 tile);
691 }
692 }
693 }
694 }
695}
696
698 // ASM: RoomDraw_3x3FloorIn4x4SuperSquare ($018D8A)
699 // Draws 3x3 floor patterns within super square units.
700 int size_x = ((ctx.object.size_ >> 2) & 0x03) + 1;
701 int size_y = (ctx.object.size_ & 0x03) + 1;
702
703 if (ctx.tiles.empty())
704 return;
705
706 const auto& tile = ctx.tiles[0];
707 for (int sy = 0; sy < size_y; ++sy) {
708 for (int sx = 0; sx < size_x; ++sx) {
709 int base_x = ctx.object.x_ + (sx * 3);
710 int base_y = ctx.object.y_ + (sy * 3);
711
712 for (int y = 0; y < 3; ++y) {
713 for (int x = 0; x < 3; ++x) {
714 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + y,
715 tile);
716 }
717 }
718 }
719 }
720}
721
723 // ASM: RoomDraw_4x4FloorIn4x4SuperSquare ($018FA5)
724 // Most common floor pattern - draws 4x4 floor tiles in super square units.
725 // Uses RoomDraw_A_Many32x32Blocks internally in assembly.
726 int size_x = ((ctx.object.size_ >> 2) & 0x03) + 1;
727 int size_y = (ctx.object.size_ & 0x03) + 1;
728
729 if (ctx.tiles.empty())
730 return;
731 if (ctx.tiles.size() < 8) {
732 // Some hacks provide abbreviated tile payloads for these objects.
733 // Fall back to a visible fill instead of silently skipping draw.
735 return;
736 }
737
738 for (int sy = 0; sy < size_y; ++sy) {
739 for (int sx = 0; sx < size_x; ++sx) {
740 int base_x = ctx.object.x_ + (sx * 4);
741 int base_y = ctx.object.y_ + (sy * 4);
742
743 DrawMany32x32Block(ctx.target_bg, base_x, base_y, ctx.tiles);
744 }
745 }
746}
747
749 // ASM: RoomDraw_4x4FloorOneIn4x4SuperSquare ($018FA2)
750 // Single 4x4 floor pattern (starts at different tile offset in assembly).
751 // For our purposes, same as 4x4FloorIn4x4SuperSquare with offset tiles.
752 int size_x = ((ctx.object.size_ >> 2) & 0x03) + 1;
753 int size_y = (ctx.object.size_ & 0x03) + 1;
754
755 if (ctx.tiles.size() < 8) {
757 return;
758 }
759
760 for (int sy = 0; sy < size_y; ++sy) {
761 for (int sx = 0; sx < size_x; ++sx) {
762 int base_x = ctx.object.x_ + (sx * 4);
763 int base_y = ctx.object.y_ + (sy * 4);
764
765 DrawMany32x32Block(ctx.target_bg, base_x, base_y, ctx.tiles);
766 }
767 }
768}
769
771 // ASM: RoomDraw_4x4FloorTwoIn4x4SuperSquare ($018F9D)
772 // Two 4x4 floor patterns (uses $0490 offset in assembly).
773 int size_x = ((ctx.object.size_ >> 2) & 0x03) + 1;
774 int size_y = (ctx.object.size_ & 0x03) + 1;
775
776 if (ctx.tiles.size() < 8) {
778 return;
779 }
780
781 for (int sy = 0; sy < size_y; ++sy) {
782 for (int sx = 0; sx < size_x; ++sx) {
783 int base_x = ctx.object.x_ + (sx * 4);
784 int base_y = ctx.object.y_ + (sy * 4);
785
786 DrawMany32x32Block(ctx.target_bg, base_x, base_y, ctx.tiles);
787 }
788 }
789}
790
792 // ASM: Object 0xA4 - Big hole pattern
793 // Draws a rectangular hole with border tiles using Size as the expansion.
794 int size = ctx.object.size_ & 0x0F;
795
796 if (ctx.tiles.size() < 24)
797 return;
798
799 int base_x = ctx.object.x_;
800 int base_y = ctx.object.y_;
801 int max = size + 3; // Bottom/right edge offset
802
803 // Corners
804 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x, base_y, ctx.tiles[8]);
805 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + max, base_y,
806 ctx.tiles[14]);
807 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x, base_y + max,
808 ctx.tiles[17]);
809 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + max, base_y + max,
810 ctx.tiles[23]);
811
812 // Edges and interior
813 for (int xx = 1; xx < max; ++xx) {
814 for (int yy = 1; yy < max; ++yy) {
815 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + xx, base_y + yy,
816 ctx.tiles[0]);
817 }
818 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + xx, base_y,
819 ctx.tiles[10]);
820 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + xx, base_y + max,
821 ctx.tiles[19]);
822 }
823
824 for (int yy = 1; yy < max; ++yy) {
825 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x, base_y + yy,
826 ctx.tiles[9]);
827 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + max, base_y + yy,
828 ctx.tiles[15]);
829 }
830}
831
833 // ASM: RoomDraw_Spike2x2In4x4SuperSquare ($019708)
834 // Draws 2x2 spike patterns in a tiled grid
835 int size_x = ((ctx.object.size_ >> 2) & 0x03) + 1;
836 int size_y = (ctx.object.size_ & 0x03) + 1;
837
838 if (ctx.tiles.size() < 4)
839 return;
840
841 for (int sy = 0; sy < size_y; ++sy) {
842 for (int sx = 0; sx < size_x; ++sx) {
843 int base_x = ctx.object.x_ + (sx * 2);
844 int base_y = ctx.object.y_ + (sy * 2);
845
846 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x, base_y, ctx.tiles[0]);
847 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 1, base_y,
848 ctx.tiles[2]);
849 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x, base_y + 1,
850 ctx.tiles[1]);
851 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 1, base_y + 1,
852 ctx.tiles[3]);
853 }
854 }
855}
856
858 // ASM: Object 0xDD - Table rock pattern
859 int size_x = ((ctx.object.size_ >> 2) & 0x03);
860 int size_y = (ctx.object.size_ & 0x03);
861
862 if (ctx.tiles.size() < 16)
863 return;
864
865 int right_x = ctx.object.x_ + (3 + (size_x * 2));
866 int bottom_y = ctx.object.y_ + (3 + (size_y * 2));
867
868 // Interior
869 for (int xx = 0; xx < size_x + 1; ++xx) {
870 for (int yy = 0; yy < size_y + 1; ++yy) {
871 int base_x = ctx.object.x_ + (xx * 2);
872 int base_y = ctx.object.y_ + (yy * 2);
873 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 1, base_y + 1,
874 ctx.tiles[5]);
875 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 2, base_y + 1,
876 ctx.tiles[6]);
877 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 1, base_y + 2,
878 ctx.tiles[9]);
879 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 2, base_y + 2,
880 ctx.tiles[10]);
881 }
882 }
883
884 // Left/right borders
885 for (int yy = 0; yy < size_y + 1; ++yy) {
886 int base_y = ctx.object.y_ + (yy * 2);
887 DrawRoutineUtils::WriteTile8(ctx.target_bg, ctx.object.x_, base_y + 1,
888 ctx.tiles[4]);
889 DrawRoutineUtils::WriteTile8(ctx.target_bg, ctx.object.x_, base_y + 2,
890 ctx.tiles[8]);
891
892 DrawRoutineUtils::WriteTile8(ctx.target_bg, right_x, base_y + 1,
893 ctx.tiles[7]);
894 DrawRoutineUtils::WriteTile8(ctx.target_bg, right_x, base_y + 2,
895 ctx.tiles[11]);
896 }
897
898 // Top/bottom borders
899 for (int xx = 0; xx < size_x + 1; ++xx) {
900 int base_x = ctx.object.x_ + (xx * 2);
901 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 1, ctx.object.y_,
902 ctx.tiles[1]);
903 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 2, ctx.object.y_,
904 ctx.tiles[2]);
905
906 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 1, bottom_y,
907 ctx.tiles[13]);
908 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 2, bottom_y,
909 ctx.tiles[14]);
910 }
911
912 // Corners
914 ctx.tiles[0]);
916 ctx.tiles[12]);
918 ctx.tiles[3]);
919 DrawRoutineUtils::WriteTile8(ctx.target_bg, right_x, bottom_y, ctx.tiles[15]);
920}
921
923 // ASM: RoomDraw_WaterOverlayA8x8_1to16 ($0195D6) / RoomDraw_WaterOverlayB8x8
924 // NOTE: In the original game, this is an HDMA control object that sets up
925 // the wavy water distortion effect. It doesn't draw tiles directly.
926 // For the editor, we draw the available tile data as a visual indicator.
927
928 int size_x = ((ctx.object.size_ >> 2) & 0x03);
929 int size_y = (ctx.object.size_ & 0x03);
930
931 int count_x = size_x + 2;
932 int count_y = size_y + 2;
933
934 if (ctx.tiles.empty())
935 return;
936 if (ctx.tiles.size() < 8) {
937 // Fallback for abbreviated tile payloads: still stamp a visible overlay.
938 for (int yy = 0; yy < count_y; ++yy) {
939 for (int xx = 0; xx < count_x; ++xx) {
940 int base_x = ctx.object.x_ + (xx * 4);
941 int base_y = ctx.object.y_ + (yy * 4);
942 const auto& tile =
943 ctx.tiles[static_cast<size_t>((xx + yy) % ctx.tiles.size())];
944 for (int y = 0; y < 4; ++y) {
945 for (int x = 0; x < 4; ++x) {
946 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + y,
947 tile);
948 }
949 }
950 }
951 }
952 return;
953 }
954
955 for (int yy = 0; yy < count_y; ++yy) {
956 for (int xx = 0; xx < count_x; ++xx) {
957 int base_x = ctx.object.x_ + (xx * 4);
958 int base_y = ctx.object.y_ + (yy * 4);
959
960 for (int x = 0; x < 4; ++x) {
961 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y,
962 ctx.tiles[x]);
963 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 2,
964 ctx.tiles[x]);
965 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 1,
966 ctx.tiles[4 + x]);
967 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 3,
968 ctx.tiles[4 + x]);
969 }
970 }
971 }
972}
973
974// ============================================================================
975// Stair Routines
976// ============================================================================
977
979 // ASM: RoomDraw_InterRoomFatStairsUp ($01A41B)
980 // Uses RoomDraw_4x4, which consumes a 4x4 tile block in COLUMN-MAJOR order.
981 // In original game, registers position in $06B0 for transition handling
982 // For editor display, we just draw the visual representation
983
984 if (ctx.tiles.size() < 16)
985 return;
986
987 Draw4x4ColumnMajorTo(ctx.target_bg, ctx.object, ctx.tiles);
988}
989
991 // ASM: RoomDraw_InterRoomFatStairsDownA ($01A458)
992 // Uses tile data at obj10A8
993 DrawInterRoomFatStairsUp(ctx); // Same visual structure
994}
995
997 // ASM: RoomDraw_InterRoomFatStairsDownB ($01A486)
998 // Uses tile data at obj10A8
999 DrawInterRoomFatStairsUp(ctx); // Same visual structure
1000}
1001
1002void DrawSpiralStairs(const DrawContext& ctx, bool going_up, bool is_upper) {
1003 // ASM: RoomDraw_SpiralStairsGoingUpUpper, etc.
1004 // Calls RoomDraw_1x3N_rightwards with A=4 -> 4 columns x 3 rows = 12 tiles
1005 // Tile order is COLUMN-MAJOR (down first, then right).
1006 // Upper variants render to BG1, lower variants render to BG2.
1007 (void)going_up;
1008
1009 if (ctx.tiles.size() < 12)
1010 return;
1011
1012 gfx::BackgroundBuffer* dest = &ctx.target_bg;
1013 if (!is_upper && ctx.secondary_bg != nullptr) {
1014 dest = ctx.secondary_bg;
1015 }
1016
1017 DrawColumnMajor(*dest, ctx.object.x_, ctx.object.y_, 4, 3, ctx.tiles);
1018}
1019
1020void DrawAutoStairs(const DrawContext& ctx) {
1021 // ASM: RoomDraw_AutoStairs* routines
1022 // The multi-layer variants stamp both BG tilemaps; merged/single-layer
1023 // variants stay on the active target layer.
1024 if (ctx.tiles.size() < 16)
1025 return;
1026
1027 Draw4x4ColumnMajorTo(ctx.target_bg, ctx.object, ctx.tiles);
1028
1029 if (ctx.secondary_bg != nullptr && IsAutoStairsDualLayer(ctx.object.id_)) {
1030 Draw4x4ColumnMajorTo(*ctx.secondary_bg, ctx.object, ctx.tiles);
1031 }
1032}
1033
1035 // ASM: RoomDraw_StraightInterroomStairs* routines
1036 // Upper variants render on BG1 only. Lower variants render the 4x4 block on
1037 // BG2 and expose the front edge on BG1 as well.
1038 if (ctx.tiles.size() < 16)
1039 return;
1040
1041 if (IsStraightInterroomUpper(ctx.object.id_) || ctx.secondary_bg == nullptr ||
1042 !IsStraightInterroomLower(ctx.object.id_)) {
1043 Draw4x4ColumnMajorTo(ctx.target_bg, ctx.object, ctx.tiles);
1044 return;
1045 }
1046
1047 const bool south_lower = IsSouthLowerStraightInterroomStairs(ctx.object.id_);
1048 size_t tile_index = 0;
1049 for (int col = 0; col < 4; ++col) {
1050 for (int row = 0; row < 4; ++row) {
1051 const auto& tile = TileAtWrapped(ctx.tiles, tile_index++);
1053 ctx.object.y_ + row, tile);
1054 if ((!south_lower && row == 0) || (south_lower && row == 3)) {
1056 ctx.object.y_ + row, tile);
1057 }
1058 }
1059 }
1060}
1061
1062// ============================================================================
1063// Interactive Object Routines
1064// ============================================================================
1065
1066void DrawPrisonCell(const DrawContext& ctx) {
1067 // ASM: RoomDraw_PrisonCell ($019C44)
1068 // The routine selects the current object-list tilemap through $BF, then
1069 // writes one sparse 16x4 cell. Offsets below are the tile-coordinate form of
1070 // the long writes at $019C5A-$019CC5. In particular, columns 7 and 8 remain
1071 // open on rows 1-3 so objects drawn earlier (room 0x080's big-key lock) show
1072 // through the cell opening.
1073 if (ctx.tiles.size() < 6) {
1074 return;
1075 }
1076
1077 const int base_x = ctx.object.x_;
1078 const int base_y = ctx.object.y_;
1079
1080 auto with_horizontal_mirror = [](gfx::TileInfo tile) {
1081 // USDASM uses ORA #$4000 for these writes: force H-flip on rather than
1082 // toggling whatever bit a modified ROM supplied in obj1488.
1083 tile.horizontal_mirror_ = true;
1084 return tile;
1085 };
1086
1087 for (int col = 0; col < 5; ++col) {
1088 const int left_x = base_x + 2 + col;
1089 const int right_x = base_x + 9 + col;
1090 DrawRoutineUtils::WriteTile8(ctx.target_bg, left_x, base_y, ctx.tiles[1]);
1091 DrawRoutineUtils::WriteTile8(ctx.target_bg, right_x, base_y, ctx.tiles[1]);
1092 DrawRoutineUtils::WriteTile8(ctx.target_bg, left_x, base_y + 1,
1093 ctx.tiles[2]);
1094 DrawRoutineUtils::WriteTile8(ctx.target_bg, right_x, base_y + 1,
1095 with_horizontal_mirror(ctx.tiles[2]));
1096 DrawRoutineUtils::WriteTile8(ctx.target_bg, left_x, base_y + 2,
1097 ctx.tiles[4]);
1098 DrawRoutineUtils::WriteTile8(ctx.target_bg, right_x, base_y + 2,
1099 with_horizontal_mirror(ctx.tiles[4]));
1100 DrawRoutineUtils::WriteTile8(ctx.target_bg, left_x, base_y + 3,
1101 ctx.tiles[5]);
1102 DrawRoutineUtils::WriteTile8(ctx.target_bg, right_x, base_y + 3,
1103 with_horizontal_mirror(ctx.tiles[5]));
1104 }
1105
1106 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x, base_y, ctx.tiles[0]);
1107 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 15, base_y,
1108 with_horizontal_mirror(ctx.tiles[0]));
1109 for (int x_offset : {1, 7, 8, 14}) {
1110 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x_offset, base_y,
1111 ctx.tiles[1]);
1112 }
1113 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 1, base_y + 2,
1114 ctx.tiles[3]);
1115 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 14, base_y + 2,
1116 with_horizontal_mirror(ctx.tiles[3]));
1117}
1118
1119void DrawBigKeyLock(const DrawContext& ctx) {
1120 // ASM: RoomDraw_BigKeyLock ($0198AE)
1121 // The opened-state room-event check and shared chest/lock slot advancement
1122 // live in ObjectDrawer, which owns stream ordering. The ROM routine's closed
1123 // branch jumps to RoomDraw_Rightwards2x2 and consumes obj1494 in column-major
1124 // order; there is no second tile state.
1125 DrawSingle2x2(ctx);
1126}
1127
1129 // USDASM RoomDraw_BombableFloor ($01:B3E1) draws four 2x2 quadrants.
1130 // ParseSubtype3 stores the intact obj0220 block at tiles[0..15] and the
1131 // non-contiguous obj05BA replacement block at tiles[16..31]. Each quadrant
1132 // is ordered upper-left, lower-left, upper-right, lower-right.
1133 if (ctx.tiles.size() < 16) {
1134 return;
1135 }
1136
1137 // Vanilla persists this state for room 0x65, while ROM hacks can relocate
1138 // the floor (Oracle of Secrets uses 0xAD). Keep preview selection keyed to
1139 // the current room instead of hardcoding a vanilla room ID.
1140 const bool is_open = ctx.state != nullptr &&
1141 ctx.state->IsFloorBombable(ctx.room_id) &&
1142 ctx.tiles.size() >= 32;
1143 const size_t state_offset = is_open ? 16 : 0;
1144
1145 for (int block_y = 0; block_y < 2; ++block_y) {
1146 for (int block_x = 0; block_x < 2; ++block_x) {
1147 const size_t block_offset =
1148 state_offset + static_cast<size_t>((block_y * 2 + block_x) * 4);
1149 for (int x = 0; x < 2; ++x) {
1150 for (int y = 0; y < 2; ++y) {
1152 ctx.target_bg, ctx.object.x_ + block_x * 2 + x,
1153 ctx.object.y_ + block_y * 2 + y,
1154 ctx.tiles[block_offset + static_cast<size_t>(x * 2 + y)]);
1155 }
1156 }
1157 }
1158 }
1159}
1160
1162 // USDASM RoomDraw_MovingWallWest ($01:9190): moved walls emit no tiles.
1163 if ((ctx.state != nullptr && ctx.state->IsWallMoved(ctx.room_id)) ||
1164 ctx.tiles.size() < 24) {
1165 return;
1166 }
1167
1168 const int direction = moving_wall::DirectionForSize(ctx.object.size_);
1169 const int column_count = moving_wall::ObjectCountForSize(ctx.object.size_);
1170 const int height = direction * 2 + 6;
1171
1172 // West writes the obj03D8 fill first, growing left from the object anchor,
1173 // then stamps the 3x3 corner and repeated 3x2 vertical wall at the anchor.
1174 DrawMovingWallFill(ctx, ctx.object.x_ - column_count, column_count, height);
1175 DrawMovingWallPlatform(ctx, direction);
1176}
1177
1179 // USDASM RoomDraw_MovingWallEast ($01:921C): moved walls emit no tiles.
1180 if ((ctx.state != nullptr && ctx.state->IsWallMoved(ctx.room_id)) ||
1181 ctx.tiles.size() < 24) {
1182 return;
1183 }
1184
1185 const int direction = moving_wall::DirectionForSize(ctx.object.size_);
1186 const int column_count = moving_wall::ObjectCountForSize(ctx.object.size_);
1187 const int height = direction * 2 + 6;
1188
1189 // East stamps the platform first, then grows the obj03D8 fill right from
1190 // the first column beyond the three-tile-wide platform.
1191 DrawMovingWallPlatform(ctx, direction);
1192 DrawMovingWallFill(ctx, ctx.object.x_ + 3, column_count, height);
1193}
1194
1195// ============================================================================
1196// Water Face Variants
1197// ============================================================================
1198
1199namespace {
1200constexpr int kWaterFaceWidthTiles = 4;
1201
1202void DrawWaterFaceRows(const DrawContext& ctx, int row_count, int tile_offset) {
1203 if (row_count <= 0 || tile_offset < 0 || ctx.tiles.empty()) {
1204 return;
1205 }
1206
1207 if (tile_offset >= static_cast<int>(ctx.tiles.size())) {
1208 return;
1209 }
1210
1211 const int available_tiles = static_cast<int>(ctx.tiles.size()) - tile_offset;
1212 const int available_rows = available_tiles / kWaterFaceWidthTiles;
1213 const int rows_to_draw = std::min(row_count, available_rows);
1214
1215 for (int row = 0; row < rows_to_draw; ++row) {
1216 const int row_base = tile_offset + (row * kWaterFaceWidthTiles);
1217 for (int col = 0; col < kWaterFaceWidthTiles; ++col) {
1219 ctx.object.y_ + row,
1220 ctx.tiles[row_base + col]);
1221 }
1222 }
1223}
1224} // namespace
1225
1227 // ASM: RoomDraw_EmptyWaterFace ($019D29)
1228 //
1229 // usdasm behavior:
1230 // - Base state draws a 4x3 face using data at offset 0x1614.
1231 // - "Water active" branch draws a 4x5 variant using data at offset 0x162C.
1232 //
1233 // IMPORTANT: this uses dedicated water-face state, not door state. Tying
1234 // this branch to IsDoorOpen created cross-feature rendering regressions.
1235 const bool water_active =
1236 (ctx.state != nullptr) && ctx.state->IsWaterFaceActive(ctx.room_id);
1237
1238 const int row_count = water_active ? 5 : 3;
1239 const int tile_offset = water_active ? 12 : 0; // 0x162C - 0x1614 = 24 bytes
1240 DrawWaterFaceRows(ctx, row_count, tile_offset);
1241}
1242
1244 // ASM: RoomDraw_SpittingWaterFace ($019D64)
1245 // Draws a 4x5 face/spout shape.
1246 DrawWaterFaceRows(ctx, /*row_count=*/5, /*tile_offset=*/0);
1247}
1248
1250 // ASM: RoomDraw_DrenchingWaterFace ($019D83)
1251 // Draws a 4x7 continuous stream.
1252 DrawWaterFaceRows(ctx, /*row_count=*/7, /*tile_offset=*/0);
1253}
1254
1255// ============================================================================
1256// Chest Platform Multi-Part Routines
1257// ============================================================================
1258
1260 // ASM: RoomDraw_ClosedChestPlatform ($018CC7)
1261 // Size fields are subtype-1 packed 2-bit values: $B2=size_x, $B4=size_y.
1262 // The routine expands them to width_blocks=size_x+4 and
1263 // height_blocks=size_y+1 before drawing top, center, bottom, and the center
1264 // 2x2 overlay.
1265
1266 if (ctx.tiles.size() < 68)
1267 return;
1268
1269 const int size_x = (ctx.object.size_ >> 2) & 0x03;
1270 const int size_y = ctx.object.size_ & 0x03;
1271 const int width_blocks = size_x + 4;
1272 const int height_blocks = size_y + 1;
1273
1274 // Top 3 rows: left cap, repeated 2-column middle, right cap.
1275 DrawPlatform1x3Rightwards(ctx, /*dx=*/0, /*dy=*/0, /*start_index=*/0,
1276 /*columns=*/3);
1277 for (int x = 0; x < width_blocks; ++x) {
1278 DrawPlatform1x3Rightwards(ctx, /*dx=*/3 + x * 2, /*dy=*/0,
1279 /*start_index=*/9, /*columns=*/2);
1280 }
1281 DrawPlatform1x3Rightwards(ctx, /*dx=*/3 + width_blocks * 2, /*dy=*/0,
1282 /*start_index=*/15, /*columns=*/3);
1283
1284 // Center rows: left wall, repeated 2x2 carpet, right wall.
1285 for (int y = 0; y < height_blocks; ++y) {
1286 const int dy = 3 + y * 2;
1287 DrawPlatform3x2(ctx, /*dx=*/0, dy, /*start_index=*/24);
1288 for (int x = 0; x < width_blocks; ++x) {
1289 DrawPlatform2x2(ctx, /*dx=*/3 + x * 2, dy, /*start_index=*/30);
1290 }
1291 DrawPlatform3x2(ctx, /*dx=*/3 + width_blocks * 2, dy,
1292 /*start_index=*/34);
1293 }
1294
1295 // Bottom 3 rows.
1296 const int bottom_y = 3 + height_blocks * 2;
1297 DrawPlatform1x3Rightwards(ctx, /*dx=*/0, bottom_y, /*start_index=*/40,
1298 /*columns=*/3);
1299 for (int x = 0; x < width_blocks; ++x) {
1300 DrawPlatform1x3Rightwards(ctx, /*dx=*/3 + x * 2, bottom_y,
1301 /*start_index=*/49, /*columns=*/2);
1302 }
1303 DrawPlatform1x3Rightwards(ctx, /*dx=*/3 + width_blocks * 2, bottom_y,
1304 /*start_index=*/55, /*columns=*/3);
1305
1306 // Center 2x2 overlay, matching the final SrcPtr(0x590) draw in the native
1307 // C++ port and ZScream's tile indices 64,66/65,67.
1308 DrawPlatform2x2(ctx, /*dx=*/width_blocks + 2,
1309 /*dy=*/bottom_y - (height_blocks + 1),
1310 /*start_index=*/64);
1311}
1312
1314 // ASM: RoomDraw_OpenChestPlatform ($019733). The helper draws one row of a
1315 // 10+2*size_x tile platform, repeated (2*size_y+5) times, then two closing
1316 // rows from src+1/src+2.
1317 if (ctx.tiles.size() < 21)
1318 return;
1319
1320 const int size_x = (ctx.object.size_ >> 2) & 0x03;
1321 const int size_y = ctx.object.size_ & 0x03;
1322 const int fill_width = size_x + 1;
1323 const int body_rows = size_y * 2 + 5;
1324
1325 for (int row = 0; row < body_rows; ++row) {
1326 DrawOpenChestPlatformSegment(ctx, row, /*start_index=*/0, fill_width);
1327 }
1328 DrawOpenChestPlatformSegment(ctx, body_rows, /*start_index=*/1, fill_width);
1329 DrawOpenChestPlatformSegment(ctx, body_rows + 1, /*start_index=*/2,
1330 fill_width);
1331}
1332
1334 // ASM: RoomDraw_ChestPlatformHorizontalWallWithCorners ($018D0D)
1335 int width = (ctx.object.size_ & 0x0F) + 1;
1336
1337 if (ctx.tiles.size() < 3)
1338 return;
1339
1340 for (int x = 0; x < width; ++x) {
1341 size_t tile_idx = (x == 0) ? 0 : ((x == width - 1) ? 2 : 1);
1343 ctx.object.y_, ctx.tiles[tile_idx]);
1344 }
1345}
1346
1348 // ASM: RoomDraw_ChestPlatformVerticalWall ($019E70)
1349 int height = (ctx.object.size_ & 0x0F) + 1;
1350
1351 if (ctx.tiles.empty())
1352 return;
1353
1354 for (int y = 0; y < height; ++y) {
1356 ctx.object.y_ + y, ctx.tiles[0]);
1357 }
1358}
1359
1360void RegisterSpecialRoutines(std::vector<DrawRoutineInfo>& registry) {
1361 // Note: Routine IDs are assigned based on the assembly routine table
1362 // These special routines handle chests, doors, and other non-standard objects
1363
1364 // Chest routine - uses a wrapper since it needs chest_index
1365 registry.push_back(DrawRoutineInfo{
1366 .id = 39, // DrawChest (special index)
1367 .name = "Chest",
1368 .function =
1369 [](const DrawContext& ctx) {
1370 // Default chest index 0 - actual index tracked externally
1371 DrawChest(ctx, 0);
1372 },
1373 .draws_to_both_bgs = false,
1374 .base_width = 2,
1375 .base_height = 2,
1376 .min_tiles = 4, // 2x2 block
1378 });
1379
1380 registry.push_back(DrawRoutineInfo{
1381 .id = 38, // DrawNothing
1382 .name = "Nothing",
1383 .function = DrawNothing,
1384 .draws_to_both_bgs = false,
1385 .base_width = 0,
1386 .base_height = 0,
1388 });
1389
1390 registry.push_back(DrawRoutineInfo{
1391 .id = 26, // DrawDoorSwitcherer
1392 .name = "DoorSwitcherer",
1393 .function = DrawDoorSwitcherer,
1394 .draws_to_both_bgs = false,
1395 .base_width = 1,
1396 .base_height = 1,
1397 .min_tiles = 1, // at least one tile for door graphic
1399 });
1400
1401 registry.push_back(DrawRoutineInfo{
1402 .id = 33, // DrawSomariaLine
1403 .name = "SomariaLine",
1404 .function = DrawSomariaLine,
1405 .draws_to_both_bgs = false,
1406 .base_width = 1,
1407 .base_height = 1,
1408 .min_tiles = 1,
1410 });
1411
1412 registry.push_back(DrawRoutineInfo{
1413 .id = 34, // DrawWaterFace
1414 .name = "WaterFace",
1415 .function = DrawWaterFace,
1416 .draws_to_both_bgs = false,
1417 .base_width = 2,
1418 .base_height = 2,
1419 .min_tiles = 4, // 2x2 block
1421 });
1422
1423 // ============================================================================
1424 // SuperSquare Routines (Phase 4) - IDs 56-64
1425 // ============================================================================
1426
1427 registry.push_back(DrawRoutineInfo{
1428 .id = 56, // Draw4x4BlocksIn4x4SuperSquare
1429 .name = "4x4BlocksIn4x4SuperSquare",
1431 .draws_to_both_bgs = false,
1432 .base_width = 0, // Variable: width = (((size >> 2) & 3) + 1) * 4
1433 .base_height = 0, // Variable: height = ((size & 3) + 1) * 4
1435 });
1436
1437 registry.push_back(DrawRoutineInfo{
1438 .id = 57, // Draw3x3FloorIn4x4SuperSquare
1439 .name = "3x3FloorIn4x4SuperSquare",
1440 .function = Draw3x3FloorIn4x4SuperSquare,
1441 .draws_to_both_bgs = false,
1442 .base_width = 0, // Variable
1443 .base_height = 0, // Variable
1445 });
1446
1447 registry.push_back(DrawRoutineInfo{
1448 .id = 58, // Draw4x4FloorIn4x4SuperSquare
1449 .name = "4x4FloorIn4x4SuperSquare",
1450 .function = Draw4x4FloorIn4x4SuperSquare,
1451 .draws_to_both_bgs = false,
1452 .base_width = 0, // Variable
1453 .base_height = 0, // Variable
1455 });
1456
1457 registry.push_back(DrawRoutineInfo{
1458 .id = 59, // Draw4x4FloorOneIn4x4SuperSquare
1459 .name = "4x4FloorOneIn4x4SuperSquare",
1461 .draws_to_both_bgs = false,
1462 .base_width = 0, // Variable
1463 .base_height = 0, // Variable
1465 });
1466
1467 registry.push_back(DrawRoutineInfo{
1468 .id = 60, // Draw4x4FloorTwoIn4x4SuperSquare
1469 .name = "4x4FloorTwoIn4x4SuperSquare",
1471 .draws_to_both_bgs = false,
1472 .base_width = 0, // Variable
1473 .base_height = 0, // Variable
1475 });
1476
1477 registry.push_back(DrawRoutineInfo{
1478 .id = 61, // DrawBigHole4x4_1to16
1479 .name = "BigHole4x4_1to16",
1480 .function = DrawBigHole4x4_1to16,
1481 .draws_to_both_bgs = false,
1482 .base_width = 0, // Variable
1483 .base_height = 0, // Variable
1485 });
1486
1487 registry.push_back(DrawRoutineInfo{
1488 .id = 62, // DrawSpike2x2In4x4SuperSquare
1489 .name = "Spike2x2In4x4SuperSquare",
1490 .function = DrawSpike2x2In4x4SuperSquare,
1491 .draws_to_both_bgs = false,
1492 .base_width = 0, // Variable
1493 .base_height = 0, // Variable
1495 });
1496
1497 registry.push_back(DrawRoutineInfo{
1498 .id = 63, // DrawTableRock4x4_1to16
1499 .name = "TableRock4x4_1to16",
1500 .function = DrawTableRock4x4_1to16,
1501 .draws_to_both_bgs = false,
1502 .base_width = 0, // Variable
1503 .base_height = 0, // Variable
1505 });
1506
1507 registry.push_back(DrawRoutineInfo{
1508 .id = 64, // DrawWaterOverlay8x8_1to16
1509 .name = "WaterOverlay8x8_1to16",
1510 .function = DrawWaterOverlay8x8_1to16,
1511 .draws_to_both_bgs = false,
1512 .base_width = 0, // Variable
1513 .base_height = 0, // Variable
1515 });
1516
1517 // Stair routines (IDs 83-88)
1518 registry.push_back(DrawRoutineInfo{
1519 .id = 83, // DrawInterRoomFatStairsUp
1520 .name = "InterRoomFatStairsUp",
1521 .function = DrawInterRoomFatStairsUp,
1522 .draws_to_both_bgs = false,
1523 .base_width = 4,
1524 .base_height = 4,
1525 .min_tiles = 16, // 4x4 stair pattern
1527 });
1528
1529 registry.push_back(DrawRoutineInfo{
1530 .id = 84, // DrawInterRoomFatStairsDownA
1531 .name = "InterRoomFatStairsDownA",
1532 .function = DrawInterRoomFatStairsDownA,
1533 .draws_to_both_bgs = false,
1534 .base_width = 4,
1535 .base_height = 4,
1536 .min_tiles = 16, // 4x4 stair pattern
1538 });
1539
1540 registry.push_back(DrawRoutineInfo{
1541 .id = 85, // DrawInterRoomFatStairsDownB
1542 .name = "InterRoomFatStairsDownB",
1543 .function = DrawInterRoomFatStairsDownB,
1544 .draws_to_both_bgs = false,
1545 .base_width = 4,
1546 .base_height = 4,
1547 .min_tiles = 16, // 4x4 stair pattern
1549 });
1550
1551 registry.push_back(DrawRoutineInfo{
1552 .id = 86, // DrawAutoStairs
1553 .name = "AutoStairs",
1554 .function = DrawAutoStairs,
1555 .draws_to_both_bgs = false,
1556 .base_width = 4,
1557 .base_height = 4,
1558 .min_tiles = 16, // 4x4 stair pattern
1560 });
1561
1562 registry.push_back(DrawRoutineInfo{
1563 .id = 87, // DrawStraightInterRoomStairs
1564 .name = "StraightInterRoomStairs",
1565 .function = DrawStraightInterRoomStairs,
1566 .draws_to_both_bgs = false,
1567 .base_width = 4,
1568 .base_height = 4,
1569 .min_tiles = 16, // 4x4 stair pattern
1571 });
1572
1573 // Spiral stairs variants (IDs 88-91)
1574 registry.push_back(DrawRoutineInfo{
1575 .id = 88, // DrawSpiralStairsGoingUpUpper
1576 .name = "SpiralStairsGoingUpUpper",
1577 .function =
1578 [](const DrawContext& ctx) { DrawSpiralStairs(ctx, true, true); },
1579 .draws_to_both_bgs = false,
1580 .base_width = 4,
1581 .base_height = 3, // 4x3 pattern
1582 .min_tiles = 12, // 4x3 block
1584 });
1585
1586 registry.push_back(DrawRoutineInfo{
1587 .id = 89, // DrawSpiralStairsGoingDownUpper
1588 .name = "SpiralStairsGoingDownUpper",
1589 .function =
1590 [](const DrawContext& ctx) { DrawSpiralStairs(ctx, false, true); },
1591 .draws_to_both_bgs = false,
1592 .base_width = 4,
1593 .base_height = 3, // 4x3 pattern
1594 .min_tiles = 12, // 4x3 block
1596 });
1597
1598 registry.push_back(DrawRoutineInfo{
1599 .id = 90, // DrawSpiralStairsGoingUpLower
1600 .name = "SpiralStairsGoingUpLower",
1601 .function =
1602 [](const DrawContext& ctx) { DrawSpiralStairs(ctx, true, false); },
1603 .draws_to_both_bgs = false,
1604 .base_width = 4,
1605 .base_height = 3, // 4x3 pattern
1606 .min_tiles = 12, // 4x3 block
1608 });
1609
1610 registry.push_back(DrawRoutineInfo{
1611 .id = 91, // DrawSpiralStairsGoingDownLower
1612 .name = "SpiralStairsGoingDownLower",
1613 .function =
1614 [](const DrawContext& ctx) { DrawSpiralStairs(ctx, false, false); },
1615 .draws_to_both_bgs = false,
1616 .base_width = 4,
1617 .base_height = 3, // 4x3 pattern
1618 .min_tiles = 12, // 4x3 block
1620 });
1621
1622 registry.push_back(DrawRoutineInfo{
1624 .name = "WaterHopStairsA",
1625 .function = DrawWaterHopStairsA,
1626 .draws_to_both_bgs = false,
1627 .base_width = 4,
1628 .base_height = 2,
1629 .min_tiles = 8,
1631 });
1632
1633 registry.push_back(DrawRoutineInfo{
1635 .name = "WaterHopStairsB",
1636 .function = DrawWaterHopStairsB,
1637 .draws_to_both_bgs = true,
1638 .base_width = 4,
1639 .base_height = 2,
1640 .min_tiles = 8,
1642 });
1643
1644 registry.push_back(DrawRoutineInfo{
1646 .name = "DamFloodGate",
1647 .function = DrawDamFloodGate,
1648 .draws_to_both_bgs = false,
1649 .base_width = 10,
1650 .base_height = 4,
1651 .min_tiles = 40,
1653 });
1654
1655 // Interactive object routines (IDs 92-95)
1656 registry.push_back(DrawRoutineInfo{
1657 .id = 92, // DrawBigKeyLock
1658 .name = "BigKeyLock",
1659 .function = DrawBigKeyLock,
1660 .draws_to_both_bgs = false,
1661 .base_width = 2,
1662 .base_height = 2,
1663 .min_tiles = 4, // 2x2 block
1665 });
1666
1667 registry.push_back(DrawRoutineInfo{
1668 .id = 93, // DrawBombableFloor
1669 .name = "BombableFloor",
1670 .function = DrawBombableFloor,
1671 .draws_to_both_bgs = false,
1672 .base_width = 4,
1673 .base_height = 4,
1674 .min_tiles = 16, // One complete 4x4 state
1676 });
1677
1678 // Water face variants (IDs 94-96)
1679 registry.push_back(DrawRoutineInfo{
1680 .id = 94, // DrawEmptyWaterFace
1681 .name = "EmptyWaterFace",
1682 .function = DrawEmptyWaterFace,
1683 .draws_to_both_bgs = false,
1684 .base_width = 4,
1685 .base_height = 3,
1686 .min_tiles = 12, // base 4x3 variant
1688 });
1689
1690 registry.push_back(DrawRoutineInfo{
1691 .id = 95, // DrawSpittingWaterFace
1692 .name = "SpittingWaterFace",
1693 .function = DrawSpittingWaterFace,
1694 .draws_to_both_bgs = false,
1695 .base_width = 4,
1696 .base_height = 5,
1697 .min_tiles = 20, // 4x5 variant
1699 });
1700
1701 registry.push_back(DrawRoutineInfo{
1702 .id = 96, // DrawDrenchingWaterFace
1703 .name = "DrenchingWaterFace",
1704 .function = DrawDrenchingWaterFace,
1705 .draws_to_both_bgs = false,
1706 .base_width = 4,
1707 .base_height = 7,
1708 .min_tiles = 28, // 4x7 variant
1710 });
1711
1712 // Prison cell (Type 3 objects 0x20D, 0x217) writes only to the tilemap
1713 // selected by the current object stream ($BF in USDASM).
1714 registry.push_back(DrawRoutineInfo{
1715 .id = 97, // DrawPrisonCell
1716 .name = "PrisonCell",
1717 .function = DrawPrisonCell,
1718 .draws_to_both_bgs = false,
1719 .base_width = 16,
1720 .base_height = 4,
1721 .min_tiles = 6,
1723 });
1724
1725 registry.push_back(DrawRoutineInfo{
1726 .id = DrawRoutineIds::kBed4x5, // 98
1727 .name = "Bed4x5",
1728 .function = DrawBed4x5,
1729 .draws_to_both_bgs = false,
1730 .base_width = 4,
1731 .base_height = 5,
1733 });
1734
1735 registry.push_back(DrawRoutineInfo{
1737 .name = "Rightwards3x6",
1738 .function = DrawRightwards3x6,
1739 .draws_to_both_bgs = false,
1740 .base_width = 6,
1741 .base_height = 3,
1743 });
1744
1745 registry.push_back(DrawRoutineInfo{
1747 .name = "Utility6x3",
1748 .function = DrawUtility6x3,
1749 .draws_to_both_bgs = false,
1750 .base_width = 6,
1751 .base_height = 3,
1753 });
1754
1755 registry.push_back(DrawRoutineInfo{
1757 .name = "Utility3x5",
1758 .function = DrawUtility3x5,
1759 .draws_to_both_bgs = false,
1760 .base_width = 3,
1761 .base_height = 5,
1763 });
1764
1765 registry.push_back(DrawRoutineInfo{
1767 .name = "VerticalTurtleRockPipe",
1768 .function = DrawVerticalTurtleRockPipe,
1769 .draws_to_both_bgs = false,
1770 .base_width = 4,
1771 .base_height = 6,
1773 });
1774
1775 registry.push_back(DrawRoutineInfo{
1777 .name = "HorizontalTurtleRockPipe",
1778 .function = DrawHorizontalTurtleRockPipe,
1779 .draws_to_both_bgs = false,
1780 .base_width = 6,
1781 .base_height = 4,
1783 });
1784
1785 registry.push_back(DrawRoutineInfo{
1787 .name = "LightBeam",
1788 .function = DrawLightBeamOnFloor,
1789 .draws_to_both_bgs = false,
1790 .base_width = 4,
1791 .base_height = 10,
1792 .min_tiles = 32,
1794 });
1795
1796 registry.push_back(DrawRoutineInfo{
1798 .name = "BigLightBeam",
1799 .function = DrawBigLightBeamOnFloor,
1800 .draws_to_both_bgs = false,
1801 .base_width = 8,
1802 .base_height = 8,
1803 .min_tiles = 64,
1805 });
1806
1807 registry.push_back(DrawRoutineInfo{
1809 .name = "FloorLight",
1810 .function = DrawFloorLight,
1811 .draws_to_both_bgs = false,
1812 .base_width = 8,
1813 .base_height = 8,
1814 .min_tiles = 64,
1816 });
1817
1818 registry.push_back(DrawRoutineInfo{
1820 .name = "BossShell4x4",
1821 .function = DrawBossShell4x4,
1822 .draws_to_both_bgs = false,
1823 .base_width = 4,
1824 .base_height = 4,
1826 });
1827
1828 registry.push_back(DrawRoutineInfo{
1830 .name = "SolidWallDecor3x4",
1831 .function = DrawSolidWallDecor3x4,
1832 .draws_to_both_bgs = false,
1833 .base_width = 3,
1834 .base_height = 4,
1836 });
1837
1838 registry.push_back(DrawRoutineInfo{
1840 .name = "ArcheryGameTargetDoor",
1841 .function = DrawArcheryGameTargetDoor,
1842 .draws_to_both_bgs = false,
1843 .base_width = 3,
1844 .base_height = 6,
1846 });
1847
1848 registry.push_back(DrawRoutineInfo{
1850 .name = "GanonTriforceFloorDecor",
1851 .function = DrawGanonTriforceFloorDecor,
1852 .draws_to_both_bgs = false,
1853 .base_width = 8,
1854 .base_height = 8,
1856 });
1857
1858 registry.push_back(DrawRoutineInfo{
1860 .name = "Single2x2",
1861 .function = DrawSingle2x2,
1862 .draws_to_both_bgs = false,
1863 .base_width = 2,
1864 .base_height = 2,
1865 .min_tiles = 4,
1867 });
1868
1869 registry.push_back(DrawRoutineInfo{
1871 .name = "Single4x4",
1872 .function = DrawSingle4x4,
1873 .draws_to_both_bgs = false,
1874 .base_width = 4,
1875 .base_height = 4,
1876 .min_tiles = 16,
1878 });
1879
1880 registry.push_back(DrawRoutineInfo{
1882 .name = "Single4x3",
1883 .function = DrawSingle4x3,
1884 .draws_to_both_bgs = false,
1885 .base_width = 4,
1886 .base_height = 3,
1887 .min_tiles = 12,
1889 });
1890
1891 registry.push_back(DrawRoutineInfo{
1893 .name = "RupeeFloor",
1894 .function = DrawRupeeFloor,
1895 .draws_to_both_bgs = false,
1896 .base_width = 5,
1897 .base_height = 8,
1898 .min_tiles = 2,
1900 });
1901
1902 registry.push_back(DrawRoutineInfo{
1904 .name = "Actual4x4",
1905 .function = DrawActual4x4,
1906 .draws_to_both_bgs = false,
1907 .base_width = 4,
1908 .base_height = 4,
1909 .min_tiles = 16,
1911 });
1912
1913 registry.push_back(DrawRoutineInfo{
1915 .name = "Waterfall47",
1916 .function = DrawWaterfall47,
1917 .draws_to_both_bgs = false,
1918 .base_width = 0, // Variable with size
1919 .base_height = 5,
1921 });
1922
1923 registry.push_back(DrawRoutineInfo{
1925 .name = "Waterfall48",
1926 .function = DrawWaterfall48,
1927 .draws_to_both_bgs = false,
1928 .base_width = 0, // Variable with size
1929 .base_height = 3,
1931 });
1932
1933 // Chest platform routines - use canonical IDs from DrawRoutineIds
1934 registry.push_back(DrawRoutineInfo{
1936 .name = "ClosedChestPlatform",
1937 .function = DrawClosedChestPlatform,
1938 .draws_to_both_bgs = false,
1939 .base_width = 0, // Variable: width = (size & 0x0F) + 4
1940 .base_height = 0, // Variable: height = ((size >> 4) & 0x0F) + 1
1942 });
1943
1944 // Moving wall routines
1945 registry.push_back(DrawRoutineInfo{
1947 .name = "MovingWallWest",
1948 .function = DrawMovingWallWest,
1949 .draws_to_both_bgs = false,
1950 .base_width = 0, // Variable: count selector + 3 platform columns
1951 .base_height = 0, // Variable: 2 * direction selector + 6
1952 .min_tiles = 24,
1954 });
1955
1956 registry.push_back(DrawRoutineInfo{
1958 .name = "MovingWallEast",
1959 .function = DrawMovingWallEast,
1960 .draws_to_both_bgs = false,
1961 .base_width = 0, // Variable: count selector + 3 platform columns
1962 .base_height = 0, // Variable: 2 * direction selector + 6
1963 .min_tiles = 24,
1965 });
1966
1967 registry.push_back(DrawRoutineInfo{
1969 .name = "OpenChestPlatform",
1970 .function = DrawOpenChestPlatform,
1971 .draws_to_both_bgs = false,
1972 .base_width = 0, // Variable
1973 .base_height = 0, // Variable
1975 });
1976
1977 // Long vertical rail with CORNER+MIDDLE+END pattern (ID 117) - object 0x8A
1978 // Matches horizontal rail 0x22 but in vertical orientation
1979 registry.push_back(DrawRoutineInfo{
1981 .name = "DownwardsHasEdge1x1_1to16_plus23",
1982 .function =
1983 [](const DrawContext& ctx) {
1984 // CORNER+MIDDLE+END pattern vertically
1985 int size = ctx.object.size_ & 0x0F;
1986 int count = size + 21;
1987 if (ctx.tiles.size() < 3)
1988 return;
1989
1990 int tile_y = ctx.object.y_;
1991 // USDASM $01:8EC9-$01:8ED4 suppresses the leading corner when the
1992 // current slot already contains the small vertical rail corner.
1994 ctx.target_bg, ctx.object.x_, tile_y, {0x00E3})) {
1995 DrawRoutineUtils::WriteTile8(ctx.target_bg, ctx.object.x_, tile_y,
1996 ctx.tiles[0]);
1997 }
1998 tile_y++;
1999 // Middle tiles
2000 for (int s = 0; s < count; s++) {
2001 DrawRoutineUtils::WriteTile8(ctx.target_bg, ctx.object.x_, tile_y,
2002 ctx.tiles[1]);
2003 tile_y++;
2004 }
2005 // End tile
2006 DrawRoutineUtils::WriteTile8(ctx.target_bg, ctx.object.x_, tile_y,
2007 ctx.tiles[2]);
2008 },
2009 .draws_to_both_bgs = false,
2010 .base_width = 1,
2011 .base_height = 23, // size + 23
2012 .min_tiles = 3, // corner + middle + end tiles
2014 });
2015
2016 // Custom Object routine (ID 130) - Oracle of Secrets objects 0x31, 0x32
2017 // These use external binary files instead of ROM tile data.
2018 // CustomDraw() handles feature-flag gating and binary file lookup.
2019 registry.push_back(DrawRoutineInfo{
2021 .name = "CustomObject",
2022 .function = CustomDraw,
2023 .draws_to_both_bgs = false,
2024 .base_width = 0, // Variable: depends on binary file content
2025 .base_height = 0, // Variable: depends on binary file content
2027 });
2028}
2029
2030} // namespace draw_routines
2031} // namespace zelda3
2032} // 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 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 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 DrawChestPlatformVerticalWall(const DrawContext &ctx)
Draw chest platform vertical wall section.
void DrawClosedChestPlatform(const DrawContext &ctx)
Draw closed chest platform (Type 1 object 0xC1)
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 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 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:47
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.