yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
object_drawer.cc
Go to the documentation of this file.
1#include "object_drawer.h"
2
3#include <array>
4#include <cstdio>
5#include <cstring>
6#include <filesystem>
7
8#include "absl/strings/str_format.h"
10#include "core/features.h"
11#include "rom/rom.h"
12#include "rom/snes.h"
13#include "util/log.h"
21
22namespace yaze {
23namespace zelda3 {
24namespace {
25
26bool LockSurface(SDL_Surface* surface) {
27#if SDL_MAJOR_VERSION >= 3
28 return SDL_LockSurface(surface);
29#else
30 return SDL_LockSurface(surface) == 0;
31#endif
32}
33
34void SyncModifiedBitmapToSurface(gfx::Bitmap& bitmap, const char* layer_name) {
35 SDL_Surface* surface = bitmap.surface();
36 if (!bitmap.modified() || surface == nullptr || bitmap.size() == 0) {
37 return;
38 }
39
40 if (bitmap.depth() != 8) {
41 LOG_DEBUG("ObjectDrawer", "%s bitmap depth is not indexed 8bpp: %d",
42 layer_name, bitmap.depth());
43 return;
44 }
45
46 const int width = bitmap.width();
47 const int height = bitmap.height();
48 if (width <= 0 || height <= 0 || surface->w < width || surface->h < height ||
49 surface->pitch < width) {
50 LOG_DEBUG("ObjectDrawer",
51 "%s surface dimensions cannot hold bitmap: surface=%dx%d "
52 "pitch=%d bitmap=%dx%d",
53 layer_name, surface->w, surface->h, surface->pitch, width,
54 height);
55 return;
56 }
57
58 const size_t row_bytes = static_cast<size_t>(width);
59 const size_t required_bytes = row_bytes * static_cast<size_t>(height);
60 if (bitmap.size() < required_bytes) {
61 LOG_DEBUG("ObjectDrawer", "%s bitmap data too small: data=%zu needed=%zu",
62 layer_name, bitmap.size(), required_bytes);
63 return;
64 }
65
66 if (!LockSurface(surface)) {
67 LOG_DEBUG("ObjectDrawer", "%s surface lock failed: %s", layer_name,
68 SDL_GetError());
69 return;
70 }
71 auto* destination = static_cast<uint8_t*>(surface->pixels);
72 const uint8_t* source = bitmap.data();
73 for (int y = 0; y < height; ++y) {
74 std::memcpy(destination + static_cast<size_t>(y) *
75 static_cast<size_t>(surface->pitch),
76 source + static_cast<size_t>(y) * row_bytes, row_bytes);
77 }
78 SDL_UnlockSurface(surface);
79}
80
82 int tile_y) {
83 for (int py = 0; py < 8; ++py) {
84 for (int px = 0; px < 8; ++px) {
85 target.SetPriorityAt(tile_x * 8 + px, tile_y * 8 + py, 1);
86 }
87 }
88}
89
91 gfx::BackgroundBuffer* layout_owner,
92 int tile_x, int tile_y) {
93 // The SNES mutates one physical tilemap. Yaze temporarily splits that map
94 // between layout and object buffers, so update both possible pixel owners
95 // without writing bitmap pixels, coverage, or a new tile word.
96 PromoteTilePriorityOnly(object_owner, tile_x, tile_y);
97 if (layout_owner != nullptr && layout_owner != &object_owner) {
98 PromoteTilePriorityOnly(*layout_owner, tile_x, tile_y);
99 }
100}
101
102} // namespace
103
105 const uint8_t* room_gfx_buffer)
106 : rom_(rom), room_id_(room_id), room_gfx_buffer_(room_gfx_buffer) {
108}
109
110void ObjectDrawer::SetTraceCollector(std::vector<TileTrace>* collector,
111 bool trace_only) {
112 trace_collector_ = collector;
113 trace_only_ = trace_only;
114}
115
117 trace_collector_ = nullptr;
118 trace_only_ = false;
119}
120
122 RoomObject::LayerType layer) {
123 trace_context_.object_id = static_cast<uint16_t>(object.id_);
124 trace_context_.size = object.size_;
125 trace_context_.layer = static_cast<uint8_t>(layer);
126}
127
128void ObjectDrawer::PushTrace(int tile_x, int tile_y,
129 const gfx::TileInfo& tile_info) {
130 if (!trace_collector_) {
131 return;
132 }
133 uint8_t flags = 0;
134 if (tile_info.horizontal_mirror_)
135 flags |= 0x1;
136 if (tile_info.vertical_mirror_)
137 flags |= 0x2;
138 if (tile_info.over_)
139 flags |= 0x4;
140 flags |= static_cast<uint8_t>((tile_info.palette_ & 0x7) << 3);
141
142 TileTrace trace{};
144 trace.size = trace_context_.size;
145 trace.layer = trace_context_.layer;
146 trace.x_tile = static_cast<int16_t>(tile_x);
147 trace.y_tile = static_cast<int16_t>(tile_y);
148 trace.tile_id = tile_info.id_;
149 trace.flags = flags;
150 trace_collector_->push_back(trace);
151}
152
154 int tile_y, const gfx::TileInfo& tile_info,
155 void* user_data) {
156 auto* drawer = static_cast<ObjectDrawer*>(user_data);
157 if (!drawer) {
158 return;
159 }
160 drawer->PushTrace(tile_x, tile_y, tile_info);
161}
162
164 int routine_id, const RoomObject& obj, gfx::BackgroundBuffer& bg,
165 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
166 // Many DrawRoutineRegistry routines are implemented as pure functions that
167 // call DrawRoutineUtils::WriteTile8(), which only writes to BackgroundBuffer's
168 // tile buffer (not the bitmap). Runtime rendering/compositing uses the
169 // bitmap-backed buffers, so we capture tile writes from the pure routine and
170 // replay them via ObjectDrawer::WriteTile8().
171 const auto* info = DrawRoutineRegistry::Get().GetRoutineInfo(routine_id);
172 if (info == nullptr) {
173 LOG_DEBUG("ObjectDrawer", "DrawUsingRegistryRoutine: unknown routine %d",
174 routine_id);
175 return;
176 }
177
178 struct CapturedWrite {
179 int x = 0;
180 int y = 0;
181 gfx::TileInfo tile{};
182 bool secondary = false;
183 };
184
185 struct CaptureState {
186 std::vector<CapturedWrite>* writes = nullptr;
187 gfx::BackgroundBuffer* secondary_bg = nullptr;
188 };
189
190 std::vector<CapturedWrite> writes;
191 writes.reserve(256);
192 CaptureState capture_state{.writes = &writes,
193 .secondary_bg = registry_secondary_bg_};
194
196 [](gfx::BackgroundBuffer* target_bg, int tile_x, int tile_y,
197 const gfx::TileInfo& tile_info, void* user_data) {
198 auto* capture = static_cast<CaptureState*>(user_data);
199 if (!capture || !capture->writes) {
200 return;
201 }
202 capture->writes->push_back(CapturedWrite{
203 .x = tile_x,
204 .y = tile_y,
205 .tile = tile_info,
206 .secondary =
207 target_bg != nullptr && target_bg == capture->secondary_bg,
208 });
209 },
210 &capture_state,
211 /*trace_only=*/true);
212
213 DrawContext ctx{
214 .target_bg = bg,
215 .object = obj,
216 .tiles = tiles,
217 .state = state,
218 .rom = rom_,
219 .room_id = room_id_,
220 .room_gfx_buffer = room_gfx_buffer_,
221 .secondary_bg = registry_secondary_bg_,
222 .target_layout_bg = registry_primary_layout_bg_,
223 };
224 info->function(ctx);
225
227
228 for (const auto& w : writes) {
229 if (w.secondary && registry_secondary_bg_ != nullptr) {
231 WriteTile8(*registry_secondary_bg_, w.x, w.y, w.tile);
232 continue;
233 }
235 WriteTile8(bg, w.x, w.y, w.tile);
236 }
237}
238
240 const RoomObject& object, gfx::BackgroundBuffer& bg1,
241 gfx::BackgroundBuffer& bg2, const gfx::PaletteGroup& palette_group,
242 [[maybe_unused]] const DungeonState* state,
243 gfx::BackgroundBuffer* layout_bg1, gfx::BackgroundBuffer* layout_bg2) {
244 if (!rom_ || !rom_->is_loaded()) {
245 return absl::FailedPreconditionError("ROM not loaded");
246 }
247
249 return absl::FailedPreconditionError("Draw routines not initialized");
250 }
251
252 // Ensure object has tiles loaded
253 auto mutable_obj = const_cast<RoomObject&>(object);
254 mutable_obj.SetRom(rom_);
255 mutable_obj.EnsureTilesLoaded();
256
257 // Select buffer based on layer
258 // Layer 0 (BG1): Main objects - drawn to BG1_Objects (on top of layout)
259 // Layer 1 (BG2): Overlay objects - drawn to BG2_Objects (behind layout)
260 // Layer 2 (BG3): Priority objects (torches) - drawn to BG1_Objects (on top)
261 bool use_bg2 = (object.layer_ == RoomObject::LayerType::BG2);
262 auto& target_bg = use_bg2 ? bg2 : bg1;
263 auto& other_bg = use_bg2 ? bg1 : bg2;
264
265 // Log buffer selection for debugging layer routing
266 LOG_DEBUG("ObjectDrawer", "Object 0x%03X layer=%d -> drawing to %s buffer",
267 object.id_, static_cast<int>(object.layer_),
268 use_bg2 ? "BG2 (behind layout)" : "BG1 (on top of layout)");
269
270 // Check for custom object override first (guarded by feature flag).
271 // We check this BEFORE routine lookup to allow overriding vanilla objects.
272 if (HasActiveCustomObjectOverride(object)) {
273 // Custom objects default to drawing on the target layer only, unless all_bgs_ is set
274 // Mask propagation is difficult without dimensions, so we rely on explicit transparency in the custom object tiles if needed
275
276 // Draw to target layer
279 DrawCustomObject(object, target_bg, mutable_obj.tiles(), state);
280
281 // If marked for both BGs, draw to the other layer too
282 if (object.all_bgs_) {
283 SetTraceContext(object, (&other_bg == &bg1) ? RoomObject::LayerType::BG1
285 DrawCustomObject(object, other_bg, mutable_obj.tiles(), state);
286 }
287 return absl::OkStatus();
288 }
289
290 std::array<gfx::TileInfo, 8> room_floor_tiles;
291 std::span<const gfx::TileInfo> render_tiles = mutable_obj.tiles();
293 (object.id_ == 0x00C4 || object.id_ == 0x00DB)) {
294 const uint8_t floor_graphics =
295 object.id_ == 0x00C4 ? floor1_graphics_ : floor2_graphics_;
296 const auto decoded = gfx::DecodeDungeonFloorTilePattern(
298 floor_graphics);
299 if (!decoded.has_value()) {
300 return absl::DataLossError(
301 absl::StrFormat("Object 0x%03X cannot read room floor pattern %d",
302 object.id_, static_cast<int>(floor_graphics)));
303 }
304 room_floor_tiles = *decoded;
305 render_tiles = room_floor_tiles;
306 }
307
308 // Skip objects that don't have tiles loaded
309 if (render_tiles.empty()) {
310 LOG_DEBUG("ObjectDrawer",
311 "Object 0x%03X at (%d,%d) has NO TILES - skipping", object.id_,
312 object.x_, object.y_);
313 return absl::OkStatus();
314 }
315
316 // Look up draw routine for this object
317 int routine_id = GetDrawRoutineId(object.id_);
318
319 // Log draw routine lookup with tile info
320 LOG_DEBUG("ObjectDrawer",
321 "Object 0x%03X at (%d,%d) size=%d -> routine=%d tiles=%zu",
322 object.id_, object.x_, object.y_, object.size_, routine_id,
323 render_tiles.size());
324
325 if (routine_id < 0 || routine_id >= static_cast<int>(draw_routines_.size())) {
326 LOG_DEBUG("ObjectDrawer",
327 "Object 0x%03X: NO ROUTINE (id=%d, max=%zu) - using fallback 1x1",
328 object.id_, routine_id, draw_routines_.size());
329 // Fallback to simple 1x1 drawing using first 8x8 tile
330 if (!render_tiles.empty()) {
331 const auto& tile_info = render_tiles[0];
334 WriteTile8(target_bg, object.x_, object.y_, tile_info);
335 }
336 return absl::OkStatus();
337 }
338
339 // Null-tile guard: skip routines whose tile payload is too small.
340 // Hack ROMs with abbreviated tile tables would otherwise cause
341 // out-of-bounds access in fixed-size draw patterns.
342 const DrawRoutineInfo* routine_info =
344 if (routine_info && routine_info->min_tiles > 0 &&
345 static_cast<int>(render_tiles.size()) < routine_info->min_tiles) {
346 LOG_WARN("ObjectDrawer",
347 "Object 0x%03X at (%d,%d): tile payload too small "
348 "(%zu < %d required by routine '%s') - skipping",
349 object.id_, object.x_, object.y_, render_tiles.size(),
350 routine_info->min_tiles, routine_info->name.c_str());
351 // Fall through to 1x1 fallback if any tiles are present
352 if (!render_tiles.empty()) {
353 const auto& tile_info = render_tiles[0];
356 WriteTile8(target_bg, object.x_, object.y_, tile_info);
357 }
358 return absl::OkStatus();
359 }
360
361 bool trace_hook_active = false;
362 if (trace_collector_) {
365 trace_hook_active = true;
366 }
367
368 // Check if this should draw to both BG layers.
369 // In the original engine, BothBG routines explicitly write to both tilemaps
370 // regardless of which object list or pass they are executed from.
371 bool is_both_bg = (object.all_bgs_ || RoutineDrawsToBothBGs(routine_id));
372 const bool use_rectangular_bg1_mask =
373 !trace_only_ && object.layer_ == RoomObject::LayerType::BG2 &&
374 !is_both_bg && RequiresRectangularBg1Mask(object);
375 const bool use_pixel_bg1_mask = !trace_only_ &&
376 object.layer_ == RoomObject::LayerType::BG2 &&
377 !is_both_bg && !use_rectangular_bg1_mask;
378
379 registry_secondary_bg_ = nullptr;
381 use_bg2 ? static_cast<const gfx::BackgroundBuffer*>(layout_bg2)
382 : static_cast<const gfx::BackgroundBuffer*>(layout_bg1);
387 gfx::BackgroundBuffer* dispatch_bg = &target_bg;
388
389 // Special routines may need a second buffer for mixed-layer draws or fixed
390 // BG1/BG2 routing regardless of the parsed object-layer flag.
391 if (!is_both_bg && (routine_id == DrawRoutineIds::kAgahnimsAltar ||
392 routine_id == DrawRoutineIds::kFortuneTellerRoom)) {
393 // USDASM writes these fixed room facades directly to the upper tilemap at
394 // $7E2000.
395 dispatch_bg = &bg1;
397 } else if (!is_both_bg && (routine_id == DrawRoutineIds::kAutoStairs ||
398 routine_id == DrawRoutineIds::kSanctuaryWall)) {
399 registry_secondary_bg_ = &other_bg;
400 } else if (!is_both_bg &&
402 dispatch_bg = &bg1;
406 }
407
408 if (use_pixel_bg1_mask) {
410 active_layout_bg1_mask_ = layout_bg1;
412 }
413
414 if (is_both_bg) {
415 // Draw to both background layers
416 registry_secondary_bg_ = nullptr;
418 registry_primary_layout_bg_ = layout_bg1;
420 draw_routines_[routine_id](this, object, bg1, render_tiles, state);
422 registry_primary_layout_bg_ = layout_bg2;
424 draw_routines_[routine_id](this, object, bg2, render_tiles, state);
425 } else {
426 // Execute the appropriate draw routine on target buffer only
428 dispatch_bg == &bg2
429 ? static_cast<const gfx::BackgroundBuffer*>(layout_bg2)
430 : static_cast<const gfx::BackgroundBuffer*>(layout_bg1);
432 draw_routines_[routine_id](this, object, *dispatch_bg, render_tiles, state);
433 }
434
435 const bool is_upper_spiral =
438 const bool is_lower_spiral =
441 if (!trace_only_ && (is_upper_spiral || is_lower_spiral)) {
442 auto& priority_object_owner = is_upper_spiral ? bg1 : bg2;
443 auto* priority_layout_owner = is_upper_spiral ? layout_bg1 : layout_bg2;
444 // USDASM ORs $2000 into the tile immediately left of the 4x3 raster and
445 // the tile immediately right of it.
446 for (const int tile_x : {object.x_ - 1, object.x_ + 4}) {
447 PromoteTilePriorityOnOwners(priority_object_owner, priority_layout_owner,
448 tile_x, object.y_);
449 }
450 }
451
452 if (!trace_only_ &&
454 // USDASM promotes a fixed BG1 column outside the lower staircase raster.
455 // North variants touch y-4..y-1; south variants touch y+4..y+7.
456 const int priority_y =
458 ? object.y_ - 4
459 : object.y_ + 4;
460 for (int row = 0; row < 4; ++row) {
461 PromoteTilePriorityOnOwners(bg1, layout_bg1, object.x_, priority_y + row);
462 }
463 }
464
465 if (trace_hook_active) {
467 }
468
469 active_object_bg1_mask_ = nullptr;
470 active_layout_bg1_mask_ = nullptr;
471 active_mask_source_bg_ = nullptr;
472 registry_secondary_bg_ = nullptr;
474
475 // BG2 mask propagation is deferred to compositing so raw BG1 stays intact.
476 //
477 // Ordinary BG2 overlay objects mask per-pixel as they draw, which keeps
478 // transparent cutouts intact for platforms/statues/stairs. Full-rect masking
479 // remains only for true pit/ceiling mask families that intentionally clear an
480 // area larger than their opaque tile pixels. Layer mode 6 ignores these legacy
481 // cross-BG masks because its upper-main/lower-sub PPU setup is resolved by
482 // transparency instead.
483 if (use_rectangular_bg1_mask) {
484 // Route through DimensionService so the mask rect comes from the same
485 // source as selection bounds (ObjectGeometry if available, then
486 // ObjectDimensionTable, then the size-nibble fallback). Keeps the
487 // transparent cutout aligned with what the user sees in the editor.
489 const auto [mask_px_x, mask_px_y, pixel_width, pixel_height] =
491
492 LOG_DEBUG("ObjectDrawer",
493 "Pit mask 0x%03X at (%d,%d) -> recording %dx%d BG1 reveal pixels",
494 object.id_, mask_px_x / 8, mask_px_y / 8, pixel_width,
495 pixel_height);
496
497 MarkBg1RectRevealed(bg1, mask_px_x, mask_px_y, pixel_width, pixel_height);
498 if (layout_bg1 != nullptr) {
499 MarkBg1RectRevealed(*layout_bg1, mask_px_x, mask_px_y, pixel_width,
500 pixel_height);
501 }
502 }
503
504 return absl::OkStatus();
505}
506
508 return (object.id_ == 0xA4) || // Pit
509 (object.id_ >= 0xA5 && object.id_ <= 0xA8) || // Diagonal masks A
510 (object.id_ == 0xC0) || // Large ceiling overlay
511 (object.id_ == 0xC2) || // Layer 2 pit mask
512 (object.id_ == 0xC3) || // Layer 2 pit mask
513 (object.id_ == 0xC6) || // Layer 2 mask
514 (object.id_ == 0xC8) || // Water floor overlay
515 (object.id_ == 0xD7) || // Layer 2 mask
516 (object.id_ == 0xD8) || // Flood water overlay
517 (object.id_ == 0xD9) || // Layer 2 swim mask
518 (object.id_ == 0xDA) || // Flood water overlay B
519 (object.id_ == 0xFE6) || // Type 3 pit
520 (object.id_ == 0xFF3); // Type 3 full mask
521}
522
524 const std::vector<RoomObject>& objects, gfx::BackgroundBuffer& bg1,
525 gfx::BackgroundBuffer& bg2, const gfx::PaletteGroup& palette_group,
526 [[maybe_unused]] const DungeonState* state,
527 gfx::BackgroundBuffer* layout_bg1, bool reset_room_event_indices,
528 gfx::BackgroundBuffer* layout_bg2) {
529 if (reset_room_event_indices) {
531 }
532 absl::Status status = absl::OkStatus();
533
534 // DEBUG: Count objects routed to each buffer
535 int to_bg1 = 0, to_bg2 = 0, both_bgs = 0;
536
537 for (const auto& object : objects) {
538 const auto semantics = GetEffectiveObjectLayerSemantics(object);
539 switch (semantics.effective_bg_layer) {
541 ++to_bg1;
542 break;
544 ++to_bg2;
545 break;
547 ++both_bgs;
548 break;
549 }
550
551 auto s = DrawObject(object, bg1, bg2, palette_group, state, layout_bg1,
552 layout_bg2);
553 if (!s.ok() && status.ok()) {
554 status = s;
555 }
556 }
557
558 LOG_DEBUG("ObjectDrawer", "Buffer routing: to_BG1=%d, to_BG2=%d, BothBGs=%d",
559 to_bg1, to_bg2, both_bgs);
560
561 // The palette is already applied by Room::RenderRoomGraphics(). SDL can pad
562 // indexed surface rows, so synchronize each row using the surface pitch.
563 SyncModifiedBitmapToSurface(bg1.bitmap(), "BG1");
564 SyncModifiedBitmapToSurface(bg2.bitmap(), "BG2");
565
566 return status;
567}
568
569// ============================================================================
570// Metadata-based BothBG Detection
571// ============================================================================
572
574 // Use DrawRoutineRegistry as the single source of truth for BothBG metadata.
576}
577
578// ============================================================================
579// Draw Routine Registry Initialization
580// ============================================================================
581
583 // This function maps object IDs to their corresponding draw routines.
584 // The mapping is based on ZScream's DungeonObjectData.cs and the game's
585 // assembly code. The order of functions in draw_routines_ MUST match the
586 // indices used here.
587 //
588 // ASM Reference (Bank 01):
589 // Subtype 1 Data Offset: $018000 (DrawObjects.type1_subtype_1_data_offset)
590 // Subtype 1 Routine Ptr: $018200 (DrawObjects.type1_subtype_1_routine)
591 // Subtype 2 Data Offset: $0183F0 (DrawObjects.type1_subtype_2_data_offset)
592 // Subtype 2 Routine Ptr: $018470 (DrawObjects.type1_subtype_2_routine)
593 // Subtype 3 Data Offset: $0184F0 (DrawObjects.type1_subtype_3_data_offset)
594 // Subtype 3 Routine Ptr: $0185F0 (DrawObjects.type1_subtype_3_routine)
595
596 draw_routines_.clear();
597
598 // Object-to-routine mapping now lives in DrawRoutineRegistry::BuildObjectMapping().
599 // ObjectDrawer::GetDrawRoutineId() delegates to the registry singleton.
600 // Initialize draw routine function array in the correct order
601 // Routines 0-82 (existing), 80-98 (new special routines for stairs, locks, etc.)
602 draw_routines_.reserve(100);
603
604 // Routine 0
605 draw_routines_.push_back(
606 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
607 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
608 self->DrawUsingRegistryRoutine(0, obj, bg, tiles, state);
609 });
610 // Routine 1
611 draw_routines_.push_back(
612 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
613 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
614 self->DrawUsingRegistryRoutine(1, obj, bg, tiles, state);
615 });
616 // Routine 2 - 2x4 tiles with adjacent spacing (s * 2), count = size + 1
617 draw_routines_.push_back(
618 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
619 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
620 self->DrawUsingRegistryRoutine(2, obj, bg, tiles, state);
621 });
622 // Routine 3 - Same as routine 2 but draws to both BG1 and BG2
623 draw_routines_.push_back(
624 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
625 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
626 self->DrawUsingRegistryRoutine(3, obj, bg, tiles, state);
627 });
628 // Routine 4
629 draw_routines_.push_back(
630 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
631 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
632 self->DrawUsingRegistryRoutine(4, obj, bg, tiles, state);
633 });
634 // Routine 5
635 draw_routines_.push_back(
636 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
637 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
638 self->DrawUsingRegistryRoutine(5, obj, bg, tiles, state);
639 });
640 // Routine 6
641 draw_routines_.push_back(
642 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
643 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
644 self->DrawUsingRegistryRoutine(6, obj, bg, tiles, state);
645 });
646 // Routine 7
647 draw_routines_.push_back(
648 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
649 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
650 self->DrawUsingRegistryRoutine(7, obj, bg, tiles, state);
651 });
652 // Routine 8
653 draw_routines_.push_back(
654 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
655 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
656 self->DrawUsingRegistryRoutine(8, obj, bg, tiles, state);
657 });
658 // Routine 9
659 draw_routines_.push_back(
660 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
661 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
662 self->DrawUsingRegistryRoutine(9, obj, bg, tiles, state);
663 });
664 // Routine 10
665 draw_routines_.push_back(
666 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
667 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
668 self->DrawUsingRegistryRoutine(10, obj, bg, tiles, state);
669 });
670 // Routine 11
671 draw_routines_.push_back(
672 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
673 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
674 self->DrawUsingRegistryRoutine(11, obj, bg, tiles, state);
675 });
676 // Routine 12
677 draw_routines_.push_back(
678 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
679 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
680 self->DrawUsingRegistryRoutine(12, obj, bg, tiles, state);
681 });
682 // Routine 13
683 draw_routines_.push_back(
684 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
685 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
686 self->DrawUsingRegistryRoutine(13, obj, bg, tiles, state);
687 });
688 // Routine 14
689 draw_routines_.push_back(
690 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
691 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
692 self->DrawUsingRegistryRoutine(14, obj, bg, tiles, state);
693 });
694 // Routine 15
695 draw_routines_.push_back(
696 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
697 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
698 self->DrawUsingRegistryRoutine(15, obj, bg, tiles, state);
699 });
700 // Routine 16
701 draw_routines_.push_back(
702 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
703 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
704 self->DrawUsingRegistryRoutine(16, obj, bg, tiles, state);
705 });
706 // Routine 17 - Diagonal Acute BothBG
707 draw_routines_.push_back(
708 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
709 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
710 self->DrawUsingRegistryRoutine(17, obj, bg, tiles, state);
711 });
712 // Routine 18 - Diagonal Grave BothBG
713 draw_routines_.push_back(
714 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
715 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
716 self->DrawUsingRegistryRoutine(18, obj, bg, tiles, state);
717 });
718 // Routine 19 - 4x4 Corner (Type 2 corners)
719 draw_routines_.push_back(
720 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
721 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
722 self->DrawUsingRegistryRoutine(19, obj, bg, tiles, state);
723 });
724
725 // Routine 20 - Edge objects 1x2 +2
726 draw_routines_.push_back(
727 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
728 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
729 self->DrawUsingRegistryRoutine(20, obj, bg, tiles, state);
730 });
731 // Routine 21 - Edge with perimeter 1x1 +3
732 draw_routines_.push_back(
733 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
734 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
735 self->DrawUsingRegistryRoutine(21, obj, bg, tiles, state);
736 });
737 // Routine 22 - Edge variant 1x1 +2
738 draw_routines_.push_back(
739 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
740 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
741 self->DrawUsingRegistryRoutine(22, obj, bg, tiles, state);
742 });
743 // Routine 23 - Top corners 1x2 +13
744 draw_routines_.push_back(
745 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
746 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
747 self->DrawUsingRegistryRoutine(23, obj, bg, tiles, state);
748 });
749 // Routine 24 - Bottom corners 1x2 +13
750 draw_routines_.push_back(
751 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
752 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
753 self->DrawUsingRegistryRoutine(24, obj, bg, tiles, state);
754 });
755 // Routine 25 - Solid fill 1x1 +3 (floor patterns)
756 draw_routines_.push_back(
757 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
758 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
759 self->DrawUsingRegistryRoutine(25, obj, bg, tiles, state);
760 });
761 // Routine 26 - Door switcherer
762 draw_routines_.push_back(
763 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
764 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
765 self->DrawUsingRegistryRoutine(26, obj, bg, tiles, state);
766 });
767 // Routine 27 - Decorations 4x4 spaced 2
768 draw_routines_.push_back(
769 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
770 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
771 self->DrawUsingRegistryRoutine(27, obj, bg, tiles, state);
772 });
773 // Routine 28 - Statues 2x3 spaced 2
774 draw_routines_.push_back(
775 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
776 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
777 self->DrawUsingRegistryRoutine(28, obj, bg, tiles, state);
778 });
779 // Routine 29 - Pillars 2x4 spaced 4
780 draw_routines_.push_back(
781 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
782 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
783 self->DrawUsingRegistryRoutine(29, obj, bg, tiles, state);
784 });
785 // Routine 30 - Decorations 4x3 spaced 4
786 draw_routines_.push_back(
787 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
788 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
789 self->DrawUsingRegistryRoutine(30, obj, bg, tiles, state);
790 });
791 // Routine 31 - Doubled 2x2 spaced 2
792 draw_routines_.push_back(
793 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
794 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
795 self->DrawUsingRegistryRoutine(31, obj, bg, tiles, state);
796 });
797 // Routine 32 - Decorations 2x2 spaced 12
798 draw_routines_.push_back(
799 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
800 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
801 self->DrawUsingRegistryRoutine(32, obj, bg, tiles, state);
802 });
803 // Routine 33 - Somaria Line
804 draw_routines_.push_back(
805 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
806 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
807 self->DrawUsingRegistryRoutine(33, obj, bg, tiles, state);
808 });
809 // Routine 34 - Water Face
810 draw_routines_.push_back(
811 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
812 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
813 self->DrawUsingRegistryRoutine(34, obj, bg, tiles, state);
814 });
815 // Routine 35 - 4x4 Corner BothBG
816 draw_routines_.push_back(
817 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
818 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
819 self->DrawUsingRegistryRoutine(35, obj, bg, tiles, state);
820 });
821 // Routine 36 - Weird Corner Bottom BothBG
822 draw_routines_.push_back(
823 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
824 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
825 self->DrawUsingRegistryRoutine(36, obj, bg, tiles, state);
826 });
827 // Routine 37 - Weird Corner Top BothBG
828 draw_routines_.push_back(
829 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
830 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
831 self->DrawUsingRegistryRoutine(37, obj, bg, tiles, state);
832 });
833 // Routine 38 - Nothing
834 draw_routines_.push_back(
835 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
836 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
837 self->DrawUsingRegistryRoutine(38, obj, bg, tiles, state);
838 });
839 // Routine 39 - Small chest rendering (stateful F99 / fixed-open F9A)
840 draw_routines_.push_back(
841 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
842 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
843 self->DrawChest(obj, bg, tiles, state);
844 });
845 // Routine 40 - Rightwards 4x2 (Floor Tile)
846 draw_routines_.push_back(
847 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
848 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
849 self->DrawUsingRegistryRoutine(40, obj, bg, tiles, state);
850 });
851 // Routine 41 - Rightwards Decor 4x2 spaced 8 (12-column spacing)
852 draw_routines_.push_back(
853 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
854 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
855 self->DrawUsingRegistryRoutine(41, obj, bg, tiles, state);
856 });
857 // Routine 42 - Rightwards Cannon Hole 4x3
858 draw_routines_.push_back(
859 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
860 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
861 self->DrawUsingRegistryRoutine(42, obj, bg, tiles, state);
862 });
863 // Routine 43 - Downwards Floor 4x4 (object 0x70)
864 draw_routines_.push_back(
865 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
866 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
867 self->DrawUsingRegistryRoutine(43, obj, bg, tiles, state);
868 });
869 // Routine 44 - Downwards 1x1 Solid +3 (object 0x71)
870 draw_routines_.push_back(
871 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
872 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
873 self->DrawUsingRegistryRoutine(44, obj, bg, tiles, state);
874 });
875 // Routine 45 - Downwards Decor 4x4 spaced 2 (objects 0x73-0x74)
876 draw_routines_.push_back(
877 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
878 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
879 self->DrawUsingRegistryRoutine(45, obj, bg, tiles, state);
880 });
881 // Routine 46 - Downwards Pillar 2x4 spaced 2 (objects 0x75, 0x87)
882 draw_routines_.push_back(
883 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
884 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
885 self->DrawUsingRegistryRoutine(46, obj, bg, tiles, state);
886 });
887 // Routine 47 - Downwards Decor 3x4 spaced 4 (objects 0x76-0x77)
888 draw_routines_.push_back(
889 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
890 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
891 self->DrawUsingRegistryRoutine(47, obj, bg, tiles, state);
892 });
893 // Routine 48 - Downwards Decor 2x2 spaced 12 (objects 0x78, 0x7B)
894 draw_routines_.push_back(
895 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
896 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
897 self->DrawUsingRegistryRoutine(48, obj, bg, tiles, state);
898 });
899 // Routine 49 - Downwards Line 1x1 +1 (object 0x7C)
900 draw_routines_.push_back(
901 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
902 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
903 self->DrawUsingRegistryRoutine(49, obj, bg, tiles, state);
904 });
905 // Routine 50 - Downwards Decor 2x4 spaced 8 (objects 0x7F, 0x80)
906 draw_routines_.push_back(
907 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
908 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
909 self->DrawUsingRegistryRoutine(50, obj, bg, tiles, state);
910 });
911 // Routine 51 - Rightwards Line 1x1 +1 (object 0x50)
912 draw_routines_.push_back(
913 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
914 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
915 self->DrawUsingRegistryRoutine(51, obj, bg, tiles, state);
916 });
917 // Routine 52 - Rightwards Bar 4x3 (object 0x4C)
918 draw_routines_.push_back(
919 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
920 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
921 self->DrawUsingRegistryRoutine(52, obj, bg, tiles, state);
922 });
923 // Routine 53 - Rightwards Shelf 4x4 (objects 0x4D-0x4F)
924 draw_routines_.push_back(
925 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
926 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
927 self->DrawUsingRegistryRoutine(53, obj, bg, tiles, state);
928 });
929 // Routine 54 - Rightwards Big Rail 1x3 +5 (object 0x5D)
930 draw_routines_.push_back(
931 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
932 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
933 self->DrawUsingRegistryRoutine(54, obj, bg, tiles, state);
934 });
935 // Routine 55 - Rightwards Block 2x2 spaced 2 (object 0x5E)
936 draw_routines_.push_back(
937 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
938 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
939 self->DrawUsingRegistryRoutine(55, obj, bg, tiles, state);
940 });
941
942 // ============================================================================
943 // Phase 4: SuperSquare Routines (routines 56-64)
944 // ============================================================================
945
946 // Routine 56 - 4x4 Blocks in 4x4 SuperSquare (objects 0xC0, 0xC2)
947 draw_routines_.push_back(
948 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
949 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
950 self->DrawUsingRegistryRoutine(56, obj, bg, tiles, state);
951 });
952
953 // Routine 57 - 3x3 Floor in 4x4 SuperSquare (objects 0xC3, 0xD7)
954 draw_routines_.push_back(
955 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
956 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
957 self->DrawUsingRegistryRoutine(57, obj, bg, tiles, state);
958 });
959
960 // Routine 58 - 4x4 Floor in 4x4 SuperSquare (objects 0xC5-0xCA, 0xD1-0xD2,
961 // 0xD9, 0xDF-0xE8)
962 draw_routines_.push_back(
963 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
964 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
965 self->DrawUsingRegistryRoutine(58, obj, bg, tiles, state);
966 });
967
968 // Routine 59 - 4x4 Floor One in 4x4 SuperSquare (object 0xC4)
969 draw_routines_.push_back(
970 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
971 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
972 self->DrawUsingRegistryRoutine(59, obj, bg, tiles, state);
973 });
974
975 // Routine 60 - 4x4 Floor Two in 4x4 SuperSquare (object 0xDB)
976 draw_routines_.push_back(
977 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
978 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
979 self->DrawUsingRegistryRoutine(60, obj, bg, tiles, state);
980 });
981
982 // Routine 61 - Big Hole 4x4 (object 0xA4)
983 draw_routines_.push_back(
984 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
985 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
986 self->DrawUsingRegistryRoutine(61, obj, bg, tiles, state);
987 });
988
989 // Routine 62 - Spike 2x2 in 4x4 SuperSquare (object 0xDE)
990 draw_routines_.push_back(
991 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
992 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
993 self->DrawUsingRegistryRoutine(62, obj, bg, tiles, state);
994 });
995
996 // Routine 63 - Table Rock 4x4 (object 0xDD)
997 draw_routines_.push_back(
998 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
999 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1000 self->DrawUsingRegistryRoutine(63, obj, bg, tiles, state);
1001 });
1002
1003 // Routine 64 - Water Overlay 8x8 (objects 0xD8, 0xDA)
1004 draw_routines_.push_back(
1005 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1006 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1007 self->DrawUsingRegistryRoutine(64, obj, bg, tiles, state);
1008 });
1009
1010 // ============================================================================
1011 // Phase 4 Step 2: Simple Variant Routines (routines 65-74)
1012 // ============================================================================
1013
1014 // Routine 65 - Downwards Decor 3x4 spaced 2 (objects 0x81-0x84)
1015 draw_routines_.push_back(
1016 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1017 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1018 self->DrawUsingRegistryRoutine(65, obj, bg, tiles, state);
1019 });
1020
1021 // Routine 66 - Downwards Big Rail 3x1 plus 5 (object 0x88)
1022 draw_routines_.push_back(
1023 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1024 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1025 self->DrawUsingRegistryRoutine(66, obj, bg, tiles, state);
1026 });
1027
1028 // Routine 67 - Downwards Block 2x2 spaced 2 (object 0x89)
1029 draw_routines_.push_back(
1030 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1031 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1032 self->DrawUsingRegistryRoutine(67, obj, bg, tiles, state);
1033 });
1034
1035 // Routine 68 - Downwards Cannon Hole 3x6 (objects 0x85-0x86)
1036 draw_routines_.push_back(
1037 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1038 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1039 self->DrawUsingRegistryRoutine(68, obj, bg, tiles, state);
1040 });
1041
1042 // Routine 69 - Downwards Bar 2x3 (object 0x8F)
1043 draw_routines_.push_back(
1044 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1045 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1046 self->DrawUsingRegistryRoutine(69, obj, bg, tiles, state);
1047 });
1048
1049 // Routine 70 - Downwards Pots 2x2 (object 0x95)
1050 draw_routines_.push_back(
1051 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1052 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1053 self->DrawUsingRegistryRoutine(70, obj, bg, tiles, state);
1054 });
1055
1056 // Routine 71 - Downwards Hammer Pegs 2x2 (object 0x96)
1057 draw_routines_.push_back(
1058 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1059 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1060 self->DrawUsingRegistryRoutine(71, obj, bg, tiles, state);
1061 });
1062
1063 // Routine 72 - Rightwards Edge 1x1 plus 7 (objects 0xB0-0xB1)
1064 draw_routines_.push_back(
1065 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1066 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1067 self->DrawUsingRegistryRoutine(72, obj, bg, tiles, state);
1068 });
1069
1070 // Routine 73 - Rightwards Pots 2x2 (object 0xBC)
1071 draw_routines_.push_back(
1072 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1073 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1074 self->DrawUsingRegistryRoutine(73, obj, bg, tiles, state);
1075 });
1076
1077 // Routine 74 - Rightwards Hammer Pegs 2x2 (object 0xBD)
1078 draw_routines_.push_back(
1079 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1080 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1081 self->DrawUsingRegistryRoutine(74, obj, bg, tiles, state);
1082 });
1083
1084 // ============================================================================
1085 // Phase 4 Step 3: Diagonal Ceiling Routines (routines 75-78)
1086 // ============================================================================
1087
1088 // Routine 75 - Diagonal Ceiling Top Left (objects 0xA0, 0xA5, 0xA9)
1089 draw_routines_.push_back(
1090 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1091 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1092 self->DrawUsingRegistryRoutine(75, obj, bg, tiles, state);
1093 });
1094
1095 // Routine 76 - Diagonal Ceiling Bottom Left (objects 0xA1, 0xA6, 0xAA)
1096 draw_routines_.push_back(
1097 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1098 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1099 self->DrawUsingRegistryRoutine(76, obj, bg, tiles, state);
1100 });
1101
1102 // Routine 77 - Diagonal Ceiling Top Right (objects 0xA2, 0xA7, 0xAB)
1103 draw_routines_.push_back(
1104 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1105 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1106 self->DrawUsingRegistryRoutine(77, obj, bg, tiles, state);
1107 });
1108
1109 // Routine 78 - Diagonal Ceiling Bottom Right (objects 0xA3, 0xA8, 0xAC)
1110 draw_routines_.push_back(
1111 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1112 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1113 self->DrawUsingRegistryRoutine(78, obj, bg, tiles, state);
1114 });
1115
1116 // ============================================================================
1117 // Phase 4 Step 5: Special Routines (routines 79-82)
1118 // ============================================================================
1119
1120 // Routine 79 - Closed Chest Platform (object 0xC1, 68 tiles)
1121 draw_routines_.push_back(
1122 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1123 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1124 self->DrawUsingRegistryRoutine(79, obj, bg, tiles, state);
1125 });
1126
1127 // Routine 80 - Moving Wall West (object 0xCD, 24 tiles)
1128 draw_routines_.push_back(
1129 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1130 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1131 self->DrawUsingRegistryRoutine(80, obj, bg, tiles, state);
1132 });
1133
1134 // Routine 81 - Moving Wall East (object 0xCE, 24 tiles)
1135 draw_routines_.push_back(
1136 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1137 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1138 self->DrawUsingRegistryRoutine(81, obj, bg, tiles, state);
1139 });
1140
1141 // Routine 82 - Open Chest Platform (object 0xDC, 21 tiles)
1142 draw_routines_.push_back(
1143 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1144 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1145 self->DrawUsingRegistryRoutine(82, obj, bg, tiles, state);
1146 });
1147
1148 // ============================================================================
1149 // New Special Routines (Phase 5) - Stairs, Locks, Interactive Objects
1150 // ============================================================================
1151
1152 // Routine 83 - InterRoom Fat Stairs Up (object 0x12D)
1153 draw_routines_.push_back(
1154 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1155 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1156 self->DrawUsingRegistryRoutine(83, obj, bg, tiles, state);
1157 });
1158
1159 // Routine 84 - InterRoom Fat Stairs Down A (object 0x12E)
1160 draw_routines_.push_back(
1161 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1162 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1163 self->DrawUsingRegistryRoutine(84, obj, bg, tiles, state);
1164 });
1165
1166 // Routine 85 - InterRoom Fat Stairs Down B (object 0x12F)
1167 draw_routines_.push_back(
1168 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1169 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1170 self->DrawUsingRegistryRoutine(85, obj, bg, tiles, state);
1171 });
1172
1173 // Routine 86 - Auto Stairs (objects 0x130-0x133)
1174 draw_routines_.push_back(
1175 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1176 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1177 self->DrawUsingRegistryRoutine(86, obj, bg, tiles, state);
1178 });
1179
1180 // Routine 87 - Straight InterRoom Stairs (Type 3 objects 0x21E-0x229)
1181 draw_routines_.push_back(
1182 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1183 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1184 self->DrawUsingRegistryRoutine(87, obj, bg, tiles, state);
1185 });
1186
1187 // Routine 88 - Spiral Stairs Going Up Upper (object 0x138)
1188 draw_routines_.push_back(
1189 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1190 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1191 self->DrawUsingRegistryRoutine(88, obj, bg, tiles, state);
1192 });
1193
1194 // Routine 89 - Spiral Stairs Going Down Upper (object 0x139)
1195 draw_routines_.push_back(
1196 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1197 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1198 self->DrawUsingRegistryRoutine(89, obj, bg, tiles, state);
1199 });
1200
1201 // Routine 90 - Spiral Stairs Going Up Lower (object 0x13A)
1202 draw_routines_.push_back(
1203 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1204 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1205 self->DrawUsingRegistryRoutine(90, obj, bg, tiles, state);
1206 });
1207
1208 // Routine 91 - Spiral Stairs Going Down Lower (object 0x13B)
1209 draw_routines_.push_back(
1210 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1211 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1212 self->DrawUsingRegistryRoutine(91, obj, bg, tiles, state);
1213 });
1214
1215 // Routine 92 - Big Key Lock (Yaze 0xF98 / ASM object 0x218)
1216 draw_routines_.push_back(
1217 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1218 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1219 self->DrawBigKeyLock(obj, bg, tiles, state);
1220 });
1221
1222 // Routine 93 - Bombable Floor (Type 3 object 0x247)
1223 draw_routines_.push_back(
1224 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1225 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1226 self->DrawUsingRegistryRoutine(93, obj, bg, tiles, state);
1227 });
1228
1229 // Routine 94 - Empty Water Face (Type 3 object 0x200)
1230 draw_routines_.push_back(
1231 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1232 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1233 self->DrawUsingRegistryRoutine(94, obj, bg, tiles, state);
1234 });
1235
1236 // Routine 95 - Spitting Water Face (Type 3 object 0x201)
1237 draw_routines_.push_back(
1238 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1239 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1240 self->DrawUsingRegistryRoutine(95, obj, bg, tiles, state);
1241 });
1242
1243 // Routine 96 - Drenching Water Face (Type 3 object 0x202)
1244 draw_routines_.push_back(
1245 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1246 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1247 self->DrawUsingRegistryRoutine(96, obj, bg, tiles, state);
1248 });
1249
1250 // Routine 97 - Prison Cell (Type 3 objects 0x20D, 0x217)
1251 // USDASM selects one tilemap through $BF and draws a sparse 16x4 pattern.
1252 draw_routines_.push_back(
1253 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1254 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1255 self->DrawUsingRegistryRoutine(97, obj, bg, tiles, state);
1256 });
1257
1258 // Routine 98 - Bed 4x5 (Type 2 objects 0x122, 0x128)
1259 draw_routines_.push_back(
1260 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1261 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1263 state);
1264 });
1265
1266 // Routine 99 - Rightwards 3x6 (Type 2 object 0x12C, Type 3 0x236-0x237)
1267 draw_routines_.push_back(
1268 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1269 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1271 tiles, state);
1272 });
1273
1274 // Routine 100 - Utility 6x3 (Type 2 object 0x13E, Type 3 0x24D, 0x25D)
1275 draw_routines_.push_back(
1276 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1277 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1279 tiles, state);
1280 });
1281
1282 // Routine 101 - Utility 3x5 (Type 3 objects 0x255, 0x25B)
1283 draw_routines_.push_back(
1284 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1285 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1287 tiles, state);
1288 });
1289
1290 // Routine 102 - Vertical Turtle Rock Pipe (Type 3 objects 0x23A, 0x23B)
1291 draw_routines_.push_back(
1292 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1293 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1295 obj, bg, tiles, state);
1296 });
1297
1298 // Routine 103 - Horizontal Turtle Rock Pipe (Type 3 objects 0x23C, 0x23D,
1299 // 0x25C)
1300 draw_routines_.push_back(
1301 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1302 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1304 DrawRoutineIds::kHorizontalTurtleRockPipe, obj, bg, tiles, state);
1305 });
1306
1307 // Routine 104 - Light Beam on Floor (Type 3 object 0x270)
1308 draw_routines_.push_back(
1309 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1310 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1312 tiles, state);
1313 });
1314
1315 // Routine 105 - Big Light Beam on Floor (Type 3 object 0x271)
1316 draw_routines_.push_back(
1317 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1318 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1320 tiles, state);
1321 });
1322
1323 // Routine 106 - Boss Shell 4x4 (Yaze 0xF95/0xFF2; ASM 0x215/0x272)
1324 draw_routines_.push_back(
1325 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1326 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1328 tiles, state);
1329 });
1330
1331 // Routine 107 - Solid Wall Decor 3x4 (Type 3 objects 0x269-0x26A, 0x26E-0x26F)
1332 draw_routines_.push_back(
1333 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1334 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1336 bg, tiles, state);
1337 });
1338
1339 // Routine 108 - Archery Game Target Door (Type 3 objects 0x260-0x261)
1340 draw_routines_.push_back(
1341 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1342 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1344 obj, bg, tiles, state);
1345 });
1346
1347 // Routine 109 - Ganon Triforce Floor Decor (Type 3 object 0x278)
1348 draw_routines_.push_back(
1349 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1350 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1352 obj, bg, tiles, state);
1353 });
1354
1355 // Routine 110 - Single 2x2 (pots, statues, single-instance 2x2 objects)
1356 draw_routines_.push_back(
1357 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1358 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1360 tiles, state);
1361 });
1362
1363 // Routine 111 - Waterfall47 (object 0x47)
1364 draw_routines_.push_back(
1365 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1366 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1368 tiles, state);
1369 });
1370
1371 // Routine 112 - Waterfall48 (object 0x48)
1372 draw_routines_.push_back(
1373 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1374 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1376 tiles, state);
1377 });
1378
1379 // Routine 113 - Single 4x4 (NO repetition)
1380 // ASM: RoomDraw_4x4 - draws a single 4x4 pattern (16 tiles)
1381 // Used for: 0xFEB (large decor), and other single 4x4 objects
1382 draw_routines_.push_back(
1383 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1384 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1386 tiles, state);
1387 });
1388
1389 // Routine 114 - Single 4x3 (NO repetition)
1390 // ASM: RoomDraw_TableRock4x3 - draws a single 4x3 pattern (12 tiles)
1391 // Used for: 0xFED (water grate), 0xFB1 (big chest), etc.
1392 draw_routines_.push_back(
1393 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1394 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1395 if (obj.id_ == 0xFB1) {
1396 self->DrawBigChest(obj, bg, tiles, state);
1397 return;
1398 }
1400 tiles, state);
1401 });
1402
1403 // Routine 115 - RupeeFloor (special pattern for 0xF92)
1404 // ASM: RoomDraw_RupeeFloor - draws 3 one-tile columns two tiles apart.
1405 // Pattern: 5 tiles wide, 8 rows tall with gaps (rows 2 and 5 are empty).
1406 draw_routines_.push_back(
1407 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1408 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1410 tiles, state);
1411 });
1412
1413 // Routine 116 - Actual 4x4 tile8 pattern (32x32 pixels, NO repetition)
1414 // ASM: RoomDraw_4x4 - draws exactly 4 columns x 4 rows = 16 tiles
1415 // Used for: 0xFE6 (pit)
1416 draw_routines_.push_back(
1417 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1418 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1420 tiles, state);
1421 });
1422
1423 auto ensure_index = [this](size_t index) {
1424 while (draw_routines_.size() <= index) {
1425 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1427 std::span<const gfx::TileInfo> tiles,
1428 [[maybe_unused]] const DungeonState* state) {
1429 self->DrawNothing(obj, bg, tiles, state);
1430 });
1431 }
1432 };
1433
1434 // Routine 117 - Long vertical rail with CORNER+MIDDLE+END pattern (0x8A)
1435 // ASM: RoomDraw_DownwardsHasEdge1x1_1to16_plus23 - matches horizontal 0x22
1436 ensure_index(117);
1437 draw_routines_[117] =
1438 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1439 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1440 self->DrawUsingRegistryRoutine(117, obj, bg, tiles, state);
1441 };
1442
1443 // Routine 118 - Horizontal long rails with CORNER+MIDDLE+END pattern (0x5F)
1444 ensure_index(118);
1445 draw_routines_[118] =
1446 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1447 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1448 self->DrawUsingRegistryRoutine(118, obj, bg, tiles, state);
1449 };
1450
1453 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1454 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1456 bg, tiles, state);
1457 };
1458
1461 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1462 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1464 bg, tiles, state);
1465 };
1466
1467 ensure_index(DrawRoutineIds::kDamFloodGate);
1469 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1470 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1472 tiles, state);
1473 };
1474
1477 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1478 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1481 state);
1482 };
1483
1484 ensure_index(DrawRoutineIds::kFloorLight);
1486 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1487 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1489 tiles, state);
1490 };
1491
1492 ensure_index(DrawRoutineIds::kWeird2x4_1to16);
1494 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1495 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1497 tiles, state);
1498 };
1499
1500 ensure_index(DrawRoutineIds::kBigWallDecor);
1502 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1503 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1505 tiles, state);
1506 };
1507
1508 ensure_index(DrawRoutineIds::kTableBowl);
1510 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1511 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1513 tiles, state);
1514 };
1515
1516 ensure_index(DrawRoutineIds::kSmithyFurnace);
1518 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1519 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1521 tiles, state);
1522 };
1523
1524 ensure_index(DrawRoutineIds::kBigGrayRock);
1526 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1527 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1529 tiles, state);
1530 };
1531
1532 ensure_index(DrawRoutineIds::kAgahnimsAltar);
1534 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1535 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1537 tiles, state);
1538 };
1539
1542 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1543 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1545 bg, tiles, state);
1546 };
1547
1548 ensure_index(DrawRoutineIds::kMagicBatAltar);
1550 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1551 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1553 tiles, state);
1554 };
1555
1558 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1559 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1561 bg, tiles, state);
1562 };
1563
1564 ensure_index(DrawRoutineIds::kSanctuaryWall);
1566 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1567 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1569 tiles, state);
1570 };
1571
1572 // Routine 130 - Custom Object (Oracle of Secrets 0x31, 0x32)
1573 // Uses external binary files instead of ROM tile data.
1574 // Requires CustomObjectManager initialization and enable_custom_objects flag.
1575 ensure_index(130);
1576 draw_routines_[130] = [](ObjectDrawer* self, const RoomObject& obj,
1578 std::span<const gfx::TileInfo> tiles,
1579 [[maybe_unused]] const DungeonState* state) {
1580 self->DrawCustomObject(obj, bg, tiles, state);
1581 };
1582
1583 routines_initialized_ = true;
1584}
1585
1586int ObjectDrawer::GetDrawRoutineId(int16_t object_id) const {
1587 // Delegate to the unified registry for the canonical mapping
1589}
1590
1591// ============================================================================
1592// Draw Routine Implementations (Based on ZScream patterns)
1593// ============================================================================
1594
1595void ObjectDrawer::DrawDoor(const DoorDef& door, int door_index,
1598 const DungeonState* state,
1599 gfx::BackgroundBuffer* layout_bg1,
1600 gfx::BackgroundBuffer* layout_bg2) {
1601 // Door rendering based on ZELDA3_DUNGEON_SPEC.md Section 5 and disassembly
1602 // Uses DoorType and DoorDirection enums for type safety
1603 // Position calculations via DoorPositionManager
1604
1605 LOG_DEBUG("ObjectDrawer", "DrawDoor: idx=%d type=%d dir=%d pos=%d",
1606 door_index, static_cast<int>(door.type),
1607 static_cast<int>(door.direction), door.position);
1608
1609 if (!rom_ || !rom_->is_loaded() || !room_gfx_buffer_) {
1610 LOG_DEBUG("ObjectDrawer", "DrawDoor: SKIPPED - rom=%p loaded=%d gfx=%p",
1611 (void*)rom_, rom_ ? rom_->is_loaded() : 0,
1612 (void*)room_gfx_buffer_);
1613 return;
1614 }
1615
1616 auto& bitmap = bg1.bitmap();
1617 if (!bitmap.is_active() || bitmap.width() == 0) {
1618 LOG_DEBUG("ObjectDrawer",
1619 "DrawDoor: SKIPPED - bitmap not active or zero width");
1620 return;
1621 }
1622
1623 // DungeonState intentionally exposes the editor's logical door index. The
1624 // runtime derives a separate marker-aware physical slot from $0460 before it
1625 // probes $068C; reproducing that slot aliasing belongs in the state adapter,
1626 // not in this renderer's vector-index contract.
1627 const bool is_door_open = state && state->IsDoorOpen(room_id_, door_index);
1628
1629 // Get door position from DoorPositionManager
1630 auto [tile_x, tile_y] = door.GetTileCoords();
1631 auto dims = door.GetDimensions();
1632 int door_width = dims.width_tiles;
1633 int door_height = dims.height_tiles;
1634
1635 LOG_DEBUG("ObjectDrawer", "DrawDoor: tile_pos=(%d,%d) dims=%dx%d", tile_x,
1636 tile_y, door_width, door_height);
1637
1638 constexpr int kRoomDrawObjectDataBase = 0x1B52;
1639 constexpr int kDoorwayReplacementDoorGfxBase = 0x1A02;
1640 constexpr int kExplodingWallTilemapPositionBase = 0x19DE;
1641 constexpr int kExplodingWallOpenReplacementType = 0x54;
1642 constexpr int kNorthCurtainClosedOffset = 0x078A;
1643 constexpr int kFancyDungeonExitObjectOffset = 0x2656;
1644 constexpr int kCaveExitLightObjectOffset = 0x26F6;
1645 const auto& rom_data = rom_->data();
1646
1647 auto resolve_effective_door_type = [&]() -> uint16_t {
1648 const auto stored_type = static_cast<uint16_t>(door.type);
1649 if (!is_door_open) {
1650 return stored_type;
1651 }
1652
1653 // RoomDraw_FlagDoorsAndGetFinalType leaves shutter graphics closed while
1654 // the room's shutter controller ($0468) is active, even when the persistent
1655 // door-open bit is set.
1656 const bool is_controlled_shutter =
1659 if (is_controlled_shutter && state->IsDoorSwitchActive(room_id_)) {
1660 return stored_type;
1661 }
1662
1663 // USDASM performs a 16-bit read from DoorwayReplacementDoorGFX using the
1664 // original even-valued door type as a byte offset. Keep the bounds check on
1665 // both bytes so malformed or undersized ROMs fall back to the stored type.
1666 const int replacement_addr =
1667 kDoorwayReplacementDoorGfxBase + static_cast<int>(door.type);
1668 if (replacement_addr < 0 ||
1669 replacement_addr + 1 >= static_cast<int>(rom_->size())) {
1670 return stored_type;
1671 }
1672
1673 return static_cast<uint16_t>(rom_data[replacement_addr] |
1674 (rom_data[replacement_addr + 1] << 8));
1675 };
1676
1677 const uint16_t effective_door_type = resolve_effective_door_type();
1678
1679 auto draw_tile_word = [&](gfx::BackgroundBuffer& target, int tile_x,
1680 int tile_y, uint16_t tile_word) {
1681 auto& bitmap = target.bitmap();
1682 auto& priority_buffer = target.mutable_priority_data();
1683 auto& coverage_buffer = target.mutable_coverage_data();
1684 const int bitmap_width = bitmap.width();
1685 const auto tile_info = gfx::WordToTileInfo(tile_word);
1686 const int pixel_x = tile_x * 8;
1687 const int pixel_y = tile_y * 8;
1688
1689 target.SetTileAt(tile_x, tile_y, tile_word);
1690 target.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, pixel_x, pixel_y, 8,
1691 8);
1692 DrawTileToBitmap(bitmap, tile_info, pixel_x, pixel_y, room_gfx_buffer_);
1693
1694 const uint8_t priority = tile_info.over_ ? 1 : 0;
1695 const auto& bitmap_data = bitmap.vector();
1696 for (int py = 0; py < 8; py++) {
1697 const int dest_y = pixel_y + py;
1698 if (dest_y < 0 || dest_y >= bitmap.height()) {
1699 continue;
1700 }
1701 for (int px = 0; px < 8; px++) {
1702 const int dest_x = pixel_x + px;
1703 if (dest_x < 0 || dest_x >= bitmap_width) {
1704 continue;
1705 }
1706 const int dest_index = dest_y * bitmap_width + dest_x;
1707 if (dest_index >= 0 &&
1708 dest_index < static_cast<int>(coverage_buffer.size())) {
1709 coverage_buffer[dest_index] = 1;
1710 }
1711 if (dest_index < static_cast<int>(bitmap_data.size()) &&
1712 bitmap_data[dest_index] != 255) {
1713 priority_buffer[dest_index] = priority;
1714 }
1715 }
1716 }
1717 };
1718
1719 auto draw_object_data_tile = [&](gfx::BackgroundBuffer& target, int tile_x,
1720 int tile_y, int tile_data_addr, int tile_idx,
1721 uint16_t word_mask = 0) -> bool {
1722 const int addr = tile_data_addr + (tile_idx * 2);
1723 if (addr < 0 || addr + 1 >= static_cast<int>(rom_->size())) {
1724 return false;
1725 }
1726
1727 const uint16_t tile_word = static_cast<uint16_t>(
1728 (rom_data[addr] | (rom_data[addr + 1] << 8)) | word_mask);
1729 draw_tile_word(target, tile_x, tile_y, tile_word);
1730 return true;
1731 };
1732
1733 auto draw_from_object_data = [&](gfx::BackgroundBuffer& target,
1734 int start_tile_x, int start_tile_y,
1735 int width, int height, int tile_data_addr) {
1736 int tile_idx = 0;
1737
1738 for (int dx = 0; dx < width; dx++) {
1739 for (int dy = 0; dy < height; dy++) {
1740 (void)draw_object_data_tile(target, start_tile_x + dx,
1741 start_tile_y + dy, tile_data_addr,
1742 tile_idx++);
1743 }
1744 }
1745 };
1746
1747 auto draw_from_object_data_row_major =
1748 [&](gfx::BackgroundBuffer& target, int start_tile_x, int start_tile_y,
1749 int width, int height, int tile_data_addr) {
1750 int tile_idx = 0;
1751 for (int dy = 0; dy < height; ++dy) {
1752 for (int dx = 0; dx < width; ++dx) {
1753 (void)draw_object_data_tile(target, start_tile_x + dx,
1754 start_tile_y + dy, tile_data_addr,
1755 tile_idx++);
1756 }
1757 }
1758 };
1759
1760 auto draw_repeated_tile = [&](gfx::BackgroundBuffer& target, int start_tile_x,
1761 int start_tile_y, int width, int height,
1762 uint16_t tile_word) {
1763 auto& bitmap = target.bitmap();
1764 auto& priority_buffer = target.mutable_priority_data();
1765 auto& coverage_buffer = target.mutable_coverage_data();
1766 const int bitmap_width = bitmap.width();
1767 const auto tile_info = gfx::WordToTileInfo(tile_word);
1768
1769 for (int dx = 0; dx < width; dx++) {
1770 for (int dy = 0; dy < height; dy++) {
1771 const int pixel_x = (start_tile_x + dx) * 8;
1772 const int pixel_y = (start_tile_y + dy) * 8;
1773
1774 target.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, pixel_x, pixel_y,
1775 8, 8);
1776 DrawTileToBitmap(bitmap, tile_info, pixel_x, pixel_y, room_gfx_buffer_);
1777
1778 const uint8_t priority = tile_info.over_ ? 1 : 0;
1779 const auto& bitmap_data = bitmap.vector();
1780 for (int py = 0; py < 8; py++) {
1781 const int dest_y = pixel_y + py;
1782 if (dest_y < 0 || dest_y >= bitmap.height()) {
1783 continue;
1784 }
1785 for (int px = 0; px < 8; px++) {
1786 const int dest_x = pixel_x + px;
1787 if (dest_x < 0 || dest_x >= bitmap_width) {
1788 continue;
1789 }
1790 const int dest_index = dest_y * bitmap_width + dest_x;
1791 if (dest_index >= 0 &&
1792 dest_index < static_cast<int>(coverage_buffer.size())) {
1793 coverage_buffer[dest_index] = 1;
1794 }
1795 if (dest_index < static_cast<int>(bitmap_data.size()) &&
1796 bitmap_data[dest_index] != 255) {
1797 priority_buffer[dest_index] = priority;
1798 }
1799 }
1800 }
1801 }
1802 }
1803 };
1804
1805 auto tilemap_offset_to_tile_coords = [](uint16_t offset) {
1806 return std::pair<int, int>{static_cast<int>((offset % 0x80) / 2),
1807 static_cast<int>(offset / 0x80) - 4};
1808 };
1809 const int position_index = std::min<int>(door.position & 0x0F, 11);
1810
1811 enum class DoorPrioritySpan {
1812 // RoomDraw_MakeDoorPartsHighPriority_{Vertical,Horizontal}.
1813 kNormalLower,
1814 // RoomDraw_MakeDoorHighPriorityLowerLayer_* (type $06).
1815 kLowerLayerOnly,
1816 // RoomDraw_MakeDoorHighPriority_* after a type $40-$66 raster.
1817 kHighRange,
1818 };
1819
1820 auto promote_priority_rect_on_layer =
1821 [&](gfx::BackgroundBuffer& object_buffer,
1822 gfx::BackgroundBuffer* layout_buffer, int start_tile_x,
1823 int start_tile_y, int width_tiles, int height_tiles) {
1824 auto promote_target = [&](gfx::BackgroundBuffer& target) {
1825 for (int y = start_tile_y * 8; y < (start_tile_y + height_tiles) * 8;
1826 ++y) {
1827 for (int x = start_tile_x * 8; x < (start_tile_x + width_tiles) * 8;
1828 ++x) {
1829 target.SetPriorityAt(x, y, 1);
1830 }
1831 }
1832 };
1833
1834 // USDASM has one tilemap per background. Yaze splits each into layout and
1835 // object buffers, so promote both potential pixel owners without changing
1836 // either bitmap or coverage mask.
1837 promote_target(object_buffer);
1838 if (layout_buffer != nullptr && layout_buffer != &object_buffer) {
1839 promote_target(*layout_buffer);
1840 }
1841 };
1842
1843 auto promote_upper_priority_rect = [&](int start_tile_x, int start_tile_y,
1844 int width_tiles, int height_tiles) {
1845 promote_priority_rect_on_layer(bg1, layout_bg1, start_tile_x, start_tile_y,
1846 width_tiles, height_tiles);
1847 };
1848
1849 auto promote_lower_priority_rect = [&](int start_tile_x, int start_tile_y,
1850 int width_tiles, int height_tiles) {
1851 promote_priority_rect_on_layer(bg2, layout_bg2, start_tile_x, start_tile_y,
1852 width_tiles, height_tiles);
1853 };
1854
1855 auto promote_door_priority = [&](DoorDirection render_direction,
1856 int render_tile_x, int render_tile_y,
1857 DoorPrioritySpan span) {
1858 constexpr int kSectionSize = 32;
1859 const auto section_start = [](int coordinate) {
1860 return (coordinate / kSectionSize) * kSectionSize;
1861 };
1862 const auto section_end = [&](int coordinate) {
1863 return section_start(coordinate) + kSectionSize;
1864 };
1865
1866 int start_x = render_tile_x;
1867 int start_y = render_tile_y;
1868 int width = 0;
1869 int height = 0;
1870
1871 switch (span) {
1872 case DoorPrioritySpan::kNormalLower:
1873 switch (render_direction) {
1875 start_y = section_start(render_tile_y);
1876 width = 4;
1877 height = 7;
1878 break;
1880 // South raster coordinates are one row below the USDASM table
1881 // anchor; the fixed priority span starts four rows below it.
1882 start_y = render_tile_y + 3;
1883 width = 4;
1884 height = 7;
1885 break;
1887 start_x = section_start(render_tile_x);
1888 width = 5;
1889 height = 4;
1890 break;
1892 // East raster coordinates are one column right of the table
1893 // anchor; the fixed priority span starts four columns right.
1894 start_x = render_tile_x + 3;
1895 width = 5;
1896 height = 4;
1897 break;
1898 }
1899 break;
1900
1901 case DoorPrioritySpan::kLowerLayerOnly:
1902 switch (render_direction) {
1904 start_y = section_start(render_tile_y);
1905 width = 4;
1906 height = render_tile_y - start_y + 1;
1907 break;
1909 start_y = render_tile_y + 1;
1910 width = 4;
1911 height = section_end(render_tile_y) - start_y;
1912 break;
1914 start_x = section_start(render_tile_x);
1915 width = render_tile_x - start_x + 1;
1916 height = 4;
1917 break;
1919 start_x = render_tile_x + 1;
1920 width = section_end(render_tile_x) - start_x;
1921 height = 4;
1922 break;
1923 }
1924 break;
1925
1926 case DoorPrioritySpan::kHighRange:
1927 switch (render_direction) {
1929 start_y = section_start(render_tile_y);
1930 width = 4;
1931 height = render_tile_y - start_y;
1932 break;
1934 start_y = render_tile_y + 3;
1935 width = 4;
1936 height = section_end(render_tile_y) - start_y;
1937 break;
1939 start_x = section_start(render_tile_x);
1940 width = render_tile_x - start_x;
1941 height = 4;
1942 break;
1944 // The visible raster starts one column right of the USDASM table
1945 // anchor and spans three columns. Priority begins at anchor+4.
1946 start_x = render_tile_x + 3;
1947 width = section_end(render_tile_x) - start_x;
1948 height = 4;
1949 break;
1950 }
1951 break;
1952 }
1953
1954 if (width > 0 && height > 0) {
1955 promote_upper_priority_rect(start_x, start_y, width, height);
1956 }
1957 };
1958
1959 // Door markers update room-transition metadata in USDASM; they do not
1960 // stamp tiles. In real room streams they commonly follow a physical door
1961 // at the same position, so treating them as art overwrites that door with
1962 // the marker table's mirrored shutter tiles.
1963 const bool is_nonvisual_marker = door.type == DoorType::DungeonSwapMarker ||
1965 (door.type == DoorType::ExitMarker &&
1968 if (is_nonvisual_marker) {
1969 return;
1970 }
1971
1972 // Type $06 only promotes existing upper-layer wall tiles in USDASM. It
1973 // neither looks up nor stamps door graphics.
1974 if (door.type == DoorType::UnusedCaveExit) {
1975 promote_door_priority(door.direction, tile_x, tile_y,
1976 DoorPrioritySpan::kLowerLayerOnly);
1977 return;
1978 }
1979
1980 // South has several dedicated routines that bypass the ordinary 4x3 door
1981 // tables. Their coordinates begin at the raw USDASM tilemap anchor rather
1982 // than the generic South render anchor one row below it.
1983 if (door.direction == DoorDirection::South) {
1984 const auto [raw_tile_x, raw_tile_y] =
1986 door.direction);
1987 const int fancy_data_addr =
1988 kRoomDrawObjectDataBase + kFancyDungeonExitObjectOffset;
1989 const int cave_light_data_addr =
1990 kRoomDrawObjectDataBase + kCaveExitLightObjectOffset;
1991 const auto has_object_words = [&](int address, int word_count) {
1992 return address >= 0 &&
1993 address + word_count * 2 <= static_cast<int>(rom_->size());
1994 };
1995
1996 switch (door.type) {
1998 if (!has_object_words(fancy_data_addr, 80)) {
1999 DrawDoorIndicator(bg1, raw_tile_x - 3, raw_tile_y - 4,
2000 /*width=*/10, /*height=*/8, door.type,
2001 door.direction);
2002 return;
2003 }
2004 draw_from_object_data_row_major(bg1, raw_tile_x - 3, raw_tile_y - 4,
2005 /*width=*/10, /*height=*/8,
2006 fancy_data_addr);
2007 return;
2008
2010 if (!has_object_words(fancy_data_addr, 80)) {
2011 DrawDoorIndicator(bg2, raw_tile_x - 3, raw_tile_y - 4,
2012 /*width=*/10, /*height=*/8, door.type,
2013 door.direction);
2014 return;
2015 }
2016 draw_from_object_data_row_major(bg2, raw_tile_x - 3, raw_tile_y - 4,
2017 /*width=*/10, /*height=*/8,
2018 fancy_data_addr);
2019 // RoomDraw_NormalRangedDoors_South copies the final lower-layer row
2020 // back to the upper tilemap and forces high priority.
2021 for (int dx = 0; dx < 10; ++dx) {
2022 (void)draw_object_data_tile(
2023 bg1, raw_tile_x - 3 + dx, raw_tile_y + 3, fancy_data_addr,
2024 /*tile_idx=*/70 + dx, /*word_mask=*/0x2000);
2025 }
2026 return;
2027
2029 if (!has_object_words(cave_light_data_addr, 16)) {
2030 DrawDoorIndicator(bg2, raw_tile_x, raw_tile_y, /*width=*/4,
2031 /*height=*/4, door.type, door.direction);
2032 return;
2033 }
2034 promote_lower_priority_rect(raw_tile_x, raw_tile_y + 4,
2035 /*width_tiles=*/4, /*height_tiles=*/7);
2036 draw_from_object_data(bg2, raw_tile_x, raw_tile_y, /*width=*/4,
2037 /*height=*/4, cave_light_data_addr);
2038 for (int dx = 0; dx < 4; ++dx) {
2039 (void)draw_object_data_tile(
2040 bg1, raw_tile_x + dx, raw_tile_y + 3, cave_light_data_addr,
2041 /*tile_idx=*/dx * 4 + 3, /*word_mask=*/0x2000);
2042 }
2043 return;
2044
2045 case DoorType::CaveExit:
2046 if (!has_object_words(cave_light_data_addr, 16)) {
2047 DrawDoorIndicator(bg1, raw_tile_x, raw_tile_y, /*width=*/4,
2048 /*height=*/4, door.type, door.direction);
2049 return;
2050 }
2051 draw_from_object_data(bg1, raw_tile_x, raw_tile_y, /*width=*/4,
2052 /*height=*/4, cave_light_data_addr);
2053 return;
2054
2056 if (!has_object_words(cave_light_data_addr, 16)) {
2057 DrawDoorIndicator(bg1, raw_tile_x, raw_tile_y, /*width=*/4,
2058 /*height=*/4, door.type, door.direction);
2059 return;
2060 }
2061 promote_upper_priority_rect(raw_tile_x, raw_tile_y + 4,
2062 /*width_tiles=*/4, /*height_tiles=*/7);
2063 draw_from_object_data(bg1, raw_tile_x, raw_tile_y, /*width=*/4,
2064 /*height=*/4, cave_light_data_addr);
2065 return;
2066
2067 default:
2068 break;
2069 }
2070 }
2071
2072 auto resolve_render_type = [](DoorDirection render_direction,
2073 uint16_t render_type) -> uint16_t {
2074 switch (render_type) {
2075 case static_cast<uint16_t>(DoorType::BigKeyDoor):
2076 // South's closed big-key door is coerced to the normal-door table
2077 // entry by RoomDraw_OneSidedShutters_South.
2078 return render_direction == DoorDirection::South
2079 ? static_cast<uint16_t>(DoorType::NormalDoor)
2080 : render_type;
2081 case static_cast<uint16_t>(DoorType::BottomSidedShutter):
2082 return (render_direction == DoorDirection::North ||
2083 render_direction == DoorDirection::West)
2084 ? static_cast<uint16_t>(DoorType::DoubleSidedShutter)
2085 : static_cast<uint16_t>(DoorType::NormalDoor);
2086 case static_cast<uint16_t>(DoorType::TopSidedShutter):
2087 return (render_direction == DoorDirection::North ||
2088 render_direction == DoorDirection::West)
2089 ? static_cast<uint16_t>(DoorType::NormalDoor)
2090 : static_cast<uint16_t>(DoorType::DoubleSidedShutter);
2091 case static_cast<uint16_t>(DoorType::BottomShutterLower):
2092 return (render_direction == DoorDirection::North ||
2093 render_direction == DoorDirection::West)
2094 ? static_cast<uint16_t>(DoorType::DoubleSidedShutterLower)
2095 : static_cast<uint16_t>(DoorType::NormalDoorOneSidedShutter);
2096 case static_cast<uint16_t>(DoorType::TopShutterLower):
2097 return (render_direction == DoorDirection::North ||
2098 render_direction == DoorDirection::West)
2099 ? static_cast<uint16_t>(DoorType::NormalDoorOneSidedShutter)
2100 : static_cast<uint16_t>(DoorType::DoubleSidedShutterLower);
2101 default:
2102 return render_type;
2103 }
2104 };
2105
2106 auto resolve_table_door_data = [&](DoorDirection render_direction,
2107 uint16_t render_type,
2108 int* tile_data_addr) -> bool {
2109 int offset_table_addr = 0;
2110 switch (render_direction) {
2112 offset_table_addr = kDoorGfxUp;
2113 break;
2115 offset_table_addr = kDoorGfxDown;
2116 break;
2118 offset_table_addr = kDoorGfxLeft;
2119 break;
2121 offset_table_addr = kDoorGfxRight;
2122 break;
2123 }
2124
2125 const uint16_t resolved_type =
2126 resolve_render_type(render_direction, render_type);
2127 // DoorGFXDataOffset_* is an absolute-indexed word table and Y is already
2128 // the byte offset. Do not divide/re-multiply: hacked replacement tables may
2129 // intentionally supply an odd or wider offset.
2130 const int table_entry_addr = offset_table_addr + resolved_type;
2131 if (table_entry_addr + 1 >= static_cast<int>(rom_->size())) {
2132 return false;
2133 }
2134
2135 const uint16_t tile_offset =
2136 rom_data[table_entry_addr] | (rom_data[table_entry_addr + 1] << 8);
2137 *tile_data_addr = kRoomDrawObjectDataBase + tile_offset;
2138 const auto dims = GetDoorDimensions(render_direction);
2139 const int data_size = dims.width_tiles * dims.height_tiles * 2;
2140 if (*tile_data_addr < 0 ||
2141 *tile_data_addr + data_size > static_cast<int>(rom_->size())) {
2142 return false;
2143 }
2144
2145 return true;
2146 };
2147
2148 auto draw_table_door = [&](gfx::BackgroundBuffer& target,
2149 DoorDirection render_direction, int start_tile_x,
2150 int start_tile_y, uint16_t render_type) -> bool {
2151 int tile_data_addr = 0;
2152 if (!resolve_table_door_data(render_direction, render_type,
2153 &tile_data_addr)) {
2154 return false;
2155 }
2156
2157 const auto render_dims = GetDoorDimensions(render_direction);
2158 draw_from_object_data(target, start_tile_x, start_tile_y,
2159 render_dims.width_tiles, render_dims.height_tiles,
2160 tile_data_addr);
2161 return true;
2162 };
2163
2164 auto draw_high_range_table_door = [&](DoorDirection render_direction,
2165 int start_tile_x, int start_tile_y,
2166 uint16_t render_type) -> bool {
2167 int tile_data_addr = 0;
2168 if (!resolve_table_door_data(render_direction, render_type,
2169 &tile_data_addr)) {
2170 return false;
2171 }
2172
2173 const auto dims = GetDoorDimensions(render_direction);
2174 int tile_idx = 0;
2175 for (int dx = 0; dx < dims.width_tiles; ++dx) {
2176 for (int dy = 0; dy < dims.height_tiles; ++dy) {
2177 // RoomDraw_OneSidedLowerShutters_* splits one logical door across
2178 // the upper ($7E2000/BG1) and lower ($7E4000/BG2) tilemaps.
2179 bool writes_upper = false;
2180 switch (render_direction) {
2182 writes_upper = dy == 0;
2183 break;
2185 writes_upper = dy == dims.height_tiles - 1;
2186 break;
2188 writes_upper = dx == 0;
2189 break;
2191 writes_upper = dx == dims.width_tiles - 1;
2192 break;
2193 }
2194 gfx::BackgroundBuffer& target = writes_upper ? bg1 : bg2;
2195 if (!draw_object_data_tile(target, start_tile_x + dx, start_tile_y + dy,
2196 tile_data_addr, tile_idx++)) {
2197 return false;
2198 }
2199 }
2200 }
2201 return true;
2202 };
2203
2204 auto draw_ranged_door = [&](DoorDirection render_direction, int start_tile_x,
2205 int start_tile_y, DoorType original_type,
2206 uint16_t table_type) -> bool {
2207 if (original_type == DoorType::NormalDoorLower) {
2208 // Type $02 stamps the ordinary door art after promoting the fixed wall
2209 // rectangle selected by RoomDraw_MakeDoorPartsHighPriority_*.
2210 promote_door_priority(render_direction, start_tile_x, start_tile_y,
2211 DoorPrioritySpan::kNormalLower);
2212 }
2213
2214 // The dispatcher selects the normal/high-range writer before
2215 // RoomDraw_FlagDoorsAndGetFinalType substitutes open-door graphics. Keep
2216 // that choice tied to the stored type while looking up art by final type.
2217 const int render_type_value = static_cast<int>(original_type);
2218 const bool is_high_range =
2219 render_type_value >= 0x40 && render_type_value <= 0x66;
2220 if (is_high_range) {
2221 const bool drew = draw_high_range_table_door(
2222 render_direction, start_tile_x, start_tile_y, table_type);
2223 // North type $46 deliberately skips RoomDraw_MakeDoorHighPriority_North;
2224 // every other high-range direction/type promotes the adjacent upper wall.
2225 if (drew && !(render_direction == DoorDirection::North &&
2226 original_type == DoorType::ExplicitRoomDoor)) {
2227 promote_door_priority(render_direction, start_tile_x, start_tile_y,
2228 DoorPrioritySpan::kHighRange);
2229 }
2230 return drew;
2231 }
2232 return draw_table_door(bg1, render_direction, start_tile_x, start_tile_y,
2233 table_type);
2234 };
2235
2236 // USDASM has special north-door branches that do not follow the generic 4x3
2237 // ranged-door path.
2238 if (door.direction == DoorDirection::North &&
2239 door.type == DoorType::ExplodingWall && !is_door_open) {
2240 LOG_DEBUG("ObjectDrawer",
2241 "DrawDoor: closed exploding wall intentionally draws nothing");
2242 (void)bg2;
2243 return;
2244 }
2245
2246 if (door.direction == DoorDirection::North &&
2247 door.type == DoorType::CurtainDoor && !is_door_open) {
2248 const int tile_data_addr =
2249 kRoomDrawObjectDataBase + kNorthCurtainClosedOffset;
2250 const int data_size = 16 * 2; // RoomDraw_4x4 closed curtain path.
2251 if (tile_data_addr < 0 ||
2252 tile_data_addr + data_size > static_cast<int>(rom_->size())) {
2253 DrawDoorIndicator(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
2254 door.type, door.direction);
2255 return;
2256 }
2257 draw_from_object_data(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
2258 tile_data_addr);
2259 return;
2260 }
2261
2262 if (door.direction == DoorDirection::North &&
2263 door.type == DoorType::CurtainDoor && is_door_open) {
2264 const int table_entry_addr =
2265 kDoorGfxUp + static_cast<int>(effective_door_type);
2266 if (table_entry_addr < 0 ||
2267 table_entry_addr + 1 >= static_cast<int>(rom_->size())) {
2268 DrawDoorIndicator(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
2269 door.type, door.direction);
2270 return;
2271 }
2272
2273 const uint16_t tile_offset =
2274 rom_data[table_entry_addr] | (rom_data[table_entry_addr + 1] << 8);
2275 const int tile_data_addr = kRoomDrawObjectDataBase + tile_offset;
2276 const int data_size = 16 * 2; // RoomDraw_4x4 open curtain path.
2277 if (tile_data_addr < 0 ||
2278 tile_data_addr + data_size > static_cast<int>(rom_->size())) {
2279 DrawDoorIndicator(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
2280 door.type, door.direction);
2281 return;
2282 }
2283
2284 draw_from_object_data(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
2285 tile_data_addr);
2286 return;
2287 }
2288
2289 if (door.direction == DoorDirection::North &&
2290 door.type == DoorType::ExplodingWall && is_door_open) {
2291 const int position_index = std::min<int>(door.position & 0x0F, 5);
2292 const int tilemap_entry_addr =
2293 kExplodingWallTilemapPositionBase + (position_index * 2);
2294 if (tilemap_entry_addr < 0 ||
2295 tilemap_entry_addr + 1 >= static_cast<int>(rom_->size())) {
2296 DrawDoorIndicator(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
2297 door.type, door.direction);
2298 return;
2299 }
2300
2301 const uint16_t tilemap_offset =
2302 rom_data[tilemap_entry_addr] | (rom_data[tilemap_entry_addr + 1] << 8);
2303 const auto explosion_tile_coords =
2304 tilemap_offset_to_tile_coords(tilemap_offset);
2305 const int explosion_tile_x = explosion_tile_coords.first;
2306 const int explosion_tile_y = explosion_tile_coords.second;
2307
2308 auto draw_exploding_wall_segment = [&](int table_entry_addr,
2309 int segment_tile_y) -> bool {
2310 if (table_entry_addr < 0 ||
2311 table_entry_addr + 1 >= static_cast<int>(rom_->size())) {
2312 return false;
2313 }
2314
2315 const uint16_t tile_offset =
2316 rom_data[table_entry_addr] | (rom_data[table_entry_addr + 1] << 8);
2317 const int tile_data_addr = kRoomDrawObjectDataBase + tile_offset;
2318 constexpr int kFillWordIndex = 12;
2319 const int min_data_size = (kFillWordIndex + 1) * 2;
2320 if (tile_data_addr < 0 ||
2321 tile_data_addr + min_data_size > static_cast<int>(rom_->size())) {
2322 return false;
2323 }
2324
2325 draw_from_object_data(bg1, explosion_tile_x, segment_tile_y,
2326 /*width=*/2, /*height=*/6, tile_data_addr);
2327 const uint16_t fill_word =
2328 rom_data[tile_data_addr + (kFillWordIndex * 2)] |
2329 (rom_data[tile_data_addr + (kFillWordIndex * 2) + 1] << 8);
2330 draw_repeated_tile(bg1, explosion_tile_x + 2, segment_tile_y,
2331 /*width=*/18, /*height=*/6, fill_word);
2332 return true;
2333 };
2334
2335 const int south_table_entry_addr =
2336 kDoorGfxDown + kExplodingWallOpenReplacementType;
2337 const int north_table_entry_addr =
2338 kDoorGfxUp + kExplodingWallOpenReplacementType;
2339 if (!draw_exploding_wall_segment(south_table_entry_addr,
2340 explosion_tile_y) ||
2341 !draw_exploding_wall_segment(north_table_entry_addr,
2342 explosion_tile_y + 6)) {
2343 DrawDoorIndicator(bg1, explosion_tile_x, explosion_tile_y, /*width=*/20,
2344 /*height=*/12, door.type, door.direction);
2345 }
2346 return;
2347 }
2348
2349 // RoomDraw_North treats all four key-stair types ($20/$22/$24/$26) as
2350 // stateful stair records. Once their persistent bit is open, USDASM returns
2351 // without drawing replacement art.
2352 const bool is_north_key_stairs =
2358 if (is_north_key_stairs && is_door_open) {
2359 return;
2360 }
2361
2362 // Closed North lower-layer key stairs ($24/$26) use the ordinary 4x3 table
2363 // layout, but every tile is written to $7E4000/BG2 and no middle-door
2364 // counterpart is emitted.
2365 const bool is_north_lower_key_stairs =
2369 if (is_north_lower_key_stairs) {
2370 if (!draw_table_door(bg2, door.direction, tile_x, tile_y,
2371 static_cast<uint16_t>(door.type))) {
2372 DrawDoorIndicator(bg2, tile_x, tile_y, door_width, door_height, door.type,
2373 door.direction);
2374 }
2375 return;
2376 }
2377
2378 // The normal ranged-door callers treat curtain and waterfall final types as
2379 // metadata-only results (carry clear from FlagDoorsAndGetFinalType). North
2380 // curtain and lower key stairs have already taken their dedicated branches.
2381 const bool uses_normal_ranged_writer =
2382 static_cast<int>(door.type) <
2383 static_cast<int>(DoorType::NormalDoorOneSidedShutter);
2384 if (uses_normal_ranged_writer &&
2385 (effective_door_type == static_cast<uint16_t>(DoorType::CurtainDoor) ||
2386 effective_door_type == static_cast<uint16_t>(DoorType::WaterfallDoor))) {
2387 return;
2388 }
2389
2390 // Door graphics use an indirect addressing scheme:
2391 // 1. kDoorGfxUp/Down/Left/Right point to offset tables (DoorGFXDataOffset_*)
2392 // 2. Each table entry is a 16-bit offset into RoomDrawObjectData
2393 // 3. RoomDrawObjectData base is at PC 0x1B52 (SNES $00:9B52)
2394 // 4. Actual tile data = 0x1B52 + offset_from_table
2395 const bool north_explicit_door = door.direction == DoorDirection::North &&
2397 if ((door.direction == DoorDirection::North ||
2398 door.direction == DoorDirection::West) &&
2399 position_index >= 6 && !north_explicit_door) {
2400 const DoorDirection counterpart_direction =
2403 // USDASM indexes the table immediately following NorthMiddle/WestMiddle
2404 // with the original 6..11 position offset. That lands on counterpart
2405 // positions 0..5; the two rasters are separated by the middle wall rather
2406 // than overlapping at the current anchor.
2407 const auto [counterpart_tile_x, counterpart_tile_y] =
2409 static_cast<uint8_t>(position_index - 6), counterpart_direction);
2410 (void)draw_ranged_door(counterpart_direction, counterpart_tile_x,
2411 counterpart_tile_y, door.type, effective_door_type);
2412 }
2413
2414 const bool drew_current = draw_ranged_door(door.direction, tile_x, tile_y,
2415 door.type, effective_door_type);
2416 if (!drew_current) {
2417 LOG_DEBUG("ObjectDrawer",
2418 "DrawDoor: INVALID ADDRESS - falling back to indicator");
2419 DrawDoorIndicator(bg1, tile_x, tile_y, door_width, door_height, door.type,
2420 door.direction);
2421 return;
2422 }
2423
2424 LOG_DEBUG("ObjectDrawer",
2425 "DrawDoor: type=%s effective=0x%04X dir=%s pos=%d at tile(%d,%d) "
2426 "size=%dx%d",
2427 std::string(GetDoorTypeName(door.type)).c_str(),
2428 static_cast<unsigned int>(effective_door_type),
2429 std::string(GetDoorDirectionName(door.direction)).c_str(),
2430 door.position, tile_x, tile_y, door_width, door_height);
2431}
2432
2434 int tile_y, int width, int height,
2435 DoorType type, DoorDirection direction) {
2436 // Draw a simple colored rectangle as door indicator when graphics unavailable
2437 // Different colors for different door types using DoorType enum
2438
2439 auto& bitmap = bg.bitmap();
2440 auto& coverage_buffer = bg.mutable_coverage_data();
2441
2442 uint8_t color_idx;
2443 switch (type) {
2446 color_idx = 45; // Standard door color (brown)
2447 break;
2448
2454 color_idx = 60; // Key door - yellowish
2455 break;
2456
2459 color_idx = 58; // Big key - golden
2460 break;
2461
2465 case DoorType::DashWall:
2466 color_idx = 15; // Bombable/destructible - brownish/cracked
2467 break;
2468
2475 color_idx = 30; // Shutter - greenish
2476 break;
2477
2479 color_idx = 42; // Eye watch - lighter brown
2480 break;
2481
2483 color_idx = 35; // Curtain - special
2484 break;
2485
2486 case DoorType::CaveExit:
2491 color_idx = 25; // Cave/dungeon exit - dark
2492 break;
2493
2497 color_idx = 5; // Markers - very faint
2498 break;
2499
2500 default:
2501 color_idx = 50; // Default door color
2502 break;
2503 }
2504
2505 int pixel_x = tile_x * 8;
2506 int pixel_y = tile_y * 8;
2507 int pixel_width = width * 8;
2508 int pixel_height = height * 8;
2509
2510 int bitmap_width = bitmap.width();
2511 int bitmap_height = bitmap.height();
2512
2514 pixel_width, pixel_height);
2515
2516 // Draw filled rectangle with border
2517 for (int py = 0; py < pixel_height; py++) {
2518 for (int px = 0; px < pixel_width; px++) {
2519 int dest_x = pixel_x + px;
2520 int dest_y = pixel_y + py;
2521
2522 if (dest_x >= 0 && dest_x < bitmap_width && dest_y >= 0 &&
2523 dest_y < bitmap_height) {
2524 // Draw border (2 pixel thick) or fill
2525 bool is_border = (px < 2 || px >= pixel_width - 2 || py < 2 ||
2526 py >= pixel_height - 2);
2527 uint8_t final_color = is_border ? (color_idx + 5) : color_idx;
2528
2529 int offset = (dest_y * bitmap_width) + dest_x;
2530 bitmap.WriteToPixel(offset, final_color);
2531
2532 if (offset >= 0 && offset < static_cast<int>(coverage_buffer.size())) {
2533 coverage_buffer[offset] = 1;
2534 }
2535 }
2536 }
2537 }
2538}
2539
2541 std::span<const gfx::TileInfo> tiles,
2542 [[maybe_unused]] const DungeonState* state) {
2543 // USDASM RoomDraw_OpenChest draws F9A's fixed open graphic directly and
2544 // does not read or advance either chest/event counter.
2545 if (obj.id_ == 0xF9A) {
2547 return;
2548 }
2549
2550 // USDASM RoomDraw_Chest draws F99 as a single stateful 2x2 chest. The size
2551 // byte is not used for repetition.
2552
2553 // Determine if chest is open
2554 bool is_open = false;
2555 if (state) {
2556 is_open = state->IsChestOpen(room_id_, current_chest_index_);
2557 }
2558
2559 // RoomDraw_Chest advances the chest-only $0496 counter, then copies its next
2560 // value into the shared chest/lock $0498 counter.
2563
2564 // Draw SINGLE chest - no repetition based on size
2565 // Standard chests are 2x2 (4 tiles)
2566 // If we have extra tiles loaded, the second 4 are for open state
2567
2568 if (is_open && tiles.size() >= 8) {
2569 // Small chest open tiles (indices 4-7) - SINGLE 2x2 draw
2570 if (tiles.size() >= 8) {
2571 WriteTile8(bg, obj.x_, obj.y_, tiles[4]); // top-left
2572 WriteTile8(bg, obj.x_, obj.y_ + 1, tiles[5]); // bottom-left
2573 WriteTile8(bg, obj.x_ + 1, obj.y_, tiles[6]); // top-right
2574 WriteTile8(bg, obj.x_ + 1, obj.y_ + 1, tiles[7]); // bottom-right
2575 }
2576 return;
2577 }
2578
2579 // Draw closed chest - SINGLE 2x2 pattern (column-major order)
2580 if (tiles.size() >= 4) {
2581 WriteTile8(bg, obj.x_, obj.y_, tiles[0]); // top-left
2582 WriteTile8(bg, obj.x_, obj.y_ + 1, tiles[1]); // bottom-left
2583 WriteTile8(bg, obj.x_ + 1, obj.y_, tiles[2]); // top-right
2584 WriteTile8(bg, obj.x_ + 1, obj.y_ + 1, tiles[3]); // bottom-right
2585 }
2586}
2587
2590 std::span<const gfx::TileInfo> tiles,
2591 const DungeonState* state) {
2592 // USDASM RoomDraw_BigChest uses the chest-only $0496 slot for its room flag,
2593 // advances it once, then copies the next value into shared $0498. FB2 uses
2594 // RoomDraw_OpenBigChest directly and never reaches this stateful wrapper.
2595 bool is_open = false;
2596 if (state) {
2597 is_open = state->IsBigChestOpen(room_id_, current_chest_index_);
2598 }
2599
2602
2603 constexpr size_t kBigChestStateTileCount = 12;
2604 if (is_open && tiles.size() >= kBigChestStateTileCount * 2) {
2605 tiles = tiles.subspan(kBigChestStateTileCount, kBigChestStateTileCount);
2606 }
2608}
2609
2612 std::span<const gfx::TileInfo> tiles,
2613 const DungeonState* state) {
2614 // USDASM RoomDraw_BigKeyLock indexes $0402 through the shared $0498
2615 // chest/lock slot. An opened lock advances the slot but writes no tiles.
2616 const int room_event_index = current_room_event_index_++;
2617 if (state && state->IsBigKeyLockOpen(room_id_, room_event_index)) {
2618 return;
2619 }
2620
2622}
2623
2625 std::span<const gfx::TileInfo> tiles,
2626 [[maybe_unused]] const DungeonState* state) {
2627 // Intentionally empty - represents invisible logic objects or placeholders
2628 // ASM: RoomDraw_Nothing_A ($0190F2), RoomDraw_Nothing_B ($01932E), etc.
2629 // These routines typically just RTS.
2630 LOG_DEBUG("ObjectDrawer", "DrawNothing for object 0x%02X (logic/invisible)",
2631 obj.id_);
2632}
2633
2635 std::span<const gfx::TileInfo> tiles,
2636 [[maybe_unused]] const DungeonState* state) {
2637 // Pattern: Custom draw routine (objects 0x31-0x32)
2638 // For now, fall back to simple 1x1
2639 if (tiles.size() >= 1) {
2640 // Use first 8x8 tile from span
2641 WriteTile8(bg, obj.x_, obj.y_, tiles[0]);
2642 }
2643}
2644
2646 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2647 std::span<const gfx::TileInfo> tiles,
2648 [[maybe_unused]] const DungeonState* state) {
2649 // Pattern: 4x4 block rightward (objects 0x33, 0xBA = large ceiling, etc.)
2650 int size = obj.size_ & 0x0F;
2651
2652 // Assembly: GetSize_1to16, so count = size + 1
2653 int count = size + 1;
2654
2655 // Debug: Log large ceiling objects (0xBA)
2656 if (obj.id_ == 0xBA && tiles.size() >= 16) {
2657 LOG_DEBUG("ObjectDrawer",
2658 "Large Ceiling Draw: obj=0x%02X pos=(%d,%d) size=%d tiles=%zu",
2659 obj.id_, obj.x_, obj.y_, size, tiles.size());
2660 LOG_DEBUG("ObjectDrawer", " First 4 Tile IDs: [%d, %d, %d, %d]",
2661 tiles[0].id_, tiles[1].id_, tiles[2].id_, tiles[3].id_);
2662 LOG_DEBUG("ObjectDrawer", " First 4 Palettes: [%d, %d, %d, %d]",
2663 tiles[0].palette_, tiles[1].palette_, tiles[2].palette_,
2664 tiles[3].palette_);
2665 }
2666
2667 for (int s = 0; s < count; s++) {
2668 if (tiles.size() >= 16) {
2669 // Draw 4x4 pattern in COLUMN-MAJOR order (matching assembly)
2670 // Iterate columns (x) first, then rows (y) within each column
2671 for (int x = 0; x < 4; ++x) {
2672 for (int y = 0; y < 4; ++y) {
2673 WriteTile8(bg, obj.x_ + (s * 4) + x, obj.y_ + y, tiles[x * 4 + y]);
2674 }
2675 }
2676 }
2677 }
2678}
2679
2681 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2682 std::span<const gfx::TileInfo> tiles,
2683 [[maybe_unused]] const DungeonState* state) {
2684 // Pattern: 4x3 decoration with spacing (objects 0x3A-0x3B)
2685 // 4 columns × 3 rows = 12 tiles in COLUMN-MAJOR order
2686 // ASM: ADC #$0008 to Y = 8-byte advance = 4 tiles per iteration
2687 // Total spacing: 4 (object width) + 4 (gap) = 8 tiles between starts
2688 int size = obj.size_ & 0x0F;
2689
2690 // Assembly: GetSize_1to16, so count = size + 1
2691 int count = size + 1;
2692
2693 for (int s = 0; s < count; s++) {
2694 if (tiles.size() >= 12) {
2695 // Draw 4x3 pattern in COLUMN-MAJOR order (matching assembly)
2696 // Spacing: 8 tiles (4 object + 4 gap) per ASM ADC #$0008
2697 for (int x = 0; x < 4; ++x) {
2698 for (int y = 0; y < 3; ++y) {
2699 WriteTile8(bg, obj.x_ + (s * 8) + x, obj.y_ + y, tiles[x * 3 + y]);
2700 }
2701 }
2702 }
2703 }
2704}
2705
2706// ============================================================================
2707// Utility Methods
2708// ============================================================================
2709
2711 int start_py, int pixel_width,
2712 int pixel_height) {
2713 bg1.SetBG1RevealMaskRect(bg1_reveal_mask_source_, start_px, start_py,
2714 pixel_width, pixel_height);
2715}
2716
2718 gfx::BackgroundBuffer& bg1, const gfx::TileInfo& tile_info, int pixel_x,
2719 int pixel_y, const uint8_t* tiledata) {
2720 auto& bitmap = bg1.bitmap();
2721 if (!bitmap.is_active() || bitmap.width() == 0 || bitmap.height() == 0 ||
2722 tiledata == nullptr) {
2723 return;
2724 }
2725
2726 constexpr int kMaxTileRow = 63;
2727 const int tile_col = tile_info.id_ % 16;
2728 const int tile_row = tile_info.id_ / 16;
2729 if (tile_row > kMaxTileRow) {
2730 return;
2731 }
2732
2733 const int tile_base_x = tile_col * 8;
2734 const int tile_base_y = tile_row * 1024;
2735 auto& reveal_mask = bg1.mutable_bg1_reveal_mask_data();
2736 const uint8_t source_mask = static_cast<uint8_t>(bg1_reveal_mask_source_);
2737
2738 for (int py = 0; py < 8; ++py) {
2739 const int src_row = tile_info.vertical_mirror_ ? (7 - py) : py;
2740 const int dest_y = pixel_y + py;
2741 if (dest_y < 0 || dest_y >= bitmap.height()) {
2742 continue;
2743 }
2744
2745 for (int px = 0; px < 8; ++px) {
2746 const int src_col = tile_info.horizontal_mirror_ ? (7 - px) : px;
2747 const int src_index =
2748 (src_row * 128) + src_col + tile_base_x + tile_base_y;
2749 if (tiledata[src_index] == 0) {
2750 continue;
2751 }
2752
2753 const int dest_x = pixel_x + px;
2754 if (dest_x < 0 || dest_x >= bitmap.width()) {
2755 continue;
2756 }
2757
2758 const int dest_index = dest_y * bitmap.width() + dest_x;
2759 reveal_mask[dest_index] |= source_mask;
2760 }
2761 }
2762}
2763
2764void ObjectDrawer::WriteTile8(gfx::BackgroundBuffer& bg, int tile_x, int tile_y,
2765 const gfx::TileInfo& tile_info) {
2766 if (!IsValidTilePosition(tile_x, tile_y)) {
2767 return;
2768 }
2769 PushTrace(tile_x, tile_y, tile_info);
2770 if (trace_only_) {
2771 return;
2772 }
2773 // Keep the logical tilemap synchronized with the bitmap/coverage owner.
2774 // Conditional edge routines query this word after coverage selects whether
2775 // the object or layout half owns the effective physical BG entry.
2776 bg.SetTileAt(tile_x, tile_y, gfx::TileInfoToWord(tile_info));
2777 // Draw directly to bitmap instead of tile buffer to avoid being overwritten
2778 auto& bitmap = bg.bitmap();
2779 if (!bitmap.is_active() || bitmap.width() == 0) {
2780 return; // Bitmap not ready
2781 }
2782
2783 // The room-specific graphics buffer (current_gfx16_) contains the assembled
2784 // tile graphics for the current room. Object tile IDs are relative to this
2785 // buffer.
2786 const uint8_t* gfx_data = room_gfx_buffer_;
2787
2788 if (!gfx_data) {
2789 LOG_DEBUG("ObjectDrawer", "ERROR: No graphics data available");
2790 return;
2791 }
2792
2793 // A later BG1 tilemap write supersedes an earlier reveal request from the
2794 // same logical stream, including transparent pixels in the 8x8 footprint.
2795 bg.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, tile_x * 8, tile_y * 8, 8,
2796 8);
2797
2798 const bool should_mark_bg1_mask =
2799 active_mask_source_bg_ != nullptr && (&bg == active_mask_source_bg_);
2800 // Draw single 8x8 tile directly to bitmap.
2801 DrawTileToBitmap(bitmap, tile_info, tile_x * 8, tile_y * 8, gfx_data);
2802 if (should_mark_bg1_mask && active_object_bg1_mask_ != nullptr) {
2804 tile_x * 8, tile_y * 8, gfx_data);
2805 }
2806 if (should_mark_bg1_mask && active_layout_bg1_mask_ != nullptr) {
2808 tile_x * 8, tile_y * 8, gfx_data);
2809 }
2810
2811 // Mark coverage for the full 8x8 tile region (even if pixels are transparent).
2812 //
2813 // This distinguishes "tilemap entry written but transparent" from "no write",
2814 // which is required to emulate SNES behavior where a transparent tile still
2815 // overwrites the previous tilemap entry (clearing BG1 and revealing BG2/backdrop).
2816 auto& coverage_buffer = bg.mutable_coverage_data();
2817
2818 // Also update priority buffer with tile's priority bit.
2819 // Priority (over_) affects Z-ordering in SNES Mode 1 compositing.
2820 uint8_t priority = tile_info.over_ ? 1 : 0;
2821 int pixel_x = tile_x * 8;
2822 int pixel_y = tile_y * 8;
2823 auto& priority_buffer = bg.mutable_priority_data();
2824 int width = bitmap.width();
2825
2826 // Update priority for each pixel in the 8x8 tile
2827 const auto& bitmap_data = bitmap.vector();
2828 for (int py = 0; py < 8; py++) {
2829 int dest_y = pixel_y + py;
2830 if (dest_y < 0 || dest_y >= bitmap.height())
2831 continue;
2832
2833 for (int px = 0; px < 8; px++) {
2834 int dest_x = pixel_x + px;
2835 if (dest_x < 0 || dest_x >= width)
2836 continue;
2837
2838 int dest_index = dest_y * width + dest_x;
2839
2840 // Coverage is set for all pixels in the tile footprint.
2841 if (dest_index >= 0 &&
2842 dest_index < static_cast<int>(coverage_buffer.size())) {
2843 coverage_buffer[dest_index] = 1;
2844 }
2845
2846 // Store priority only for opaque pixels; transparent writes clear stale
2847 // priority at this location.
2848 if (dest_index < static_cast<int>(bitmap_data.size()) &&
2849 bitmap_data[dest_index] != 255) {
2850 priority_buffer[dest_index] = priority;
2851 } else {
2852 priority_buffer[dest_index] = 0xFF;
2853 }
2854 }
2855 }
2856}
2857
2858bool ObjectDrawer::IsValidTilePosition(int tile_x, int tile_y) const {
2859 return tile_x >= 0 && tile_x < kMaxTilesX && tile_y >= 0 &&
2860 tile_y < kMaxTilesY;
2861}
2862
2864 const gfx::TileInfo& tile_info, int pixel_x,
2865 int pixel_y, const uint8_t* tiledata) {
2866 // Draw an 8x8 tile directly to bitmap at pixel coordinates
2867 // Graphics data is in 8BPP linear format (1 pixel per byte)
2868 if (!tiledata)
2869 return;
2870
2871 // DEBUG: Check if bitmap is valid
2872 if (!bitmap.is_active() || bitmap.width() == 0 || bitmap.height() == 0) {
2873 LOG_DEBUG("ObjectDrawer", "ERROR: Invalid bitmap - active=%d, size=%dx%d",
2874 bitmap.is_active(), bitmap.width(), bitmap.height());
2875 return;
2876 }
2877
2878 // Calculate tile position in 8BPP graphics buffer
2879 // Layout: 16 tiles per row, each tile is 8 pixels wide (8 bytes)
2880 // Row stride: 128 bytes (16 tiles * 8 bytes)
2881 // Buffer size: 0x10000 (65536 bytes) = 64 tile rows max
2882 constexpr int kGfxBufferSize = 0x10000;
2883 constexpr int kMaxTileRow = 63; // 64 rows (0-63), each 1024 bytes
2884
2885 int tile_col = tile_info.id_ % 16;
2886 int tile_row = tile_info.id_ / 16;
2887
2888 // CRITICAL: Validate tile_row to prevent index out of bounds
2889 if (tile_row > kMaxTileRow) {
2890 LOG_DEBUG("ObjectDrawer", "Tile ID 0x%03X out of bounds (row %d > %d)",
2891 tile_info.id_, tile_row, kMaxTileRow);
2892 return;
2893 }
2894
2895 int tile_base_x = tile_col * 8; // 8 bytes per tile horizontally
2896 int tile_base_y =
2897 tile_row * 1024; // 1024 bytes per tile row (8 rows * 128 bytes)
2898
2899 // DEBUG: Log first few tiles being drawn with their graphics data
2900 static int draw_debug_count = 0;
2901 if (draw_debug_count < 5) {
2902 int sample_index = tile_base_y + tile_base_x;
2903 LOG_DEBUG("ObjectDrawer",
2904 "DrawTile: id=%d (col=%d,row=%d) gfx_offset=%d (0x%04X)",
2905 tile_info.id_, tile_col, tile_row, sample_index, sample_index);
2906 draw_debug_count++;
2907 }
2908
2909 // Palette offset calculation using direct CGRAM row mirroring.
2910 //
2911 // Room::RenderRoomGraphics loads dungeon main palettes into SDL bank rows 2-7,
2912 // leaving rows 0-1 as transparent HUD placeholders. The tile palette bits are
2913 // therefore already the correct SDL bank row index.
2914 //
2915 // Drawing formula: final_color = pixel + (pal * 16)
2916 // Where pixel 0 = transparent (not written), pixel 1-15 = colors within bank.
2917 uint8_t pal = tile_info.palette_ & 0x07;
2918 const uint8_t palette_offset = static_cast<uint8_t>(pal * 16);
2919
2920 // Draw 8x8 pixels with overwrite semantics.
2921 //
2922 // Important SNES behavior: writing a tilemap entry replaces the previous
2923 // contents for the full 8x8 footprint. Source pixel 0 is transparent, but it
2924 // still clears what was there before. We model that by writing 255
2925 // (transparent key) for zero pixels.
2926 bool any_pixels_changed = false;
2927
2928 for (int py = 0; py < 8; py++) {
2929 // Source row with vertical mirroring
2930 int src_row = tile_info.vertical_mirror_ ? (7 - py) : py;
2931
2932 for (int px = 0; px < 8; px++) {
2933 // Source column with horizontal mirroring
2934 int src_col = tile_info.horizontal_mirror_ ? (7 - px) : px;
2935
2936 // Calculate source index in 8BPP buffer
2937 // Stride is 128 bytes (sheet width)
2938 int src_index = (src_row * 128) + src_col + tile_base_x + tile_base_y;
2939 uint8_t pixel = tiledata[src_index];
2940 uint8_t out_pixel = 255; // transparent/clear
2941 if (pixel != 0) {
2942 // Pixels 1-15 map into a 16-color bank chunk.
2943 out_pixel = static_cast<uint8_t>(pixel + palette_offset);
2944 }
2945
2946 int dest_x = pixel_x + px;
2947 int dest_y = pixel_y + py;
2948 if (dest_x < 0 || dest_x >= bitmap.width() || dest_y < 0 ||
2949 dest_y >= bitmap.height()) {
2950 continue;
2951 }
2952
2953 int dest_index = dest_y * bitmap.width() + dest_x;
2954 if (dest_index < 0 ||
2955 dest_index >= static_cast<int>(bitmap.mutable_data().size())) {
2956 continue;
2957 }
2958
2959 auto& dst = bitmap.mutable_data()[dest_index];
2960 if (dst != out_pixel) {
2961 dst = out_pixel;
2962 any_pixels_changed = true;
2963 }
2964 }
2965 }
2966
2967 if (any_pixels_changed) {
2968 bitmap.set_modified(true);
2969 }
2970}
2971
2973 uint16_t object_id, int tile_x, int tile_y, RoomObject::LayerType layer,
2974 uint16_t room_draw_object_data_offset, gfx::BackgroundBuffer& bg1,
2975 gfx::BackgroundBuffer& bg2) {
2976 if (!rom_ || !rom_->is_loaded()) {
2977 return absl::FailedPreconditionError("ROM not loaded");
2978 }
2979
2980 const auto& rom_data = rom_->vector();
2981 const int base =
2982 kRoomObjectTileAddress + static_cast<int>(room_draw_object_data_offset);
2983 if (base < 0 || base + 7 >= static_cast<int>(rom_data.size())) {
2984 return absl::OutOfRangeError(absl::StrFormat(
2985 "RoomDrawObjectData 2x2 out of range: base=0x%X", base));
2986 }
2987
2988 auto read_word = [&](int off) -> uint16_t {
2989 return static_cast<uint16_t>(rom_data[off]) |
2990 (static_cast<uint16_t>(rom_data[off + 1]) << 8);
2991 };
2992
2993 const uint16_t w0 = read_word(base + 0);
2994 const uint16_t w1 = read_word(base + 2);
2995 const uint16_t w2 = read_word(base + 4);
2996 const uint16_t w3 = read_word(base + 6);
2997
2998 const gfx::TileInfo t0 = gfx::WordToTileInfo(w0);
2999 const gfx::TileInfo t1 = gfx::WordToTileInfo(w1);
3000 const gfx::TileInfo t2 = gfx::WordToTileInfo(w2);
3001 const gfx::TileInfo t3 = gfx::WordToTileInfo(w3);
3002
3003 // Set trace context once; WriteTile8 will emit per-tile traces.
3004 RoomObject trace_obj(
3005 static_cast<int16_t>(object_id), static_cast<uint8_t>(tile_x),
3006 static_cast<uint8_t>(tile_y), 0, static_cast<uint8_t>(layer));
3007 SetTraceContext(trace_obj, layer);
3008
3009 gfx::BackgroundBuffer& target_bg =
3010 (layer == RoomObject::LayerType::BG2) ? bg2 : bg1;
3011
3012 // Column-major order (matches USDASM $BF/$CB/$C2/$CE writes).
3013 WriteTile8(target_bg, tile_x + 0, tile_y + 0, t0); // top-left
3014 WriteTile8(target_bg, tile_x + 0, tile_y + 1, t1); // bottom-left
3015 WriteTile8(target_bg, tile_x + 1, tile_y + 0, t2); // top-right
3016 WriteTile8(target_bg, tile_x + 1, tile_y + 1, t3); // bottom-right
3017
3018 return absl::OkStatus();
3019}
3020
3021// ============================================================================
3022// Type 3 / Special Routine Implementations
3023// ============================================================================
3024
3027 std::span<const gfx::TileInfo> tiles,
3028 int width, int height) {
3029 // Generic large object drawer
3030 if (tiles.size() >= static_cast<size_t>(width * height)) {
3031 for (int y = 0; y < height; ++y) {
3032 for (int x = 0; x < width; ++x) {
3033 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[y * width + x]);
3034 }
3035 }
3036 }
3037}
3038
3039} // namespace zelda3
3040} // namespace yaze
3041
3043 const RoomObject& object) {
3045}
3046
3048 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3049 [[maybe_unused]] std::span<const gfx::TileInfo> tiles,
3050 [[maybe_unused]] const DungeonState* state) {
3051 // CustomObjectManager should be initialized by DungeonEditorV2 with the
3052 // project's custom_objects_folder path before any objects are drawn
3053 auto& manager = CustomObjectManager::Get();
3054
3055 int subtype = obj.size_ & 0x1F;
3056 const std::string filename = manager.ResolveFilename(obj.id_, subtype);
3057 auto result = manager.GetObjectInternal(obj.id_, subtype);
3058 if (!result.ok()) {
3059 DrawMissingCustomObjectPlaceholder(bg, obj.x_, obj.y_);
3060 LOG_DEBUG("ObjectDrawer",
3061 "Custom object 0x%03X subtype %d (%s) not found: %s", obj.id_,
3062 subtype, filename.empty() ? "<unmapped>" : filename.c_str(),
3063 result.status().message().data());
3064 return;
3065 }
3066
3067 auto custom_obj = result.value();
3068 if (!custom_obj || custom_obj->IsEmpty())
3069 return;
3070
3071 int tile_x = obj.x_;
3072 int tile_y = obj.y_;
3073
3074 for (const auto& entry : custom_obj->tiles) {
3075 // Oracle's custom-object handlers advance past a zero payload word without
3076 // storing it. Preserve the tile already underneath this object.
3077 if (entry.tile_data == 0) {
3078 continue;
3079 }
3080 // entry.tile_data is the raw vhopppcc cccccccc source word. Resolve the
3081 // effective runtime word here so source editing remains lossless.
3082 const uint16_t runtime_word =
3083 CustomObjectRuntimeTileWord(obj.id_, entry.tile_data);
3084 // Convert to TileInfo and render using WriteTile8 (not SetTileAt which
3085 // only stores to buffer without rendering)
3086 gfx::TileInfo tile_info = gfx::WordToTileInfo(runtime_word);
3087 WriteTile8(bg, tile_x + entry.rel_x, tile_y + entry.rel_y, tile_info);
3088 }
3089}
3090
3092 gfx::BackgroundBuffer& bg, int tile_x, int tile_y) {
3093 if (trace_only_) {
3094 return;
3095 }
3096
3097 auto& bitmap = bg.bitmap();
3098 if (!bitmap.is_active() || bitmap.width() <= 0 || bitmap.height() <= 0) {
3099 return;
3100 }
3101
3102 auto& pixels = bitmap.mutable_data();
3103 auto& coverage = bg.mutable_coverage_data();
3104 auto& priority = bg.mutable_priority_data();
3105
3106 constexpr int kPlaceholderSizePx = 16;
3107 constexpr uint8_t kFillColor = 33;
3108 constexpr uint8_t kAccentColor = 47;
3109
3110 const int start_x = tile_x * 8;
3111 const int start_y = tile_y * 8;
3112 const int width = bitmap.width();
3113 const int height = bitmap.height();
3114
3115 bg.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, start_x, start_y,
3116 kPlaceholderSizePx, kPlaceholderSizePx);
3117
3118 for (int py = 0; py < kPlaceholderSizePx; ++py) {
3119 const int dest_y = start_y + py;
3120 if (dest_y < 0 || dest_y >= height)
3121 continue;
3122 for (int px = 0; px < kPlaceholderSizePx; ++px) {
3123 const int dest_x = start_x + px;
3124 if (dest_x < 0 || dest_x >= width)
3125 continue;
3126 const bool border = (px == 0 || py == 0 || px == kPlaceholderSizePx - 1 ||
3127 py == kPlaceholderSizePx - 1);
3128 const bool diagonal = (px == py) || (px + py == kPlaceholderSizePx - 1);
3129 const int dest_index = dest_y * width + dest_x;
3130 pixels[dest_index] = (border || diagonal) ? kAccentColor : kFillColor;
3131 if (dest_index < static_cast<int>(coverage.size()))
3132 coverage[dest_index] = 1;
3133 if (dest_index < static_cast<int>(priority.size()))
3134 priority[dest_index] = 0;
3135 }
3136 }
3137 bitmap.set_modified(true);
3138}
3139
3140void yaze::zelda3::ObjectDrawer::DrawPotItem(uint8_t item_id, int x, int y,
3142 // Draw a small colored indicator for pot items
3143 // Item types from ZELDA3_DUNGEON_SPEC.md Section 7.2
3144 // Uses palette indices that map to recognizable colors
3145
3146 if (item_id == 0)
3147 return; // Nothing - skip
3148
3149 auto& bitmap = bg.bitmap();
3150 auto& coverage_buffer = bg.mutable_coverage_data();
3151 if (!bitmap.is_active() || bitmap.width() == 0)
3152 return;
3153
3154 // Convert tile coordinates to pixel coordinates
3155 // Items are drawn offset from pot position (centered on pot)
3156 int pixel_x = (x * 8) + 2; // Offset 2 pixels into the pot tile
3157 int pixel_y = (y * 8) + 2;
3158
3159 // Choose color based on item category
3160 // Using palette indices that should be visible in dungeon palettes
3161 uint8_t color_idx;
3162 switch (item_id) {
3163 // Rupees (green/blue/red tones)
3164 case 1: // Green rupee
3165 case 7: // Blue rupee
3166 case 12: // Blue rupee variant
3167 color_idx = 30; // Greenish (palette 2, index 0)
3168 break;
3169
3170 // Hearts (red tones)
3171 case 6: // Heart
3172 case 11: // Heart
3173 case 13: // Heart variant
3174 // NOTE: Avoid palette indices 0/16/32/.. which are transparent in SNES
3175 // CGRAM rows. Using 5 gives a consistently visible indicator.
3176 color_idx = 5;
3177 break;
3178
3179 // Keys (yellow/gold)
3180 case 8: // Key*8
3181 case 19: // Key
3182 color_idx = 45; // Yellowish (palette 3)
3183 break;
3184
3185 // Bombs (dark/black)
3186 case 5: // Bomb
3187 case 10: // 1 bomb
3188 case 16: // Bomb refill
3189 color_idx = 60; // Darker color (palette 4)
3190 break;
3191
3192 // Arrows (brown/wood)
3193 case 9: // Arrow
3194 case 17: // Arrow refill
3195 color_idx = 15; // Brownish (palette 1)
3196 break;
3197
3198 // Magic (blue/purple)
3199 case 14: // Small magic
3200 case 15: // Big magic
3201 color_idx = 75; // Bluish (palette 5)
3202 break;
3203
3204 // Fairy (pink/light)
3205 case 18: // Fairy
3206 case 20: // Fairy*8
3207 color_idx = 5; // Pinkish
3208 break;
3209
3210 // Special/Traps (distinct colors)
3211 case 2: // Rock crab
3212 case 3: // Bee
3213 color_idx = 20; // Enemy indicator
3214 break;
3215
3216 case 23: // Hole
3217 case 24: // Warp
3218 case 25: // Staircase
3219 color_idx = 10; // Transport indicator
3220 break;
3221
3222 case 26: // Bombable
3223 case 27: // Switch
3224 color_idx = 35; // Interactive indicator
3225 break;
3226
3227 case 4: // Random
3228 default:
3229 color_idx = 50; // Default/random indicator
3230 break;
3231 }
3232
3233 // Safety: never use CGRAM transparent slots (0,16,32,...) for the indicator.
3234 // In the editor these would appear invisible or "missing" depending on
3235 // compositing.
3236 if (color_idx != 255 && (color_idx % 16) == 0) {
3237 color_idx++;
3238 }
3239
3240 // Draw a 4x4 colored square as item indicator
3241 int bitmap_width = bitmap.width();
3242 int bitmap_height = bitmap.height();
3243
3244 bg.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, pixel_x, pixel_y, 4, 4);
3245
3246 for (int py = 0; py < 4; py++) {
3247 for (int px = 0; px < 4; px++) {
3248 int dest_x = pixel_x + px;
3249 int dest_y = pixel_y + py;
3250
3251 // Bounds check
3252 if (dest_x >= 0 && dest_x < bitmap_width && dest_y >= 0 &&
3253 dest_y < bitmap_height) {
3254 int offset = (dest_y * bitmap_width) + dest_x;
3255 bitmap.WriteToPixel(offset, color_idx);
3256 if (offset >= 0 && offset < static_cast<int>(coverage_buffer.size())) {
3257 coverage_buffer[offset] = 1;
3258 }
3259 }
3260 }
3261 }
3262}
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
const auto & vector() const
Definition rom.h:173
auto data() const
Definition rom.h:169
auto size() const
Definition rom.h:168
bool is_loaded() const
Definition rom.h:155
void SetBG1RevealMaskRect(BG1RevealMaskSource source, int start_x, int start_y, int width, int height)
std::vector< uint8_t > & mutable_bg1_reveal_mask_data()
void SetPriorityAt(int x, int y, uint8_t priority)
std::vector< uint8_t > & mutable_priority_data()
void SetTileAt(int x, int y, uint16_t value)
std::vector< uint8_t > & mutable_coverage_data()
void ClearBG1RevealMaskRect(BG1RevealMaskSource source, int start_x, int start_y, int width, int height)
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:69
const uint8_t * data() const
Definition bitmap.h:400
auto size() const
Definition bitmap.h:399
bool is_active() const
Definition bitmap.h:407
void set_modified(bool modified)
Definition bitmap.h:411
int height() const
Definition bitmap.h:397
int width() const
Definition bitmap.h:396
int depth() const
Definition bitmap.h:398
std::vector< uint8_t > & mutable_data()
Definition bitmap.h:401
SDL_Surface * surface() const
Definition bitmap.h:402
bool modified() const
Definition bitmap.h:406
SNES 16-bit tile metadata container.
Definition snes_tile.h:52
static CustomObjectManager & Get()
static DimensionService & Get()
std::tuple< int, int, int, int > GetSelectionBoundsPixels(const RoomObject &obj) const
std::pair< int, int > GetPixelDimensions(const RoomObject &obj) const
static std::pair< int, int > PositionToRenderTileCoords(uint8_t position, DoorDirection direction)
Convert encoded position to the top-left tile of the visible door.
static std::pair< int, int > PositionToTileCoords(uint8_t position, DoorDirection direction)
Convert encoded position to tile coordinates.
const DrawRoutineInfo * GetRoutineInfo(int routine_id) const
bool RoutineDrawsToBothBGs(int routine_id) const
int GetRoutineIdForObject(int16_t object_id) const
static DrawRoutineRegistry & Get()
Interface for accessing dungeon game state.
virtual bool IsDoorSwitchActive(int room_id) const =0
virtual bool IsDoorOpen(int room_id, int door_index) const =0
virtual bool IsBigKeyLockOpen(int room_id, int room_event_index) const
virtual bool IsBigChestOpen() const =0
static ObjectDimensionTable & Get()
Draws dungeon objects to background buffers using game patterns.
int GetDrawRoutineId(int16_t object_id) const
Get draw routine ID for an object.
void WriteTile8(gfx::BackgroundBuffer &bg, int tile_x, int tile_y, const gfx::TileInfo &tile_info)
void DrawTileToBitmap(gfx::Bitmap &bitmap, const gfx::TileInfo &tile_info, int pixel_x, int pixel_y, const uint8_t *tiledata)
Draw a single tile directly to bitmap.
void InitializeDrawRoutines()
Initialize draw routine registry Must be called before drawing objects.
void DrawRightwards4x4_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
gfx::BackgroundBuffer * active_mask_source_bg_
void DrawBigChest(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
std::vector< TileTrace > * trace_collector_
gfx::BackgroundBuffer * active_layout_bg1_mask_
std::vector< DrawRoutine > draw_routines_
void DrawUsingRegistryRoutine(int routine_id, const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state)
gfx::BackgroundBuffer * registry_secondary_bg_
static bool RequiresRectangularBg1Mask(const RoomObject &object)
static void TraceHookThunk(gfx::BackgroundBuffer *bg, int tile_x, int tile_y, const gfx::TileInfo &tile_info, void *user_data)
void MarkBg1OpaqueTilePixelsRevealed(gfx::BackgroundBuffer &bg1, const gfx::TileInfo &tile_info, int pixel_x, int pixel_y, const uint8_t *tiledata)
std::pair< int, int > CalculateObjectDimensions(const RoomObject &object)
Calculate the dimensions (width, height) of an object in pixels.
void SetTraceContext(const RoomObject &object, RoomObject::LayerType layer)
void MarkBg1RectRevealed(gfx::BackgroundBuffer &bg1, int start_px, int start_py, int pixel_width, int pixel_height)
gfx::BackgroundBuffer * active_object_bg1_mask_
const uint8_t * room_gfx_buffer_
static bool RoutineDrawsToBothBGs(int routine_id)
void DrawNothing(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDoor(const DoorDef &door, int door_index, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2, const DungeonState *state=nullptr, gfx::BackgroundBuffer *layout_bg1=nullptr, gfx::BackgroundBuffer *layout_bg2=nullptr)
Draw a door to background buffers.
void DrawBigKeyLock(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
RoomObject::LayerType registry_primary_layer_
void DrawPotItem(uint8_t item_id, int x, int y, gfx::BackgroundBuffer &bg)
Draw a pot item visualization.
void DrawDoorIndicator(gfx::BackgroundBuffer &bg, int tile_x, int tile_y, int width, int height, DoorType type, DoorDirection direction)
void DrawLargeCanvasObject(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, int width, int height)
void PushTrace(int tile_x, int tile_y, const gfx::TileInfo &tile_info)
absl::Status DrawObjectList(const std::vector< RoomObject > &objects, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2, const gfx::PaletteGroup &palette_group, const DungeonState *state=nullptr, gfx::BackgroundBuffer *layout_bg1=nullptr, bool reset_room_event_indices=true, gfx::BackgroundBuffer *layout_bg2=nullptr)
Draw all objects in a room.
void DrawCustomObject(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwardsDecor4x3spaced4_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawChest(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void CustomDraw(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawMissingCustomObjectPlaceholder(gfx::BackgroundBuffer &bg, int tile_x, int tile_y)
absl::Status DrawObject(const RoomObject &object, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2, const gfx::PaletteGroup &palette_group, const DungeonState *state=nullptr, gfx::BackgroundBuffer *layout_bg1=nullptr, gfx::BackgroundBuffer *layout_bg2=nullptr)
Draw a room object to background buffers.
ObjectDrawer(Rom *rom, int room_id, const uint8_t *room_gfx_buffer=nullptr)
static constexpr int kMaxTilesY
absl::Status DrawRoomDrawObjectData2x2(uint16_t object_id, int tile_x, int tile_y, RoomObject::LayerType layer, uint16_t room_draw_object_data_offset, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2)
Draw a fixed 2x2 (16x16) tile pattern from RoomDrawObjectData.
void SetTraceCollector(std::vector< TileTrace > *collector, bool trace_only=false)
bool IsValidTilePosition(int tile_x, int tile_y) const
gfx::BG1RevealMaskSource bg1_reveal_mask_source_
const gfx::BackgroundBuffer * registry_primary_layout_bg_
RoomObject::LayerType registry_secondary_layer_
const std::vector< gfx::TileInfo > & tiles() const
Definition room_object.h:99
void SetRom(Rom *rom)
Definition room_object.h:78
#define LOG_DEBUG(category, format,...)
Definition log.h:103
#define LOG_WARN(category, format,...)
Definition log.h:108
std::optional< std::array< TileInfo, 8 > > DecodeDungeonFloorTilePattern(const std::vector< uint8_t > &rom_data, int tile_address, int tile_address_floor, uint8_t floor_graphics)
uint16_t TileInfoToWord(TileInfo tile_info)
Definition snes_tile.cc:361
TileInfo WordToTileInfo(uint16_t word)
Definition snes_tile.cc:378
void SetTraceHook(TraceHookFn hook, void *user_data, bool trace_only)
void PromoteTilePriorityOnly(gfx::BackgroundBuffer &target, int tile_x, int tile_y)
void PromoteTilePriorityOnOwners(gfx::BackgroundBuffer &object_owner, gfx::BackgroundBuffer *layout_owner, int tile_x, int tile_y)
void SyncModifiedBitmapToSurface(gfx::Bitmap &bitmap, const char *layer_name)
constexpr bool IsNorthMixedStraightInterroomObject(int object_id)
constexpr bool IsMixedStraightInterroomObject(int object_id)
constexpr DoorDimensions GetDoorDimensions(DoorDirection dir)
Get door dimensions based on direction.
Definition door_types.h:253
DoorType
Door types from ALTTP.
Definition door_types.h:33
@ NormalDoorOneSidedShutter
Normal door (lower layer; with one-sided shutters)
@ TopShutterLower
Top-sided shutter door (lower layer)
@ FancyDungeonExitLower
Fancy dungeon exit (lower layer)
@ FancyDungeonExit
Fancy dungeon exit.
@ SmallKeyDoor
Small key door.
@ ExitLower
Exit (lower layer)
@ SmallKeyStairsDown
Small key stairs (downwards)
@ BombableCaveExit
Bombable cave exit.
@ SmallKeyStairsUp
Small key stairs (upwards)
@ UnusedCaveExit
Unused cave exit (lower layer)
@ DungeonSwapMarker
Dungeon swap marker.
@ NormalDoor
Normal door (upper layer)
@ BombableDoor
Bombable door.
@ LayerSwapMarker
Layer swap marker.
@ ExplicitRoomDoor
Explicit room door.
@ BottomShutterLower
Bottom-sided shutter door (lower layer)
@ ExplodingWall
Exploding wall.
@ TopSidedShutter
Top-sided shutter door.
@ LitCaveExitLower
Lit cave exit (lower layer)
@ DoubleSidedShutterLower
Double-sided shutter (lower layer)
@ UnopenableBigKeyDoor
Unopenable, double-sided big key door.
@ NormalDoorLower
Normal door (lower layer)
@ BottomSidedShutter
Bottom-sided shutter door.
@ SmallKeyStairsDownLower
Small key stairs (lower layer; downwards)
@ CurtainDoor
Curtain door.
@ WaterfallDoor
Waterfall door.
@ BigKeyDoor
Big key door.
@ EyeWatchDoor
Eye watch door.
@ SmallKeyStairsUpLower
Small key stairs (lower layer; upwards)
@ ExitMarker
Exit marker.
@ DoubleSidedShutter
Double sided shutter door.
constexpr int kDoorGfxDown
ObjectLayerSemantics GetEffectiveObjectLayerSemantics(const RoomObject &object)
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
Definition door_types.h:204
uint16_t CustomObjectRuntimeTileWord(int object_id, uint16_t source_word)
constexpr int kDoorGfxLeft
constexpr int kRoomObjectTileAddress
Definition room_object.h:48
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
Definition door_types.h:110
bool HasActiveCustomObjectOverride(const RoomObject &object)
constexpr int kDoorGfxUp
DoorDirection
Door direction on room walls.
Definition door_types.h:18
@ South
Bottom wall (horizontal door, 4x3 tiles)
@ North
Top wall (horizontal door, 4x3 tiles)
@ East
Right wall (vertical door, 3x4 tiles)
@ West
Left wall (vertical door, 3x4 tiles)
constexpr int kDoorGfxRight
constexpr int kRoomObjectTileAddressFloor
Definition room_object.h:49
Room transition destination.
Definition zelda.h:448
Represents a group of palettes.
int width_tiles
Width in 8x8 tiles.
Definition door_types.h:222
Context passed to draw routines containing all necessary state.
gfx::BackgroundBuffer & target_bg
Metadata about a draw routine.
std::pair< int, int > GetTileCoords() const
DoorDimensions GetDimensions() const