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
5#include "core/features.h"
6#include "util/log.h"
11
12namespace yaze {
13namespace zelda3 {
14namespace draw_routines {
15
16void DrawChest(const DrawContext& ctx, int chest_index) {
17 // Determine if chest is open
18 bool is_open = false;
19 if (ctx.state) {
20 is_open = ctx.state->IsChestOpen(ctx.room_id, chest_index);
21 }
22
23 // Draw logic
24 // Heuristic: If we have extra tiles loaded, assume they are for the open
25 // state. Standard chests are 2x2 (4 tiles). Big chests are 4x4 (16 tiles). If
26 // we have double the tiles, use the second half for open state.
27
28 if (is_open) {
29 if (ctx.tiles.size() >= 32) {
30 // Big chest open tiles (indices 16-31)
31 // Draw 4x4 pattern
32 for (int x = 0; x < 4; ++x) {
33 for (int y = 0; y < 4; ++y) {
35 ctx.object.y_ + y,
36 ctx.tiles[16 + x * 4 + y]);
37 }
38 }
39 return;
40 }
41 if (ctx.tiles.size() >= 8 && ctx.tiles.size() < 16) {
42 // Small chest open tiles (indices 4-7)
44 ctx.tiles[4]);
46 ctx.object.y_ + 1, ctx.tiles[5]);
48 ctx.object.y_, ctx.tiles[6]);
50 ctx.object.y_ + 1, ctx.tiles[7]);
51 return;
52 }
53 // If no extra tiles, fall through to draw closed chest (better than
54 // nothing)
55 }
56
57 // Fallback to standard 4x4 or 2x2 drawing based on tile count
58 if (ctx.tiles.size() >= 16) {
59 // Draw 4x4 pattern in COLUMN-MAJOR order
60 for (int x = 0; x < 4; ++x) {
61 for (int y = 0; y < 4; ++y) {
63 ctx.object.y_ + y, ctx.tiles[x * 4 + y]);
64 }
65 }
66 } else if (ctx.tiles.size() >= 4) {
67 // Draw 2x2 pattern in COLUMN-MAJOR order
69 ctx.tiles[0]);
71 ctx.object.y_ + 1, ctx.tiles[1]);
73 ctx.object.y_, ctx.tiles[2]);
75 ctx.object.y_ + 1, ctx.tiles[3]);
76 }
77}
78
79void DrawNothing([[maybe_unused]] const DrawContext& ctx) {
80 // Intentionally empty - represents invisible logic objects or placeholders
81 // ASM: RoomDraw_Nothing_A ($0190F2), RoomDraw_Nothing_B ($01932E), etc.
82 // These routines typically just RTS.
83}
84
85void CustomDraw(const DrawContext& ctx) {
86 // Pattern: Custom draw routine (objects 0x31-0x32)
87 // When custom objects are enabled, load tile data from external binary files
88 // managed by CustomObjectManager. Each binary encodes SNES tilemap entries
89 // with relative x/y positions computed from the buffer stride layout.
90
91 if (!core::FeatureFlags::get().kEnableCustomObjects) {
92 // Feature disabled: fall back to vanilla 1x1 draw from ROM tile span
93 if (ctx.tiles.size() >= 1) {
95 ctx.tiles[0]);
96 }
97 return;
98 }
99
100 // Look up the custom object by ID and subtype.
101 // ctx.object.id_ is 0x31 or 0x32; ctx.object.size_ encodes the subtype.
103 ctx.object.id_, ctx.object.size_);
104
105 if (!result.ok() || !result.value() || result.value()->IsEmpty()) {
106 // Custom object not found or empty: fall back to 1x1 draw
107 if (ctx.tiles.size() >= 1) {
109 ctx.tiles[0]);
110 }
111 return;
112 }
113
114 const auto& custom_obj = *result.value();
115
116 for (const auto& entry : custom_obj.tiles) {
117 // Convert SNES tilemap word (vhopppcc cccccccc) to TileInfo.
118 // Low byte = entry.tile_data & 0xFF, high byte = (entry.tile_data >> 8).
119 uint8_t lo = static_cast<uint8_t>(entry.tile_data & 0xFF);
120 uint8_t hi = static_cast<uint8_t>((entry.tile_data >> 8) & 0xFF);
121 gfx::TileInfo tile_info(lo, hi);
122
123 // rel_x/rel_y are already decoded as object-relative coordinates from the
124 // binary stream's buffer position arithmetic; preserve those offsets.
125 int draw_x = ctx.object.x_ + entry.rel_x;
126 int draw_y = ctx.object.y_ + entry.rel_y;
127
128 DrawRoutineUtils::WriteTile8(ctx.target_bg, draw_x, draw_y, tile_info);
129 }
130}
131
133 // Pattern: Door switcher (object 0x35)
134 // Special door logic
135 // Check state to decide graphic
136 int tile_index = 0;
137 if (ctx.state && ctx.state->IsDoorSwitchActive(ctx.room_id)) {
138 // Use active tile if available (assuming 2nd tile is active state)
139 if (ctx.tiles.size() >= 2) {
140 tile_index = 1;
141 }
142 }
143
144 if (ctx.tiles.size() > static_cast<size_t>(tile_index)) {
146 ctx.tiles[tile_index]);
147 }
148}
149
150void DrawSomariaLine(const DrawContext& ctx) {
151 // Pattern: Somaria Line (objects 0x203-0x20F, 0x214)
152 // Draws a line of tiles based on direction encoded in object ID
153 // Direction mapping based on ZScream reference:
154 // 0x03: Horizontal right
155 // 0x04: Vertical down
156 // 0x05: Diagonal down-right
157 // 0x06: Diagonal down-left
158 // 0x07-0x09: Variations
159 // 0x0A-0x0C: More variations
160 // 0x0E-0x0F: Additional patterns
161 // 0x14: Another line type
162
163 if (ctx.tiles.empty()) return;
164
165 int length = (ctx.object.size_ & 0x0F) + 1;
166 int obj_subid = ctx.object.id_ & 0x0F; // Low nibble determines direction
167
168 // Determine direction based on object sub-ID
169 int dx = 1, dy = 0; // Default: horizontal right
170 switch (obj_subid) {
171 case 0x03:
172 dx = 1;
173 dy = 0;
174 break; // Horizontal right
175 case 0x04:
176 dx = 0;
177 dy = 1;
178 break; // Vertical down
179 case 0x05:
180 dx = 1;
181 dy = 1;
182 break; // Diagonal down-right
183 case 0x06:
184 dx = -1;
185 dy = 1;
186 break; // Diagonal down-left
187 case 0x07:
188 dx = 1;
189 dy = 0;
190 break; // Horizontal (variant)
191 case 0x08:
192 dx = 0;
193 dy = 1;
194 break; // Vertical (variant)
195 case 0x09:
196 dx = 1;
197 dy = 1;
198 break; // Diagonal (variant)
199 case 0x0A:
200 dx = 1;
201 dy = 0;
202 break; // Horizontal
203 case 0x0B:
204 dx = 0;
205 dy = 1;
206 break; // Vertical
207 case 0x0C:
208 dx = 1;
209 dy = 1;
210 break; // Diagonal
211 case 0x0E:
212 dx = 1;
213 dy = 0;
214 break; // Horizontal
215 case 0x0F:
216 dx = 0;
217 dy = 1;
218 break; // Vertical
219 default:
220 dx = 1;
221 dy = 0;
222 break; // Default horizontal
223 }
224
225 // Draw tiles along the path using first tile (Somaria uses single tile)
226 for (int i = 0; i < length; ++i) {
227 size_t tile_idx = i % ctx.tiles.size(); // Cycle through tiles if multiple
229 ctx.object.y_ + (i * dy), ctx.tiles[tile_idx]);
230 }
231}
232
233void DrawWaterFace(const DrawContext& ctx) {
234 // Pattern: Water Face (Type 3 objects 0xF80-0xF82)
235 // Draws a 2x2 face in COLUMN-MAJOR order
236 // TODO: Implement state check from RoomDraw_EmptyWaterFace ($019D29)
237 // Checks Room ID ($AF), Room State ($7EF000), Door Flags ($0403) to switch
238 // graphic
239 if (ctx.tiles.size() >= 4) {
241 ctx.tiles[0]); // col 0, row 0
243 ctx.object.y_ + 1,
244 ctx.tiles[1]); // col 0, row 1
246 ctx.object.y_,
247 ctx.tiles[2]); // col 1, row 0
249 ctx.object.y_ + 1,
250 ctx.tiles[3]); // col 1, row 1
251 }
252}
253
254void DrawLargeCanvasObject(const DrawContext& ctx, int width, int height) {
255 // Generic large object drawer
256 if (ctx.tiles.size() >= static_cast<size_t>(width * height)) {
257 for (int y = 0; y < height; ++y) {
258 for (int x = 0; x < width; ++x) {
260 ctx.object.y_ + y,
261 ctx.tiles[y * width + x]);
262 }
263 }
264 }
265}
266
267// ============================================================================
268// SuperSquare Routines (Phase 4)
269// ============================================================================
270// A "SuperSquare" is a 16x16 tile area (4 rows of 4 tiles each).
271// These routines draw floor patterns that repeat in super square units.
272
274 // ASM: RoomDraw_4x4BlocksIn4x4SuperSquare ($018B94)
275 // Draws solid 4x4 blocks using a single tile, repeated in a grid pattern.
276 // Size determines number of super squares in X and Y directions.
277 int size_x = ((ctx.object.size_ >> 2) & 0x03) + 1;
278 int size_y = (ctx.object.size_ & 0x03) + 1;
279
280 LOG_DEBUG("DrawRoutines",
281 "Draw4x4BlocksIn4x4SuperSquare: obj=0x%03X pos=(%d,%d) size=0x%02X "
282 "tiles=%zu size_x=%d size_y=%d",
283 ctx.object.id_, ctx.object.x_, ctx.object.y_, ctx.object.size_,
284 ctx.tiles.size(), size_x, size_y);
285
286 if (ctx.tiles.empty()) {
287 LOG_DEBUG("DrawRoutines",
288 "Draw4x4BlocksIn4x4SuperSquare: SKIPPING - no tiles loaded!");
289 return;
290 }
291
292 // Use first tile for all blocks
293 const auto& tile = ctx.tiles[0];
294 LOG_DEBUG("DrawRoutines",
295 "Draw4x4BlocksIn4x4SuperSquare: tile[0] id=%d palette=%d",
296 tile.id_, tile.palette_);
297
298 for (int sy = 0; sy < size_y; ++sy) {
299 for (int sx = 0; sx < size_x; ++sx) {
300 // Each super square is 4x4 tiles
301 int base_x = ctx.object.x_ + (sx * 4);
302 int base_y = ctx.object.y_ + (sy * 4);
303
304 // Draw 4x4 block with same tile
305 for (int y = 0; y < 4; ++y) {
306 for (int x = 0; x < 4; ++x) {
307 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + y,
308 tile);
309 }
310 }
311 }
312 }
313}
314
316 // ASM: RoomDraw_3x3FloorIn4x4SuperSquare ($018D8A)
317 // Draws 3x3 floor patterns within super square units.
318 int size_x = ((ctx.object.size_ >> 2) & 0x03) + 1;
319 int size_y = (ctx.object.size_ & 0x03) + 1;
320
321 if (ctx.tiles.empty()) return;
322
323 const auto& tile = ctx.tiles[0];
324 for (int sy = 0; sy < size_y; ++sy) {
325 for (int sx = 0; sx < size_x; ++sx) {
326 int base_x = ctx.object.x_ + (sx * 3);
327 int base_y = ctx.object.y_ + (sy * 3);
328
329 for (int y = 0; y < 3; ++y) {
330 for (int x = 0; x < 3; ++x) {
331 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + y,
332 tile);
333 }
334 }
335 }
336 }
337}
338
340 // ASM: RoomDraw_4x4FloorIn4x4SuperSquare ($018FA5)
341 // Most common floor pattern - draws 4x4 floor tiles in super square units.
342 // Uses RoomDraw_A_Many32x32Blocks internally in assembly.
343 int size_x = ((ctx.object.size_ >> 2) & 0x03) + 1;
344 int size_y = (ctx.object.size_ & 0x03) + 1;
345
346 if (ctx.tiles.empty()) return;
347 if (ctx.tiles.size() < 8) {
348 // Some hacks provide abbreviated tile payloads for these objects.
349 // Fall back to a visible fill instead of silently skipping draw.
351 return;
352 }
353
354 for (int sy = 0; sy < size_y; ++sy) {
355 for (int sx = 0; sx < size_x; ++sx) {
356 int base_x = ctx.object.x_ + (sx * 4);
357 int base_y = ctx.object.y_ + (sy * 4);
358
359 // Tile order is COLUMN-MAJOR 4x2, matching RoomDraw_A_Many32x32Blocks:
360 // [col0 row0, col0 row1, col1 row0, col1 row1, ...].
361 for (int x = 0; x < 4; ++x) {
362 const auto& row0 = ctx.tiles[(x * 2) + 0];
363 const auto& row1 = ctx.tiles[(x * 2) + 1];
364
365 // Top half (rows 0-1)
366 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 0,
367 row0);
368 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 1,
369 row1);
370
371 // Bottom half (rows 2-3) repeats the same 4x2 pattern.
372 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 2,
373 row0);
374 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 3,
375 row1);
376 }
377 }
378 }
379}
380
382 // ASM: RoomDraw_4x4FloorOneIn4x4SuperSquare ($018FA2)
383 // Single 4x4 floor pattern (starts at different tile offset in assembly).
384 // For our purposes, same as 4x4FloorIn4x4SuperSquare with offset tiles.
385 int size_x = ((ctx.object.size_ >> 2) & 0x03) + 1;
386 int size_y = (ctx.object.size_ & 0x03) + 1;
387
388 if (ctx.tiles.size() < 8) {
390 return;
391 }
392
393 for (int sy = 0; sy < size_y; ++sy) {
394 for (int sx = 0; sx < size_x; ++sx) {
395 int base_x = ctx.object.x_ + (sx * 4);
396 int base_y = ctx.object.y_ + (sy * 4);
397
398 for (int x = 0; x < 4; ++x) {
399 const auto& row0 = ctx.tiles[(x * 2) + 0];
400 const auto& row1 = ctx.tiles[(x * 2) + 1];
401
402 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 0,
403 row0);
404 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 1,
405 row1);
406 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 2,
407 row0);
408 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 3,
409 row1);
410 }
411 }
412 }
413}
414
416 // ASM: RoomDraw_4x4FloorTwoIn4x4SuperSquare ($018F9D)
417 // Two 4x4 floor patterns (uses $0490 offset in assembly).
418 int size_x = ((ctx.object.size_ >> 2) & 0x03) + 1;
419 int size_y = (ctx.object.size_ & 0x03) + 1;
420
421 if (ctx.tiles.size() < 8) {
423 return;
424 }
425
426 for (int sy = 0; sy < size_y; ++sy) {
427 for (int sx = 0; sx < size_x; ++sx) {
428 int base_x = ctx.object.x_ + (sx * 4);
429 int base_y = ctx.object.y_ + (sy * 4);
430
431 for (int x = 0; x < 4; ++x) {
432 const auto& row0 = ctx.tiles[(x * 2) + 0];
433 const auto& row1 = ctx.tiles[(x * 2) + 1];
434
435 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 0,
436 row0);
437 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 1,
438 row1);
439 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 2,
440 row0);
441 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 3,
442 row1);
443 }
444 }
445 }
446}
447
449 // ASM: Object 0xA4 - Big hole pattern
450 // Draws a rectangular hole with border tiles using Size as the expansion.
451 int size = ctx.object.size_ & 0x0F;
452
453 if (ctx.tiles.size() < 24) return;
454
455 int base_x = ctx.object.x_;
456 int base_y = ctx.object.y_;
457 int max = size + 3; // Bottom/right edge offset
458
459 // Corners
460 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x, base_y, ctx.tiles[8]);
461 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + max, base_y,
462 ctx.tiles[14]);
463 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x, base_y + max,
464 ctx.tiles[17]);
465 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + max, base_y + max,
466 ctx.tiles[23]);
467
468 // Edges and interior
469 for (int xx = 1; xx < max; ++xx) {
470 for (int yy = 1; yy < max; ++yy) {
471 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + xx, base_y + yy,
472 ctx.tiles[0]);
473 }
474 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + xx, base_y,
475 ctx.tiles[10]);
476 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + xx, base_y + max,
477 ctx.tiles[19]);
478 }
479
480 for (int yy = 1; yy < max; ++yy) {
481 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x, base_y + yy,
482 ctx.tiles[9]);
483 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + max, base_y + yy,
484 ctx.tiles[15]);
485 }
486}
487
489 // ASM: RoomDraw_Spike2x2In4x4SuperSquare ($019708)
490 // Draws 2x2 spike patterns in a tiled grid
491 int size_x = ((ctx.object.size_ >> 2) & 0x03) + 1;
492 int size_y = (ctx.object.size_ & 0x03) + 1;
493
494 if (ctx.tiles.size() < 4) return;
495
496 for (int sy = 0; sy < size_y; ++sy) {
497 for (int sx = 0; sx < size_x; ++sx) {
498 int base_x = ctx.object.x_ + (sx * 2);
499 int base_y = ctx.object.y_ + (sy * 2);
500
501 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x, base_y,
502 ctx.tiles[0]);
503 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 1, base_y,
504 ctx.tiles[2]);
505 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x, base_y + 1,
506 ctx.tiles[1]);
507 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 1, base_y + 1,
508 ctx.tiles[3]);
509 }
510 }
511}
512
514 // ASM: Object 0xDD - Table rock pattern
515 int size_x = ((ctx.object.size_ >> 2) & 0x03);
516 int size_y = (ctx.object.size_ & 0x03);
517
518 if (ctx.tiles.size() < 16) return;
519
520 int right_x = ctx.object.x_ + (3 + (size_x * 2));
521 int bottom_y = ctx.object.y_ + (3 + (size_y * 2));
522
523 // Interior
524 for (int xx = 0; xx < size_x + 1; ++xx) {
525 for (int yy = 0; yy < size_y + 1; ++yy) {
526 int base_x = ctx.object.x_ + (xx * 2);
527 int base_y = ctx.object.y_ + (yy * 2);
528 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 1, base_y + 1,
529 ctx.tiles[5]);
530 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 2, base_y + 1,
531 ctx.tiles[6]);
532 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 1, base_y + 2,
533 ctx.tiles[9]);
534 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 2, base_y + 2,
535 ctx.tiles[10]);
536 }
537 }
538
539 // Left/right borders
540 for (int yy = 0; yy < size_y + 1; ++yy) {
541 int base_y = ctx.object.y_ + (yy * 2);
542 DrawRoutineUtils::WriteTile8(ctx.target_bg, ctx.object.x_, base_y + 1,
543 ctx.tiles[4]);
544 DrawRoutineUtils::WriteTile8(ctx.target_bg, ctx.object.x_, base_y + 2,
545 ctx.tiles[8]);
546
547 DrawRoutineUtils::WriteTile8(ctx.target_bg, right_x, base_y + 1,
548 ctx.tiles[7]);
549 DrawRoutineUtils::WriteTile8(ctx.target_bg, right_x, base_y + 2,
550 ctx.tiles[11]);
551 }
552
553 // Top/bottom borders
554 for (int xx = 0; xx < size_x + 1; ++xx) {
555 int base_x = ctx.object.x_ + (xx * 2);
556 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 1, ctx.object.y_,
557 ctx.tiles[1]);
558 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 2, ctx.object.y_,
559 ctx.tiles[2]);
560
561 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 1, bottom_y,
562 ctx.tiles[13]);
563 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 2, bottom_y,
564 ctx.tiles[14]);
565 }
566
567 // Corners
569 ctx.tiles[0]);
571 ctx.tiles[12]);
573 ctx.tiles[3]);
574 DrawRoutineUtils::WriteTile8(ctx.target_bg, right_x, bottom_y,
575 ctx.tiles[15]);
576}
577
579 // ASM: RoomDraw_WaterOverlayA8x8_1to16 ($0195D6) / RoomDraw_WaterOverlayB8x8
580 // NOTE: In the original game, this is an HDMA control object that sets up
581 // the wavy water distortion effect. It doesn't draw tiles directly.
582 // For the editor, we draw the available tile data as a visual indicator.
583
584 int size_x = ((ctx.object.size_ >> 2) & 0x03);
585 int size_y = (ctx.object.size_ & 0x03);
586
587 int count_x = size_x + 2;
588 int count_y = size_y + 2;
589
590 if (ctx.tiles.empty()) return;
591 if (ctx.tiles.size() < 8) {
592 // Fallback for abbreviated tile payloads: still stamp a visible overlay.
593 for (int yy = 0; yy < count_y; ++yy) {
594 for (int xx = 0; xx < count_x; ++xx) {
595 int base_x = ctx.object.x_ + (xx * 4);
596 int base_y = ctx.object.y_ + (yy * 4);
597 const auto& tile =
598 ctx.tiles[static_cast<size_t>((xx + yy) % ctx.tiles.size())];
599 for (int y = 0; y < 4; ++y) {
600 for (int x = 0; x < 4; ++x) {
601 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + y,
602 tile);
603 }
604 }
605 }
606 }
607 return;
608 }
609
610 for (int yy = 0; yy < count_y; ++yy) {
611 for (int xx = 0; xx < count_x; ++xx) {
612 int base_x = ctx.object.x_ + (xx * 4);
613 int base_y = ctx.object.y_ + (yy * 4);
614
615 for (int x = 0; x < 4; ++x) {
616 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y,
617 ctx.tiles[x]);
618 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 2,
619 ctx.tiles[x]);
620 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 1,
621 ctx.tiles[4 + x]);
622 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x, base_y + 3,
623 ctx.tiles[4 + x]);
624 }
625 }
626 }
627}
628
629// ============================================================================
630// Stair Routines
631// ============================================================================
632
634 // ASM: RoomDraw_InterRoomFatStairsUp ($01A41B)
635 // Uses tile data at obj1088, draws 4x4 pattern
636 // In original game, registers position in $06B0 for transition handling
637 // For editor display, we just draw the visual representation
638
639 if (ctx.tiles.size() < 16) return;
640
641 // Draw 4x4 stair pattern
642 for (int y = 0; y < 4; ++y) {
643 for (int x = 0; x < 4; ++x) {
644 size_t tile_idx = static_cast<size_t>(y * 4 + x);
646 ctx.object.y_ + y, ctx.tiles[tile_idx]);
647 }
648 }
649}
650
652 // ASM: RoomDraw_InterRoomFatStairsDownA ($01A458)
653 // Uses tile data at obj10A8
654 DrawInterRoomFatStairsUp(ctx); // Same visual structure
655}
656
658 // ASM: RoomDraw_InterRoomFatStairsDownB ($01A486)
659 // Uses tile data at obj10A8
660 DrawInterRoomFatStairsUp(ctx); // Same visual structure
661}
662
663void DrawSpiralStairs(const DrawContext& ctx, bool going_up, bool is_upper) {
664 // ASM: RoomDraw_SpiralStairsGoingUpUpper, etc.
665 // Calls RoomDraw_1x3N_rightwards with A=4 -> 4 columns x 3 rows = 12 tiles
666 // Tile order is COLUMN-MAJOR (down first, then right)
667 (void)going_up;
668 (void)is_upper;
669
670 if (ctx.tiles.size() < 12) return;
671
672 // Draw 4x3 pattern in COLUMN-MAJOR order (matching ASM)
673 int tid = 0;
674 for (int x = 0; x < 4; ++x) {
675 for (int y = 0; y < 3; ++y) {
677 ctx.object.y_ + y, ctx.tiles[tid++]);
678 }
679 }
680}
681
682void DrawAutoStairs(const DrawContext& ctx) {
683 // ASM: RoomDraw_AutoStairs* routines
684 // Multi-layer or merged layer stair patterns
685 if (ctx.tiles.size() < 16) return;
686
687 for (int y = 0; y < 4; ++y) {
688 for (int x = 0; x < 4; ++x) {
689 size_t tile_idx = static_cast<size_t>(y * 4 + x);
691 ctx.object.y_ + y, ctx.tiles[tile_idx]);
692 }
693 }
694}
695
697 // ASM: RoomDraw_StraightInterroomStairs* routines
698 // North/South, Up/Down variants
699 if (ctx.tiles.size() < 16) return;
700
701 for (int y = 0; y < 4; ++y) {
702 for (int x = 0; x < 4; ++x) {
703 size_t tile_idx = static_cast<size_t>(y * 4 + x);
705 ctx.object.y_ + y, ctx.tiles[tile_idx]);
706 }
707 }
708}
709
710// ============================================================================
711// Interactive Object Routines
712// ============================================================================
713
714void DrawPrisonCell(const DrawContext& ctx) {
715 // ASM: RoomDraw_PrisonCell ($019C44)
716 // Draws prison cell bars to BOTH BG layers with horizontal flip for symmetry
717 // The ASM writes to $7E2xxx (BG1) and also uses ORA #$4000 for horizontal flip
718 // Pattern: 5 iterations drawing a complex bar pattern
719
720 if (ctx.tiles.size() < 6) return;
721
722 // Prison cell layout based on ASM analysis:
723 // The routine draws 5 columns of bars, each with specific tile patterns
724 // Tiles at positions: (x, y), (x+7, y) for outer bars
725 // Middle bars with horizontal flip on one side
726
727 int base_x = ctx.object.x_;
728 int base_y = ctx.object.y_;
729
730 // Draw the prison cell pattern - 5 vertical bar segments
731 for (int col = 0; col < 5; ++col) {
732 int x_offset = col;
733
734 // Each column has 4 rows of tiles
735 for (int row = 0; row < 4; ++row) {
736 size_t tile_idx = (row < static_cast<int>(ctx.tiles.size())) ? row : 0;
737
738 // Left side bar
739 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + x_offset,
740 base_y + row, ctx.tiles[tile_idx]);
741
742 // Right side bar (mirrored horizontally)
743 auto mirrored_tile = ctx.tiles[tile_idx];
744 mirrored_tile.horizontal_mirror_ = !mirrored_tile.horizontal_mirror_;
745 DrawRoutineUtils::WriteTile8(ctx.target_bg, base_x + 9 - x_offset,
746 base_y + row, mirrored_tile);
747 }
748 }
749
750 // If we have a secondary BG buffer, draw the same pattern there
751 // This ensures the prison bars appear on both background layers
752 if (ctx.HasSecondaryBG()) {
753 for (int col = 0; col < 5; ++col) {
754 int x_offset = col;
755
756 for (int row = 0; row < 4; ++row) {
757 size_t tile_idx = (row < static_cast<int>(ctx.tiles.size())) ? row : 0;
758
759 // Left side bar
760 DrawRoutineUtils::WriteTile8(*ctx.secondary_bg, base_x + x_offset,
761 base_y + row, ctx.tiles[tile_idx]);
762
763 // Right side bar (mirrored)
764 auto mirrored_tile = ctx.tiles[tile_idx];
765 mirrored_tile.horizontal_mirror_ = !mirrored_tile.horizontal_mirror_;
766 DrawRoutineUtils::WriteTile8(*ctx.secondary_bg, base_x + 9 - x_offset,
767 base_y + row, mirrored_tile);
768 }
769 }
770 }
771}
772
773void DrawBigKeyLock(const DrawContext& ctx) {
774 // ASM: RoomDraw_BigKeyLock ($0198AE)
775 // Checks room flags via RoomFlagMask to see if lock is already opened
776 // For editor, we draw the closed state by default
777
778 bool is_opened = false;
779 if (ctx.state) {
780 // Check if this specific lock has been opened via door state
781 is_opened = ctx.state->IsDoorOpen(ctx.room_id, 0); // Lock uses door slot 0
782 }
783
784 if (is_opened) {
785 // Draw open lock (if different tiles available)
786 if (ctx.tiles.size() >= 8) {
787 // Use second set of tiles for open state
788 for (int y = 0; y < 2; ++y) {
789 for (int x = 0; x < 2; ++x) {
791 ctx.object.y_ + y,
792 ctx.tiles[4 + y * 2 + x]);
793 }
794 }
795 return;
796 }
797 }
798
799 // Draw closed lock (2x2 pattern)
800 if (ctx.tiles.size() >= 4) {
801 for (int y = 0; y < 2; ++y) {
802 for (int x = 0; x < 2; ++x) {
804 ctx.object.y_ + y, ctx.tiles[y * 2 + x]);
805 }
806 }
807 }
808}
809
811 // ASM: RoomDraw_BombableFloor ($019B7A)
812 // Checks room flags to see if floor has been bombed
813
814 bool is_bombed = false;
815 if (ctx.state) {
816 is_bombed = ctx.state->IsFloorBombable(ctx.room_id);
817 }
818
819 if (is_bombed) {
820 // Draw hole (use second tile set if available)
821 if (ctx.tiles.size() >= 8) {
822 for (int y = 0; y < 2; ++y) {
823 for (int x = 0; x < 2; ++x) {
825 ctx.object.y_ + y,
826 ctx.tiles[4 + y * 2 + x]);
827 }
828 }
829 return;
830 }
831 }
832
833 // Draw intact floor (2x2 pattern)
834 if (ctx.tiles.size() >= 4) {
835 for (int y = 0; y < 2; ++y) {
836 for (int x = 0; x < 2; ++x) {
838 ctx.object.y_ + y, ctx.tiles[y * 2 + x]);
839 }
840 }
841 }
842}
843
844void DrawMovingWall(const DrawContext& ctx, bool is_west) {
845 // ASM: RoomDraw_MovingWallWest ($019316), RoomDraw_MovingWallEast ($01935C)
846 // Checks if wall has moved based on game state
847 (void)is_west; // Direction affects which way wall moves
848
849 bool has_moved = false;
850 if (ctx.state) {
851 has_moved = ctx.state->IsWallMoved(ctx.room_id);
852 }
853
854 // Draw wall in current position
855 // Size determines wall length
856 int size = (ctx.object.size_ & 0x0F) + 1;
857
858 if (ctx.tiles.size() < 4) return;
859
860 for (int s = 0; s < size; ++s) {
861 int offset = has_moved ? 2 : 0; // Offset position if wall has moved
862 int x = ctx.object.x_ + offset;
863 int y = ctx.object.y_ + (s * 2);
864
865 // Draw 2x2 wall segment
866 for (int dy = 0; dy < 2; ++dy) {
867 for (int dx = 0; dx < 2; ++dx) {
868 DrawRoutineUtils::WriteTile8(ctx.target_bg, x + dx, y + dy,
869 ctx.tiles[dy * 2 + dx]);
870 }
871 }
872 }
873}
874
875// ============================================================================
876// Water Face Variants
877// ============================================================================
878
879namespace {
880constexpr int kWaterFaceWidthTiles = 4;
881
882void DrawWaterFaceRows(const DrawContext& ctx, int row_count, int tile_offset) {
883 if (row_count <= 0 || tile_offset < 0 || ctx.tiles.empty()) {
884 return;
885 }
886
887 if (tile_offset >= static_cast<int>(ctx.tiles.size())) {
888 return;
889 }
890
891 const int available_tiles = static_cast<int>(ctx.tiles.size()) - tile_offset;
892 const int available_rows = available_tiles / kWaterFaceWidthTiles;
893 const int rows_to_draw = std::min(row_count, available_rows);
894
895 for (int row = 0; row < rows_to_draw; ++row) {
896 const int row_base = tile_offset + (row * kWaterFaceWidthTiles);
897 for (int col = 0; col < kWaterFaceWidthTiles; ++col) {
899 ctx.object.y_ + row,
900 ctx.tiles[row_base + col]);
901 }
902 }
903}
904} // namespace
905
907 // ASM: RoomDraw_EmptyWaterFace ($019D29)
908 //
909 // usdasm behavior:
910 // - Base state draws a 4x3 face using data at offset 0x1614.
911 // - "Water active" branch draws a 4x5 variant using data at offset 0x162C.
912 //
913 // IMPORTANT: this uses dedicated water-face state, not door state. Tying
914 // this branch to IsDoorOpen created cross-feature rendering regressions.
915 const bool water_active =
916 (ctx.state != nullptr) && ctx.state->IsWaterFaceActive(ctx.room_id);
917
918 const int row_count = water_active ? 5 : 3;
919 const int tile_offset = water_active ? 12 : 0; // 0x162C - 0x1614 = 24 bytes
920 DrawWaterFaceRows(ctx, row_count, tile_offset);
921}
922
924 // ASM: RoomDraw_SpittingWaterFace ($019D64)
925 // Draws a 4x5 face/spout shape.
926 DrawWaterFaceRows(ctx, /*row_count=*/5, /*tile_offset=*/0);
927}
928
930 // ASM: RoomDraw_DrenchingWaterFace ($019D83)
931 // Draws a 4x7 continuous stream.
932 DrawWaterFaceRows(ctx, /*row_count=*/7, /*tile_offset=*/0);
933}
934
935// ============================================================================
936// Chest Platform Multi-Part Routines
937// ============================================================================
938
940 // ASM: RoomDraw_ClosedChestPlatform ($018CC7)
941 // Complex structure: horizontal wall top, vertical walls sides
942
943 int size_x = (ctx.object.size_ & 0x0F) + 4; // Width is size + 4
944 int size_y = ((ctx.object.size_ >> 4) & 0x0F) + 1;
945
946 if (ctx.tiles.size() < 16) return;
947
948 // Draw top horizontal wall with corners
949 for (int x = 0; x < size_x; ++x) {
950 // Top row
951 size_t tile_idx = (x == 0) ? 0 : ((x == size_x - 1) ? 2 : 1);
953 ctx.object.y_, ctx.tiles[tile_idx]);
954 }
955
956 // Draw vertical walls on sides
957 for (int y = 1; y < size_y + 1; ++y) {
958 // Left wall
960 ctx.object.y_ + y, ctx.tiles[3]);
961 // Right wall
962 DrawRoutineUtils::WriteTile8(ctx.target_bg, ctx.object.x_ + size_x - 1,
963 ctx.object.y_ + y, ctx.tiles[4]);
964 }
965
966 // Draw bottom horizontal wall with corners
967 int bottom_y = ctx.object.y_ + size_y + 1;
968 for (int x = 0; x < size_x; ++x) {
969 size_t tile_idx = (x == 0) ? 5 : ((x == size_x - 1) ? 7 : 6);
970 DrawRoutineUtils::WriteTile8(ctx.target_bg, ctx.object.x_ + x, bottom_y,
971 ctx.tiles[tile_idx]);
972 }
973}
974
976 // ASM: RoomDraw_ChestPlatformHorizontalWallWithCorners ($018D0D)
977 int width = (ctx.object.size_ & 0x0F) + 1;
978
979 if (ctx.tiles.size() < 3) return;
980
981 for (int x = 0; x < width; ++x) {
982 size_t tile_idx = (x == 0) ? 0 : ((x == width - 1) ? 2 : 1);
984 ctx.object.y_, ctx.tiles[tile_idx]);
985 }
986}
987
989 // ASM: RoomDraw_ChestPlatformVerticalWall ($019E70)
990 int height = (ctx.object.size_ & 0x0F) + 1;
991
992 if (ctx.tiles.empty()) return;
993
994 for (int y = 0; y < height; ++y) {
996 ctx.object.y_ + y, ctx.tiles[0]);
997 }
998}
999
1000void RegisterSpecialRoutines(std::vector<DrawRoutineInfo>& registry) {
1001 // Note: Routine IDs are assigned based on the assembly routine table
1002 // These special routines handle chests, doors, and other non-standard objects
1003
1004 // Chest routine - uses a wrapper since it needs chest_index
1005 registry.push_back(DrawRoutineInfo{
1006 .id = 39, // DrawChest (special index)
1007 .name = "Chest",
1008 .function =
1009 [](const DrawContext& ctx) {
1010 // Default chest index 0 - actual index tracked externally
1011 DrawChest(ctx, 0);
1012 },
1013 .draws_to_both_bgs = false,
1014 .base_width = 2,
1015 .base_height = 2,
1016 .min_tiles = 4, // 2x2 block
1018 });
1019
1020 registry.push_back(DrawRoutineInfo{
1021 .id = 38, // DrawNothing
1022 .name = "Nothing",
1023 .function = DrawNothing,
1024 .draws_to_both_bgs = false,
1025 .base_width = 0,
1026 .base_height = 0,
1028 });
1029
1030 registry.push_back(DrawRoutineInfo{
1031 .id = 26, // DrawDoorSwitcherer
1032 .name = "DoorSwitcherer",
1033 .function = DrawDoorSwitcherer,
1034 .draws_to_both_bgs = false,
1035 .base_width = 1,
1036 .base_height = 1,
1037 .min_tiles = 1, // at least one tile for door graphic
1039 });
1040
1041 registry.push_back(DrawRoutineInfo{
1042 .id = 33, // DrawSomariaLine
1043 .name = "SomariaLine",
1044 .function = DrawSomariaLine,
1045 .draws_to_both_bgs = false,
1046 .base_width = 0, // Variable
1047 .base_height = 0,
1049 });
1050
1051 registry.push_back(DrawRoutineInfo{
1052 .id = 34, // DrawWaterFace
1053 .name = "WaterFace",
1054 .function = DrawWaterFace,
1055 .draws_to_both_bgs = false,
1056 .base_width = 2,
1057 .base_height = 2,
1058 .min_tiles = 4, // 2x2 block
1060 });
1061
1062 // ============================================================================
1063 // SuperSquare Routines (Phase 4) - IDs 56-64
1064 // ============================================================================
1065
1066 registry.push_back(DrawRoutineInfo{
1067 .id = 56, // Draw4x4BlocksIn4x4SuperSquare
1068 .name = "4x4BlocksIn4x4SuperSquare",
1070 .draws_to_both_bgs = false,
1071 .base_width = 0, // Variable: width = (((size >> 2) & 3) + 1) * 4
1072 .base_height = 0, // Variable: height = ((size & 3) + 1) * 4
1074 });
1075
1076 registry.push_back(DrawRoutineInfo{
1077 .id = 57, // Draw3x3FloorIn4x4SuperSquare
1078 .name = "3x3FloorIn4x4SuperSquare",
1079 .function = Draw3x3FloorIn4x4SuperSquare,
1080 .draws_to_both_bgs = false,
1081 .base_width = 0, // Variable
1082 .base_height = 0, // Variable
1084 });
1085
1086 registry.push_back(DrawRoutineInfo{
1087 .id = 58, // Draw4x4FloorIn4x4SuperSquare
1088 .name = "4x4FloorIn4x4SuperSquare",
1089 .function = Draw4x4FloorIn4x4SuperSquare,
1090 .draws_to_both_bgs = false,
1091 .base_width = 0, // Variable
1092 .base_height = 0, // Variable
1094 });
1095
1096 registry.push_back(DrawRoutineInfo{
1097 .id = 59, // Draw4x4FloorOneIn4x4SuperSquare
1098 .name = "4x4FloorOneIn4x4SuperSquare",
1100 .draws_to_both_bgs = false,
1101 .base_width = 0, // Variable
1102 .base_height = 0, // Variable
1104 });
1105
1106 registry.push_back(DrawRoutineInfo{
1107 .id = 60, // Draw4x4FloorTwoIn4x4SuperSquare
1108 .name = "4x4FloorTwoIn4x4SuperSquare",
1110 .draws_to_both_bgs = false,
1111 .base_width = 0, // Variable
1112 .base_height = 0, // Variable
1114 });
1115
1116 registry.push_back(DrawRoutineInfo{
1117 .id = 61, // DrawBigHole4x4_1to16
1118 .name = "BigHole4x4_1to16",
1119 .function = DrawBigHole4x4_1to16,
1120 .draws_to_both_bgs = false,
1121 .base_width = 0, // Variable
1122 .base_height = 0, // Variable
1124 });
1125
1126 registry.push_back(DrawRoutineInfo{
1127 .id = 62, // DrawSpike2x2In4x4SuperSquare
1128 .name = "Spike2x2In4x4SuperSquare",
1129 .function = DrawSpike2x2In4x4SuperSquare,
1130 .draws_to_both_bgs = false,
1131 .base_width = 0, // Variable
1132 .base_height = 0, // Variable
1134 });
1135
1136 registry.push_back(DrawRoutineInfo{
1137 .id = 63, // DrawTableRock4x4_1to16
1138 .name = "TableRock4x4_1to16",
1139 .function = DrawTableRock4x4_1to16,
1140 .draws_to_both_bgs = false,
1141 .base_width = 0, // Variable
1142 .base_height = 0, // Variable
1144 });
1145
1146 registry.push_back(DrawRoutineInfo{
1147 .id = 64, // DrawWaterOverlay8x8_1to16
1148 .name = "WaterOverlay8x8_1to16",
1149 .function = DrawWaterOverlay8x8_1to16,
1150 .draws_to_both_bgs = false,
1151 .base_width = 0, // Variable
1152 .base_height = 0, // Variable
1154 });
1155
1156 // Stair routines (IDs 83-88)
1157 registry.push_back(DrawRoutineInfo{
1158 .id = 83, // DrawInterRoomFatStairsUp
1159 .name = "InterRoomFatStairsUp",
1160 .function = DrawInterRoomFatStairsUp,
1161 .draws_to_both_bgs = false,
1162 .base_width = 4,
1163 .base_height = 4,
1164 .min_tiles = 16, // 4x4 stair pattern
1166 });
1167
1168 registry.push_back(DrawRoutineInfo{
1169 .id = 84, // DrawInterRoomFatStairsDownA
1170 .name = "InterRoomFatStairsDownA",
1171 .function = DrawInterRoomFatStairsDownA,
1172 .draws_to_both_bgs = false,
1173 .base_width = 4,
1174 .base_height = 4,
1175 .min_tiles = 16, // 4x4 stair pattern
1177 });
1178
1179 registry.push_back(DrawRoutineInfo{
1180 .id = 85, // DrawInterRoomFatStairsDownB
1181 .name = "InterRoomFatStairsDownB",
1182 .function = DrawInterRoomFatStairsDownB,
1183 .draws_to_both_bgs = false,
1184 .base_width = 4,
1185 .base_height = 4,
1186 .min_tiles = 16, // 4x4 stair pattern
1188 });
1189
1190 registry.push_back(DrawRoutineInfo{
1191 .id = 86, // DrawAutoStairs
1192 .name = "AutoStairs",
1193 .function = DrawAutoStairs,
1194 .draws_to_both_bgs = false,
1195 .base_width = 4,
1196 .base_height = 4,
1197 .min_tiles = 16, // 4x4 stair pattern
1199 });
1200
1201 registry.push_back(DrawRoutineInfo{
1202 .id = 87, // DrawStraightInterRoomStairs
1203 .name = "StraightInterRoomStairs",
1204 .function = DrawStraightInterRoomStairs,
1205 .draws_to_both_bgs = false,
1206 .base_width = 4,
1207 .base_height = 4,
1208 .min_tiles = 16, // 4x4 stair pattern
1210 });
1211
1212 // Spiral stairs variants (IDs 88-91)
1213 registry.push_back(DrawRoutineInfo{
1214 .id = 88, // DrawSpiralStairsGoingUpUpper
1215 .name = "SpiralStairsGoingUpUpper",
1216 .function =
1217 [](const DrawContext& ctx) { DrawSpiralStairs(ctx, true, true); },
1218 .draws_to_both_bgs = false,
1219 .base_width = 4,
1220 .base_height = 3, // 4x3 pattern
1221 .min_tiles = 12, // 4x3 block
1223 });
1224
1225 registry.push_back(DrawRoutineInfo{
1226 .id = 89, // DrawSpiralStairsGoingDownUpper
1227 .name = "SpiralStairsGoingDownUpper",
1228 .function =
1229 [](const DrawContext& ctx) { DrawSpiralStairs(ctx, false, true); },
1230 .draws_to_both_bgs = false,
1231 .base_width = 4,
1232 .base_height = 3, // 4x3 pattern
1233 .min_tiles = 12, // 4x3 block
1235 });
1236
1237 registry.push_back(DrawRoutineInfo{
1238 .id = 90, // DrawSpiralStairsGoingUpLower
1239 .name = "SpiralStairsGoingUpLower",
1240 .function =
1241 [](const DrawContext& ctx) { DrawSpiralStairs(ctx, true, false); },
1242 .draws_to_both_bgs = false,
1243 .base_width = 4,
1244 .base_height = 3, // 4x3 pattern
1245 .min_tiles = 12, // 4x3 block
1247 });
1248
1249 registry.push_back(DrawRoutineInfo{
1250 .id = 91, // DrawSpiralStairsGoingDownLower
1251 .name = "SpiralStairsGoingDownLower",
1252 .function =
1253 [](const DrawContext& ctx) { DrawSpiralStairs(ctx, false, false); },
1254 .draws_to_both_bgs = false,
1255 .base_width = 4,
1256 .base_height = 3, // 4x3 pattern
1257 .min_tiles = 12, // 4x3 block
1259 });
1260
1261 // Interactive object routines (IDs 92-95)
1262 registry.push_back(DrawRoutineInfo{
1263 .id = 92, // DrawBigKeyLock
1264 .name = "BigKeyLock",
1265 .function = DrawBigKeyLock,
1266 .draws_to_both_bgs = false,
1267 .base_width = 2,
1268 .base_height = 2,
1269 .min_tiles = 4, // 2x2 block
1271 });
1272
1273 registry.push_back(DrawRoutineInfo{
1274 .id = 93, // DrawBombableFloor
1275 .name = "BombableFloor",
1276 .function = DrawBombableFloor,
1277 .draws_to_both_bgs = false,
1278 .base_width = 2,
1279 .base_height = 2,
1280 .min_tiles = 4, // 2x2 block
1282 });
1283
1284 // Water face variants (IDs 94-96)
1285 registry.push_back(DrawRoutineInfo{
1286 .id = 94, // DrawEmptyWaterFace
1287 .name = "EmptyWaterFace",
1288 .function = DrawEmptyWaterFace,
1289 .draws_to_both_bgs = false,
1290 .base_width = 4,
1291 .base_height = 3,
1292 .min_tiles = 12, // base 4x3 variant
1294 });
1295
1296 registry.push_back(DrawRoutineInfo{
1297 .id = 95, // DrawSpittingWaterFace
1298 .name = "SpittingWaterFace",
1299 .function = DrawSpittingWaterFace,
1300 .draws_to_both_bgs = false,
1301 .base_width = 4,
1302 .base_height = 5,
1303 .min_tiles = 20, // 4x5 variant
1305 });
1306
1307 registry.push_back(DrawRoutineInfo{
1308 .id = 96, // DrawDrenchingWaterFace
1309 .name = "DrenchingWaterFace",
1310 .function = DrawDrenchingWaterFace,
1311 .draws_to_both_bgs = false,
1312 .base_width = 4,
1313 .base_height = 7,
1314 .min_tiles = 28, // 4x7 variant
1316 });
1317
1318 // Prison cell (Type 3 objects 0x20D, 0x217) draws to both BG layers.
1319 registry.push_back(DrawRoutineInfo{
1320 .id = 97, // DrawPrisonCell
1321 .name = "PrisonCell",
1322 .function = DrawPrisonCell,
1323 .draws_to_both_bgs = true,
1324 .base_width = 10, // Columns x..x+9
1325 .base_height = 4,
1327 });
1328
1329 // Chest platform routines - use canonical IDs from DrawRoutineIds
1330 registry.push_back(DrawRoutineInfo{
1332 .name = "ClosedChestPlatform",
1333 .function = DrawClosedChestPlatform,
1334 .draws_to_both_bgs = false,
1335 .base_width = 0, // Variable: width = (size & 0x0F) + 4
1336 .base_height = 0, // Variable: height = ((size >> 4) & 0x0F) + 1
1338 });
1339
1340 // Moving wall routines
1341 registry.push_back(DrawRoutineInfo{
1343 .name = "MovingWallWest",
1344 .function = [](const DrawContext& ctx) {
1345 DrawMovingWall(ctx, /*is_west=*/true);
1346 },
1347 .draws_to_both_bgs = false,
1348 .base_width = 4,
1349 .base_height = 8,
1350 .min_tiles = 4,
1352 });
1353
1354 registry.push_back(DrawRoutineInfo{
1356 .name = "MovingWallEast",
1357 .function = [](const DrawContext& ctx) {
1358 DrawMovingWall(ctx, /*is_west=*/false);
1359 },
1360 .draws_to_both_bgs = false,
1361 .base_width = 4,
1362 .base_height = 8,
1363 .min_tiles = 4,
1365 });
1366
1367 registry.push_back(DrawRoutineInfo{
1369 .name = "OpenChestPlatform",
1370 .function = [](const DrawContext& ctx) {
1371 // Open chest platform - draws multi-segment pattern
1372 // Size: width = (size & 0x0F) + 1, segments = ((size >> 4) & 0x0F) * 2 + 5
1373 int width = (ctx.object.size_ & 0x0F) + 1;
1374 int segments = ((ctx.object.size_ >> 4) & 0x0F) * 2 + 5;
1375 // For geometry purposes, just set reasonable bounds
1376 for (int s = 0; s < segments && s < 8; ++s) {
1377 for (int x = 0; x < width && x < 8; ++x) {
1378 if (ctx.tiles.size() > 0) {
1379 size_t idx = (s * width + x) % ctx.tiles.size();
1380 DrawRoutineUtils::WriteTile8(ctx.target_bg,
1381 ctx.object.x_ + x, ctx.object.y_ + s, ctx.tiles[idx]);
1382 }
1383 }
1384 }
1385 },
1386 .draws_to_both_bgs = false,
1387 .base_width = 0, // Variable
1388 .base_height = 0, // Variable
1390 });
1391
1392 // Vertical rails with CORNER+MIDDLE+END pattern (ID 117) - objects 0x8A-0x8C
1393 // Matches horizontal rail 0x22 but in vertical orientation
1394 registry.push_back(DrawRoutineInfo{
1396 .name = "DownwardsHasEdge1x1_1to16_plus23",
1397 .function = [](const DrawContext& ctx) {
1398 // CORNER+MIDDLE+END pattern vertically
1399 int size = ctx.object.size_ & 0x0F;
1400 int count = size + 21;
1401 if (ctx.tiles.size() < 3) return;
1402
1403 int tile_y = ctx.object.y_;
1404 // Corner
1405 DrawRoutineUtils::WriteTile8(ctx.target_bg, ctx.object.x_, tile_y, ctx.tiles[0]);
1406 tile_y++;
1407 // Middle tiles
1408 for (int s = 0; s < count; s++) {
1409 DrawRoutineUtils::WriteTile8(ctx.target_bg, ctx.object.x_, tile_y, ctx.tiles[1]);
1410 tile_y++;
1411 }
1412 // End tile
1413 DrawRoutineUtils::WriteTile8(ctx.target_bg, ctx.object.x_, tile_y, ctx.tiles[2]);
1414 },
1415 .draws_to_both_bgs = false,
1416 .base_width = 1,
1417 .base_height = 23, // size + 23
1418 .min_tiles = 3, // corner + middle + end tiles
1420 });
1421
1422 // Custom Object routine (ID 130) - Oracle of Secrets objects 0x31, 0x32
1423 // These use external binary files instead of ROM tile data.
1424 // CustomDraw() handles feature-flag gating and binary file lookup.
1425 registry.push_back(DrawRoutineInfo{
1427 .name = "CustomObject",
1428 .function = CustomDraw,
1429 .draws_to_both_bgs = false,
1430 .base_width = 0, // Variable: depends on binary file content
1431 .base_height = 0, // Variable: depends on binary file content
1433 });
1434}
1435
1436} // namespace draw_routines
1437} // namespace zelda3
1438} // namespace yaze
static Flags & get()
Definition features.h:118
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 IsDoorOpen(int room_id, int door_index) const =0
#define LOG_DEBUG(category, format,...)
Definition log.h:103
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 DrawWaterFaceRows(const DrawContext &ctx, int row_count, int tile_offset)
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 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 DrawChest(const DrawContext &ctx, int chest_index)
Draw chest object (big or small) with open/closed state support.
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 Draw4x4FloorTwoIn4x4SuperSquare(const DrawContext &ctx)
Draw two 4x4 floor pattern variant.
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 DrawSpiralStairs(const DrawContext &ctx, bool going_up, bool is_upper)
Draw spiral stairs (Type 2 objects 0x138-0x13B)
void DrawSomariaLine(const DrawContext &ctx)
Draw Somaria line in various directions.
void DrawBombableFloor(const DrawContext &ctx)
Draw bombable floor (Type 3 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 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 DrawStraightInterRoomStairs(const DrawContext &ctx)
Draw straight inter-room stairs (Type 3 objects 0x21E-0x229)
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 DrawMovingWall(const DrawContext &ctx, bool is_west)
Draw moving wall (Type 1 objects 0xCD, 0xCE)
void DrawChestPlatformHorizontalWall(const DrawContext &ctx)
Draw chest platform horizontal wall section.
void DrawWaterOverlay8x8_1to16(const DrawContext &ctx)
Draw water overlay 8x8 pattern.
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 DrawBigKeyLock(const DrawContext &ctx)
Draw big key lock (Type 3 object 0x218)
void DrawPrisonCell(const DrawContext &ctx)
Draw prison cell with bars (Type 3 objects 0x20D, 0x217)
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.