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 <algorithm>
4#include <array>
5#include <cstdio>
6#include <cstring>
7#include <filesystem>
8
9#include "absl/strings/str_format.h"
11#include "core/features.h"
12#include "rom/rom.h"
13#include "rom/snes.h"
14#include "util/log.h"
23
24namespace yaze {
25namespace zelda3 {
26namespace {
27
28bool LockSurface(SDL_Surface* surface) {
29#if SDL_MAJOR_VERSION >= 3
30 return SDL_LockSurface(surface);
31#else
32 return SDL_LockSurface(surface) == 0;
33#endif
34}
35
36void SyncModifiedBitmapToSurface(gfx::Bitmap& bitmap, const char* layer_name) {
37 SDL_Surface* surface = bitmap.surface();
38 if (!bitmap.modified() || surface == nullptr || bitmap.size() == 0) {
39 return;
40 }
41
42 if (bitmap.depth() != 8) {
43 LOG_DEBUG("ObjectDrawer", "%s bitmap depth is not indexed 8bpp: %d",
44 layer_name, bitmap.depth());
45 return;
46 }
47
48 const int width = bitmap.width();
49 const int height = bitmap.height();
50 if (width <= 0 || height <= 0 || surface->w < width || surface->h < height ||
51 surface->pitch < width) {
52 LOG_DEBUG("ObjectDrawer",
53 "%s surface dimensions cannot hold bitmap: surface=%dx%d "
54 "pitch=%d bitmap=%dx%d",
55 layer_name, surface->w, surface->h, surface->pitch, width,
56 height);
57 return;
58 }
59
60 const size_t row_bytes = static_cast<size_t>(width);
61 const size_t required_bytes = row_bytes * static_cast<size_t>(height);
62 if (bitmap.size() < required_bytes) {
63 LOG_DEBUG("ObjectDrawer", "%s bitmap data too small: data=%zu needed=%zu",
64 layer_name, bitmap.size(), required_bytes);
65 return;
66 }
67
68 if (!LockSurface(surface)) {
69 LOG_DEBUG("ObjectDrawer", "%s surface lock failed: %s", layer_name,
70 SDL_GetError());
71 return;
72 }
73 auto* destination = static_cast<uint8_t*>(surface->pixels);
74 const uint8_t* source = bitmap.data();
75 for (int y = 0; y < height; ++y) {
76 std::memcpy(destination + static_cast<size_t>(y) *
77 static_cast<size_t>(surface->pitch),
78 source + static_cast<size_t>(y) * row_bytes, row_bytes);
79 }
80 SDL_UnlockSurface(surface);
81}
82
83// Sets the priority bit of the tile word already stored at (tile_x, tile_y),
84// as USDASM's ORA #$2000 does. An empty word means this buffer did not draw
85// the tile, so it is left empty; the other buffer's word is what shows there.
87 int tile_y) {
88 constexpr uint16_t kPriorityBit = 0x2000;
89 const uint16_t word = target.GetTileAt(tile_x, tile_y);
90 if (word != 0) {
91 target.SetTileAt(tile_x, tile_y, word | kPriorityBit);
92 }
93}
94
96 int tile_y) {
97 PromoteTileWordPriority(target, tile_x, tile_y);
98 for (int py = 0; py < 8; ++py) {
99 for (int px = 0; px < 8; ++px) {
100 target.SetPriorityAt(tile_x * 8 + px, tile_y * 8 + py, 1);
101 }
102 }
103}
104
106 gfx::BackgroundBuffer* layout_owner,
107 int tile_x, int tile_y) {
108 // The SNES mutates one physical tilemap. Yaze temporarily splits that map
109 // between layout and object buffers, so update both possible pixel owners
110 // without writing bitmap pixels, coverage, or a new tile word.
111 PromoteTilePriorityOnly(object_owner, tile_x, tile_y);
112 if (layout_owner != nullptr && layout_owner != &object_owner) {
113 PromoteTilePriorityOnly(*layout_owner, tile_x, tile_y);
114 }
115}
116
117} // namespace
118
120 const uint8_t* room_gfx_buffer,
121 const CustomObjectRenderSnapshot* custom_objects)
122 : custom_objects_(custom_objects),
123 rom_(rom),
124 room_id_(room_id),
125 room_gfx_buffer_(room_gfx_buffer) {
127}
128
129void ObjectDrawer::SetTraceCollector(std::vector<TileTrace>* collector,
130 bool trace_only) {
131 trace_collector_ = collector;
132 trace_only_ = trace_only;
133}
134
136 trace_collector_ = nullptr;
137 trace_only_ = false;
138}
139
141 RoomObject::LayerType layer) {
142 trace_context_.object_id = static_cast<uint16_t>(object.id_);
143 trace_context_.size = object.size_;
144 trace_context_.layer = static_cast<uint8_t>(layer);
145}
146
147void ObjectDrawer::PushTrace(int tile_x, int tile_y,
148 const gfx::TileInfo& tile_info) {
149 if (!trace_collector_) {
150 return;
151 }
152 uint8_t flags = 0;
153 if (tile_info.horizontal_mirror_)
154 flags |= 0x1;
155 if (tile_info.vertical_mirror_)
156 flags |= 0x2;
157 if (tile_info.over_)
158 flags |= 0x4;
159 flags |= static_cast<uint8_t>((tile_info.palette_ & 0x7) << 3);
160
161 TileTrace trace{};
163 trace.size = trace_context_.size;
164 trace.layer = trace_context_.layer;
165 trace.x_tile = static_cast<int16_t>(tile_x);
166 trace.y_tile = static_cast<int16_t>(tile_y);
167 trace.tile_id = tile_info.id_;
168 trace.flags = flags;
169 trace_collector_->push_back(trace);
170}
171
173 int tile_y, const gfx::TileInfo& tile_info,
174 void* user_data) {
175 auto* drawer = static_cast<ObjectDrawer*>(user_data);
176 if (!drawer || tile_y >= DrawContext::kMaxTilesY) {
177 return;
178 }
179 drawer->PushTrace(tile_x, tile_y, tile_info);
180}
181
183 int routine_id, const RoomObject& obj, gfx::BackgroundBuffer& bg,
184 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
185 // Many DrawRoutineRegistry routines are implemented as pure functions that
186 // call DrawRoutineUtils::WriteTile8(), which only writes to BackgroundBuffer's
187 // tile buffer (not the bitmap). Runtime rendering/compositing uses the
188 // bitmap-backed buffers, so we capture tile writes from the pure routine and
189 // replay them via ObjectDrawer::WriteTile8().
190 const auto* info = DrawRoutineRegistry::Get().GetRoutineInfo(routine_id);
191 if (info == nullptr) {
192 LOG_DEBUG("ObjectDrawer", "DrawUsingRegistryRoutine: unknown routine %d",
193 routine_id);
194 return;
195 }
196
197 struct CapturedWrite {
198 int x = 0;
199 int y = 0;
200 gfx::TileInfo tile{};
201 bool secondary = false;
202 };
203
204 struct CaptureState {
205 std::vector<CapturedWrite>* writes = nullptr;
206 gfx::BackgroundBuffer* secondary_bg = nullptr;
207 };
208
209 std::vector<CapturedWrite> writes;
210 writes.reserve(256);
211 CaptureState capture_state{.writes = &writes,
212 .secondary_bg = registry_secondary_bg_};
213
215 [](gfx::BackgroundBuffer* target_bg, int tile_x, int tile_y,
216 const gfx::TileInfo& tile_info, void* user_data) {
217 auto* capture = static_cast<CaptureState*>(user_data);
218 if (!capture || !capture->writes) {
219 return;
220 }
221 capture->writes->push_back(CapturedWrite{
222 .x = tile_x,
223 .y = tile_y,
224 .tile = tile_info,
225 .secondary =
226 target_bg != nullptr && target_bg == capture->secondary_bg,
227 });
228 },
229 &capture_state,
230 /*trace_only=*/true);
231
232 DrawContext ctx{
233 .target_bg = bg,
234 .object = obj,
235 .tiles = tiles,
236 .state = state,
237 .rom = rom_,
238 .room_id = room_id_,
239 .room_gfx_buffer = room_gfx_buffer_,
240 .secondary_bg = registry_secondary_bg_,
241 .target_layout_bg = registry_primary_layout_bg_,
242 .room_tag2 = room_tag2_,
243 .custom_objects = custom_objects_,
244 };
245 info->function(ctx);
246
248
249 for (const auto& w : writes) {
250 gfx::BackgroundBuffer* target = &bg;
252 if (w.secondary && registry_secondary_bg_ != nullptr) {
253 target = registry_secondary_bg_;
255 }
256 int tile_y = w.y;
257 if (tile_y >= DrawContext::kMaxTilesY) {
258 // $7E2000 row 64+ is $7E4000 (BG2) row - 64, e.g. a size-0 0x60
259 // column from row 4 in room 0x10C. BG2 rows past 63 leave the
260 // tilemaps entirely.
261 if (layer != RoomObject::LayerType::BG1 || registry_bg2_ == nullptr) {
262 continue;
263 }
264 target = registry_bg2_;
266 tile_y -= DrawContext::kMaxTilesY;
267 }
268 SetTraceContext(obj, layer);
269 WriteTile8(*target, w.x, tile_y, w.tile);
270 }
271}
272
274 const RoomObject& object, gfx::BackgroundBuffer& bg1,
275 gfx::BackgroundBuffer& bg2, const gfx::PaletteGroup& palette_group,
276 [[maybe_unused]] const DungeonState* state,
277 gfx::BackgroundBuffer* layout_bg1, gfx::BackgroundBuffer* layout_bg2) {
278 if (!rom_ || !rom_->is_loaded()) {
279 return absl::FailedPreconditionError("ROM not loaded");
280 }
281
283 return absl::FailedPreconditionError("Draw routines not initialized");
284 }
285
286 // Ensure object has tiles loaded
287 auto mutable_obj = const_cast<RoomObject&>(object);
288 mutable_obj.SetRom(rom_);
289 mutable_obj.EnsureTilesLoaded();
290
291 // Select buffer based on layer
292 // Layer 0 (BG1): Main objects - drawn to BG1_Objects (on top of layout)
293 // Layer 1 (BG2): Overlay objects - drawn to BG2_Objects (behind layout)
294 // Layer 2 (BG3): Priority objects (torches) - drawn to BG1_Objects (on top)
295 bool use_bg2 = (object.layer_ == RoomObject::LayerType::BG2);
296 auto& target_bg = use_bg2 ? bg2 : bg1;
297 auto& other_bg = use_bg2 ? bg1 : bg2;
298
299 // Log buffer selection for debugging layer routing
300 LOG_DEBUG("ObjectDrawer", "Object 0x%03X layer=%d -> drawing to %s buffer",
301 object.id_, static_cast<int>(object.layer_),
302 use_bg2 ? "BG2 (behind layout)" : "BG1 (on top of layout)");
303
304 // Check for custom object override first (guarded by feature flag).
305 // We check this BEFORE routine lookup to allow overriding vanilla objects.
306 const bool custom_override =
307 custom_objects_ != nullptr
309 custom_objects_->GetObject(object.id_, object.size_ & 0x1F).ok()
311 if (custom_override) {
312 // Custom objects default to drawing on the target layer only, unless all_bgs_ is set
313 // Mask propagation is difficult without dimensions, so we rely on explicit transparency in the custom object tiles if needed
314
315 // Draw to target layer
318 DrawCustomObject(object, target_bg, mutable_obj.tiles(), state);
319
320 // If marked for both BGs, draw to the other layer too
321 if (object.all_bgs_) {
322 SetTraceContext(object, (&other_bg == &bg1) ? RoomObject::LayerType::BG1
324 DrawCustomObject(object, other_bg, mutable_obj.tiles(), state);
325 }
326 return absl::OkStatus();
327 }
328
329 // Type-3 routines that write fixed tilemap data regardless of the object's
330 // position and size. Checked against tilemaps captured from the game.
331 if (object.id_ == 0xFF3) {
332 return DrawLayerMaskFull(
333 object, target_bg,
335 }
336 if (object.id_ == 0xFAA) {
337 return DrawLampCones(object, bg2);
338 }
339 if (object.id_ == 0xFAE) {
340 return DrawAgahnimsWindows(object, bg1, bg2, layout_bg1, layout_bg2);
341 }
342 if (object.id_ == 0xFE2) {
343 return DrawVitreousGoo(object, bg2);
344 }
345 if ((object.id_ == 0xF95 || object.id_ == 0xFF2) && state != nullptr &&
346 state->IsBossShellCleared(room_id_)) {
347 return absl::OkStatus(); // $0402 bit 15: shell already gone.
348 }
349 if (object.id_ == 0xF95 || object.id_ == 0xFF2) {
350 // Kholdstare's shell reads fixed data (obj1DFA); Trinexx's shell reads
351 // its own payload (TXY). Both skip drawing once $0402 bit 15 is set in
352 // the game; the editor shows the room as first entered.
353 int data_pc = kRoomObjectTileAddress + 0x1DFA;
354 if (object.id_ == 0xFF2) {
355 // The object's own data: .type1_subtype_3_data_offset[id - 0xF80].
356 const auto& rom_data = rom_->vector();
357 const int entry = kRoomObjectSubtype3 + (object.id_ - 0xF80) * 2;
358 if (entry < 0 || entry + 1 >= static_cast<int>(rom_data.size())) {
359 return absl::OutOfRangeError("Trinexx shell data offset out of range");
360 }
361 data_pc =
363 static_cast<int16_t>(rom_data[entry] | (rom_data[entry + 1] << 8));
364 }
365 return DrawBigDecor10x8(
366 object, target_bg,
368 data_pc);
369 }
370
371 std::array<gfx::TileInfo, 8> room_floor_tiles;
372 std::span<const gfx::TileInfo> render_tiles = mutable_obj.tiles();
373 // Keep source patterns out of the RoomObject cache and ROM save path. Worker
374 // draws use their immutable snapshot rather than the active session.
375 auto patterns = custom_objects_ != nullptr
378 if (!patterns.ok())
379 return patterns.status();
380 std::vector<gfx::TileInfo> source_tiles;
381 if (*patterns) {
382 ObjectParser parser(rom_, patterns->get());
383 auto parsed = parser.ParseObject(object.id_);
384 if (!parsed.ok())
385 return parsed.status();
386 source_tiles = std::move(*parsed);
387 render_tiles = source_tiles;
388 }
390 (object.id_ == 0x00C4 || object.id_ == 0x00DB)) {
391 const uint8_t floor_graphics =
392 object.id_ == 0x00C4 ? floor1_graphics_ : floor2_graphics_;
393 const auto decoded = gfx::DecodeDungeonFloorTilePattern(
395 floor_graphics);
396 if (!decoded.has_value()) {
397 return absl::DataLossError(
398 absl::StrFormat("Object 0x%03X cannot read room floor pattern %d",
399 object.id_, static_cast<int>(floor_graphics)));
400 }
401 room_floor_tiles = *decoded;
402 render_tiles = room_floor_tiles;
403 }
404
405 // Skip objects that don't have tiles loaded
406 if (render_tiles.empty()) {
407 LOG_DEBUG("ObjectDrawer",
408 "Object 0x%03X at (%d,%d) has NO TILES - skipping", object.id_,
409 object.x_, object.y_);
410 return absl::OkStatus();
411 }
412
413 // Look up draw routine for this object
414 int routine_id = GetDrawRoutineId(object.id_);
415
416 // Log draw routine lookup with tile info
417 LOG_DEBUG("ObjectDrawer",
418 "Object 0x%03X at (%d,%d) size=%d -> routine=%d tiles=%zu",
419 object.id_, object.x_, object.y_, object.size_, routine_id,
420 render_tiles.size());
421
422 if (routine_id < 0 || routine_id >= static_cast<int>(draw_routines_.size())) {
423 LOG_DEBUG("ObjectDrawer",
424 "Object 0x%03X: NO ROUTINE (id=%d, max=%zu) - using fallback 1x1",
425 object.id_, routine_id, draw_routines_.size());
426 // Fallback to simple 1x1 drawing using first 8x8 tile
427 if (!render_tiles.empty()) {
428 const auto& tile_info = render_tiles[0];
431 WriteTile8(target_bg, object.x_, object.y_, tile_info);
432 }
433 return absl::OkStatus();
434 }
435
436 // Null-tile guard: skip routines whose tile payload is too small.
437 // Hack ROMs with abbreviated tile tables would otherwise cause
438 // out-of-bounds access in fixed-size draw patterns.
439 const DrawRoutineInfo* routine_info =
441 if (routine_info && routine_info->min_tiles > 0 &&
442 static_cast<int>(render_tiles.size()) < routine_info->min_tiles) {
443 LOG_WARN("ObjectDrawer",
444 "Object 0x%03X at (%d,%d): tile payload too small "
445 "(%zu < %d required by routine '%s') - skipping",
446 object.id_, object.x_, object.y_, render_tiles.size(),
447 routine_info->min_tiles, routine_info->name.c_str());
448 // Fall through to 1x1 fallback if any tiles are present
449 if (!render_tiles.empty()) {
450 const auto& tile_info = render_tiles[0];
453 WriteTile8(target_bg, object.x_, object.y_, tile_info);
454 }
455 return absl::OkStatus();
456 }
457
458 bool trace_hook_active = false;
459 if (trace_collector_) {
462 trace_hook_active = true;
463 }
464
465 // Check if this should draw to both BG layers.
466 // In the original engine, BothBG routines explicitly write to both tilemaps
467 // regardless of which object list or pass they are executed from.
468 bool is_both_bg = (object.all_bgs_ || RoutineDrawsToBothBGs(routine_id));
469 const bool use_rectangular_bg1_mask =
470 !trace_only_ && object.layer_ == RoomObject::LayerType::BG2 &&
471 !is_both_bg && RequiresRectangularBg1Mask(object);
472 const bool use_pixel_bg1_mask = !trace_only_ &&
473 object.layer_ == RoomObject::LayerType::BG2 &&
474 !is_both_bg && !use_rectangular_bg1_mask;
475
476 registry_secondary_bg_ = nullptr;
477 registry_bg2_ = &bg2;
479 use_bg2 ? static_cast<const gfx::BackgroundBuffer*>(layout_bg2)
480 : static_cast<const gfx::BackgroundBuffer*>(layout_bg1);
485 gfx::BackgroundBuffer* dispatch_bg = &target_bg;
486
487 // Special routines may need a second buffer for mixed-layer draws or fixed
488 // BG1/BG2 routing regardless of the parsed object-layer flag.
489 if (!is_both_bg && (routine_id == DrawRoutineIds::kAgahnimsAltar ||
490 routine_id == DrawRoutineIds::kFortuneTellerRoom)) {
491 // USDASM writes these fixed room facades directly to the upper tilemap at
492 // $7E2000.
493 dispatch_bg = &bg1;
495 } else if (!is_both_bg && (routine_id == DrawRoutineIds::kAutoStairs ||
496 routine_id == DrawRoutineIds::kSanctuaryWall)) {
497 registry_secondary_bg_ = &other_bg;
498 } else if (!is_both_bg && routine_id == DrawRoutineIds::kDamFloodGate) {
499 // The open gate's water flooring goes to $7E4000.
501 } else if (!is_both_bg &&
503 // The water-on stamp goes to $7E2000.
505 } else if (!is_both_bg &&
507 dispatch_bg = &bg1;
511 }
512
513 if (use_pixel_bg1_mask) {
515 active_layout_bg1_mask_ = layout_bg1;
517 }
518
519 if (is_both_bg) {
520 // Draw to both background layers
521 registry_secondary_bg_ = nullptr;
523 registry_primary_layout_bg_ = layout_bg1;
525 draw_routines_[routine_id](this, object, bg1, render_tiles, state);
527 registry_primary_layout_bg_ = layout_bg2;
529 draw_routines_[routine_id](this, object, bg2, render_tiles, state);
530 } else {
531 // Execute the appropriate draw routine on target buffer only
533 dispatch_bg == &bg2
534 ? static_cast<const gfx::BackgroundBuffer*>(layout_bg2)
535 : static_cast<const gfx::BackgroundBuffer*>(layout_bg1);
537 draw_routines_[routine_id](this, object, *dispatch_bg, render_tiles, state);
538 }
539
540 const bool is_upper_spiral =
543 const bool is_lower_spiral =
546 if (!trace_only_ && (is_upper_spiral || is_lower_spiral)) {
547 auto& priority_object_owner = is_upper_spiral ? bg1 : bg2;
548 auto* priority_layout_owner = is_upper_spiral ? layout_bg1 : layout_bg2;
549 // USDASM ORs $2000 into the tile immediately left of the 4x3 raster and
550 // the tile immediately right of it.
551 for (const int tile_x : {object.x_ - 1, object.x_ + 4}) {
552 PromoteTilePriorityOnOwners(priority_object_owner, priority_layout_owner,
553 tile_x, object.y_);
554 }
555 }
556
557 if (!trace_only_ &&
559 // USDASM promotes a fixed BG1 column outside the lower staircase raster.
560 // North variants touch y-4..y-1; south variants touch y+4..y+7.
561 const int priority_y =
563 ? object.y_ - 4
564 : object.y_ + 4;
565 for (int row = 0; row < 4; ++row) {
566 PromoteTilePriorityOnOwners(bg1, layout_bg1, object.x_, priority_y + row);
567 }
568 }
569
570 if (trace_hook_active) {
572 }
573
574 active_object_bg1_mask_ = nullptr;
575 active_layout_bg1_mask_ = nullptr;
576 active_mask_source_bg_ = nullptr;
577 registry_secondary_bg_ = nullptr;
578 registry_bg2_ = nullptr;
580
581 // BG2 mask propagation is deferred to compositing so raw BG1 stays intact.
582 //
583 // Ordinary BG2 overlay objects mask per-pixel as they draw, which keeps
584 // transparent cutouts intact for platforms/statues/stairs. Full-rect masking
585 // remains only for true pit/ceiling mask families that intentionally clear an
586 // area larger than their opaque tile pixels. Layer mode 6 ignores these legacy
587 // cross-BG masks because its upper-main/lower-sub PPU setup is resolved by
588 // transparency instead.
589 if (use_rectangular_bg1_mask) {
590 // Route through DimensionService so the mask rect comes from the same
591 // source as selection bounds (ObjectGeometry if available, then
592 // ObjectDimensionTable, then the size-nibble fallback). Keeps the
593 // transparent cutout aligned with what the user sees in the editor.
595 const auto [mask_px_x, mask_px_y, pixel_width, pixel_height] =
597
598 LOG_DEBUG("ObjectDrawer",
599 "Pit mask 0x%03X at (%d,%d) -> recording %dx%d BG1 reveal pixels",
600 object.id_, mask_px_x / 8, mask_px_y / 8, pixel_width,
601 pixel_height);
602
603 MarkBg1RectRevealed(bg1, mask_px_x, mask_px_y, pixel_width, pixel_height);
604 if (layout_bg1 != nullptr) {
605 MarkBg1RectRevealed(*layout_bg1, mask_px_x, mask_px_y, pixel_width,
606 pixel_height);
607 }
608 }
609
610 return absl::OkStatus();
611}
612
614 return (object.id_ == 0xA4) || // Pit
615 (object.id_ >= 0xA5 && object.id_ <= 0xA8) || // Diagonal masks A
616 (object.id_ == 0xC0) || // Large ceiling overlay
617 (object.id_ == 0xC2) || // Layer 2 pit mask
618 (object.id_ == 0xC3) || // Layer 2 pit mask
619 (object.id_ == 0xC6) || // Layer 2 mask
620 (object.id_ == 0xC8) || // Water floor overlay
621 (object.id_ == 0xD7) || // Layer 2 mask
622 (object.id_ == 0xD8) || // Flood water overlay
623 (object.id_ == 0xD9) || // Layer 2 swim mask
624 (object.id_ == 0xDA) || // Flood water overlay B
625 (object.id_ == 0xFE6) || // Type 3 pit
626 (object.id_ == 0xFF3); // Type 3 full mask
627}
628
630 const std::vector<RoomObject>& objects, gfx::BackgroundBuffer& bg1,
631 gfx::BackgroundBuffer& bg2, const gfx::PaletteGroup& palette_group,
632 [[maybe_unused]] const DungeonState* state,
633 gfx::BackgroundBuffer* layout_bg1, bool reset_room_event_indices,
634 gfx::BackgroundBuffer* layout_bg2) {
635 if (reset_room_event_indices) {
637 }
638 absl::Status status = absl::OkStatus();
639
640 // DEBUG: Count objects routed to each buffer
641 int to_bg1 = 0, to_bg2 = 0, both_bgs = 0;
642
643 for (const auto& object : objects) {
644 const auto semantics = GetEffectiveObjectLayerSemantics(object);
645 switch (semantics.effective_bg_layer) {
647 ++to_bg1;
648 break;
650 ++to_bg2;
651 break;
653 ++both_bgs;
654 break;
655 }
656
657 auto s = DrawObject(object, bg1, bg2, palette_group, state, layout_bg1,
658 layout_bg2);
659 if (!s.ok() && status.ok()) {
660 status = s;
661 }
662 }
663
664 LOG_DEBUG("ObjectDrawer", "Buffer routing: to_BG1=%d, to_BG2=%d, BothBGs=%d",
665 to_bg1, to_bg2, both_bgs);
666
667 // The palette is already applied by Room::RenderRoomGraphics(). SDL can pad
668 // indexed surface rows, so synchronize each row using the surface pitch.
669 SyncModifiedBitmapToSurface(bg1.bitmap(), "BG1");
670 SyncModifiedBitmapToSurface(bg2.bitmap(), "BG2");
671
672 return status;
673}
674
675// ============================================================================
676// Metadata-based BothBG Detection
677// ============================================================================
678
680 // Use DrawRoutineRegistry as the single source of truth for BothBG metadata.
682}
683
684// ============================================================================
685// Draw Routine Registry Initialization
686// ============================================================================
687
689 // This function maps object IDs to their corresponding draw routines.
690 // The mapping is based on ZScream's DungeonObjectData.cs and the game's
691 // assembly code. The order of functions in draw_routines_ MUST match the
692 // indices used here.
693 //
694 // ASM Reference (Bank 01):
695 // Subtype 1 Data Offset: $018000 (DrawObjects.type1_subtype_1_data_offset)
696 // Subtype 1 Routine Ptr: $018200 (DrawObjects.type1_subtype_1_routine)
697 // Subtype 2 Data Offset: $0183F0 (DrawObjects.type1_subtype_2_data_offset)
698 // Subtype 2 Routine Ptr: $018470 (DrawObjects.type1_subtype_2_routine)
699 // Subtype 3 Data Offset: $0184F0 (DrawObjects.type1_subtype_3_data_offset)
700 // Subtype 3 Routine Ptr: $0185F0 (DrawObjects.type1_subtype_3_routine)
701
702 draw_routines_.clear();
703
704 // Object-to-routine mapping now lives in DrawRoutineRegistry::BuildObjectMapping().
705 // ObjectDrawer::GetDrawRoutineId() delegates to the registry singleton.
706 // Initialize draw routine function array in the correct order
707 // Routines 0-82 (existing), 80-98 (new special routines for stairs, locks, etc.)
708 draw_routines_.reserve(100);
709
710 // Routine 0
711 draw_routines_.push_back(
712 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
713 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
714 self->DrawUsingRegistryRoutine(0, obj, bg, tiles, state);
715 });
716 // Routine 1
717 draw_routines_.push_back(
718 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
719 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
720 self->DrawUsingRegistryRoutine(1, obj, bg, tiles, state);
721 });
722 // Routine 2 - 2x4 tiles with adjacent spacing (s * 2), count = size + 1
723 draw_routines_.push_back(
724 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
725 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
726 self->DrawUsingRegistryRoutine(2, obj, bg, tiles, state);
727 });
728 // Routine 3 - Same as routine 2 but draws to both BG1 and BG2
729 draw_routines_.push_back(
730 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
731 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
732 self->DrawUsingRegistryRoutine(3, obj, bg, tiles, state);
733 });
734 // Routine 4
735 draw_routines_.push_back(
736 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
737 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
738 self->DrawUsingRegistryRoutine(4, obj, bg, tiles, state);
739 });
740 // Routine 5
741 draw_routines_.push_back(
742 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
743 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
744 self->DrawUsingRegistryRoutine(5, obj, bg, tiles, state);
745 });
746 // Routine 6
747 draw_routines_.push_back(
748 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
749 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
750 self->DrawUsingRegistryRoutine(6, obj, bg, tiles, state);
751 });
752 // Routine 7
753 draw_routines_.push_back(
754 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
755 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
756 self->DrawUsingRegistryRoutine(7, obj, bg, tiles, state);
757 });
758 // Routine 8
759 draw_routines_.push_back(
760 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
761 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
762 self->DrawUsingRegistryRoutine(8, obj, bg, tiles, state);
763 });
764 // Routine 9
765 draw_routines_.push_back(
766 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
767 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
768 self->DrawUsingRegistryRoutine(9, obj, bg, tiles, state);
769 });
770 // Routine 10
771 draw_routines_.push_back(
772 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
773 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
774 self->DrawUsingRegistryRoutine(10, obj, bg, tiles, state);
775 });
776 // Routine 11
777 draw_routines_.push_back(
778 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
779 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
780 self->DrawUsingRegistryRoutine(11, obj, bg, tiles, state);
781 });
782 // Routine 12
783 draw_routines_.push_back(
784 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
785 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
786 self->DrawUsingRegistryRoutine(12, obj, bg, tiles, state);
787 });
788 // Routine 13
789 draw_routines_.push_back(
790 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
791 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
792 self->DrawUsingRegistryRoutine(13, obj, bg, tiles, state);
793 });
794 // Routine 14
795 draw_routines_.push_back(
796 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
797 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
798 self->DrawUsingRegistryRoutine(14, obj, bg, tiles, state);
799 });
800 // Routine 15
801 draw_routines_.push_back(
802 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
803 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
804 self->DrawUsingRegistryRoutine(15, obj, bg, tiles, state);
805 });
806 // Routine 16
807 draw_routines_.push_back(
808 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
809 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
810 self->DrawUsingRegistryRoutine(16, obj, bg, tiles, state);
811 });
812 // Routine 17 - Diagonal Acute BothBG
813 draw_routines_.push_back(
814 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
815 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
816 self->DrawUsingRegistryRoutine(17, obj, bg, tiles, state);
817 });
818 // Routine 18 - Diagonal Grave BothBG
819 draw_routines_.push_back(
820 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
821 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
822 self->DrawUsingRegistryRoutine(18, obj, bg, tiles, state);
823 });
824 // Routine 19 - 4x4 Corner (Type 2 corners)
825 draw_routines_.push_back(
826 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
827 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
828 self->DrawUsingRegistryRoutine(19, obj, bg, tiles, state);
829 });
830
831 // Routine 20 - Edge objects 1x2 +2
832 draw_routines_.push_back(
833 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
834 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
835 self->DrawUsingRegistryRoutine(20, obj, bg, tiles, state);
836 });
837 // Routine 21 - Edge with perimeter 1x1 +3
838 draw_routines_.push_back(
839 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
840 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
841 self->DrawUsingRegistryRoutine(21, obj, bg, tiles, state);
842 });
843 // Routine 22 - Edge variant 1x1 +2
844 draw_routines_.push_back(
845 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
846 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
847 self->DrawUsingRegistryRoutine(22, obj, bg, tiles, state);
848 });
849 // Routine 23 - Top corners 1x2 +13
850 draw_routines_.push_back(
851 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
852 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
853 self->DrawUsingRegistryRoutine(23, obj, bg, tiles, state);
854 });
855 // Routine 24 - Bottom corners 1x2 +13
856 draw_routines_.push_back(
857 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
858 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
859 self->DrawUsingRegistryRoutine(24, obj, bg, tiles, state);
860 });
861 // Routine 25 - Solid fill 1x1 +3 (floor patterns)
862 draw_routines_.push_back(
863 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
864 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
865 self->DrawUsingRegistryRoutine(25, obj, bg, tiles, state);
866 });
867 // Routine 26 - Door switcherer
868 draw_routines_.push_back(
869 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
870 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
871 self->DrawUsingRegistryRoutine(26, obj, bg, tiles, state);
872 });
873 // Routine 27 - Decorations 4x4 spaced 2
874 draw_routines_.push_back(
875 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
876 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
877 self->DrawUsingRegistryRoutine(27, obj, bg, tiles, state);
878 });
879 // Routine 28 - Statues 2x3 spaced 2
880 draw_routines_.push_back(
881 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
882 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
883 self->DrawUsingRegistryRoutine(28, obj, bg, tiles, state);
884 });
885 // Routine 29 - Pillars 2x4 spaced 4
886 draw_routines_.push_back(
887 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
888 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
889 self->DrawUsingRegistryRoutine(29, obj, bg, tiles, state);
890 });
891 // Routine 30 - Decorations 4x3 spaced 4
892 draw_routines_.push_back(
893 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
894 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
895 self->DrawUsingRegistryRoutine(30, obj, bg, tiles, state);
896 });
897 // Routine 31 - Doubled 2x2 spaced 2
898 draw_routines_.push_back(
899 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
900 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
901 self->DrawUsingRegistryRoutine(31, obj, bg, tiles, state);
902 });
903 // Routine 32 - Decorations 2x2 spaced 12
904 draw_routines_.push_back(
905 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
906 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
907 self->DrawUsingRegistryRoutine(32, obj, bg, tiles, state);
908 });
909 // Routine 33 - Somaria Line
910 draw_routines_.push_back(
911 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
912 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
913 self->DrawUsingRegistryRoutine(33, obj, bg, tiles, state);
914 });
915 // Routine 34 - Water Face
916 draw_routines_.push_back(
917 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
918 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
919 self->DrawUsingRegistryRoutine(34, obj, bg, tiles, state);
920 });
921 // Routine 35 - 4x4 Corner BothBG
922 draw_routines_.push_back(
923 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
924 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
925 self->DrawUsingRegistryRoutine(35, obj, bg, tiles, state);
926 });
927 // Routine 36 - Weird Corner Bottom BothBG
928 draw_routines_.push_back(
929 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
930 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
931 self->DrawUsingRegistryRoutine(36, obj, bg, tiles, state);
932 });
933 // Routine 37 - Weird Corner Top BothBG
934 draw_routines_.push_back(
935 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
936 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
937 self->DrawUsingRegistryRoutine(37, obj, bg, tiles, state);
938 });
939 // Routine 38 - Nothing
940 draw_routines_.push_back(
941 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
942 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
943 self->DrawUsingRegistryRoutine(38, obj, bg, tiles, state);
944 });
945 // Routine 39 - Small chest rendering (stateful F99 / fixed-open F9A)
946 draw_routines_.push_back(
947 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
948 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
949 self->DrawChest(obj, bg, tiles, state);
950 });
951 // Routine 40 - Rightwards 4x2 (Floor Tile)
952 draw_routines_.push_back(
953 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
954 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
955 self->DrawUsingRegistryRoutine(40, obj, bg, tiles, state);
956 });
957 // Routine 41 - Rightwards Decor 4x2 spaced 8 (12-column spacing)
958 draw_routines_.push_back(
959 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
960 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
961 self->DrawUsingRegistryRoutine(41, obj, bg, tiles, state);
962 });
963 // Routine 42 - Rightwards Cannon Hole 4x3
964 draw_routines_.push_back(
965 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
966 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
967 self->DrawUsingRegistryRoutine(42, obj, bg, tiles, state);
968 });
969 // Routine 43 - Downwards Floor 4x4 (object 0x70)
970 draw_routines_.push_back(
971 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
972 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
973 self->DrawUsingRegistryRoutine(43, obj, bg, tiles, state);
974 });
975 // Routine 44 - Downwards 1x1 Solid +3 (object 0x71)
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(44, obj, bg, tiles, state);
980 });
981 // Routine 45 - Downwards Decor 4x4 spaced 2 (objects 0x73-0x74)
982 draw_routines_.push_back(
983 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
984 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
985 self->DrawUsingRegistryRoutine(45, obj, bg, tiles, state);
986 });
987 // Routine 46 - Downwards Pillar 2x4 spaced 2 (objects 0x75, 0x87)
988 draw_routines_.push_back(
989 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
990 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
991 self->DrawUsingRegistryRoutine(46, obj, bg, tiles, state);
992 });
993 // Routine 47 - Downwards Decor 3x4 spaced 4 (objects 0x76-0x77)
994 draw_routines_.push_back(
995 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
996 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
997 self->DrawUsingRegistryRoutine(47, obj, bg, tiles, state);
998 });
999 // Routine 48 - Downwards Decor 2x2 spaced 12 (objects 0x78, 0x7B)
1000 draw_routines_.push_back(
1001 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1002 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1003 self->DrawUsingRegistryRoutine(48, obj, bg, tiles, state);
1004 });
1005 // Routine 49 - Downwards Line 1x1 +1 (object 0x7C)
1006 draw_routines_.push_back(
1007 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1008 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1009 self->DrawUsingRegistryRoutine(49, obj, bg, tiles, state);
1010 });
1011 // Routine 50 - Downwards Decor 2x4 spaced 8 (objects 0x7F, 0x80)
1012 draw_routines_.push_back(
1013 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1014 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1015 self->DrawUsingRegistryRoutine(50, obj, bg, tiles, state);
1016 });
1017 // Routine 51 - Rightwards Line 1x1 +1 (object 0x50)
1018 draw_routines_.push_back(
1019 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1020 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1021 self->DrawUsingRegistryRoutine(51, obj, bg, tiles, state);
1022 });
1023 // Routine 52 - Rightwards Bar 4x3 (object 0x4C)
1024 draw_routines_.push_back(
1025 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1026 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1027 self->DrawUsingRegistryRoutine(52, obj, bg, tiles, state);
1028 });
1029 // Routine 53 - Rightwards Shelf 4x4 (objects 0x4D-0x4F)
1030 draw_routines_.push_back(
1031 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1032 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1033 self->DrawUsingRegistryRoutine(53, obj, bg, tiles, state);
1034 });
1035 // Routine 54 - Rightwards Big Rail 1x3 +5 (object 0x5D)
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(54, obj, bg, tiles, state);
1040 });
1041 // Routine 55 - Rightwards Block 2x2 spaced 2 (object 0x5E)
1042 draw_routines_.push_back(
1043 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1044 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1045 self->DrawUsingRegistryRoutine(55, obj, bg, tiles, state);
1046 });
1047
1048 // ============================================================================
1049 // Phase 4: SuperSquare Routines (routines 56-64)
1050 // ============================================================================
1051
1052 // Routine 56 - 4x4 Blocks in 4x4 SuperSquare (objects 0xC0, 0xC2)
1053 draw_routines_.push_back(
1054 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1055 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1056 self->DrawUsingRegistryRoutine(56, obj, bg, tiles, state);
1057 });
1058
1059 // Routine 57 - 3x3 Floor in 4x4 SuperSquare (objects 0xC3, 0xD7)
1060 draw_routines_.push_back(
1061 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1062 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1063 self->DrawUsingRegistryRoutine(57, obj, bg, tiles, state);
1064 });
1065
1066 // Routine 58 - 4x4 Floor in 4x4 SuperSquare (objects 0xC5-0xCA, 0xD1-0xD2,
1067 // 0xD9, 0xDF-0xE8)
1068 draw_routines_.push_back(
1069 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1070 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1071 self->DrawUsingRegistryRoutine(58, obj, bg, tiles, state);
1072 });
1073
1074 // Routine 59 - 4x4 Floor One in 4x4 SuperSquare (object 0xC4)
1075 draw_routines_.push_back(
1076 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1077 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1078 self->DrawUsingRegistryRoutine(59, obj, bg, tiles, state);
1079 });
1080
1081 // Routine 60 - 4x4 Floor Two in 4x4 SuperSquare (object 0xDB)
1082 draw_routines_.push_back(
1083 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1084 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1085 self->DrawUsingRegistryRoutine(60, obj, bg, tiles, state);
1086 });
1087
1088 // Routine 61 - Big Hole 4x4 (object 0xA4)
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(61, obj, bg, tiles, state);
1093 });
1094
1095 // Routine 62 - Spike 2x2 in 4x4 SuperSquare (object 0xDE)
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(62, obj, bg, tiles, state);
1100 });
1101
1102 // Routine 63 - Table Rock 4x4 (object 0xDD)
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(63, obj, bg, tiles, state);
1107 });
1108
1109 // Routine 64 - Water Overlay 8x8 (objects 0xD8, 0xDA)
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(64, obj, bg, tiles, state);
1114 });
1115
1116 // ============================================================================
1117 // Phase 4 Step 2: Simple Variant Routines (routines 65-74)
1118 // ============================================================================
1119
1120 // Routine 65 - Downwards Decor 3x4 spaced 2 (objects 0x81-0x84)
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(65, obj, bg, tiles, state);
1125 });
1126
1127 // Routine 66 - Downwards Big Rail 3x1 plus 5 (object 0x88)
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(66, obj, bg, tiles, state);
1132 });
1133
1134 // Routine 67 - Downwards Block 2x2 spaced 2 (object 0x89)
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(67, obj, bg, tiles, state);
1139 });
1140
1141 // Routine 68 - Downwards Cannon Hole 3x6 (objects 0x85-0x86)
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(68, obj, bg, tiles, state);
1146 });
1147
1148 // Routine 69 - Downwards Bar 2x3 (object 0x8F)
1149 draw_routines_.push_back(
1150 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1151 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1152 self->DrawUsingRegistryRoutine(69, obj, bg, tiles, state);
1153 });
1154
1155 // Routine 70 - Downwards Pots 2x2 (object 0x95)
1156 draw_routines_.push_back(
1157 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1158 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1159 self->DrawUsingRegistryRoutine(70, obj, bg, tiles, state);
1160 });
1161
1162 // Routine 71 - Downwards Hammer Pegs 2x2 (object 0x96)
1163 draw_routines_.push_back(
1164 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1165 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1166 self->DrawUsingRegistryRoutine(71, obj, bg, tiles, state);
1167 });
1168
1169 // Routine 72 - Rightwards Edge 1x1 plus 7 (objects 0xB0-0xB1)
1170 draw_routines_.push_back(
1171 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1172 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1173 self->DrawUsingRegistryRoutine(72, obj, bg, tiles, state);
1174 });
1175
1176 // Routine 73 - Rightwards Pots 2x2 (object 0xBC)
1177 draw_routines_.push_back(
1178 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1179 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1180 self->DrawUsingRegistryRoutine(73, obj, bg, tiles, state);
1181 });
1182
1183 // Routine 74 - Rightwards Hammer Pegs 2x2 (object 0xBD)
1184 draw_routines_.push_back(
1185 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1186 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1187 self->DrawUsingRegistryRoutine(74, obj, bg, tiles, state);
1188 });
1189
1190 // ============================================================================
1191 // Phase 4 Step 3: Diagonal Ceiling Routines (routines 75-78)
1192 // ============================================================================
1193
1194 // Routine 75 - Diagonal Ceiling Top Left (objects 0xA0, 0xA5, 0xA9)
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(75, obj, bg, tiles, state);
1199 });
1200
1201 // Routine 76 - Diagonal Ceiling Bottom Left (objects 0xA1, 0xA6, 0xAA)
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(76, obj, bg, tiles, state);
1206 });
1207
1208 // Routine 77 - Diagonal Ceiling Top Right (objects 0xA2, 0xA7, 0xAB)
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(77, obj, bg, tiles, state);
1213 });
1214
1215 // Routine 78 - Diagonal Ceiling Bottom Right (objects 0xA3, 0xA8, 0xAC)
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->DrawUsingRegistryRoutine(78, obj, bg, tiles, state);
1220 });
1221
1222 // ============================================================================
1223 // Phase 4 Step 5: Special Routines (routines 79-82)
1224 // ============================================================================
1225
1226 // Routine 79 - Closed Chest Platform (object 0xC1, 68 tiles)
1227 draw_routines_.push_back(
1228 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1229 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1230 self->DrawUsingRegistryRoutine(79, obj, bg, tiles, state);
1231 });
1232
1233 // Routine 80 - Moving Wall West (object 0xCD, 24 tiles)
1234 draw_routines_.push_back(
1235 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1236 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1237 self->DrawUsingRegistryRoutine(80, obj, bg, tiles, state);
1238 });
1239
1240 // Routine 81 - Moving Wall East (object 0xCE, 24 tiles)
1241 draw_routines_.push_back(
1242 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1243 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1244 self->DrawUsingRegistryRoutine(81, obj, bg, tiles, state);
1245 });
1246
1247 // Routine 82 - Open Chest Platform (object 0xDC, 21 tiles)
1248 draw_routines_.push_back(
1249 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1250 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1251 self->DrawUsingRegistryRoutine(82, obj, bg, tiles, state);
1252 });
1253
1254 // ============================================================================
1255 // New Special Routines (Phase 5) - Stairs, Locks, Interactive Objects
1256 // ============================================================================
1257
1258 // Routine 83 - InterRoom Fat Stairs Up (object 0x12D)
1259 draw_routines_.push_back(
1260 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1261 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1262 self->DrawUsingRegistryRoutine(83, obj, bg, tiles, state);
1263 });
1264
1265 // Routine 84 - InterRoom Fat Stairs Down A (object 0x12E)
1266 draw_routines_.push_back(
1267 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1268 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1269 self->DrawUsingRegistryRoutine(84, obj, bg, tiles, state);
1270 });
1271
1272 // Routine 85 - InterRoom Fat Stairs Down B (object 0x12F)
1273 draw_routines_.push_back(
1274 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1275 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1276 self->DrawUsingRegistryRoutine(85, obj, bg, tiles, state);
1277 });
1278
1279 // Routine 86 - Auto Stairs (objects 0x130-0x133)
1280 draw_routines_.push_back(
1281 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1282 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1283 self->DrawUsingRegistryRoutine(86, obj, bg, tiles, state);
1284 });
1285
1286 // Routine 87 - Straight InterRoom Stairs (Type 3 objects 0x21E-0x229)
1287 draw_routines_.push_back(
1288 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1289 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1290 self->DrawUsingRegistryRoutine(87, obj, bg, tiles, state);
1291 });
1292
1293 // Routine 88 - Spiral Stairs Going Up Upper (object 0x138)
1294 draw_routines_.push_back(
1295 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1296 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1297 self->DrawUsingRegistryRoutine(88, obj, bg, tiles, state);
1298 });
1299
1300 // Routine 89 - Spiral Stairs Going Down Upper (object 0x139)
1301 draw_routines_.push_back(
1302 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1303 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1304 self->DrawUsingRegistryRoutine(89, obj, bg, tiles, state);
1305 });
1306
1307 // Routine 90 - Spiral Stairs Going Up Lower (object 0x13A)
1308 draw_routines_.push_back(
1309 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1310 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1311 self->DrawUsingRegistryRoutine(90, obj, bg, tiles, state);
1312 });
1313
1314 // Routine 91 - Spiral Stairs Going Down Lower (object 0x13B)
1315 draw_routines_.push_back(
1316 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1317 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1318 self->DrawUsingRegistryRoutine(91, obj, bg, tiles, state);
1319 });
1320
1321 // Routine 92 - Big Key Lock (Yaze 0xF98 / ASM object 0x218)
1322 draw_routines_.push_back(
1323 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1324 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1325 self->DrawBigKeyLock(obj, bg, tiles, state);
1326 });
1327
1328 // Routine 93 - Bombable Floor (Type 3 object 0x247)
1329 draw_routines_.push_back(
1330 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1331 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1332 self->DrawUsingRegistryRoutine(93, obj, bg, tiles, state);
1333 });
1334
1335 // Routine 94 - Empty Water Face (Type 3 object 0x200)
1336 draw_routines_.push_back(
1337 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1338 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1339 self->DrawUsingRegistryRoutine(94, obj, bg, tiles, state);
1340 });
1341
1342 // Routine 95 - Spitting Water Face (Type 3 object 0x201)
1343 draw_routines_.push_back(
1344 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1345 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1346 self->DrawUsingRegistryRoutine(95, obj, bg, tiles, state);
1347 });
1348
1349 // Routine 96 - Drenching Water Face (Type 3 object 0x202)
1350 draw_routines_.push_back(
1351 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1352 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1353 self->DrawUsingRegistryRoutine(96, obj, bg, tiles, state);
1354 });
1355
1356 // Routine 97 - Prison Cell (Type 3 objects 0x20D, 0x217)
1357 // USDASM selects one tilemap through $BF and draws a sparse 16x4 pattern.
1358 draw_routines_.push_back(
1359 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1360 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1361 self->DrawUsingRegistryRoutine(97, obj, bg, tiles, state);
1362 });
1363
1364 // Routine 98 - Bed 4x5 (Type 2 objects 0x122, 0x128)
1365 draw_routines_.push_back(
1366 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1367 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1369 state);
1370 });
1371
1372 // Routine 99 - Rightwards 3x6 (Type 2 object 0x12C, Type 3 0x236-0x237)
1373 draw_routines_.push_back(
1374 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1375 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1377 tiles, state);
1378 });
1379
1380 // Routine 100 - Utility 6x3 (Type 2 object 0x13E, Type 3 0x24D, 0x25D)
1381 draw_routines_.push_back(
1382 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1383 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1385 tiles, state);
1386 });
1387
1388 // Routine 101 - Utility 3x5 (Type 3 objects 0x255, 0x25B)
1389 draw_routines_.push_back(
1390 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1391 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1393 tiles, state);
1394 });
1395
1396 // Routine 102 - Vertical Turtle Rock Pipe (Type 3 objects 0x23A, 0x23B)
1397 draw_routines_.push_back(
1398 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1399 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1401 obj, bg, tiles, state);
1402 });
1403
1404 // Routine 103 - Horizontal Turtle Rock Pipe (Type 3 objects 0x23C, 0x23D,
1405 // 0x25C)
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 DrawRoutineIds::kHorizontalTurtleRockPipe, obj, bg, tiles, state);
1411 });
1412
1413 // Routine 104 - Light Beam on Floor (Type 3 object 0x270)
1414 draw_routines_.push_back(
1415 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1416 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1418 tiles, state);
1419 });
1420
1421 // Routine 105 - Big Light Beam on Floor (Type 3 object 0x271)
1422 draw_routines_.push_back(
1423 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1424 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1426 tiles, state);
1427 });
1428
1429 // Routine 106 - Boss Shell 4x4 (Yaze 0xF95/0xFF2; ASM 0x215/0x272)
1430 draw_routines_.push_back(
1431 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1432 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1434 tiles, state);
1435 });
1436
1437 // Routine 107 - Solid Wall Decor 3x4 (Type 3 objects 0x269-0x26A, 0x26E-0x26F)
1438 draw_routines_.push_back(
1439 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1440 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1442 bg, tiles, state);
1443 });
1444
1445 // Routine 108 - Archery Game Target Door (Type 3 objects 0x260-0x261)
1446 draw_routines_.push_back(
1447 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1448 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1450 obj, bg, tiles, state);
1451 });
1452
1453 // Routine 109 - Ganon Triforce Floor Decor (Type 3 object 0x278)
1454 draw_routines_.push_back(
1455 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1456 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1458 obj, bg, tiles, state);
1459 });
1460
1461 // Routine 110 - Single 2x2 (pots, statues, single-instance 2x2 objects)
1462 draw_routines_.push_back(
1463 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1464 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1466 tiles, state);
1467 });
1468
1469 // Routine 111 - Waterfall47 (object 0x47)
1470 draw_routines_.push_back(
1471 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1472 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1474 tiles, state);
1475 });
1476
1477 // Routine 112 - Waterfall48 (object 0x48)
1478 draw_routines_.push_back(
1479 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1480 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1482 tiles, state);
1483 });
1484
1485 // Routine 113 - Single 4x4 (NO repetition)
1486 // ASM: RoomDraw_4x4 - draws a single 4x4 pattern (16 tiles)
1487 // Used for: 0xFEB (large decor), and other single 4x4 objects
1488 draw_routines_.push_back(
1489 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1490 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1492 tiles, state);
1493 });
1494
1495 // Routine 114 - Single 4x3 (NO repetition)
1496 // ASM: RoomDraw_TableRock4x3 - draws a single 4x3 pattern (12 tiles)
1497 // Used for: 0xFED (water grate), 0xFB1 (big chest), etc.
1498 draw_routines_.push_back(
1499 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1500 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1501 if (obj.id_ == 0xFB1) {
1502 self->DrawBigChest(obj, bg, tiles, state);
1503 return;
1504 }
1506 tiles, state);
1507 });
1508
1509 // Routine 115 - RupeeFloor (special pattern for 0xF92)
1510 // ASM: RoomDraw_RupeeFloor - draws 3 one-tile columns two tiles apart.
1511 // Pattern: 5 tiles wide, 8 rows tall with gaps (rows 2 and 5 are empty).
1512 draw_routines_.push_back(
1513 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1514 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1516 tiles, state);
1517 });
1518
1519 // Routine 116 - Actual 4x4 tile8 pattern (32x32 pixels, NO repetition)
1520 // ASM: RoomDraw_4x4 - draws exactly 4 columns x 4 rows = 16 tiles
1521 // Used for: 0xFE6 (pit)
1522 draw_routines_.push_back(
1523 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1524 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1526 tiles, state);
1527 });
1528
1529 auto ensure_index = [this](size_t index) {
1530 while (draw_routines_.size() <= index) {
1531 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1533 std::span<const gfx::TileInfo> tiles,
1534 [[maybe_unused]] const DungeonState* state) {
1535 self->DrawNothing(obj, bg, tiles, state);
1536 });
1537 }
1538 };
1539
1540 // Routine 117 - Long vertical rail with CORNER+MIDDLE+END pattern (0x8A)
1541 // ASM: RoomDraw_DownwardsHasEdge1x1_1to16_plus23 - matches horizontal 0x22
1542 ensure_index(117);
1543 draw_routines_[117] =
1544 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1545 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1546 self->DrawUsingRegistryRoutine(117, obj, bg, tiles, state);
1547 };
1548
1549 // Routine 118 - Horizontal long rails with CORNER+MIDDLE+END pattern (0x5F)
1550 ensure_index(118);
1551 draw_routines_[118] =
1552 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1553 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1554 self->DrawUsingRegistryRoutine(118, obj, bg, tiles, state);
1555 };
1556
1559 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1560 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1562 bg, tiles, state);
1563 };
1564
1567 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1568 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1570 bg, tiles, state);
1571 };
1572
1573 ensure_index(DrawRoutineIds::kDamFloodGate);
1575 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1576 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1578 tiles, state);
1579 };
1580
1583 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1584 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1587 state);
1588 };
1589
1590 ensure_index(DrawRoutineIds::kFloorLight);
1592 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1593 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1595 tiles, state);
1596 };
1597
1598 ensure_index(DrawRoutineIds::kWeird2x4_1to16);
1600 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1601 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1603 tiles, state);
1604 };
1605
1606 ensure_index(DrawRoutineIds::kBigWallDecor);
1608 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1609 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1611 tiles, state);
1612 };
1613
1614 ensure_index(DrawRoutineIds::kTableBowl);
1616 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1617 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1619 tiles, state);
1620 };
1621
1622 ensure_index(DrawRoutineIds::kSmithyFurnace);
1624 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1625 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1627 tiles, state);
1628 };
1629
1630 ensure_index(DrawRoutineIds::kBigGrayRock);
1632 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1633 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1635 tiles, state);
1636 };
1637
1638 ensure_index(DrawRoutineIds::kAgahnimsAltar);
1640 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1641 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1643 tiles, state);
1644 };
1645
1648 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1649 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1651 bg, tiles, state);
1652 };
1653
1654 ensure_index(DrawRoutineIds::kMagicBatAltar);
1656 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1657 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1659 tiles, state);
1660 };
1661
1664 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1665 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1667 bg, tiles, state);
1668 };
1669
1670 ensure_index(DrawRoutineIds::kSanctuaryWall);
1672 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1673 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1675 tiles, state);
1676 };
1677
1678 // Routine 130 - Custom Object (Oracle of Secrets 0x31, 0x32)
1679 // Uses external binary files instead of ROM tile data.
1680 // Requires CustomObjectManager initialization and enable_custom_objects flag.
1681 ensure_index(130);
1682 draw_routines_[130] = [](ObjectDrawer* self, const RoomObject& obj,
1684 std::span<const gfx::TileInfo> tiles,
1685 [[maybe_unused]] const DungeonState* state) {
1686 self->DrawCustomObject(obj, bg, tiles, state);
1687 };
1688
1689 routines_initialized_ = true;
1690}
1691
1692int ObjectDrawer::GetDrawRoutineId(int16_t object_id) const {
1693 // Delegate to the unified registry for the canonical mapping
1694 const auto& registry = DrawRoutineRegistry::Get();
1695 return custom_objects_ != nullptr ? registry.GetRoutineIdForObject(
1696 object_id, custom_objects_->enabled())
1697 : registry.GetRoutineIdForObject(object_id);
1698}
1699
1700// ============================================================================
1701// Draw Routine Implementations (Based on ZScream patterns)
1702// ============================================================================
1703
1704void ObjectDrawer::DrawDoor(const DoorDef& door, int door_index,
1707 const DungeonState* state,
1708 gfx::BackgroundBuffer* layout_bg1,
1709 gfx::BackgroundBuffer* layout_bg2) {
1710 // Door rendering based on ZELDA3_DUNGEON_SPEC.md Section 5 and disassembly
1711 // Uses DoorType and DoorDirection enums for type safety
1712 // Position calculations via DoorPositionManager
1713
1714 LOG_DEBUG("ObjectDrawer", "DrawDoor: idx=%d type=%d dir=%d pos=%d",
1715 door_index, static_cast<int>(door.type),
1716 static_cast<int>(door.direction), door.position);
1717
1718 if (!rom_ || !rom_->is_loaded() || !room_gfx_buffer_) {
1719 LOG_DEBUG("ObjectDrawer", "DrawDoor: SKIPPED - rom=%p loaded=%d gfx=%p",
1720 (void*)rom_, rom_ ? rom_->is_loaded() : 0,
1721 (void*)room_gfx_buffer_);
1722 return;
1723 }
1724
1725 auto& bitmap = bg1.bitmap();
1726 if (!bitmap.is_active() || bitmap.width() == 0) {
1727 LOG_DEBUG("ObjectDrawer",
1728 "DrawDoor: SKIPPED - bitmap not active or zero width");
1729 return;
1730 }
1731
1732 // DungeonState intentionally exposes the editor's logical door index. The
1733 // runtime derives a separate marker-aware physical slot from $0460 before it
1734 // probes $068C; reproducing that slot aliasing belongs in the state adapter,
1735 // not in this renderer's vector-index contract.
1736 const bool is_door_open = state && state->IsDoorOpen(room_id_, door_index);
1737
1738 // Get door position from DoorPositionManager
1739 auto [tile_x, tile_y] = door.GetTileCoords();
1740 auto dims = door.GetDimensions();
1741 int door_width = dims.width_tiles;
1742 int door_height = dims.height_tiles;
1743
1744 LOG_DEBUG("ObjectDrawer", "DrawDoor: tile_pos=(%d,%d) dims=%dx%d", tile_x,
1745 tile_y, door_width, door_height);
1746
1747 constexpr int kRoomDrawObjectDataBase = 0x1B52;
1748 constexpr int kDoorwayReplacementDoorGfxBase = 0x1A02;
1749 constexpr int kExplodingWallTilemapPositionBase = 0x19DE;
1750 constexpr int kExplodingWallOpenReplacementType = 0x54;
1751 constexpr int kNorthCurtainClosedOffset = 0x078A;
1752 constexpr int kFancyDungeonExitObjectOffset = 0x2656;
1753 constexpr int kCaveExitLightObjectOffset = 0x26F6;
1754 const auto& rom_data = rom_->data();
1755
1756 // RoomDraw_FlagDoorsAndGetFinalType ($01B152): an eye-watch door sets its
1757 // own $068C bit at room load unless it is the door the player just came
1758 // through, so it draws as a plain doorway. The editor has no player.
1759 const bool always_open = door.type == DoorType::EyeWatchDoor;
1760
1761 auto resolve_effective_door_type = [&]() -> uint16_t {
1762 const auto stored_type = static_cast<uint16_t>(door.type);
1763 if (!is_door_open && !always_open) {
1764 return stored_type;
1765 }
1766
1767 // RoomDraw_FlagDoorsAndGetFinalType leaves shutter graphics closed while
1768 // the room's shutter controller ($0468) is active, even when the persistent
1769 // door-open bit is set.
1770 const bool is_controlled_shutter =
1773 if (is_controlled_shutter && state != nullptr &&
1774 state->IsDoorSwitchActive(room_id_)) {
1775 return stored_type;
1776 }
1777
1778 // USDASM performs a 16-bit read from DoorwayReplacementDoorGFX using the
1779 // original even-valued door type as a byte offset. Keep the bounds check on
1780 // both bytes so malformed or undersized ROMs fall back to the stored type.
1781 const int replacement_addr =
1782 kDoorwayReplacementDoorGfxBase + static_cast<int>(door.type);
1783 if (replacement_addr < 0 ||
1784 replacement_addr + 1 >= static_cast<int>(rom_->size())) {
1785 return stored_type;
1786 }
1787
1788 return static_cast<uint16_t>(rom_data[replacement_addr] |
1789 (rom_data[replacement_addr + 1] << 8));
1790 };
1791
1792 const uint16_t effective_door_type = resolve_effective_door_type();
1793
1794 auto draw_tile_word = [&](gfx::BackgroundBuffer& target, int tile_x,
1795 int tile_y, uint16_t tile_word) {
1796 auto& bitmap = target.bitmap();
1797 auto& priority_buffer = target.mutable_priority_data();
1798 auto& coverage_buffer = target.mutable_coverage_data();
1799 const int bitmap_width = bitmap.width();
1800 const auto tile_info = gfx::WordToTileInfo(tile_word);
1801 const int pixel_x = tile_x * 8;
1802 const int pixel_y = tile_y * 8;
1803
1804 target.SetTileAt(tile_x, tile_y, tile_word);
1805 target.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, pixel_x, pixel_y, 8,
1806 8);
1807 DrawTileToBitmap(bitmap, tile_info, pixel_x, pixel_y, room_gfx_buffer_);
1808
1809 const uint8_t priority = tile_info.over_ ? 1 : 0;
1810 const auto& bitmap_data = bitmap.vector();
1811 for (int py = 0; py < 8; py++) {
1812 const int dest_y = pixel_y + py;
1813 if (dest_y < 0 || dest_y >= bitmap.height()) {
1814 continue;
1815 }
1816 for (int px = 0; px < 8; px++) {
1817 const int dest_x = pixel_x + px;
1818 if (dest_x < 0 || dest_x >= bitmap_width) {
1819 continue;
1820 }
1821 const int dest_index = dest_y * bitmap_width + dest_x;
1822 if (dest_index >= 0 &&
1823 dest_index < static_cast<int>(coverage_buffer.size())) {
1824 coverage_buffer[dest_index] = 1;
1825 }
1826 if (dest_index < static_cast<int>(bitmap_data.size()) &&
1827 bitmap_data[dest_index] != 255) {
1828 priority_buffer[dest_index] = priority;
1829 }
1830 }
1831 }
1832 };
1833
1834 auto draw_object_data_tile = [&](gfx::BackgroundBuffer& target, int tile_x,
1835 int tile_y, int tile_data_addr, int tile_idx,
1836 uint16_t word_mask = 0) -> bool {
1837 const int addr = tile_data_addr + (tile_idx * 2);
1838 if (addr < 0 || addr + 1 >= static_cast<int>(rom_->size())) {
1839 return false;
1840 }
1841
1842 const uint16_t tile_word = static_cast<uint16_t>(
1843 (rom_data[addr] | (rom_data[addr + 1] << 8)) | word_mask);
1844 draw_tile_word(target, tile_x, tile_y, tile_word);
1845 return true;
1846 };
1847
1848 auto draw_from_object_data = [&](gfx::BackgroundBuffer& target,
1849 int start_tile_x, int start_tile_y,
1850 int width, int height, int tile_data_addr) {
1851 int tile_idx = 0;
1852
1853 for (int dx = 0; dx < width; dx++) {
1854 for (int dy = 0; dy < height; dy++) {
1855 (void)draw_object_data_tile(target, start_tile_x + dx,
1856 start_tile_y + dy, tile_data_addr,
1857 tile_idx++);
1858 }
1859 }
1860 };
1861
1862 auto draw_from_object_data_row_major =
1863 [&](gfx::BackgroundBuffer& target, int start_tile_x, int start_tile_y,
1864 int width, int height, int tile_data_addr) {
1865 int tile_idx = 0;
1866 for (int dy = 0; dy < height; ++dy) {
1867 for (int dx = 0; dx < width; ++dx) {
1868 (void)draw_object_data_tile(target, start_tile_x + dx,
1869 start_tile_y + dy, tile_data_addr,
1870 tile_idx++);
1871 }
1872 }
1873 };
1874
1875 auto draw_repeated_tile = [&](gfx::BackgroundBuffer& target, int start_tile_x,
1876 int start_tile_y, int width, int height,
1877 uint16_t tile_word) {
1878 auto& bitmap = target.bitmap();
1879 auto& priority_buffer = target.mutable_priority_data();
1880 auto& coverage_buffer = target.mutable_coverage_data();
1881 const int bitmap_width = bitmap.width();
1882 const auto tile_info = gfx::WordToTileInfo(tile_word);
1883
1884 for (int dx = 0; dx < width; dx++) {
1885 for (int dy = 0; dy < height; dy++) {
1886 const int pixel_x = (start_tile_x + dx) * 8;
1887 const int pixel_y = (start_tile_y + dy) * 8;
1888
1889 target.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, pixel_x, pixel_y,
1890 8, 8);
1891 DrawTileToBitmap(bitmap, tile_info, pixel_x, pixel_y, room_gfx_buffer_);
1892 target.SetTileAt(start_tile_x + dx, start_tile_y + dy, tile_word);
1893
1894 const uint8_t priority = tile_info.over_ ? 1 : 0;
1895 const auto& bitmap_data = bitmap.vector();
1896 for (int py = 0; py < 8; py++) {
1897 const int dest_y = pixel_y + py;
1898 if (dest_y < 0 || dest_y >= bitmap.height()) {
1899 continue;
1900 }
1901 for (int px = 0; px < 8; px++) {
1902 const int dest_x = pixel_x + px;
1903 if (dest_x < 0 || dest_x >= bitmap_width) {
1904 continue;
1905 }
1906 const int dest_index = dest_y * bitmap_width + dest_x;
1907 if (dest_index >= 0 &&
1908 dest_index < static_cast<int>(coverage_buffer.size())) {
1909 coverage_buffer[dest_index] = 1;
1910 }
1911 if (dest_index < static_cast<int>(bitmap_data.size()) &&
1912 bitmap_data[dest_index] != 255) {
1913 priority_buffer[dest_index] = priority;
1914 }
1915 }
1916 }
1917 }
1918 }
1919 };
1920
1921 auto tilemap_offset_to_tile_coords = [](uint16_t offset) {
1922 return std::pair<int, int>{static_cast<int>((offset % 0x80) / 2),
1923 static_cast<int>(offset / 0x80)};
1924 };
1925 const int position_index = std::min<int>(door.position & 0x0F, 11);
1926
1927 enum class DoorPrioritySpan {
1928 // RoomDraw_MakeDoorPartsHighPriority_{Vertical,Horizontal}.
1929 kNormalLower,
1930 // RoomDraw_MakeDoorHighPriorityLowerLayer_* (type $06).
1931 kLowerLayerOnly,
1932 // RoomDraw_MakeDoorHighPriority_* after a type $40-$66 raster.
1933 kHighRange,
1934 };
1935
1936 auto promote_priority_rect_on_layer =
1937 [&](gfx::BackgroundBuffer& object_buffer,
1938 gfx::BackgroundBuffer* layout_buffer, int start_tile_x,
1939 int start_tile_y, int width_tiles, int height_tiles) {
1940 auto promote_target = [&](gfx::BackgroundBuffer& target) {
1941 for (int tile_y = start_tile_y; tile_y < start_tile_y + height_tiles;
1942 ++tile_y) {
1943 for (int tile_x = start_tile_x; tile_x < start_tile_x + width_tiles;
1944 ++tile_x) {
1945 PromoteTileWordPriority(target, tile_x, tile_y);
1946 }
1947 }
1948 for (int y = start_tile_y * 8; y < (start_tile_y + height_tiles) * 8;
1949 ++y) {
1950 for (int x = start_tile_x * 8; x < (start_tile_x + width_tiles) * 8;
1951 ++x) {
1952 target.SetPriorityAt(x, y, 1);
1953 }
1954 }
1955 };
1956
1957 // USDASM has one tilemap per background. Yaze splits each into layout and
1958 // object buffers, so promote both potential pixel owners without changing
1959 // either bitmap or coverage mask.
1960 promote_target(object_buffer);
1961 if (layout_buffer != nullptr && layout_buffer != &object_buffer) {
1962 promote_target(*layout_buffer);
1963 }
1964 };
1965
1966 auto promote_upper_priority_rect = [&](int start_tile_x, int start_tile_y,
1967 int width_tiles, int height_tiles) {
1968 // USDASM writes these through $7E2000,X. BG2's map ($7E4000) directly
1969 // follows BG1's, so rows past 63 continue on BG2 at row-64 (for example
1970 // the exit-light span under a south door at row 55).
1971 constexpr int kRows = 64;
1972 const int bg1_rows =
1973 std::max(0, std::min(height_tiles, kRows - start_tile_y));
1974 promote_priority_rect_on_layer(bg1, layout_bg1, start_tile_x, start_tile_y,
1975 width_tiles, bg1_rows);
1976 if (bg1_rows < height_tiles) {
1977 const int bg2_start = std::max(0, start_tile_y - kRows);
1978 promote_priority_rect_on_layer(
1979 bg2, layout_bg2, start_tile_x, bg2_start, width_tiles,
1980 start_tile_y + height_tiles - kRows - bg2_start);
1981 }
1982 };
1983
1984 auto promote_lower_priority_rect = [&](int start_tile_x, int start_tile_y,
1985 int width_tiles, int height_tiles) {
1986 promote_priority_rect_on_layer(bg2, layout_bg2, start_tile_x, start_tile_y,
1987 width_tiles, height_tiles);
1988 };
1989
1990 auto promote_door_priority = [&](DoorDirection render_direction,
1991 int render_tile_x, int render_tile_y,
1992 DoorPrioritySpan span) {
1993 constexpr int kSectionSize = 32;
1994 const auto section_start = [](int coordinate) {
1995 return (coordinate / kSectionSize) * kSectionSize;
1996 };
1997 const auto section_end = [&](int coordinate) {
1998 return section_start(coordinate) + kSectionSize;
1999 };
2000
2001 int start_x = render_tile_x;
2002 int start_y = render_tile_y;
2003 int width = 0;
2004 int height = 0;
2005
2006 switch (span) {
2007 case DoorPrioritySpan::kNormalLower:
2008 switch (render_direction) {
2010 start_y = section_start(render_tile_y);
2011 width = 4;
2012 height = 7;
2013 break;
2015 // South raster coordinates are one row below the USDASM table
2016 // anchor; the fixed priority span starts four rows below it.
2017 start_y = render_tile_y + 3;
2018 width = 4;
2019 height = 7;
2020 break;
2022 start_x = section_start(render_tile_x);
2023 width = 5;
2024 height = 4;
2025 break;
2027 // East raster coordinates are one column right of the table
2028 // anchor; the fixed priority span starts four columns right.
2029 start_x = render_tile_x + 3;
2030 width = 5;
2031 height = 4;
2032 break;
2033 }
2034 break;
2035
2036 case DoorPrioritySpan::kLowerLayerOnly:
2037 switch (render_direction) {
2039 start_y = section_start(render_tile_y);
2040 width = 4;
2041 height = render_tile_y - start_y + 1;
2042 break;
2044 start_y = render_tile_y + 1;
2045 width = 4;
2046 height = section_end(render_tile_y) - start_y;
2047 break;
2049 start_x = section_start(render_tile_x);
2050 width = render_tile_x - start_x + 1;
2051 height = 4;
2052 break;
2054 start_x = render_tile_x + 1;
2055 width = section_end(render_tile_x) - start_x;
2056 height = 4;
2057 break;
2058 }
2059 break;
2060
2061 case DoorPrioritySpan::kHighRange:
2062 switch (render_direction) {
2064 start_y = section_start(render_tile_y);
2065 width = 4;
2066 height = render_tile_y - start_y;
2067 break;
2069 start_y = render_tile_y + 3;
2070 width = 4;
2071 height = section_end(render_tile_y) - start_y;
2072 break;
2074 start_x = section_start(render_tile_x);
2075 width = render_tile_x - start_x;
2076 height = 4;
2077 break;
2079 // The visible raster starts one column right of the USDASM table
2080 // anchor and spans three columns. Priority begins at anchor+4.
2081 start_x = render_tile_x + 3;
2082 width = section_end(render_tile_x) - start_x;
2083 height = 4;
2084 break;
2085 }
2086 break;
2087 }
2088
2089 if (width > 0 && height > 0) {
2090 promote_upper_priority_rect(start_x, start_y, width, height);
2091 }
2092 };
2093
2094 // Door markers update room-transition metadata in USDASM; they do not
2095 // stamp tiles. In real room streams they commonly follow a physical door
2096 // at the same position, so treating them as art overwrites that door with
2097 // the marker table's mirrored shutter tiles.
2098 const bool is_nonvisual_marker = door.type == DoorType::DungeonSwapMarker ||
2100 (door.type == DoorType::ExitMarker &&
2103 if (is_nonvisual_marker) {
2104 return;
2105 }
2106
2107 // Type $06 only promotes existing upper-layer wall tiles in USDASM. It
2108 // neither looks up nor stamps door graphics.
2109 if (door.type == DoorType::UnusedCaveExit) {
2110 promote_door_priority(door.direction, tile_x, tile_y,
2111 DoorPrioritySpan::kLowerLayerOnly);
2112 return;
2113 }
2114
2115 // South has several dedicated routines that bypass the ordinary 4x3 door
2116 // tables. Their coordinates begin at the raw USDASM tilemap anchor rather
2117 // than the generic South render anchor one row below it.
2118 if (door.direction == DoorDirection::South) {
2119 const auto [raw_tile_x, raw_tile_y] =
2121 door.direction);
2122 const int fancy_data_addr =
2123 kRoomDrawObjectDataBase + kFancyDungeonExitObjectOffset;
2124 const int cave_light_data_addr =
2125 kRoomDrawObjectDataBase + kCaveExitLightObjectOffset;
2126 const auto has_object_words = [&](int address, int word_count) {
2127 return address >= 0 &&
2128 address + word_count * 2 <= static_cast<int>(rom_->size());
2129 };
2130
2131 switch (door.type) {
2133 if (!has_object_words(fancy_data_addr, 80)) {
2134 DrawDoorIndicator(bg1, raw_tile_x - 3, raw_tile_y - 4,
2135 /*width=*/10, /*height=*/8, door.type,
2136 door.direction);
2137 return;
2138 }
2139 draw_from_object_data_row_major(bg1, raw_tile_x - 3, raw_tile_y - 4,
2140 /*width=*/10, /*height=*/8,
2141 fancy_data_addr);
2142 return;
2143
2145 if (!has_object_words(fancy_data_addr, 80)) {
2146 DrawDoorIndicator(bg2, raw_tile_x - 3, raw_tile_y - 4,
2147 /*width=*/10, /*height=*/8, door.type,
2148 door.direction);
2149 return;
2150 }
2151 draw_from_object_data_row_major(bg2, raw_tile_x - 3, raw_tile_y - 4,
2152 /*width=*/10, /*height=*/8,
2153 fancy_data_addr);
2154 // RoomDraw_NormalRangedDoors_South copies the final lower-layer row
2155 // back to the upper tilemap and forces high priority.
2156 for (int dx = 0; dx < 10; ++dx) {
2157 (void)draw_object_data_tile(
2158 bg1, raw_tile_x - 3 + dx, raw_tile_y + 3, fancy_data_addr,
2159 /*tile_idx=*/70 + dx, /*word_mask=*/0x2000);
2160 }
2161 return;
2162
2164 if (!has_object_words(cave_light_data_addr, 16)) {
2165 DrawDoorIndicator(bg2, raw_tile_x, raw_tile_y, /*width=*/4,
2166 /*height=*/4, door.type, door.direction);
2167 return;
2168 }
2169 promote_lower_priority_rect(raw_tile_x, raw_tile_y + 4,
2170 /*width_tiles=*/4, /*height_tiles=*/7);
2171 draw_from_object_data(bg2, raw_tile_x, raw_tile_y, /*width=*/4,
2172 /*height=*/4, cave_light_data_addr);
2173 for (int dx = 0; dx < 4; ++dx) {
2174 (void)draw_object_data_tile(
2175 bg1, raw_tile_x + dx, raw_tile_y + 3, cave_light_data_addr,
2176 /*tile_idx=*/dx * 4 + 3, /*word_mask=*/0x2000);
2177 }
2178 return;
2179
2180 case DoorType::CaveExit:
2181 if (!has_object_words(cave_light_data_addr, 16)) {
2182 DrawDoorIndicator(bg1, raw_tile_x, raw_tile_y, /*width=*/4,
2183 /*height=*/4, door.type, door.direction);
2184 return;
2185 }
2186 draw_from_object_data(bg1, raw_tile_x, raw_tile_y, /*width=*/4,
2187 /*height=*/4, cave_light_data_addr);
2188 return;
2189
2191 if (!has_object_words(cave_light_data_addr, 16)) {
2192 DrawDoorIndicator(bg1, raw_tile_x, raw_tile_y, /*width=*/4,
2193 /*height=*/4, door.type, door.direction);
2194 return;
2195 }
2196 promote_upper_priority_rect(raw_tile_x, raw_tile_y + 4,
2197 /*width_tiles=*/4, /*height_tiles=*/7);
2198 draw_from_object_data(bg1, raw_tile_x, raw_tile_y, /*width=*/4,
2199 /*height=*/4, cave_light_data_addr);
2200 return;
2201
2202 default:
2203 break;
2204 }
2205 }
2206
2207 auto resolve_render_type = [](DoorDirection render_direction,
2208 uint16_t render_type) -> uint16_t {
2209 switch (render_type) {
2210 case static_cast<uint16_t>(DoorType::BigKeyDoor):
2211 // South's closed big-key door is coerced to the normal-door table
2212 // entry by RoomDraw_OneSidedShutters_South.
2213 return render_direction == DoorDirection::South
2214 ? static_cast<uint16_t>(DoorType::NormalDoor)
2215 : render_type;
2216 case static_cast<uint16_t>(DoorType::BottomSidedShutter):
2217 return (render_direction == DoorDirection::North ||
2218 render_direction == DoorDirection::West)
2219 ? static_cast<uint16_t>(DoorType::DoubleSidedShutter)
2220 : static_cast<uint16_t>(DoorType::NormalDoor);
2221 case static_cast<uint16_t>(DoorType::TopSidedShutter):
2222 return (render_direction == DoorDirection::North ||
2223 render_direction == DoorDirection::West)
2224 ? static_cast<uint16_t>(DoorType::NormalDoor)
2225 : static_cast<uint16_t>(DoorType::DoubleSidedShutter);
2226 case static_cast<uint16_t>(DoorType::BottomShutterLower):
2227 return (render_direction == DoorDirection::North ||
2228 render_direction == DoorDirection::West)
2229 ? static_cast<uint16_t>(DoorType::DoubleSidedShutterLower)
2230 : static_cast<uint16_t>(DoorType::NormalDoorOneSidedShutter);
2231 case static_cast<uint16_t>(DoorType::TopShutterLower):
2232 return (render_direction == DoorDirection::North ||
2233 render_direction == DoorDirection::West)
2234 ? static_cast<uint16_t>(DoorType::NormalDoorOneSidedShutter)
2235 : static_cast<uint16_t>(DoorType::DoubleSidedShutterLower);
2236 default:
2237 return render_type;
2238 }
2239 };
2240
2241 auto resolve_table_door_data = [&](DoorDirection render_direction,
2242 uint16_t render_type,
2243 int* tile_data_addr) -> bool {
2244 int offset_table_addr = 0;
2245 switch (render_direction) {
2247 offset_table_addr = kDoorGfxUp;
2248 break;
2250 offset_table_addr = kDoorGfxDown;
2251 break;
2253 offset_table_addr = kDoorGfxLeft;
2254 break;
2256 offset_table_addr = kDoorGfxRight;
2257 break;
2258 }
2259
2260 const uint16_t resolved_type =
2261 resolve_render_type(render_direction, render_type);
2262 // DoorGFXDataOffset_* is an absolute-indexed word table and Y is already
2263 // the byte offset. Do not divide/re-multiply: hacked replacement tables may
2264 // intentionally supply an odd or wider offset.
2265 const int table_entry_addr = offset_table_addr + resolved_type;
2266 if (table_entry_addr + 1 >= static_cast<int>(rom_->size())) {
2267 return false;
2268 }
2269
2270 const uint16_t tile_offset =
2271 rom_data[table_entry_addr] | (rom_data[table_entry_addr + 1] << 8);
2272 *tile_data_addr = kRoomDrawObjectDataBase + tile_offset;
2273 const auto dims = GetDoorDimensions(render_direction);
2274 const int data_size = dims.width_tiles * dims.height_tiles * 2;
2275 if (*tile_data_addr < 0 ||
2276 *tile_data_addr + data_size > static_cast<int>(rom_->size())) {
2277 return false;
2278 }
2279
2280 return true;
2281 };
2282
2283 auto draw_table_door = [&](gfx::BackgroundBuffer& target,
2284 DoorDirection render_direction, int start_tile_x,
2285 int start_tile_y, uint16_t render_type) -> bool {
2286 int tile_data_addr = 0;
2287 if (!resolve_table_door_data(render_direction, render_type,
2288 &tile_data_addr)) {
2289 return false;
2290 }
2291
2292 const auto render_dims = GetDoorDimensions(render_direction);
2293 draw_from_object_data(target, start_tile_x, start_tile_y,
2294 render_dims.width_tiles, render_dims.height_tiles,
2295 tile_data_addr);
2296 return true;
2297 };
2298
2299 auto draw_high_range_table_door = [&](DoorDirection render_direction,
2300 int start_tile_x, int start_tile_y,
2301 uint16_t render_type) -> bool {
2302 int tile_data_addr = 0;
2303 if (!resolve_table_door_data(render_direction, render_type,
2304 &tile_data_addr)) {
2305 return false;
2306 }
2307
2308 const auto dims = GetDoorDimensions(render_direction);
2309 int tile_idx = 0;
2310 for (int dx = 0; dx < dims.width_tiles; ++dx) {
2311 for (int dy = 0; dy < dims.height_tiles; ++dy) {
2312 // RoomDraw_OneSidedLowerShutters_* splits one logical door across
2313 // the upper ($7E2000/BG1) and lower ($7E4000/BG2) tilemaps.
2314 bool writes_upper = false;
2315 switch (render_direction) {
2317 writes_upper = dy == 0;
2318 break;
2320 writes_upper = dy == dims.height_tiles - 1;
2321 break;
2323 writes_upper = dx == 0;
2324 break;
2326 writes_upper = dx == dims.width_tiles - 1;
2327 break;
2328 }
2329 gfx::BackgroundBuffer& target = writes_upper ? bg1 : bg2;
2330 if (!draw_object_data_tile(target, start_tile_x + dx, start_tile_y + dy,
2331 tile_data_addr, tile_idx++)) {
2332 return false;
2333 }
2334 }
2335 }
2336 return true;
2337 };
2338
2339 auto draw_ranged_door = [&](DoorDirection render_direction, int start_tile_x,
2340 int start_tile_y, DoorType original_type,
2341 uint16_t table_type) -> bool {
2342 if (original_type == DoorType::NormalDoorLower) {
2343 // Type $02 stamps the ordinary door art after promoting the fixed wall
2344 // rectangle selected by RoomDraw_MakeDoorPartsHighPriority_*.
2345 promote_door_priority(render_direction, start_tile_x, start_tile_y,
2346 DoorPrioritySpan::kNormalLower);
2347 }
2348
2349 // The dispatcher selects the normal/high-range writer before
2350 // RoomDraw_FlagDoorsAndGetFinalType substitutes open-door graphics. Keep
2351 // that choice tied to the stored type while looking up art by final type.
2352 const int render_type_value = static_cast<int>(original_type);
2353 const bool is_high_range =
2354 render_type_value >= 0x40 && render_type_value <= 0x66;
2355 if (is_high_range) {
2356 const bool drew = draw_high_range_table_door(
2357 render_direction, start_tile_x, start_tile_y, table_type);
2358 // North type $46 deliberately skips RoomDraw_MakeDoorHighPriority_North;
2359 // every other high-range direction/type promotes the adjacent upper wall.
2360 if (drew && !(render_direction == DoorDirection::North &&
2361 original_type == DoorType::ExplicitRoomDoor)) {
2362 promote_door_priority(render_direction, start_tile_x, start_tile_y,
2363 DoorPrioritySpan::kHighRange);
2364 }
2365 return drew;
2366 }
2367 return draw_table_door(bg1, render_direction, start_tile_x, start_tile_y,
2368 table_type);
2369 };
2370
2371 // USDASM has special north-door branches that do not follow the generic 4x3
2372 // ranged-door path.
2373 if (door.direction == DoorDirection::North &&
2374 door.type == DoorType::ExplodingWall && !is_door_open) {
2375 LOG_DEBUG("ObjectDrawer",
2376 "DrawDoor: closed exploding wall intentionally draws nothing");
2377 (void)bg2;
2378 return;
2379 }
2380
2381 if (door.direction == DoorDirection::North &&
2382 door.type == DoorType::CurtainDoor && !is_door_open) {
2383 const int tile_data_addr =
2384 kRoomDrawObjectDataBase + kNorthCurtainClosedOffset;
2385 const int data_size = 16 * 2; // RoomDraw_4x4 closed curtain path.
2386 if (tile_data_addr < 0 ||
2387 tile_data_addr + data_size > static_cast<int>(rom_->size())) {
2388 DrawDoorIndicator(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
2389 door.type, door.direction);
2390 return;
2391 }
2392 draw_from_object_data(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
2393 tile_data_addr);
2394 return;
2395 }
2396
2397 if (door.direction == DoorDirection::North &&
2398 door.type == DoorType::CurtainDoor && is_door_open) {
2399 const int table_entry_addr =
2400 kDoorGfxUp + static_cast<int>(effective_door_type);
2401 if (table_entry_addr < 0 ||
2402 table_entry_addr + 1 >= static_cast<int>(rom_->size())) {
2403 DrawDoorIndicator(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
2404 door.type, door.direction);
2405 return;
2406 }
2407
2408 const uint16_t tile_offset =
2409 rom_data[table_entry_addr] | (rom_data[table_entry_addr + 1] << 8);
2410 const int tile_data_addr = kRoomDrawObjectDataBase + tile_offset;
2411 const int data_size = 16 * 2; // RoomDraw_4x4 open curtain path.
2412 if (tile_data_addr < 0 ||
2413 tile_data_addr + data_size > static_cast<int>(rom_->size())) {
2414 DrawDoorIndicator(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
2415 door.type, door.direction);
2416 return;
2417 }
2418
2419 draw_from_object_data(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
2420 tile_data_addr);
2421 return;
2422 }
2423
2424 if (door.direction == DoorDirection::North &&
2425 door.type == DoorType::ExplodingWall && is_door_open) {
2426 const int position_index = std::min<int>(door.position & 0x0F, 5);
2427 const int tilemap_entry_addr =
2428 kExplodingWallTilemapPositionBase + (position_index * 2);
2429 if (tilemap_entry_addr < 0 ||
2430 tilemap_entry_addr + 1 >= static_cast<int>(rom_->size())) {
2431 DrawDoorIndicator(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
2432 door.type, door.direction);
2433 return;
2434 }
2435
2436 const uint16_t tilemap_offset =
2437 rom_data[tilemap_entry_addr] | (rom_data[tilemap_entry_addr + 1] << 8);
2438 const auto explosion_tile_coords =
2439 tilemap_offset_to_tile_coords(tilemap_offset);
2440 const int explosion_tile_x = explosion_tile_coords.first;
2441 const int explosion_tile_y = explosion_tile_coords.second;
2442
2443 auto draw_exploding_wall_segment = [&](int table_entry_addr,
2444 int segment_tile_y) -> bool {
2445 if (table_entry_addr < 0 ||
2446 table_entry_addr + 1 >= static_cast<int>(rom_->size())) {
2447 return false;
2448 }
2449
2450 const uint16_t tile_offset =
2451 rom_data[table_entry_addr] | (rom_data[table_entry_addr + 1] << 8);
2452 const int tile_data_addr = kRoomDrawObjectDataBase + tile_offset;
2453 // RoomDraw_ExplodingWallSegment: a 2x6 column (words 0-11), an 18x6
2454 // fill of word 12, then a 2x6 column (words 13-24) at x+20.
2455 constexpr int kFillWordIndex = 12;
2456 constexpr int kRightColumnWordIndex = 13;
2457 const int min_data_size = (kRightColumnWordIndex + 12) * 2;
2458 if (tile_data_addr < 0 ||
2459 tile_data_addr + min_data_size > static_cast<int>(rom_->size())) {
2460 return false;
2461 }
2462
2463 draw_from_object_data(bg1, explosion_tile_x, segment_tile_y,
2464 /*width=*/2, /*height=*/6, tile_data_addr);
2465 const uint16_t fill_word =
2466 rom_data[tile_data_addr + (kFillWordIndex * 2)] |
2467 (rom_data[tile_data_addr + (kFillWordIndex * 2) + 1] << 8);
2468 draw_repeated_tile(bg1, explosion_tile_x + 2, segment_tile_y,
2469 /*width=*/18, /*height=*/6, fill_word);
2470 draw_from_object_data(bg1, explosion_tile_x + 20, segment_tile_y,
2471 /*width=*/2, /*height=*/6,
2472 tile_data_addr + (kRightColumnWordIndex * 2));
2473 return true;
2474 };
2475
2476 const int south_table_entry_addr =
2477 kDoorGfxDown + kExplodingWallOpenReplacementType;
2478 const int north_table_entry_addr =
2479 kDoorGfxUp + kExplodingWallOpenReplacementType;
2480 if (!draw_exploding_wall_segment(south_table_entry_addr,
2481 explosion_tile_y) ||
2482 !draw_exploding_wall_segment(north_table_entry_addr,
2483 explosion_tile_y + 6)) {
2484 DrawDoorIndicator(bg1, explosion_tile_x, explosion_tile_y, /*width=*/20,
2485 /*height=*/12, door.type, door.direction);
2486 }
2487 return;
2488 }
2489
2490 // RoomDraw_North treats all four key-stair types ($20/$22/$24/$26) as
2491 // stateful stair records. Once their persistent bit is open, USDASM returns
2492 // without drawing replacement art.
2493 const bool is_north_key_stairs =
2499 if (is_north_key_stairs && is_door_open) {
2500 return;
2501 }
2502
2503 // Closed North lower-layer key stairs ($24/$26) use the ordinary 4x3 table
2504 // layout, but every tile is written to $7E4000/BG2 and no middle-door
2505 // counterpart is emitted.
2506 const bool is_north_lower_key_stairs =
2510 if (is_north_lower_key_stairs) {
2511 if (!draw_table_door(bg2, door.direction, tile_x, tile_y,
2512 static_cast<uint16_t>(door.type))) {
2513 DrawDoorIndicator(bg2, tile_x, tile_y, door_width, door_height, door.type,
2514 door.direction);
2515 }
2516 return;
2517 }
2518
2519 // The normal ranged-door callers treat curtain and waterfall final types as
2520 // metadata-only results (carry clear from FlagDoorsAndGetFinalType). North
2521 // curtain and lower key stairs have already taken their dedicated branches.
2522 const bool uses_normal_ranged_writer =
2523 static_cast<int>(door.type) <
2524 static_cast<int>(DoorType::NormalDoorOneSidedShutter);
2525 if (uses_normal_ranged_writer &&
2526 (effective_door_type == static_cast<uint16_t>(DoorType::CurtainDoor) ||
2527 effective_door_type == static_cast<uint16_t>(DoorType::WaterfallDoor))) {
2528 return;
2529 }
2530
2531 // Door graphics use an indirect addressing scheme:
2532 // 1. kDoorGfxUp/Down/Left/Right point to offset tables (DoorGFXDataOffset_*)
2533 // 2. Each table entry is a 16-bit offset into RoomDrawObjectData
2534 // 3. RoomDrawObjectData base is at PC 0x1B52 (SNES $00:9B52)
2535 // 4. Actual tile data = 0x1B52 + offset_from_table
2536 const bool north_explicit_door = door.direction == DoorDirection::North &&
2538 // Closed north key stairs take .not_open's `CMP #$0024 / BCC
2539 // RoomDraw_OneSidedShutters_North` and never reach the ranged-door writer
2540 // that mirrors a counterpart south door (rooms 0x00E, 0x099, 0x0AB).
2541 const bool north_key_stairs = door.direction == DoorDirection::North &&
2544 if ((door.direction == DoorDirection::North ||
2545 door.direction == DoorDirection::West) &&
2546 position_index >= 6 && !north_explicit_door && !north_key_stairs) {
2547 const DoorDirection counterpart_direction =
2550 // USDASM indexes the table immediately following NorthMiddle/WestMiddle
2551 // with the original 6..11 position offset. That lands on counterpart
2552 // positions 0..5; the two rasters are separated by the middle wall rather
2553 // than overlapping at the current anchor.
2554 const auto [counterpart_tile_x, counterpart_tile_y] =
2556 static_cast<uint8_t>(position_index - 6), counterpart_direction);
2557 (void)draw_ranged_door(counterpart_direction, counterpart_tile_x,
2558 counterpart_tile_y, door.type, effective_door_type);
2559 }
2560
2561 const bool drew_current = draw_ranged_door(door.direction, tile_x, tile_y,
2562 door.type, effective_door_type);
2563 if (!drew_current) {
2564 LOG_DEBUG("ObjectDrawer",
2565 "DrawDoor: INVALID ADDRESS - falling back to indicator");
2566 DrawDoorIndicator(bg1, tile_x, tile_y, door_width, door_height, door.type,
2567 door.direction);
2568 return;
2569 }
2570
2571 LOG_DEBUG("ObjectDrawer",
2572 "DrawDoor: type=%s effective=0x%04X dir=%s pos=%d at tile(%d,%d) "
2573 "size=%dx%d",
2574 std::string(GetDoorTypeName(door.type)).c_str(),
2575 static_cast<unsigned int>(effective_door_type),
2576 std::string(GetDoorDirectionName(door.direction)).c_str(),
2577 door.position, tile_x, tile_y, door_width, door_height);
2578}
2579
2581 int tile_y, int width, int height,
2582 DoorType type, DoorDirection direction) {
2583 // Draw a simple colored rectangle as door indicator when graphics unavailable
2584 // Different colors for different door types using DoorType enum
2585
2586 auto& bitmap = bg.bitmap();
2587 auto& coverage_buffer = bg.mutable_coverage_data();
2588
2589 uint8_t color_idx;
2590 switch (type) {
2593 color_idx = 45; // Standard door color (brown)
2594 break;
2595
2601 color_idx = 60; // Key door - yellowish
2602 break;
2603
2606 color_idx = 58; // Big key - golden
2607 break;
2608
2612 case DoorType::DashWall:
2613 color_idx = 15; // Bombable/destructible - brownish/cracked
2614 break;
2615
2622 color_idx = 30; // Shutter - greenish
2623 break;
2624
2626 color_idx = 42; // Eye watch - lighter brown
2627 break;
2628
2630 color_idx = 35; // Curtain - special
2631 break;
2632
2633 case DoorType::CaveExit:
2638 color_idx = 25; // Cave/dungeon exit - dark
2639 break;
2640
2644 color_idx = 5; // Markers - very faint
2645 break;
2646
2647 default:
2648 color_idx = 50; // Default door color
2649 break;
2650 }
2651
2652 int pixel_x = tile_x * 8;
2653 int pixel_y = tile_y * 8;
2654 int pixel_width = width * 8;
2655 int pixel_height = height * 8;
2656
2657 int bitmap_width = bitmap.width();
2658 int bitmap_height = bitmap.height();
2659
2661 pixel_width, pixel_height);
2662
2663 // Draw filled rectangle with border
2664 for (int py = 0; py < pixel_height; py++) {
2665 for (int px = 0; px < pixel_width; px++) {
2666 int dest_x = pixel_x + px;
2667 int dest_y = pixel_y + py;
2668
2669 if (dest_x >= 0 && dest_x < bitmap_width && dest_y >= 0 &&
2670 dest_y < bitmap_height) {
2671 // Draw border (2 pixel thick) or fill
2672 bool is_border = (px < 2 || px >= pixel_width - 2 || py < 2 ||
2673 py >= pixel_height - 2);
2674 uint8_t final_color = is_border ? (color_idx + 5) : color_idx;
2675
2676 int offset = (dest_y * bitmap_width) + dest_x;
2677 bitmap.WriteToPixel(offset, final_color);
2678
2679 if (offset >= 0 && offset < static_cast<int>(coverage_buffer.size())) {
2680 coverage_buffer[offset] = 1;
2681 }
2682 }
2683 }
2684 }
2685}
2686
2688 std::span<const gfx::TileInfo> tiles,
2689 [[maybe_unused]] const DungeonState* state) {
2690 // USDASM RoomDraw_OpenChest draws F9A's fixed open graphic directly and
2691 // does not read or advance either chest/event counter.
2692 if (obj.id_ == 0xF9A) {
2694 return;
2695 }
2696
2697 // USDASM RoomDraw_Chest draws F99 as a single stateful 2x2 chest. The size
2698 // byte is not used for repetition.
2699
2700 // Determine if chest is open
2701 bool is_open = false;
2702 if (state) {
2703 is_open = state->IsChestOpen(room_id_, current_chest_index_);
2704 }
2705
2706 // RoomDraw_Chest advances the chest-only $0496 counter, then copies its next
2707 // value into the shared chest/lock $0498 counter.
2710
2711 // Draw SINGLE chest - no repetition based on size
2712 // Standard chests are 2x2 (4 tiles)
2713 // If we have extra tiles loaded, the second 4 are for open state
2714
2715 if (is_open && tiles.size() >= 8) {
2716 // Small chest open tiles (indices 4-7) - SINGLE 2x2 draw
2717 if (tiles.size() >= 8) {
2718 WriteTile8(bg, obj.x_, obj.y_, tiles[4]); // top-left
2719 WriteTile8(bg, obj.x_, obj.y_ + 1, tiles[5]); // bottom-left
2720 WriteTile8(bg, obj.x_ + 1, obj.y_, tiles[6]); // top-right
2721 WriteTile8(bg, obj.x_ + 1, obj.y_ + 1, tiles[7]); // bottom-right
2722 }
2723 return;
2724 }
2725
2726 // Draw closed chest - SINGLE 2x2 pattern (column-major order)
2727 if (tiles.size() >= 4) {
2728 WriteTile8(bg, obj.x_, obj.y_, tiles[0]); // top-left
2729 WriteTile8(bg, obj.x_, obj.y_ + 1, tiles[1]); // bottom-left
2730 WriteTile8(bg, obj.x_ + 1, obj.y_, tiles[2]); // top-right
2731 WriteTile8(bg, obj.x_ + 1, obj.y_ + 1, tiles[3]); // bottom-right
2732 }
2733}
2734
2737 std::span<const gfx::TileInfo> tiles,
2738 const DungeonState* state) {
2739 // USDASM RoomDraw_BigChest uses the chest-only $0496 slot for its room flag,
2740 // advances it once, then copies the next value into shared $0498. FB2 uses
2741 // RoomDraw_OpenBigChest directly and never reaches this stateful wrapper.
2742 bool is_open = false;
2743 if (state) {
2744 is_open = state->IsBigChestOpen(room_id_, current_chest_index_);
2745 }
2746
2749
2750 constexpr size_t kBigChestStateTileCount = 12;
2751 if (is_open && tiles.size() >= kBigChestStateTileCount * 2) {
2752 tiles = tiles.subspan(kBigChestStateTileCount, kBigChestStateTileCount);
2753 }
2755}
2756
2759 std::span<const gfx::TileInfo> tiles,
2760 const DungeonState* state) {
2761 // USDASM RoomDraw_BigKeyLock indexes $0402 through the shared $0498
2762 // chest/lock slot. An opened lock advances the slot but writes no tiles.
2763 const int room_event_index = current_room_event_index_++;
2764 if (state && state->IsBigKeyLockOpen(room_id_, room_event_index)) {
2765 return;
2766 }
2767
2769}
2770
2772 std::span<const gfx::TileInfo> tiles,
2773 [[maybe_unused]] const DungeonState* state) {
2774 // Intentionally empty - represents invisible logic objects or placeholders
2775 // ASM: RoomDraw_Nothing_A ($0190F2), RoomDraw_Nothing_B ($01932E), etc.
2776 // These routines typically just RTS.
2777 LOG_DEBUG("ObjectDrawer", "DrawNothing for object 0x%02X (logic/invisible)",
2778 obj.id_);
2779}
2780
2782 std::span<const gfx::TileInfo> tiles,
2783 [[maybe_unused]] const DungeonState* state) {
2784 // Pattern: Custom draw routine (objects 0x31-0x32)
2785 // For now, fall back to simple 1x1
2786 if (tiles.size() >= 1) {
2787 // Use first 8x8 tile from span
2788 WriteTile8(bg, obj.x_, obj.y_, tiles[0]);
2789 }
2790}
2791
2793 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2794 std::span<const gfx::TileInfo> tiles,
2795 [[maybe_unused]] const DungeonState* state) {
2796 // Pattern: 4x4 block rightward (objects 0x33, 0xBA = large ceiling, etc.)
2797 int size = obj.size_ & 0x0F;
2798
2799 // Assembly: GetSize_1to16, so count = size + 1
2800 int count = size + 1;
2801
2802 // Debug: Log large ceiling objects (0xBA)
2803 if (obj.id_ == 0xBA && tiles.size() >= 16) {
2804 LOG_DEBUG("ObjectDrawer",
2805 "Large Ceiling Draw: obj=0x%02X pos=(%d,%d) size=%d tiles=%zu",
2806 obj.id_, obj.x_, obj.y_, size, tiles.size());
2807 LOG_DEBUG("ObjectDrawer", " First 4 Tile IDs: [%d, %d, %d, %d]",
2808 tiles[0].id_, tiles[1].id_, tiles[2].id_, tiles[3].id_);
2809 LOG_DEBUG("ObjectDrawer", " First 4 Palettes: [%d, %d, %d, %d]",
2810 tiles[0].palette_, tiles[1].palette_, tiles[2].palette_,
2811 tiles[3].palette_);
2812 }
2813
2814 for (int s = 0; s < count; s++) {
2815 if (tiles.size() >= 16) {
2816 // Draw 4x4 pattern in COLUMN-MAJOR order (matching assembly)
2817 // Iterate columns (x) first, then rows (y) within each column
2818 for (int x = 0; x < 4; ++x) {
2819 for (int y = 0; y < 4; ++y) {
2820 WriteTile8(bg, obj.x_ + (s * 4) + x, obj.y_ + y, tiles[x * 4 + y]);
2821 }
2822 }
2823 }
2824 }
2825}
2826
2828 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2829 std::span<const gfx::TileInfo> tiles,
2830 [[maybe_unused]] const DungeonState* state) {
2831 // Pattern: 4x3 decoration with spacing (objects 0x3A-0x3B)
2832 // 4 columns × 3 rows = 12 tiles in COLUMN-MAJOR order
2833 // ASM: ADC #$0008 to Y = 8-byte advance = 4 tiles per iteration
2834 // Total spacing: 4 (object width) + 4 (gap) = 8 tiles between starts
2835 int size = obj.size_ & 0x0F;
2836
2837 // Assembly: GetSize_1to16, so count = size + 1
2838 int count = size + 1;
2839
2840 for (int s = 0; s < count; s++) {
2841 if (tiles.size() >= 12) {
2842 // Draw 4x3 pattern in COLUMN-MAJOR order (matching assembly)
2843 // Spacing: 8 tiles (4 object + 4 gap) per ASM ADC #$0008
2844 for (int x = 0; x < 4; ++x) {
2845 for (int y = 0; y < 3; ++y) {
2846 WriteTile8(bg, obj.x_ + (s * 8) + x, obj.y_ + y, tiles[x * 3 + y]);
2847 }
2848 }
2849 }
2850 }
2851}
2852
2853// ============================================================================
2854// Utility Methods
2855// ============================================================================
2856
2858 int start_py, int pixel_width,
2859 int pixel_height) {
2860 bg1.SetBG1RevealMaskRect(bg1_reveal_mask_source_, start_px, start_py,
2861 pixel_width, pixel_height);
2862}
2863
2865 gfx::BackgroundBuffer& bg1, const gfx::TileInfo& tile_info, int pixel_x,
2866 int pixel_y, const uint8_t* tiledata) {
2867 auto& bitmap = bg1.bitmap();
2868 if (!bitmap.is_active() || bitmap.width() == 0 || bitmap.height() == 0 ||
2869 tiledata == nullptr) {
2870 return;
2871 }
2872
2873 constexpr int kMaxTileRow = 63;
2874 const int tile_col = tile_info.id_ % 16;
2875 const int tile_row = tile_info.id_ / 16;
2876 if (tile_row > kMaxTileRow) {
2877 return;
2878 }
2879
2880 const int tile_base_x = tile_col * 8;
2881 const int tile_base_y = tile_row * 1024;
2882 auto& reveal_mask = bg1.mutable_bg1_reveal_mask_data();
2883 const uint8_t source_mask = static_cast<uint8_t>(bg1_reveal_mask_source_);
2884
2885 for (int py = 0; py < 8; ++py) {
2886 const int src_row = tile_info.vertical_mirror_ ? (7 - py) : py;
2887 const int dest_y = pixel_y + py;
2888 if (dest_y < 0 || dest_y >= bitmap.height()) {
2889 continue;
2890 }
2891
2892 for (int px = 0; px < 8; ++px) {
2893 const int src_col = tile_info.horizontal_mirror_ ? (7 - px) : px;
2894 const int src_index =
2895 (src_row * 128) + src_col + tile_base_x + tile_base_y;
2896 if (tiledata[src_index] == 0) {
2897 continue;
2898 }
2899
2900 const int dest_x = pixel_x + px;
2901 if (dest_x < 0 || dest_x >= bitmap.width()) {
2902 continue;
2903 }
2904
2905 const int dest_index = dest_y * bitmap.width() + dest_x;
2906 reveal_mask[dest_index] |= source_mask;
2907 }
2908 }
2909}
2910
2911void ObjectDrawer::WriteTile8(gfx::BackgroundBuffer& bg, int tile_x, int tile_y,
2912 const gfx::TileInfo& tile_info) {
2913 if (!IsValidTilePosition(tile_x, tile_y)) {
2914 return;
2915 }
2916 PushTrace(tile_x, tile_y, tile_info);
2917 if (trace_only_) {
2918 return;
2919 }
2920 // Keep the logical tilemap synchronized with the bitmap/coverage owner.
2921 // Conditional edge routines query this word after coverage selects whether
2922 // the object or layout half owns the effective physical BG entry.
2923 bg.SetTileAt(tile_x, tile_y, gfx::TileInfoToWord(tile_info));
2924 // Draw directly to bitmap instead of tile buffer to avoid being overwritten
2925 auto& bitmap = bg.bitmap();
2926 if (!bitmap.is_active() || bitmap.width() == 0) {
2927 return; // Bitmap not ready
2928 }
2929
2930 // The room-specific graphics buffer (current_gfx16_) contains the assembled
2931 // tile graphics for the current room. Object tile IDs are relative to this
2932 // buffer.
2933 const uint8_t* gfx_data = room_gfx_buffer_;
2934
2935 if (!gfx_data) {
2936 LOG_DEBUG("ObjectDrawer", "ERROR: No graphics data available");
2937 return;
2938 }
2939
2940 // A later BG1 tilemap write supersedes an earlier reveal request from the
2941 // same logical stream, including transparent pixels in the 8x8 footprint.
2942 bg.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, tile_x * 8, tile_y * 8, 8,
2943 8);
2944
2945 const bool should_mark_bg1_mask =
2946 active_mask_source_bg_ != nullptr && (&bg == active_mask_source_bg_);
2947 // Draw single 8x8 tile directly to bitmap.
2948 DrawTileToBitmap(bitmap, tile_info, tile_x * 8, tile_y * 8, gfx_data);
2949 if (should_mark_bg1_mask && active_object_bg1_mask_ != nullptr) {
2951 tile_x * 8, tile_y * 8, gfx_data);
2952 }
2953 if (should_mark_bg1_mask && active_layout_bg1_mask_ != nullptr) {
2955 tile_x * 8, tile_y * 8, gfx_data);
2956 }
2957
2958 // Mark coverage for the full 8x8 tile region (even if pixels are transparent).
2959 //
2960 // This distinguishes "tilemap entry written but transparent" from "no write",
2961 // which is required to emulate SNES behavior where a transparent tile still
2962 // overwrites the previous tilemap entry (clearing BG1 and revealing BG2/backdrop).
2963 auto& coverage_buffer = bg.mutable_coverage_data();
2964
2965 // Also update priority buffer with tile's priority bit.
2966 // Priority (over_) affects Z-ordering in SNES Mode 1 compositing.
2967 uint8_t priority = tile_info.over_ ? 1 : 0;
2968 int pixel_x = tile_x * 8;
2969 int pixel_y = tile_y * 8;
2970 auto& priority_buffer = bg.mutable_priority_data();
2971 int width = bitmap.width();
2972
2973 // Update priority for each pixel in the 8x8 tile
2974 const auto& bitmap_data = bitmap.vector();
2975 for (int py = 0; py < 8; py++) {
2976 int dest_y = pixel_y + py;
2977 if (dest_y < 0 || dest_y >= bitmap.height())
2978 continue;
2979
2980 for (int px = 0; px < 8; px++) {
2981 int dest_x = pixel_x + px;
2982 if (dest_x < 0 || dest_x >= width)
2983 continue;
2984
2985 int dest_index = dest_y * width + dest_x;
2986
2987 // Coverage is set for all pixels in the tile footprint.
2988 if (dest_index >= 0 &&
2989 dest_index < static_cast<int>(coverage_buffer.size())) {
2990 coverage_buffer[dest_index] = 1;
2991 }
2992
2993 // Store priority only for opaque pixels; transparent writes clear stale
2994 // priority at this location.
2995 if (dest_index < static_cast<int>(bitmap_data.size()) &&
2996 bitmap_data[dest_index] != 255) {
2997 priority_buffer[dest_index] = priority;
2998 } else {
2999 priority_buffer[dest_index] = 0xFF;
3000 }
3001 }
3002 }
3003}
3004
3005bool ObjectDrawer::IsValidTilePosition(int tile_x, int tile_y) const {
3006 return tile_x >= 0 && tile_x < kMaxTilesX && tile_y >= 0 &&
3007 tile_y < kMaxTilesY;
3008}
3009
3011 const gfx::TileInfo& tile_info, int pixel_x,
3012 int pixel_y, const uint8_t* tiledata) {
3013 // Draw an 8x8 tile directly to bitmap at pixel coordinates
3014 // Graphics data is in 8BPP linear format (1 pixel per byte)
3015 if (!tiledata)
3016 return;
3017
3018 // DEBUG: Check if bitmap is valid
3019 if (!bitmap.is_active() || bitmap.width() == 0 || bitmap.height() == 0) {
3020 LOG_DEBUG("ObjectDrawer", "ERROR: Invalid bitmap - active=%d, size=%dx%d",
3021 bitmap.is_active(), bitmap.width(), bitmap.height());
3022 return;
3023 }
3024
3025 // Calculate tile position in 8BPP graphics buffer
3026 // Layout: 16 tiles per row, each tile is 8 pixels wide (8 bytes)
3027 // Row stride: 128 bytes (16 tiles * 8 bytes)
3028 // Buffer size: 0x10000 (65536 bytes) = 64 tile rows max
3029 constexpr int kGfxBufferSize = 0x10000;
3030 constexpr int kMaxTileRow = 63; // 64 rows (0-63), each 1024 bytes
3031
3032 int tile_col = tile_info.id_ % 16;
3033 int tile_row = tile_info.id_ / 16;
3034
3035 // CRITICAL: Validate tile_row to prevent index out of bounds
3036 if (tile_row > kMaxTileRow) {
3037 LOG_DEBUG("ObjectDrawer", "Tile ID 0x%03X out of bounds (row %d > %d)",
3038 tile_info.id_, tile_row, kMaxTileRow);
3039 return;
3040 }
3041
3042 int tile_base_x = tile_col * 8; // 8 bytes per tile horizontally
3043 int tile_base_y =
3044 tile_row * 1024; // 1024 bytes per tile row (8 rows * 128 bytes)
3045
3046 // DEBUG: Log first few tiles being drawn with their graphics data
3047 static int draw_debug_count = 0;
3048 if (draw_debug_count < 5) {
3049 int sample_index = tile_base_y + tile_base_x;
3050 LOG_DEBUG("ObjectDrawer",
3051 "DrawTile: id=%d (col=%d,row=%d) gfx_offset=%d (0x%04X)",
3052 tile_info.id_, tile_col, tile_row, sample_index, sample_index);
3053 draw_debug_count++;
3054 }
3055
3056 // Palette offset calculation using direct CGRAM row mirroring.
3057 //
3058 // Room::RenderRoomGraphics loads dungeon main palettes into SDL bank rows 2-7,
3059 // leaving rows 0-1 as transparent HUD placeholders. The tile palette bits are
3060 // therefore already the correct SDL bank row index.
3061 //
3062 // Drawing formula: final_color = pixel + (pal * 16)
3063 // Where pixel 0 = transparent (not written), pixel 1-15 = colors within bank.
3064 uint8_t pal = tile_info.palette_ & 0x07;
3065 const uint8_t palette_offset = static_cast<uint8_t>(pal * 16);
3066
3067 // Draw 8x8 pixels with overwrite semantics.
3068 //
3069 // Important SNES behavior: writing a tilemap entry replaces the previous
3070 // contents for the full 8x8 footprint. Source pixel 0 is transparent, but it
3071 // still clears what was there before. We model that by writing 255
3072 // (transparent key) for zero pixels.
3073 bool any_pixels_changed = false;
3074
3075 for (int py = 0; py < 8; py++) {
3076 // Source row with vertical mirroring
3077 int src_row = tile_info.vertical_mirror_ ? (7 - py) : py;
3078
3079 for (int px = 0; px < 8; px++) {
3080 // Source column with horizontal mirroring
3081 int src_col = tile_info.horizontal_mirror_ ? (7 - px) : px;
3082
3083 // Calculate source index in 8BPP buffer
3084 // Stride is 128 bytes (sheet width)
3085 int src_index = (src_row * 128) + src_col + tile_base_x + tile_base_y;
3086 uint8_t pixel = tiledata[src_index];
3087 uint8_t out_pixel = 255; // transparent/clear
3088 if (pixel != 0) {
3089 // Pixels 1-15 map into a 16-color bank chunk.
3090 out_pixel = static_cast<uint8_t>(pixel + palette_offset);
3091 }
3092
3093 int dest_x = pixel_x + px;
3094 int dest_y = pixel_y + py;
3095 if (dest_x < 0 || dest_x >= bitmap.width() || dest_y < 0 ||
3096 dest_y >= bitmap.height()) {
3097 continue;
3098 }
3099
3100 int dest_index = dest_y * bitmap.width() + dest_x;
3101 if (dest_index < 0 ||
3102 dest_index >= static_cast<int>(bitmap.mutable_data().size())) {
3103 continue;
3104 }
3105
3106 auto& dst = bitmap.mutable_data()[dest_index];
3107 if (dst != out_pixel) {
3108 dst = out_pixel;
3109 any_pixels_changed = true;
3110 }
3111 }
3112 }
3113
3114 if (any_pixels_changed) {
3115 bitmap.set_modified(true);
3116 }
3117}
3118
3120 gfx::BackgroundBuffer& target_bg,
3121 RoomObject::LayerType layer) {
3122 // USDASM RoomDraw_BG2MaskFull (bank_01 table entry 0x273) runs
3123 // RoomDraw_FloorChunks with obj00E0, eight words of $01EC, over the whole
3124 // map of the current list's layer. $01EC is the game's tilemap erase value
3125 // (EraseTilemaps). Vanilla places it only in the BG2 object list, where it
3126 // erases the floor and the BothBG walls drawn before it. The transparent
3127 // erase tile also marks object coverage, so compositing hides the layout.
3128 constexpr uint16_t kTilemapEraseWord = 0x01EC;
3129 const gfx::TileInfo erase = gfx::WordToTileInfo(kTilemapEraseWord);
3130 SetTraceContext(object, layer);
3131 for (int y = 0; y < DrawContext::kMaxTilesY; ++y) {
3132 for (int x = 0; x < DrawContext::kMaxTilesX; ++x) {
3133 WriteTile8(target_bg, x, y, erase);
3134 }
3135 }
3136 return absl::OkStatus();
3137}
3138
3139absl::Status ObjectDrawer::DrawLampCones(const RoomObject& object,
3140 gfx::BackgroundBuffer& bg2) {
3141 // USDASM RoomDraw_LampCones / RoomDraw_SingleLampCone (bank_01 table entry
3142 // 0x22A): four 12x12 row-major blocks from RoomDrawObjectData stored with
3143 // STA.l $7E4000,X, i.e. always BG2, at fixed tilemap offsets.
3144 struct Cone {
3145 int data_offset; // relative to RoomDrawObjectData
3146 int tilemap_offset; // byte offset into the 64x64 tilemap
3147 };
3148 constexpr Cone kCones[] = {
3149 {0x16DC, 0x0514}, {0x17F6, 0x0554}, {0x1914, 0x1514}, {0x1A2A, 0x1554}};
3150 constexpr int kConeSize = 12;
3151 const auto& rom_data = rom_->vector();
3153 for (const Cone& cone : kCones) {
3154 const int base = kRoomObjectTileAddress + cone.data_offset;
3155 if (base < 0 ||
3156 base + kConeSize * kConeSize * 2 > static_cast<int>(rom_data.size())) {
3157 return absl::OutOfRangeError(
3158 absl::StrFormat("Lamp cone data out of range: 0x%X", base));
3159 }
3160 const int origin = cone.tilemap_offset / 2;
3161 for (int row = 0; row < kConeSize; ++row) {
3162 for (int column = 0; column < kConeSize; ++column) {
3163 const int word_address = base + (row * kConeSize + column) * 2;
3164 const uint16_t word = static_cast<uint16_t>(
3165 rom_data[word_address] | (rom_data[word_address + 1] << 8));
3166 WriteTile8(bg2, origin % 64 + column, origin / 64 + row,
3167 gfx::WordToTileInfo(word));
3168 }
3169 }
3170 }
3171 return absl::OkStatus();
3172}
3173
3175 const RoomObject& object, gfx::BackgroundBuffer& bg1,
3177 gfx::BackgroundBuffer* layout_bg2) {
3178 // Literal port of USDASM RoomDraw_AgahnimsWindows (bank_01 table entry
3179 // 0x22E). X starts at $08, the object's tilemap byte offset; each
3180 // `STA.l $7E2xxx,X` stores to byte (xxx + X) of the upper tilemap. Offsets
3181 // past $2000 bytes continue into the lower tilemap at $7E4000, as in WRAM.
3182 const auto& rom_data = rom_->vector();
3183 const int anchor = object.y_ * 0x80 + object.x_ * 2;
3184 auto read_word = [&](int data_offset) -> uint16_t {
3185 const int address = kRoomObjectTileAddress + data_offset;
3186 if (address < 0 || address + 1 >= static_cast<int>(rom_data.size())) {
3187 return 0;
3188 }
3189 return static_cast<uint16_t>(rom_data[address] |
3190 (rom_data[address + 1] << 8));
3191 };
3192 auto tile_at = [&](int store, int x) -> std::pair<int, int> {
3193 return {((store - 0x2000 + x) / 2) % 64, ((store - 0x2000 + x) / 2) / 64};
3194 };
3195 auto store = [&](int address, int x, uint16_t word) {
3196 auto [tile_x, tile_y] = tile_at(address, x);
3197 if (tile_y < 64) {
3199 WriteTile8(bg1, tile_x, tile_y, gfx::WordToTileInfo(word));
3200 } else if (tile_y < 128) {
3202 WriteTile8(bg2, tile_x, tile_y - 64, gfx::WordToTileInfo(word));
3203 }
3204 };
3205 constexpr uint16_t kHFlip = 0x4000;
3206
3207 // .next_a: 6 columns x 4 rows from obj1BF2, three copies.
3208 for (int i = 0, x = anchor, y = 0x1BF2; i < 6; ++i, x += 2, y += 8) {
3209 for (int row = 0; row < 4; ++row) {
3210 const uint16_t word = read_word(y + row * 2);
3211 for (int base : {0x220E, 0x221A, 0x2226}) {
3212 store(base + row * 0x80, x, word);
3213 }
3214 }
3215 }
3216 // .next_b: 5 rows from obj1C22, two mirrored diagonals.
3217 for (int i = 0, x = anchor, y = 0x1C22; i < 5; ++i, x += 0x80, y += 2) {
3218 const uint16_t word = read_word(y);
3219 for (int base : {0x2504, 0x2486, 0x2408, 0x238A, 0x230C, 0x228E, 0x2210}) {
3220 store(base, x, word);
3221 }
3222 for (int base : {0x222E, 0x22B0, 0x2332, 0x23B4, 0x2436, 0x24B8, 0x253A}) {
3223 store(base, x, word | kHFlip);
3224 }
3225 }
3226 // .next_c: 6 rows from obj1C2C, left edge and mirrored right edge, three
3227 // copies 6 rows apart.
3228 for (int i = 0, x = anchor, y = 0x1C2C; i < 6; ++i, x += 0x80, y += 8) {
3229 for (int k = 0; k < 4; ++k) {
3230 const uint16_t word = read_word(y + k * 2);
3231 for (int base : {0x2584, 0x2884, 0x2B84}) {
3232 store(base + k * 2, x, word);
3233 }
3234 for (int base : {0x25BA, 0x28BA, 0x2BBA}) {
3235 store(base - k * 2, x, word | kHFlip);
3236 }
3237 }
3238 }
3239 // .next_d: 6 columns from obj1C5C, two rows, two copies.
3240 for (int i = 0, x = anchor, y = 0x1C5C; i < 6; ++i, x += 2, y += 2) {
3241 const uint16_t top = read_word(y);
3242 const uint16_t bottom = read_word(y + 12);
3243 store(0x2498, x, top);
3244 store(0x24A4, x, top);
3245 store(0x2518, x, bottom);
3246 store(0x2524, x, bottom);
3247 }
3248 // .next_e: 6 rows from obj1C74, two columns, two copies.
3249 for (int i = 0, x = anchor, y = 0x1C74; i < 6; ++i, x += 0x80, y += 4) {
3250 store(0x270E, x, read_word(y));
3251 store(0x2A0E, x, read_word(y));
3252 store(0x2710, x, read_word(y + 2));
3253 store(0x2A10, x, read_word(y + 2));
3254 }
3255 // .next_f: 5 columns x 5 rows from obj1C8C, column-major.
3256 for (int i = 0, x = anchor, y = 0x1C8C; i < 5; ++i, x += 2, y += 10) {
3257 for (int row = 0; row < 5; ++row) {
3258 store(0x248E + row * 0x80, x, read_word(y + row * 2));
3259 }
3260 }
3261 // .next_g: ORA #$2000 into four tiles of two rows.
3262 if (!trace_only_) {
3263 for (int i = 0, x = anchor; i < 4; ++i, x += 2) {
3264 for (int base : {0x2E1C, 0x2E9C}) {
3265 auto [tile_x, tile_y] = tile_at(base, x);
3266 if (tile_y < 64) {
3267 PromoteTilePriorityOnOwners(bg1, layout_bg1, tile_x, tile_y);
3268 } else if (tile_y < 128) {
3269 PromoteTilePriorityOnOwners(bg2, layout_bg2, tile_x, tile_y - 64);
3270 }
3271 }
3272 }
3273 }
3274 return absl::OkStatus();
3275}
3276
3278 gfx::BackgroundBuffer& target_bg,
3280 int data_pc) {
3281 // USDASM RoomDraw_SomeBigDecors ($01:9D9B area): 8 rows of 10 words,
3282 // row-major, stored at the object's position on the current list's layer.
3283 constexpr int kColumns = 10;
3284 constexpr int kRows = 8;
3285 const auto& rom_data = rom_->vector();
3286 if (data_pc < 0 ||
3287 data_pc + kColumns * kRows * 2 > static_cast<int>(rom_data.size())) {
3288 return absl::OutOfRangeError(
3289 absl::StrFormat("Big decor data out of range: 0x%X", data_pc));
3290 }
3291 SetTraceContext(object, layer);
3292 for (int row = 0; row < kRows; ++row) {
3293 for (int column = 0; column < kColumns; ++column) {
3294 const int address = data_pc + (row * kColumns + column) * 2;
3295 const uint16_t word = static_cast<uint16_t>(rom_data[address] |
3296 (rom_data[address + 1] << 8));
3297 WriteTile8(target_bg, object.x_ + column, object.y_ + row,
3298 gfx::WordToTileInfo(word));
3299 }
3300 }
3301 return absl::OkStatus();
3302}
3303
3305 gfx::BackgroundBuffer& bg2) {
3306 // USDASM RoomDraw_VitreousGooGraphics (bank_01 table entry 0x262): 22
3307 // columns of 11 rows from obj20F6 (11 words per column) stored with
3308 // STA.l $7E4000,X, i.e. always BG2, then a 3x2 block from obj22DA at
3309 // $4592/$4612 (column +9, rows +11 and +12).
3310 const auto& rom_data = rom_->vector();
3311 auto read_word = [&](int data_offset) -> uint16_t {
3312 const int address = kRoomObjectTileAddress + data_offset;
3313 if (address < 0 || address + 1 >= static_cast<int>(rom_data.size())) {
3314 return 0;
3315 }
3316 return static_cast<uint16_t>(rom_data[address] |
3317 (rom_data[address + 1] << 8));
3318 };
3320 for (int column = 0; column < 22; ++column) {
3321 for (int row = 0; row < 11; ++row) {
3322 WriteTile8(
3323 bg2, object.x_ + column, object.y_ + row,
3324 gfx::WordToTileInfo(read_word(0x20F6 + column * 22 + row * 2)));
3325 }
3326 }
3327 for (int column = 0; column < 3; ++column) {
3328 WriteTile8(bg2, object.x_ + 9 + column, object.y_ + 11,
3329 gfx::WordToTileInfo(read_word(0x22DA + column * 2)));
3330 WriteTile8(bg2, object.x_ + 9 + column, object.y_ + 12,
3331 gfx::WordToTileInfo(read_word(0x22DA + 6 + column * 2)));
3332 }
3333 return absl::OkStatus();
3334}
3335
3337 const DungeonState* state,
3338 gfx::BackgroundBuffer& bg1) {
3339 constexpr int kChestHoles0Tag = 0x22;
3340 constexpr int kChestHoles8Tag = 0x3B;
3341 constexpr int kOverlayDataPointers = 0x026CC0; // $04:ECC0, 3-byte pointers
3342 constexpr uint8_t kPitObjectId = 0xA4;
3343 if (state == nullptr || rom_ == nullptr || !state->IsChestOpen(room_id_, 0)) {
3344 return;
3345 }
3346 int overlay = -1;
3347 if (tag1 == kChestHoles0Tag || tag2 == kChestHoles0Tag) {
3348 overlay = 0x00;
3349 } else if (tag1 == kChestHoles8Tag || tag2 == kChestHoles8Tag) {
3350 overlay = 0x12;
3351 } else {
3352 return;
3353 }
3354
3355 const auto& rom_data = rom_->vector();
3356 auto read_byte = [&](int address) -> int {
3357 return address >= 0 && address < static_cast<int>(rom_data.size())
3358 ? rom_data[address]
3359 : -1;
3360 };
3361 auto read_word = [&](int data_offset) -> uint16_t {
3362 const int address = kRoomObjectTileAddress + data_offset;
3363 const int lo = read_byte(address);
3364 const int hi = read_byte(address + 1);
3365 return lo < 0 || hi < 0 ? 0 : static_cast<uint16_t>(lo | (hi << 8));
3366 };
3367 const int pointer = kOverlayDataPointers + (overlay * 3);
3368 const int b0 = read_byte(pointer);
3369 const int b1 = read_byte(pointer + 1);
3370 const int b2 = read_byte(pointer + 2);
3371 if (b0 < 0 || b1 < 0 || b2 < 0) {
3372 return;
3373 }
3374 int entry = static_cast<int>(SnesToPc((b2 << 16) | (b1 << 8) | b0));
3375
3376 // Underworld_DrawRoomOverlay .draw_hole: row 0 obj063C+2, rows 1-2
3377 // obj05AA+0, row 3 obj0642+2, each four tiles wide.
3378 const gfx::TileInfo row_tiles[4] = {
3379 gfx::WordToTileInfo(read_word(0x063C + 2)),
3380 gfx::WordToTileInfo(read_word(0x05AA)),
3381 gfx::WordToTileInfo(read_word(0x05AA)),
3382 gfx::WordToTileInfo(read_word(0x0642 + 2)),
3383 };
3384 for (;; entry += 3) {
3385 const int x_byte = read_byte(entry);
3386 const int y_byte = read_byte(entry + 1);
3387 const int id = read_byte(entry + 2);
3388 if (x_byte < 0 || y_byte < 0 || (x_byte == 0xFF && y_byte == 0xFF)) {
3389 return;
3390 }
3391 if (id != kPitObjectId) {
3392 continue; // .draw_solid_floor needs the runtime $046A floor pointer.
3393 }
3394 for (int row = 0; row < 4; ++row) {
3395 for (int col = 0; col < 4; ++col) {
3396 WriteTile8(bg1, (x_byte >> 2) + col, (y_byte >> 2) + row,
3397 row_tiles[row]);
3398 }
3399 }
3400 }
3401}
3402
3404 uint16_t object_id, int tile_x, int tile_y, RoomObject::LayerType layer,
3405 uint16_t room_draw_object_data_offset, gfx::BackgroundBuffer& bg1,
3406 gfx::BackgroundBuffer& bg2) {
3407 if (!rom_ || !rom_->is_loaded()) {
3408 return absl::FailedPreconditionError("ROM not loaded");
3409 }
3410
3411 const auto& rom_data = rom_->vector();
3412 const int base =
3413 kRoomObjectTileAddress + static_cast<int>(room_draw_object_data_offset);
3414 if (base < 0 || base + 7 >= static_cast<int>(rom_data.size())) {
3415 return absl::OutOfRangeError(absl::StrFormat(
3416 "RoomDrawObjectData 2x2 out of range: base=0x%X", base));
3417 }
3418
3419 auto read_word = [&](int off) -> uint16_t {
3420 return static_cast<uint16_t>(rom_data[off]) |
3421 (static_cast<uint16_t>(rom_data[off + 1]) << 8);
3422 };
3423
3424 const uint16_t w0 = read_word(base + 0);
3425 const uint16_t w1 = read_word(base + 2);
3426 const uint16_t w2 = read_word(base + 4);
3427 const uint16_t w3 = read_word(base + 6);
3428
3429 const gfx::TileInfo t0 = gfx::WordToTileInfo(w0);
3430 const gfx::TileInfo t1 = gfx::WordToTileInfo(w1);
3431 const gfx::TileInfo t2 = gfx::WordToTileInfo(w2);
3432 const gfx::TileInfo t3 = gfx::WordToTileInfo(w3);
3433
3434 // Set trace context once; WriteTile8 will emit per-tile traces.
3435 RoomObject trace_obj(
3436 static_cast<int16_t>(object_id), static_cast<uint8_t>(tile_x),
3437 static_cast<uint8_t>(tile_y), 0, static_cast<uint8_t>(layer));
3438 SetTraceContext(trace_obj, layer);
3439
3440 gfx::BackgroundBuffer& target_bg =
3441 (layer == RoomObject::LayerType::BG2) ? bg2 : bg1;
3442
3443 // Column-major order (matches USDASM $BF/$CB/$C2/$CE writes).
3444 WriteTile8(target_bg, tile_x + 0, tile_y + 0, t0); // top-left
3445 WriteTile8(target_bg, tile_x + 0, tile_y + 1, t1); // bottom-left
3446 WriteTile8(target_bg, tile_x + 1, tile_y + 0, t2); // top-right
3447 WriteTile8(target_bg, tile_x + 1, tile_y + 1, t3); // bottom-right
3448
3449 return absl::OkStatus();
3450}
3451
3452// ============================================================================
3453// Type 3 / Special Routine Implementations
3454// ============================================================================
3455
3458 std::span<const gfx::TileInfo> tiles,
3459 int width, int height) {
3460 // Generic large object drawer
3461 if (tiles.size() >= static_cast<size_t>(width * height)) {
3462 for (int y = 0; y < height; ++y) {
3463 for (int x = 0; x < width; ++x) {
3464 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[y * width + x]);
3465 }
3466 }
3467 }
3468}
3469
3470} // namespace zelda3
3471} // namespace yaze
3472
3474 const RoomObject& object) {
3476}
3477
3479 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3480 [[maybe_unused]] std::span<const gfx::TileInfo> tiles,
3481 [[maybe_unused]] const DungeonState* state) {
3482 const int subtype = obj.size_ & 0x1F;
3483 absl::StatusOr<std::shared_ptr<const CustomObject>> result =
3484 custom_objects_ != nullptr
3485 ? custom_objects_->GetObject(obj.id_, subtype)
3486 : absl::StatusOr<std::shared_ptr<const CustomObject>>(
3488 if (!result.ok()) {
3489 DrawMissingCustomObjectPlaceholder(bg, obj.x_, obj.y_);
3490 LOG_DEBUG("ObjectDrawer", "Custom object 0x%03X subtype %d not found: %s",
3491 obj.id_, subtype, result.status().message().data());
3492 return;
3493 }
3494
3495 auto custom_obj = result.value();
3496 if (!custom_obj || custom_obj->IsEmpty())
3497 return;
3498
3499 int tile_x = obj.x_;
3500 int tile_y = obj.y_;
3501
3502 for (const auto& entry : custom_obj->tiles) {
3503 // Oracle's custom-object handlers advance past a zero payload word without
3504 // storing it. Preserve the tile already underneath this object.
3505 if (entry.tile_data == 0) {
3506 continue;
3507 }
3508 // entry.tile_data is the raw vhopppcc cccccccc source word. Resolve the
3509 // effective runtime word here so source editing remains lossless.
3510 const uint16_t runtime_word =
3511 CustomObjectRuntimeTileWord(obj.id_, entry.tile_data);
3512 // Convert to TileInfo and render using WriteTile8 (not SetTileAt which
3513 // only stores to buffer without rendering)
3514 gfx::TileInfo tile_info = gfx::WordToTileInfo(runtime_word);
3515 WriteTile8(bg, tile_x + entry.rel_x, tile_y + entry.rel_y, tile_info);
3516 }
3517}
3518
3520 gfx::BackgroundBuffer& bg, int tile_x, int tile_y) {
3521 if (trace_only_) {
3522 return;
3523 }
3524
3525 auto& bitmap = bg.bitmap();
3526 if (!bitmap.is_active() || bitmap.width() <= 0 || bitmap.height() <= 0) {
3527 return;
3528 }
3529
3530 auto& pixels = bitmap.mutable_data();
3531 auto& coverage = bg.mutable_coverage_data();
3532 auto& priority = bg.mutable_priority_data();
3533
3534 constexpr int kPlaceholderSizePx = 16;
3535 constexpr uint8_t kFillColor = 33;
3536 constexpr uint8_t kAccentColor = 47;
3537
3538 const int start_x = tile_x * 8;
3539 const int start_y = tile_y * 8;
3540 const int width = bitmap.width();
3541 const int height = bitmap.height();
3542
3543 bg.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, start_x, start_y,
3544 kPlaceholderSizePx, kPlaceholderSizePx);
3545
3546 for (int py = 0; py < kPlaceholderSizePx; ++py) {
3547 const int dest_y = start_y + py;
3548 if (dest_y < 0 || dest_y >= height)
3549 continue;
3550 for (int px = 0; px < kPlaceholderSizePx; ++px) {
3551 const int dest_x = start_x + px;
3552 if (dest_x < 0 || dest_x >= width)
3553 continue;
3554 const bool border = (px == 0 || py == 0 || px == kPlaceholderSizePx - 1 ||
3555 py == kPlaceholderSizePx - 1);
3556 const bool diagonal = (px == py) || (px + py == kPlaceholderSizePx - 1);
3557 const int dest_index = dest_y * width + dest_x;
3558 pixels[dest_index] = (border || diagonal) ? kAccentColor : kFillColor;
3559 if (dest_index < static_cast<int>(coverage.size()))
3560 coverage[dest_index] = 1;
3561 if (dest_index < static_cast<int>(priority.size()))
3562 priority[dest_index] = 0;
3563 }
3564 }
3565 bitmap.set_modified(true);
3566}
3567
3568void yaze::zelda3::ObjectDrawer::DrawPotItem(uint8_t item_id, int x, int y,
3570 // Draw a small colored indicator for pot items
3571 // Item types from ZELDA3_DUNGEON_SPEC.md Section 7.2
3572 // Uses palette indices that map to recognizable colors
3573
3574 if (item_id == 0)
3575 return; // Nothing - skip
3576
3577 auto& bitmap = bg.bitmap();
3578 auto& coverage_buffer = bg.mutable_coverage_data();
3579 if (!bitmap.is_active() || bitmap.width() == 0)
3580 return;
3581
3582 // Convert tile coordinates to pixel coordinates
3583 // Items are drawn offset from pot position (centered on pot)
3584 int pixel_x = (x * 8) + 2; // Offset 2 pixels into the pot tile
3585 int pixel_y = (y * 8) + 2;
3586
3587 // Choose color based on item category
3588 // Using palette indices that should be visible in dungeon palettes
3589 uint8_t color_idx;
3590 switch (item_id) {
3591 // Rupees (green/blue/red tones)
3592 case 1: // Green rupee
3593 case 7: // Blue rupee
3594 case 12: // Blue rupee variant
3595 color_idx = 30; // Greenish (palette 2, index 0)
3596 break;
3597
3598 // Hearts (red tones)
3599 case 6: // Heart
3600 case 11: // Heart
3601 case 13: // Heart variant
3602 // NOTE: Avoid palette indices 0/16/32/.. which are transparent in SNES
3603 // CGRAM rows. Using 5 gives a consistently visible indicator.
3604 color_idx = 5;
3605 break;
3606
3607 // Keys (yellow/gold)
3608 case 8: // Key*8
3609 case 19: // Key
3610 color_idx = 45; // Yellowish (palette 3)
3611 break;
3612
3613 // Bombs (dark/black)
3614 case 5: // Bomb
3615 case 10: // 1 bomb
3616 case 16: // Bomb refill
3617 color_idx = 60; // Darker color (palette 4)
3618 break;
3619
3620 // Arrows (brown/wood)
3621 case 9: // Arrow
3622 case 17: // Arrow refill
3623 color_idx = 15; // Brownish (palette 1)
3624 break;
3625
3626 // Magic (blue/purple)
3627 case 14: // Small magic
3628 case 15: // Big magic
3629 color_idx = 75; // Bluish (palette 5)
3630 break;
3631
3632 // Fairy (pink/light)
3633 case 18: // Fairy
3634 case 20: // Fairy*8
3635 color_idx = 5; // Pinkish
3636 break;
3637
3638 // Special/Traps (distinct colors)
3639 case 2: // Rock crab
3640 case 3: // Bee
3641 color_idx = 20; // Enemy indicator
3642 break;
3643
3644 case 23: // Hole
3645 case 24: // Warp
3646 case 25: // Staircase
3647 color_idx = 10; // Transport indicator
3648 break;
3649
3650 case 26: // Bombable
3651 case 27: // Switch
3652 color_idx = 35; // Interactive indicator
3653 break;
3654
3655 case 4: // Random
3656 default:
3657 color_idx = 50; // Default/random indicator
3658 break;
3659 }
3660
3661 // Safety: never use CGRAM transparent slots (0,16,32,...) for the indicator.
3662 // In the editor these would appear invisible or "missing" depending on
3663 // compositing.
3664 if (color_idx != 255 && (color_idx % 16) == 0) {
3665 color_idx++;
3666 }
3667
3668 // Draw a 4x4 colored square as item indicator
3669 int bitmap_width = bitmap.width();
3670 int bitmap_height = bitmap.height();
3671
3672 bg.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, pixel_x, pixel_y, 4, 4);
3673
3674 for (int py = 0; py < 4; py++) {
3675 for (int px = 0; px < 4; px++) {
3676 int dest_x = pixel_x + px;
3677 int dest_y = pixel_y + py;
3678
3679 // Bounds check
3680 if (dest_x >= 0 && dest_x < bitmap_width && dest_y >= 0 &&
3681 dest_y < bitmap_height) {
3682 int offset = (dest_y * bitmap_width) + dest_x;
3683 bitmap.WriteToPixel(offset, color_idx);
3684 if (offset >= 0 && offset < static_cast<int>(coverage_buffer.size())) {
3685 coverage_buffer[offset] = 1;
3686 }
3687 }
3688 }
3689 }
3690}
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:49
const auto & vector() const
Definition rom.h:205
auto data() const
Definition rom.h:201
auto size() const
Definition rom.h:200
bool is_loaded() const
Definition rom.h:187
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)
uint16_t GetTileAt(int x, int y) const
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:70
const uint8_t * data() const
Definition bitmap.h:407
auto size() const
Definition bitmap.h:406
bool is_active() const
Definition bitmap.h:414
void set_modified(bool modified)
Definition bitmap.h:418
int height() const
Definition bitmap.h:399
int width() const
Definition bitmap.h:398
int depth() const
Definition bitmap.h:400
std::vector< uint8_t > & mutable_data()
Definition bitmap.h:408
SDL_Surface * surface() const
Definition bitmap.h:409
bool modified() const
Definition bitmap.h:413
SNES 16-bit tile metadata container.
Definition snes_tile.h:52
static CustomObjectManager & Get()
absl::StatusOr< std::shared_ptr< CustomObject > > GetObjectInternal(int object_id, int subtype)
absl::StatusOr< std::shared_ptr< const DungeonTilePatterns > > GetTilePatterns()
absl::StatusOr< std::shared_ptr< const DungeonTilePatterns > > tile_patterns() const
absl::StatusOr< std::shared_ptr< const CustomObject > > GetObject(int object_id, int subtype) const
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
static DrawRoutineRegistry & Get()
Interface for accessing dungeon game state.
virtual bool IsChestOpen(int room_id, int chest_index) const =0
virtual bool IsDoorSwitchActive(int room_id) const =0
virtual bool IsDoorOpen(int room_id, int door_index) const =0
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)
absl::Status DrawVitreousGoo(const RoomObject &object, gfx::BackgroundBuffer &bg2)
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)
absl::Status DrawBigDecor10x8(const RoomObject &object, gfx::BackgroundBuffer &target_bg, RoomObject::LayerType layer, int data_pc)
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 DrawChestHoleOverlay(int tag1, int tag2, const DungeonState *state, gfx::BackgroundBuffer &bg1)
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)
const CustomObjectRenderSnapshot * custom_objects_
void DrawRightwardsDecor4x3spaced4_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
absl::Status DrawLayerMaskFull(const RoomObject &object, gfx::BackgroundBuffer &target_bg, RoomObject::LayerType layer)
gfx::BackgroundBuffer * registry_bg2_
absl::Status DrawAgahnimsWindows(const RoomObject &object, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2, gfx::BackgroundBuffer *layout_bg1, gfx::BackgroundBuffer *layout_bg2)
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, const CustomObjectRenderSnapshot *custom_objects=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_
absl::Status DrawLampCones(const RoomObject &object, gfx::BackgroundBuffer &bg2)
RoomObject::LayerType registry_secondary_layer_
Direct ROM parser for dungeon objects.
absl::StatusOr< std::vector< gfx::TileInfo > > ParseObject(int16_t object_id)
Parse object data directly from ROM.
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:133
#define LOG_WARN(category, format,...)
Definition log.h:138
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:362
TileInfo WordToTileInfo(uint16_t word)
Definition snes_tile.cc:379
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 PromoteTileWordPriority(gfx::BackgroundBuffer &target, 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
constexpr int kRoomObjectSubtype3
Definition room_object.h:47
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
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
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.
static constexpr int kMaxTilesY
gfx::BackgroundBuffer & target_bg
static constexpr int kMaxTilesX
Metadata about a draw routine.
std::pair< int, int > GetTileCoords() const
DoorDimensions GetDimensions() const