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 <cstdio>
4#include <cstring>
5#include <filesystem>
6
7#include "absl/strings/str_format.h"
9#include "core/features.h"
10#include "rom/rom.h"
11#include "rom/snes.h"
12#include "util/log.h"
19
20namespace yaze {
21namespace zelda3 {
22namespace {
23
24bool LockSurface(SDL_Surface* surface) {
25#if SDL_MAJOR_VERSION >= 3
26 return SDL_LockSurface(surface);
27#else
28 return SDL_LockSurface(surface) == 0;
29#endif
30}
31
32void SyncModifiedBitmapToSurface(gfx::Bitmap& bitmap, const char* layer_name) {
33 SDL_Surface* surface = bitmap.surface();
34 if (!bitmap.modified() || surface == nullptr || bitmap.size() == 0) {
35 return;
36 }
37
38 if (bitmap.depth() != 8) {
39 LOG_DEBUG("ObjectDrawer", "%s bitmap depth is not indexed 8bpp: %d",
40 layer_name, bitmap.depth());
41 return;
42 }
43
44 const int width = bitmap.width();
45 const int height = bitmap.height();
46 if (width <= 0 || height <= 0 || surface->w < width || surface->h < height ||
47 surface->pitch < width) {
48 LOG_DEBUG("ObjectDrawer",
49 "%s surface dimensions cannot hold bitmap: surface=%dx%d "
50 "pitch=%d bitmap=%dx%d",
51 layer_name, surface->w, surface->h, surface->pitch, width,
52 height);
53 return;
54 }
55
56 const size_t row_bytes = static_cast<size_t>(width);
57 const size_t required_bytes = row_bytes * static_cast<size_t>(height);
58 if (bitmap.size() < required_bytes) {
59 LOG_DEBUG("ObjectDrawer", "%s bitmap data too small: data=%zu needed=%zu",
60 layer_name, bitmap.size(), required_bytes);
61 return;
62 }
63
64 if (!LockSurface(surface)) {
65 LOG_DEBUG("ObjectDrawer", "%s surface lock failed: %s", layer_name,
66 SDL_GetError());
67 return;
68 }
69 auto* destination = static_cast<uint8_t*>(surface->pixels);
70 const uint8_t* source = bitmap.data();
71 for (int y = 0; y < height; ++y) {
72 std::memcpy(destination + static_cast<size_t>(y) *
73 static_cast<size_t>(surface->pitch),
74 source + static_cast<size_t>(y) * row_bytes, row_bytes);
75 }
76 SDL_UnlockSurface(surface);
77}
78
79} // namespace
80
82 const uint8_t* room_gfx_buffer)
83 : rom_(rom), room_id_(room_id), room_gfx_buffer_(room_gfx_buffer) {
85}
86
87void ObjectDrawer::SetTraceCollector(std::vector<TileTrace>* collector,
88 bool trace_only) {
89 trace_collector_ = collector;
90 trace_only_ = trace_only;
91}
92
94 trace_collector_ = nullptr;
95 trace_only_ = false;
96}
97
100 trace_context_.object_id = static_cast<uint16_t>(object.id_);
101 trace_context_.size = object.size_;
102 trace_context_.layer = static_cast<uint8_t>(layer);
103}
104
105void ObjectDrawer::PushTrace(int tile_x, int tile_y,
106 const gfx::TileInfo& tile_info) {
107 if (!trace_collector_) {
108 return;
109 }
110 uint8_t flags = 0;
111 if (tile_info.horizontal_mirror_)
112 flags |= 0x1;
113 if (tile_info.vertical_mirror_)
114 flags |= 0x2;
115 if (tile_info.over_)
116 flags |= 0x4;
117 flags |= static_cast<uint8_t>((tile_info.palette_ & 0x7) << 3);
118
119 TileTrace trace{};
121 trace.size = trace_context_.size;
122 trace.layer = trace_context_.layer;
123 trace.x_tile = static_cast<int16_t>(tile_x);
124 trace.y_tile = static_cast<int16_t>(tile_y);
125 trace.tile_id = tile_info.id_;
126 trace.flags = flags;
127 trace_collector_->push_back(trace);
128}
129
131 int tile_y, const gfx::TileInfo& tile_info,
132 void* user_data) {
133 auto* drawer = static_cast<ObjectDrawer*>(user_data);
134 if (!drawer) {
135 return;
136 }
137 drawer->PushTrace(tile_x, tile_y, tile_info);
138}
139
141 int routine_id, const RoomObject& obj, gfx::BackgroundBuffer& bg,
142 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
143 // Many DrawRoutineRegistry routines are implemented as pure functions that
144 // call DrawRoutineUtils::WriteTile8(), which only writes to BackgroundBuffer's
145 // tile buffer (not the bitmap). Runtime rendering/compositing uses the
146 // bitmap-backed buffers, so we capture tile writes from the pure routine and
147 // replay them via ObjectDrawer::WriteTile8().
148 const auto* info = DrawRoutineRegistry::Get().GetRoutineInfo(routine_id);
149 if (info == nullptr) {
150 LOG_DEBUG("ObjectDrawer", "DrawUsingRegistryRoutine: unknown routine %d",
151 routine_id);
152 return;
153 }
154
155 struct CapturedWrite {
156 int x = 0;
157 int y = 0;
158 gfx::TileInfo tile{};
159 bool secondary = false;
160 };
161
162 struct CaptureState {
163 std::vector<CapturedWrite>* writes = nullptr;
164 gfx::BackgroundBuffer* secondary_bg = nullptr;
165 };
166
167 std::vector<CapturedWrite> writes;
168 writes.reserve(256);
169 CaptureState capture_state{.writes = &writes,
170 .secondary_bg = registry_secondary_bg_};
171
173 [](gfx::BackgroundBuffer* target_bg, int tile_x, int tile_y,
174 const gfx::TileInfo& tile_info, void* user_data) {
175 auto* capture = static_cast<CaptureState*>(user_data);
176 if (!capture || !capture->writes) {
177 return;
178 }
179 capture->writes->push_back(CapturedWrite{
180 .x = tile_x,
181 .y = tile_y,
182 .tile = tile_info,
183 .secondary =
184 target_bg != nullptr && target_bg == capture->secondary_bg,
185 });
186 },
187 &capture_state,
188 /*trace_only=*/true);
189
190 DrawContext ctx{
191 .target_bg = bg,
192 .object = obj,
193 .tiles = tiles,
194 .state = state,
195 .rom = rom_,
196 .room_id = room_id_,
197 .room_gfx_buffer = room_gfx_buffer_,
198 .secondary_bg = registry_secondary_bg_,
199 };
200 info->function(ctx);
201
203
204 for (const auto& w : writes) {
205 if (w.secondary && registry_secondary_bg_ != nullptr) {
207 WriteTile8(*registry_secondary_bg_, w.x, w.y, w.tile);
208 continue;
209 }
211 WriteTile8(bg, w.x, w.y, w.tile);
212 }
213}
214
216 const RoomObject& object, gfx::BackgroundBuffer& bg1,
217 gfx::BackgroundBuffer& bg2, const gfx::PaletteGroup& palette_group,
218 [[maybe_unused]] const DungeonState* state,
219 gfx::BackgroundBuffer* layout_bg1) {
220 if (!rom_ || !rom_->is_loaded()) {
221 return absl::FailedPreconditionError("ROM not loaded");
222 }
223
225 return absl::FailedPreconditionError("Draw routines not initialized");
226 }
227
228 // Ensure object has tiles loaded
229 auto mutable_obj = const_cast<RoomObject&>(object);
230 mutable_obj.SetRom(rom_);
231 mutable_obj.EnsureTilesLoaded();
232
233 // Select buffer based on layer
234 // Layer 0 (BG1): Main objects - drawn to BG1_Objects (on top of layout)
235 // Layer 1 (BG2): Overlay objects - drawn to BG2_Objects (behind layout)
236 // Layer 2 (BG3): Priority objects (torches) - drawn to BG1_Objects (on top)
237 bool use_bg2 = (object.layer_ == RoomObject::LayerType::BG2);
238 auto& target_bg = use_bg2 ? bg2 : bg1;
239 auto& other_bg = use_bg2 ? bg1 : bg2;
240
241 // Log buffer selection for debugging layer routing
242 LOG_DEBUG("ObjectDrawer", "Object 0x%03X layer=%d -> drawing to %s buffer",
243 object.id_, static_cast<int>(object.layer_),
244 use_bg2 ? "BG2 (behind layout)" : "BG1 (on top of layout)");
245
246 // Check for custom object override first (guarded by feature flag).
247 // We check this BEFORE routine lookup to allow overriding vanilla objects.
248 int subtype = object.size_ & 0x1F;
249 bool is_custom_object = false;
250 const bool is_track_corner_alias = object.id_ >= 0x100 && object.id_ <= 0x103;
251 const bool allow_custom_override =
252 !is_track_corner_alias || this->allow_track_corner_aliases_;
253 if (core::FeatureFlags::get().kEnableCustomObjects && allow_custom_override &&
254 CustomObjectManager::Get().GetObjectInternal(object.id_, subtype).ok()) {
255 is_custom_object = true;
256 // Custom objects default to drawing on the target layer only, unless all_bgs_ is set
257 // Mask propagation is difficult without dimensions, so we rely on explicit transparency in the custom object tiles if needed
258
259 // Draw to target layer
262 DrawCustomObject(object, target_bg, mutable_obj.tiles(), state);
263
264 // If marked for both BGs, draw to the other layer too
265 if (object.all_bgs_) {
266 SetTraceContext(object, (&other_bg == &bg1) ? RoomObject::LayerType::BG1
268 DrawCustomObject(object, other_bg, mutable_obj.tiles(), state);
269 }
270 return absl::OkStatus();
271 }
272
273 // Skip objects that don't have tiles loaded
274 if (!is_custom_object && mutable_obj.tiles().empty()) {
275 LOG_DEBUG("ObjectDrawer",
276 "Object 0x%03X at (%d,%d) has NO TILES - skipping", object.id_,
277 object.x_, object.y_);
278 return absl::OkStatus();
279 }
280
281 // Look up draw routine for this object
282 int routine_id = GetDrawRoutineId(object.id_);
283
284 // Log draw routine lookup with tile info
285 LOG_DEBUG("ObjectDrawer",
286 "Object 0x%03X at (%d,%d) size=%d -> routine=%d tiles=%zu",
287 object.id_, object.x_, object.y_, object.size_, routine_id,
288 mutable_obj.tiles().size());
289
290 if (routine_id < 0 || routine_id >= static_cast<int>(draw_routines_.size())) {
291 LOG_DEBUG("ObjectDrawer",
292 "Object 0x%03X: NO ROUTINE (id=%d, max=%zu) - using fallback 1x1",
293 object.id_, routine_id, draw_routines_.size());
294 // Fallback to simple 1x1 drawing using first 8x8 tile
295 if (!mutable_obj.tiles().empty()) {
296 const auto& tile_info = mutable_obj.tiles()[0];
299 WriteTile8(target_bg, object.x_, object.y_, tile_info);
300 }
301 return absl::OkStatus();
302 }
303
304 // Null-tile guard: skip routines whose tile payload is too small.
305 // Hack ROMs with abbreviated tile tables would otherwise cause
306 // out-of-bounds access in fixed-size draw patterns.
307 const DrawRoutineInfo* routine_info =
309 if (routine_info && routine_info->min_tiles > 0 &&
310 static_cast<int>(mutable_obj.tiles().size()) < routine_info->min_tiles) {
311 LOG_WARN("ObjectDrawer",
312 "Object 0x%03X at (%d,%d): tile payload too small "
313 "(%zu < %d required by routine '%s') - skipping",
314 object.id_, object.x_, object.y_, mutable_obj.tiles().size(),
315 routine_info->min_tiles, routine_info->name.c_str());
316 // Fall through to 1x1 fallback if any tiles are present
317 if (!mutable_obj.tiles().empty()) {
318 const auto& tile_info = mutable_obj.tiles()[0];
321 WriteTile8(target_bg, object.x_, object.y_, tile_info);
322 }
323 return absl::OkStatus();
324 }
325
326 bool trace_hook_active = false;
327 if (trace_collector_) {
330 trace_hook_active = true;
331 }
332
333 // Check if this should draw to both BG layers.
334 // In the original engine, BothBG routines explicitly write to both tilemaps
335 // regardless of which object list or pass they are executed from.
336 bool is_both_bg = (object.all_bgs_ || RoutineDrawsToBothBGs(routine_id));
337 const bool use_rectangular_bg1_mask =
338 !trace_only_ && object.layer_ == RoomObject::LayerType::BG2 &&
339 !is_both_bg && RequiresRectangularBg1Mask(object);
340 const bool use_pixel_bg1_mask = !trace_only_ &&
341 object.layer_ == RoomObject::LayerType::BG2 &&
342 !is_both_bg && !use_rectangular_bg1_mask;
343
344 registry_secondary_bg_ = nullptr;
349 gfx::BackgroundBuffer* dispatch_bg = &target_bg;
350
351 // Special routines may need a second buffer for mixed-layer draws or fixed
352 // BG1/BG2 routing regardless of the parsed object-layer flag.
353 if (!is_both_bg && (routine_id == DrawRoutineIds::kAgahnimsAltar ||
354 routine_id == DrawRoutineIds::kFortuneTellerRoom)) {
355 // USDASM writes these fixed room facades directly to the upper tilemap at
356 // $7E2000.
357 dispatch_bg = &bg1;
359 } else if (!is_both_bg && routine_id == DrawRoutineIds::kAutoStairs) {
360 registry_secondary_bg_ = &other_bg;
361 } else if (!is_both_bg &&
367 dispatch_bg = &bg1;
371 }
372
373 if (use_pixel_bg1_mask) {
375 active_layout_bg1_mask_ = layout_bg1;
377 }
378
379 if (is_both_bg) {
380 // Draw to both background layers
381 registry_secondary_bg_ = nullptr;
384 draw_routines_[routine_id](this, object, bg1, mutable_obj.tiles(), state);
387 draw_routines_[routine_id](this, object, bg2, mutable_obj.tiles(), state);
388 } else {
389 // Execute the appropriate draw routine on target buffer only
391 draw_routines_[routine_id](this, object, *dispatch_bg, mutable_obj.tiles(),
392 state);
393 }
394
395 if (trace_hook_active) {
397 }
398
399 active_object_bg1_mask_ = nullptr;
400 active_layout_bg1_mask_ = nullptr;
401 active_mask_source_bg_ = nullptr;
402 registry_secondary_bg_ = nullptr;
403
404 // BG2 mask propagation is deferred to compositing so raw BG1 stays intact.
405 //
406 // Ordinary BG2 overlay objects now mask per-pixel as they draw, which keeps
407 // transparent cutouts intact for platforms/statues/stairs. Full-rect masking
408 // remains only for true pit/ceiling mask families that intentionally clear an
409 // area larger than their opaque tile pixels.
410 if (use_rectangular_bg1_mask) {
411 // Route through DimensionService so the mask rect comes from the same
412 // source as selection bounds (ObjectGeometry if available, then
413 // ObjectDimensionTable, then the size-nibble fallback). Keeps the
414 // transparent cutout aligned with what the user sees in the editor.
416 const auto [mask_px_x, mask_px_y, pixel_width, pixel_height] =
418
419 LOG_DEBUG("ObjectDrawer",
420 "Pit mask 0x%03X at (%d,%d) -> recording %dx%d BG1 reveal pixels",
421 object.id_, mask_px_x / 8, mask_px_y / 8, pixel_width,
422 pixel_height);
423
424 MarkBg1RectRevealed(bg1, mask_px_x, mask_px_y, pixel_width, pixel_height);
425 if (layout_bg1 != nullptr) {
426 MarkBg1RectRevealed(*layout_bg1, mask_px_x, mask_px_y, pixel_width,
427 pixel_height);
428 }
429 }
430
431 return absl::OkStatus();
432}
433
435 return (object.id_ == 0xA4) || // Pit
436 (object.id_ >= 0xA5 && object.id_ <= 0xA8) || // Diagonal masks A
437 (object.id_ == 0xC0) || // Large ceiling overlay
438 (object.id_ == 0xC2) || // Layer 2 pit mask
439 (object.id_ == 0xC3) || // Layer 2 pit mask
440 (object.id_ == 0xC6) || // Layer 2 mask
441 (object.id_ == 0xC8) || // Water floor overlay
442 (object.id_ == 0xD7) || // Layer 2 mask
443 (object.id_ == 0xD8) || // Flood water overlay
444 (object.id_ == 0xD9) || // Layer 2 swim mask
445 (object.id_ == 0xDA) || // Flood water overlay B
446 (object.id_ == 0xFE6) || // Type 3 pit
447 (object.id_ == 0xFF3); // Type 3 full mask
448}
449
451 const std::vector<RoomObject>& objects, gfx::BackgroundBuffer& bg1,
452 gfx::BackgroundBuffer& bg2, const gfx::PaletteGroup& palette_group,
453 [[maybe_unused]] const DungeonState* state,
454 gfx::BackgroundBuffer* layout_bg1, bool reset_room_event_indices) {
455 if (reset_room_event_indices) {
457 }
458 absl::Status status = absl::OkStatus();
459
460 // DEBUG: Count objects routed to each buffer
461 int to_bg1 = 0, to_bg2 = 0, both_bgs = 0;
462
463 for (const auto& object : objects) {
464 // Track buffer routing for summary
465 bool use_bg2 = (object.layer_ == RoomObject::LayerType::BG2);
466 int routine_id = GetDrawRoutineId(object.id_);
467 bool is_both_bg = (object.all_bgs_ || RoutineDrawsToBothBGs(routine_id));
468
469 if (is_both_bg) {
470 both_bgs++;
471 } else if (use_bg2) {
472 to_bg2++;
473 } else {
474 to_bg1++;
475 }
476
477 auto s = DrawObject(object, bg1, bg2, palette_group, state, layout_bg1);
478 if (!s.ok() && status.ok()) {
479 status = s;
480 }
481 }
482
483 LOG_DEBUG("ObjectDrawer", "Buffer routing: to_BG1=%d, to_BG2=%d, BothBGs=%d",
484 to_bg1, to_bg2, both_bgs);
485
486 // The palette is already applied by Room::RenderRoomGraphics(). SDL can pad
487 // indexed surface rows, so synchronize each row using the surface pitch.
488 SyncModifiedBitmapToSurface(bg1.bitmap(), "BG1");
489 SyncModifiedBitmapToSurface(bg2.bitmap(), "BG2");
490
491 return status;
492}
493
494// ============================================================================
495// Metadata-based BothBG Detection
496// ============================================================================
497
499 // Use DrawRoutineRegistry as the single source of truth for BothBG metadata.
501}
502
503// ============================================================================
504// Draw Routine Registry Initialization
505// ============================================================================
506
508 // This function maps object IDs to their corresponding draw routines.
509 // The mapping is based on ZScream's DungeonObjectData.cs and the game's
510 // assembly code. The order of functions in draw_routines_ MUST match the
511 // indices used here.
512 //
513 // ASM Reference (Bank 01):
514 // Subtype 1 Data Offset: $018000 (DrawObjects.type1_subtype_1_data_offset)
515 // Subtype 1 Routine Ptr: $018200 (DrawObjects.type1_subtype_1_routine)
516 // Subtype 2 Data Offset: $0183F0 (DrawObjects.type1_subtype_2_data_offset)
517 // Subtype 2 Routine Ptr: $018470 (DrawObjects.type1_subtype_2_routine)
518 // Subtype 3 Data Offset: $0184F0 (DrawObjects.type1_subtype_3_data_offset)
519 // Subtype 3 Routine Ptr: $0185F0 (DrawObjects.type1_subtype_3_routine)
520
521 draw_routines_.clear();
522
523 // Object-to-routine mapping now lives in DrawRoutineRegistry::BuildObjectMapping().
524 // ObjectDrawer::GetDrawRoutineId() delegates to the registry singleton.
525 // Initialize draw routine function array in the correct order
526 // Routines 0-82 (existing), 80-98 (new special routines for stairs, locks, etc.)
527 draw_routines_.reserve(100);
528
529 // Routine 0
530 draw_routines_.push_back(
531 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
532 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
533 self->DrawUsingRegistryRoutine(0, obj, bg, tiles, state);
534 });
535 // Routine 1
536 draw_routines_.push_back(
537 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
538 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
539 self->DrawUsingRegistryRoutine(1, obj, bg, tiles, state);
540 });
541 // Routine 2 - 2x4 tiles with adjacent spacing (s * 2), count = size + 1
542 draw_routines_.push_back(
543 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
544 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
545 self->DrawUsingRegistryRoutine(2, obj, bg, tiles, state);
546 });
547 // Routine 3 - Same as routine 2 but draws to both BG1 and BG2
548 draw_routines_.push_back(
549 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
550 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
551 self->DrawUsingRegistryRoutine(3, obj, bg, tiles, state);
552 });
553 // Routine 4
554 draw_routines_.push_back(
555 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
556 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
557 self->DrawUsingRegistryRoutine(4, obj, bg, tiles, state);
558 });
559 // Routine 5
560 draw_routines_.push_back(
561 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
562 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
563 self->DrawUsingRegistryRoutine(5, obj, bg, tiles, state);
564 });
565 // Routine 6
566 draw_routines_.push_back(
567 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
568 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
569 self->DrawUsingRegistryRoutine(6, obj, bg, tiles, state);
570 });
571 // Routine 7
572 draw_routines_.push_back(
573 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
574 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
575 self->DrawUsingRegistryRoutine(7, obj, bg, tiles, state);
576 });
577 // Routine 8
578 draw_routines_.push_back(
579 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
580 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
581 self->DrawUsingRegistryRoutine(8, obj, bg, tiles, state);
582 });
583 // Routine 9
584 draw_routines_.push_back(
585 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
586 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
587 self->DrawUsingRegistryRoutine(9, obj, bg, tiles, state);
588 });
589 // Routine 10
590 draw_routines_.push_back(
591 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
592 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
593 self->DrawUsingRegistryRoutine(10, obj, bg, tiles, state);
594 });
595 // Routine 11
596 draw_routines_.push_back(
597 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
598 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
599 self->DrawUsingRegistryRoutine(11, obj, bg, tiles, state);
600 });
601 // Routine 12
602 draw_routines_.push_back(
603 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
604 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
605 self->DrawUsingRegistryRoutine(12, obj, bg, tiles, state);
606 });
607 // Routine 13
608 draw_routines_.push_back(
609 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
610 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
611 self->DrawUsingRegistryRoutine(13, obj, bg, tiles, state);
612 });
613 // Routine 14
614 draw_routines_.push_back(
615 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
616 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
617 self->DrawUsingRegistryRoutine(14, obj, bg, tiles, state);
618 });
619 // Routine 15
620 draw_routines_.push_back(
621 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
622 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
623 self->DrawUsingRegistryRoutine(15, obj, bg, tiles, state);
624 });
625 // Routine 16
626 draw_routines_.push_back(
627 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
628 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
629 self->DrawUsingRegistryRoutine(16, obj, bg, tiles, state);
630 });
631 // Routine 17 - Diagonal Acute BothBG
632 draw_routines_.push_back(
633 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
634 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
635 self->DrawUsingRegistryRoutine(17, obj, bg, tiles, state);
636 });
637 // Routine 18 - Diagonal Grave BothBG
638 draw_routines_.push_back(
639 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
640 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
641 self->DrawUsingRegistryRoutine(18, obj, bg, tiles, state);
642 });
643 // Routine 19 - 4x4 Corner (Type 2 corners)
644 draw_routines_.push_back(
645 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
646 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
647 self->DrawUsingRegistryRoutine(19, obj, bg, tiles, state);
648 });
649
650 // Routine 20 - Edge objects 1x2 +2
651 draw_routines_.push_back(
652 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
653 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
654 self->DrawUsingRegistryRoutine(20, obj, bg, tiles, state);
655 });
656 // Routine 21 - Edge with perimeter 1x1 +3
657 draw_routines_.push_back(
658 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
659 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
660 self->DrawUsingRegistryRoutine(21, obj, bg, tiles, state);
661 });
662 // Routine 22 - Edge variant 1x1 +2
663 draw_routines_.push_back(
664 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
665 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
666 self->DrawUsingRegistryRoutine(22, obj, bg, tiles, state);
667 });
668 // Routine 23 - Top corners 1x2 +13
669 draw_routines_.push_back(
670 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
671 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
672 self->DrawUsingRegistryRoutine(23, obj, bg, tiles, state);
673 });
674 // Routine 24 - Bottom corners 1x2 +13
675 draw_routines_.push_back(
676 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
677 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
678 self->DrawUsingRegistryRoutine(24, obj, bg, tiles, state);
679 });
680 // Routine 25 - Solid fill 1x1 +3 (floor patterns)
681 draw_routines_.push_back(
682 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
683 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
684 self->DrawUsingRegistryRoutine(25, obj, bg, tiles, state);
685 });
686 // Routine 26 - Door switcherer
687 draw_routines_.push_back(
688 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
689 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
690 self->DrawUsingRegistryRoutine(26, obj, bg, tiles, state);
691 });
692 // Routine 27 - Decorations 4x4 spaced 2
693 draw_routines_.push_back(
694 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
695 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
696 self->DrawUsingRegistryRoutine(27, obj, bg, tiles, state);
697 });
698 // Routine 28 - Statues 2x3 spaced 2
699 draw_routines_.push_back(
700 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
701 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
702 self->DrawUsingRegistryRoutine(28, obj, bg, tiles, state);
703 });
704 // Routine 29 - Pillars 2x4 spaced 4
705 draw_routines_.push_back(
706 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
707 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
708 self->DrawUsingRegistryRoutine(29, obj, bg, tiles, state);
709 });
710 // Routine 30 - Decorations 4x3 spaced 4
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(30, obj, bg, tiles, state);
715 });
716 // Routine 31 - Doubled 2x2 spaced 2
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(31, obj, bg, tiles, state);
721 });
722 // Routine 32 - Decorations 2x2 spaced 12
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(32, obj, bg, tiles, state);
727 });
728 // Routine 33 - Somaria Line
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(33, obj, bg, tiles, state);
733 });
734 // Routine 34 - Water Face
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(34, obj, bg, tiles, state);
739 });
740 // Routine 35 - 4x4 Corner BothBG
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(35, obj, bg, tiles, state);
745 });
746 // Routine 36 - Weird Corner Bottom BothBG
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(36, obj, bg, tiles, state);
751 });
752 // Routine 37 - Weird Corner Top BothBG
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(37, obj, bg, tiles, state);
757 });
758 // Routine 38 - Nothing
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(38, obj, bg, tiles, state);
763 });
764 // Routine 39 - Small chest rendering (stateful F99 / fixed-open F9A)
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->DrawChest(obj, bg, tiles, state);
769 });
770 // Routine 40 - Rightwards 4x2 (Floor Tile)
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(40, obj, bg, tiles, state);
775 });
776 // Routine 41 - Rightwards Decor 4x2 spaced 8 (12-column spacing)
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(41, obj, bg, tiles, state);
781 });
782 // Routine 42 - Rightwards Cannon Hole 4x3
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(42, obj, bg, tiles, state);
787 });
788 // Routine 43 - Downwards Floor 4x4 (object 0x70)
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(43, obj, bg, tiles, state);
793 });
794 // Routine 44 - Downwards 1x1 Solid +3 (object 0x71)
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(44, obj, bg, tiles, state);
799 });
800 // Routine 45 - Downwards Decor 4x4 spaced 2 (objects 0x73-0x74)
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(45, obj, bg, tiles, state);
805 });
806 // Routine 46 - Downwards Pillar 2x4 spaced 2 (objects 0x75, 0x87)
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(46, obj, bg, tiles, state);
811 });
812 // Routine 47 - Downwards Decor 3x4 spaced 4 (objects 0x76-0x77)
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(47, obj, bg, tiles, state);
817 });
818 // Routine 48 - Downwards Decor 2x2 spaced 12 (objects 0x78, 0x7B)
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(48, obj, bg, tiles, state);
823 });
824 // Routine 49 - Downwards Line 1x1 +1 (object 0x7C)
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(49, obj, bg, tiles, state);
829 });
830 // Routine 50 - Downwards Decor 2x4 spaced 8 (objects 0x7F, 0x80)
831 draw_routines_.push_back(
832 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
833 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
834 self->DrawUsingRegistryRoutine(50, obj, bg, tiles, state);
835 });
836 // Routine 51 - Rightwards Line 1x1 +1 (object 0x50)
837 draw_routines_.push_back(
838 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
839 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
840 self->DrawUsingRegistryRoutine(51, obj, bg, tiles, state);
841 });
842 // Routine 52 - Rightwards Bar 4x3 (object 0x4C)
843 draw_routines_.push_back(
844 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
845 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
846 self->DrawUsingRegistryRoutine(52, obj, bg, tiles, state);
847 });
848 // Routine 53 - Rightwards Shelf 4x4 (objects 0x4D-0x4F)
849 draw_routines_.push_back(
850 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
851 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
852 self->DrawUsingRegistryRoutine(53, obj, bg, tiles, state);
853 });
854 // Routine 54 - Rightwards Big Rail 1x3 +5 (object 0x5D)
855 draw_routines_.push_back(
856 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
857 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
858 self->DrawUsingRegistryRoutine(54, obj, bg, tiles, state);
859 });
860 // Routine 55 - Rightwards Block 2x2 spaced 2 (object 0x5E)
861 draw_routines_.push_back(
862 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
863 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
864 self->DrawUsingRegistryRoutine(55, obj, bg, tiles, state);
865 });
866
867 // ============================================================================
868 // Phase 4: SuperSquare Routines (routines 56-64)
869 // ============================================================================
870
871 // Routine 56 - 4x4 Blocks in 4x4 SuperSquare (objects 0xC0, 0xC2)
872 draw_routines_.push_back(
873 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
874 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
875 self->DrawUsingRegistryRoutine(56, obj, bg, tiles, state);
876 });
877
878 // Routine 57 - 3x3 Floor in 4x4 SuperSquare (objects 0xC3, 0xD7)
879 draw_routines_.push_back(
880 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
881 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
882 self->DrawUsingRegistryRoutine(57, obj, bg, tiles, state);
883 });
884
885 // Routine 58 - 4x4 Floor in 4x4 SuperSquare (objects 0xC5-0xCA, 0xD1-0xD2,
886 // 0xD9, 0xDF-0xE8)
887 draw_routines_.push_back(
888 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
889 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
890 self->DrawUsingRegistryRoutine(58, obj, bg, tiles, state);
891 });
892
893 // Routine 59 - 4x4 Floor One in 4x4 SuperSquare (object 0xC4)
894 draw_routines_.push_back(
895 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
896 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
897 self->DrawUsingRegistryRoutine(59, obj, bg, tiles, state);
898 });
899
900 // Routine 60 - 4x4 Floor Two in 4x4 SuperSquare (object 0xDB)
901 draw_routines_.push_back(
902 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
903 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
904 self->DrawUsingRegistryRoutine(60, obj, bg, tiles, state);
905 });
906
907 // Routine 61 - Big Hole 4x4 (object 0xA4)
908 draw_routines_.push_back(
909 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
910 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
911 self->DrawUsingRegistryRoutine(61, obj, bg, tiles, state);
912 });
913
914 // Routine 62 - Spike 2x2 in 4x4 SuperSquare (object 0xDE)
915 draw_routines_.push_back(
916 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
917 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
918 self->DrawUsingRegistryRoutine(62, obj, bg, tiles, state);
919 });
920
921 // Routine 63 - Table Rock 4x4 (object 0xDD)
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(63, obj, bg, tiles, state);
926 });
927
928 // Routine 64 - Water Overlay 8x8 (objects 0xD8, 0xDA)
929 draw_routines_.push_back(
930 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
931 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
932 self->DrawUsingRegistryRoutine(64, obj, bg, tiles, state);
933 });
934
935 // ============================================================================
936 // Phase 4 Step 2: Simple Variant Routines (routines 65-74)
937 // ============================================================================
938
939 // Routine 65 - Downwards Decor 3x4 spaced 2 (objects 0x81-0x84)
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(65, obj, bg, tiles, state);
944 });
945
946 // Routine 66 - Downwards Big Rail 3x1 plus 5 (object 0x88)
947 draw_routines_.push_back(
948 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
949 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
950 self->DrawUsingRegistryRoutine(66, obj, bg, tiles, state);
951 });
952
953 // Routine 67 - Downwards Block 2x2 spaced 2 (object 0x89)
954 draw_routines_.push_back(
955 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
956 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
957 self->DrawUsingRegistryRoutine(67, obj, bg, tiles, state);
958 });
959
960 // Routine 68 - Downwards Cannon Hole 3x6 (objects 0x85-0x86)
961 draw_routines_.push_back(
962 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
963 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
964 self->DrawUsingRegistryRoutine(68, obj, bg, tiles, state);
965 });
966
967 // Routine 69 - Downwards Bar 2x3 (object 0x8F)
968 draw_routines_.push_back(
969 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
970 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
971 self->DrawUsingRegistryRoutine(69, obj, bg, tiles, state);
972 });
973
974 // Routine 70 - Downwards Pots 2x2 (object 0x95)
975 draw_routines_.push_back(
976 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
977 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
978 self->DrawUsingRegistryRoutine(70, obj, bg, tiles, state);
979 });
980
981 // Routine 71 - Downwards Hammer Pegs 2x2 (object 0x96)
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(71, obj, bg, tiles, state);
986 });
987
988 // Routine 72 - Rightwards Edge 1x1 plus 7 (objects 0xB0-0xB1)
989 draw_routines_.push_back(
990 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
991 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
992 self->DrawUsingRegistryRoutine(72, obj, bg, tiles, state);
993 });
994
995 // Routine 73 - Rightwards Pots 2x2 (object 0xBC)
996 draw_routines_.push_back(
997 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
998 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
999 self->DrawUsingRegistryRoutine(73, obj, bg, tiles, state);
1000 });
1001
1002 // Routine 74 - Rightwards Hammer Pegs 2x2 (object 0xBD)
1003 draw_routines_.push_back(
1004 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1005 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1006 self->DrawUsingRegistryRoutine(74, obj, bg, tiles, state);
1007 });
1008
1009 // ============================================================================
1010 // Phase 4 Step 3: Diagonal Ceiling Routines (routines 75-78)
1011 // ============================================================================
1012
1013 // Routine 75 - Diagonal Ceiling Top Left (objects 0xA0, 0xA5, 0xA9)
1014 draw_routines_.push_back(
1015 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1016 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1017 self->DrawUsingRegistryRoutine(75, obj, bg, tiles, state);
1018 });
1019
1020 // Routine 76 - Diagonal Ceiling Bottom Left (objects 0xA1, 0xA6, 0xAA)
1021 draw_routines_.push_back(
1022 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1023 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1024 self->DrawUsingRegistryRoutine(76, obj, bg, tiles, state);
1025 });
1026
1027 // Routine 77 - Diagonal Ceiling Top Right (objects 0xA2, 0xA7, 0xAB)
1028 draw_routines_.push_back(
1029 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1030 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1031 self->DrawUsingRegistryRoutine(77, obj, bg, tiles, state);
1032 });
1033
1034 // Routine 78 - Diagonal Ceiling Bottom Right (objects 0xA3, 0xA8, 0xAC)
1035 draw_routines_.push_back(
1036 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1037 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1038 self->DrawUsingRegistryRoutine(78, obj, bg, tiles, state);
1039 });
1040
1041 // ============================================================================
1042 // Phase 4 Step 5: Special Routines (routines 79-82)
1043 // ============================================================================
1044
1045 // Routine 79 - Closed Chest Platform (object 0xC1, 68 tiles)
1046 draw_routines_.push_back(
1047 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1048 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1049 self->DrawUsingRegistryRoutine(79, obj, bg, tiles, state);
1050 });
1051
1052 // Routine 80 - Moving Wall West (object 0xCD, 24 tiles)
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(80, obj, bg, tiles, state);
1057 });
1058
1059 // Routine 81 - Moving Wall East (object 0xCE, 24 tiles)
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(81, obj, bg, tiles, state);
1064 });
1065
1066 // Routine 82 - Open Chest Platform (object 0xDC, 21 tiles)
1067 draw_routines_.push_back(
1068 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1069 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1070 self->DrawUsingRegistryRoutine(82, obj, bg, tiles, state);
1071 });
1072
1073 // ============================================================================
1074 // New Special Routines (Phase 5) - Stairs, Locks, Interactive Objects
1075 // ============================================================================
1076
1077 // Routine 83 - InterRoom Fat Stairs Up (object 0x12D)
1078 draw_routines_.push_back(
1079 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1080 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1081 self->DrawUsingRegistryRoutine(83, obj, bg, tiles, state);
1082 });
1083
1084 // Routine 84 - InterRoom Fat Stairs Down A (object 0x12E)
1085 draw_routines_.push_back(
1086 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1087 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1088 self->DrawUsingRegistryRoutine(84, obj, bg, tiles, state);
1089 });
1090
1091 // Routine 85 - InterRoom Fat Stairs Down B (object 0x12F)
1092 draw_routines_.push_back(
1093 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1094 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1095 self->DrawUsingRegistryRoutine(85, obj, bg, tiles, state);
1096 });
1097
1098 // Routine 86 - Auto Stairs (objects 0x130-0x133)
1099 draw_routines_.push_back(
1100 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1101 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1102 self->DrawUsingRegistryRoutine(86, obj, bg, tiles, state);
1103 });
1104
1105 // Routine 87 - Straight InterRoom Stairs (Type 3 objects 0x21E-0x229)
1106 draw_routines_.push_back(
1107 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1108 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1109 self->DrawUsingRegistryRoutine(87, obj, bg, tiles, state);
1110 });
1111
1112 // Routine 88 - Spiral Stairs Going Up Upper (object 0x138)
1113 draw_routines_.push_back(
1114 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1115 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1116 self->DrawUsingRegistryRoutine(88, obj, bg, tiles, state);
1117 });
1118
1119 // Routine 89 - Spiral Stairs Going Down Upper (object 0x139)
1120 draw_routines_.push_back(
1121 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1122 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1123 self->DrawUsingRegistryRoutine(89, obj, bg, tiles, state);
1124 });
1125
1126 // Routine 90 - Spiral Stairs Going Up Lower (object 0x13A)
1127 draw_routines_.push_back(
1128 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1129 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1130 self->DrawUsingRegistryRoutine(90, obj, bg, tiles, state);
1131 });
1132
1133 // Routine 91 - Spiral Stairs Going Down Lower (object 0x13B)
1134 draw_routines_.push_back(
1135 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1136 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1137 self->DrawUsingRegistryRoutine(91, obj, bg, tiles, state);
1138 });
1139
1140 // Routine 92 - Big Key Lock (Yaze 0xF98 / ASM object 0x218)
1141 draw_routines_.push_back(
1142 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1143 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1144 self->DrawBigKeyLock(obj, bg, tiles, state);
1145 });
1146
1147 // Routine 93 - Bombable Floor (Type 3 object 0x247)
1148 draw_routines_.push_back(
1149 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1150 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1151 self->DrawUsingRegistryRoutine(93, obj, bg, tiles, state);
1152 });
1153
1154 // Routine 94 - Empty Water Face (Type 3 object 0x200)
1155 draw_routines_.push_back(
1156 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1157 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1158 self->DrawUsingRegistryRoutine(94, obj, bg, tiles, state);
1159 });
1160
1161 // Routine 95 - Spitting Water Face (Type 3 object 0x201)
1162 draw_routines_.push_back(
1163 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1164 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1165 self->DrawUsingRegistryRoutine(95, obj, bg, tiles, state);
1166 });
1167
1168 // Routine 96 - Drenching Water Face (Type 3 object 0x202)
1169 draw_routines_.push_back(
1170 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1171 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1172 self->DrawUsingRegistryRoutine(96, obj, bg, tiles, state);
1173 });
1174
1175 // Routine 97 - Prison Cell (Type 3 objects 0x20D, 0x217)
1176 // USDASM selects one tilemap through $BF and draws a sparse 16x4 pattern.
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(97, obj, bg, tiles, state);
1181 });
1182
1183 // Routine 98 - Bed 4x5 (Type 2 objects 0x122, 0x128)
1184 draw_routines_.push_back(
1185 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1186 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1188 state);
1189 });
1190
1191 // Routine 99 - Rightwards 3x6 (Type 2 object 0x12C, Type 3 0x236-0x237)
1192 draw_routines_.push_back(
1193 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1194 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1196 tiles, state);
1197 });
1198
1199 // Routine 100 - Utility 6x3 (Type 2 object 0x13E, Type 3 0x24D, 0x25D)
1200 draw_routines_.push_back(
1201 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1202 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1204 tiles, state);
1205 });
1206
1207 // Routine 101 - Utility 3x5 (Type 3 objects 0x255, 0x25B)
1208 draw_routines_.push_back(
1209 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1210 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1212 tiles, state);
1213 });
1214
1215 // Routine 102 - Vertical Turtle Rock Pipe (Type 3 objects 0x23A, 0x23B)
1216 draw_routines_.push_back(
1217 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1218 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1220 obj, bg, tiles, state);
1221 });
1222
1223 // Routine 103 - Horizontal Turtle Rock Pipe (Type 3 objects 0x23C, 0x23D,
1224 // 0x25C)
1225 draw_routines_.push_back(
1226 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1227 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1229 DrawRoutineIds::kHorizontalTurtleRockPipe, obj, bg, tiles, state);
1230 });
1231
1232 // Routine 104 - Light Beam on Floor (Type 3 object 0x270)
1233 draw_routines_.push_back(
1234 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1235 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1237 tiles, state);
1238 });
1239
1240 // Routine 105 - Big Light Beam on Floor (Type 3 object 0x271)
1241 draw_routines_.push_back(
1242 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1243 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1245 tiles, state);
1246 });
1247
1248 // Routine 106 - Boss Shell 4x4 (Yaze 0xF95/0xFF2; ASM 0x215/0x272)
1249 draw_routines_.push_back(
1250 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1251 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1253 tiles, state);
1254 });
1255
1256 // Routine 107 - Solid Wall Decor 3x4 (Type 3 objects 0x269-0x26A, 0x26E-0x26F)
1257 draw_routines_.push_back(
1258 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1259 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1261 bg, tiles, state);
1262 });
1263
1264 // Routine 108 - Archery Game Target Door (Type 3 objects 0x260-0x261)
1265 draw_routines_.push_back(
1266 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1267 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1269 obj, bg, tiles, state);
1270 });
1271
1272 // Routine 109 - Ganon Triforce Floor Decor (Type 3 object 0x278)
1273 draw_routines_.push_back(
1274 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1275 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1277 obj, bg, tiles, state);
1278 });
1279
1280 // Routine 110 - Single 2x2 (pots, statues, single-instance 2x2 objects)
1281 draw_routines_.push_back(
1282 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1283 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1285 tiles, state);
1286 });
1287
1288 // Routine 111 - Waterfall47 (object 0x47)
1289 draw_routines_.push_back(
1290 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1291 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1293 tiles, state);
1294 });
1295
1296 // Routine 112 - Waterfall48 (object 0x48)
1297 draw_routines_.push_back(
1298 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1299 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1301 tiles, state);
1302 });
1303
1304 // Routine 113 - Single 4x4 (NO repetition)
1305 // ASM: RoomDraw_4x4 - draws a single 4x4 pattern (16 tiles)
1306 // Used for: 0xFEB (large decor), and other single 4x4 objects
1307 draw_routines_.push_back(
1308 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1309 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1311 tiles, state);
1312 });
1313
1314 // Routine 114 - Single 4x3 (NO repetition)
1315 // ASM: RoomDraw_TableRock4x3 - draws a single 4x3 pattern (12 tiles)
1316 // Used for: 0xFED (water grate), 0xFB1 (big chest), etc.
1317 draw_routines_.push_back(
1318 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1319 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1320 if (obj.id_ == 0xFB1) {
1321 self->DrawBigChest(obj, bg, tiles, state);
1322 return;
1323 }
1325 tiles, state);
1326 });
1327
1328 // Routine 115 - RupeeFloor (special pattern for 0xF92)
1329 // ASM: RoomDraw_RupeeFloor - draws 3 one-tile columns two tiles apart.
1330 // Pattern: 5 tiles wide, 8 rows tall with gaps (rows 2 and 5 are empty).
1331 draw_routines_.push_back(
1332 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1333 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1335 tiles, state);
1336 });
1337
1338 // Routine 116 - Actual 4x4 tile8 pattern (32x32 pixels, NO repetition)
1339 // ASM: RoomDraw_4x4 - draws exactly 4 columns x 4 rows = 16 tiles
1340 // Used for: 0xFE6 (pit)
1341 draw_routines_.push_back(
1342 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1343 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1345 tiles, state);
1346 });
1347
1348 auto ensure_index = [this](size_t index) {
1349 while (draw_routines_.size() <= index) {
1350 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1352 std::span<const gfx::TileInfo> tiles,
1353 [[maybe_unused]] const DungeonState* state) {
1354 self->DrawNothing(obj, bg, tiles, state);
1355 });
1356 }
1357 };
1358
1359 // Routine 117 - Long vertical rail with CORNER+MIDDLE+END pattern (0x8A)
1360 // ASM: RoomDraw_DownwardsHasEdge1x1_1to16_plus23 - matches horizontal 0x22
1361 ensure_index(117);
1362 draw_routines_[117] =
1363 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1364 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1365 self->DrawUsingRegistryRoutine(117, obj, bg, tiles, state);
1366 };
1367
1368 // Routine 118 - Horizontal long rails with CORNER+MIDDLE+END pattern (0x5F)
1369 ensure_index(118);
1370 draw_routines_[118] =
1371 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1372 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1373 self->DrawUsingRegistryRoutine(118, obj, bg, tiles, state);
1374 };
1375
1378 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1379 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1381 bg, tiles, state);
1382 };
1383
1386 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1387 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1389 bg, tiles, state);
1390 };
1391
1392 ensure_index(DrawRoutineIds::kDamFloodGate);
1394 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1395 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1397 tiles, state);
1398 };
1399
1402 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1403 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1406 state);
1407 };
1408
1409 ensure_index(DrawRoutineIds::kFloorLight);
1411 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1412 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1414 tiles, state);
1415 };
1416
1417 ensure_index(DrawRoutineIds::kWeird2x4_1to16);
1419 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1420 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1422 tiles, state);
1423 };
1424
1425 ensure_index(DrawRoutineIds::kBigWallDecor);
1427 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1428 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1430 tiles, state);
1431 };
1432
1433 ensure_index(DrawRoutineIds::kTableBowl);
1435 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1436 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1438 tiles, state);
1439 };
1440
1441 ensure_index(DrawRoutineIds::kSmithyFurnace);
1443 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1444 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1446 tiles, state);
1447 };
1448
1449 ensure_index(DrawRoutineIds::kBigGrayRock);
1451 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1452 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1454 tiles, state);
1455 };
1456
1457 ensure_index(DrawRoutineIds::kAgahnimsAltar);
1459 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1460 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1462 tiles, state);
1463 };
1464
1467 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1468 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1470 bg, tiles, state);
1471 };
1472
1473 ensure_index(DrawRoutineIds::kMagicBatAltar);
1475 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1476 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1478 tiles, state);
1479 };
1480
1483 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1484 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1486 bg, tiles, state);
1487 };
1488
1489 // Routine 130 - Custom Object (Oracle of Secrets 0x31, 0x32)
1490 // Uses external binary files instead of ROM tile data.
1491 // Requires CustomObjectManager initialization and enable_custom_objects flag.
1492 ensure_index(130);
1493 draw_routines_[130] = [](ObjectDrawer* self, const RoomObject& obj,
1495 std::span<const gfx::TileInfo> tiles,
1496 [[maybe_unused]] const DungeonState* state) {
1497 self->DrawCustomObject(obj, bg, tiles, state);
1498 };
1499
1500 routines_initialized_ = true;
1501}
1502
1503int ObjectDrawer::GetDrawRoutineId(int16_t object_id) const {
1504 // Delegate to the unified registry for the canonical mapping
1506}
1507
1508// ============================================================================
1509// Draw Routine Implementations (Based on ZScream patterns)
1510// ============================================================================
1511
1512void ObjectDrawer::DrawDoor(const DoorDef& door, int door_index,
1515 const DungeonState* state) {
1516 // Door rendering based on ZELDA3_DUNGEON_SPEC.md Section 5 and disassembly
1517 // Uses DoorType and DoorDirection enums for type safety
1518 // Position calculations via DoorPositionManager
1519
1520 LOG_DEBUG("ObjectDrawer", "DrawDoor: idx=%d type=%d dir=%d pos=%d",
1521 door_index, static_cast<int>(door.type),
1522 static_cast<int>(door.direction), door.position);
1523
1524 if (!rom_ || !rom_->is_loaded() || !room_gfx_buffer_) {
1525 LOG_DEBUG("ObjectDrawer", "DrawDoor: SKIPPED - rom=%p loaded=%d gfx=%p",
1526 (void*)rom_, rom_ ? rom_->is_loaded() : 0,
1527 (void*)room_gfx_buffer_);
1528 return;
1529 }
1530
1531 auto& bitmap = bg1.bitmap();
1532 if (!bitmap.is_active() || bitmap.width() == 0) {
1533 LOG_DEBUG("ObjectDrawer",
1534 "DrawDoor: SKIPPED - bitmap not active or zero width");
1535 return;
1536 }
1537
1538 const bool is_door_open = state && state->IsDoorOpen(room_id_, door_index);
1539
1540 // Get door position from DoorPositionManager
1541 auto [tile_x, tile_y] = door.GetTileCoords();
1542 auto dims = door.GetDimensions();
1543 int door_width = dims.width_tiles;
1544 int door_height = dims.height_tiles;
1545
1546 LOG_DEBUG("ObjectDrawer", "DrawDoor: tile_pos=(%d,%d) dims=%dx%d", tile_x,
1547 tile_y, door_width, door_height);
1548
1549 constexpr int kRoomDrawObjectDataBase = 0x1B52;
1550 constexpr int kDoorwayReplacementDoorGfxBase = 0x1A02;
1551 constexpr int kExplodingWallTilemapPositionBase = 0x19DE;
1552 constexpr int kExplodingWallOpenReplacementType = 0x54;
1553 constexpr int kNorthCurtainClosedOffset = 0x078A;
1554 const auto& rom_data = rom_->data();
1555
1556 auto draw_from_object_data = [&](gfx::BackgroundBuffer& target,
1557 int start_tile_x, int start_tile_y,
1558 int width, int height, int tile_data_addr) {
1559 auto& bitmap = target.bitmap();
1560 auto& priority_buffer = target.mutable_priority_data();
1561 auto& coverage_buffer = target.mutable_coverage_data();
1562 const int bitmap_width = bitmap.width();
1563 int tile_idx = 0;
1564
1565 for (int dx = 0; dx < width; dx++) {
1566 for (int dy = 0; dy < height; dy++) {
1567 const int addr = tile_data_addr + (tile_idx * 2);
1568 const uint16_t tile_word = rom_data[addr] | (rom_data[addr + 1] << 8);
1569 const auto tile_info = gfx::WordToTileInfo(tile_word);
1570 const int pixel_x = (start_tile_x + dx) * 8;
1571 const int pixel_y = (start_tile_y + dy) * 8;
1572
1573 target.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, pixel_x, pixel_y,
1574 8, 8);
1575 DrawTileToBitmap(bitmap, tile_info, pixel_x, pixel_y, room_gfx_buffer_);
1576
1577 const uint8_t priority = tile_info.over_ ? 1 : 0;
1578 const auto& bitmap_data = bitmap.vector();
1579 for (int py = 0; py < 8; py++) {
1580 const int dest_y = pixel_y + py;
1581 if (dest_y < 0 || dest_y >= bitmap.height()) {
1582 continue;
1583 }
1584 for (int px = 0; px < 8; px++) {
1585 const int dest_x = pixel_x + px;
1586 if (dest_x < 0 || dest_x >= bitmap_width) {
1587 continue;
1588 }
1589 const int dest_index = dest_y * bitmap_width + dest_x;
1590 if (dest_index >= 0 &&
1591 dest_index < static_cast<int>(coverage_buffer.size())) {
1592 coverage_buffer[dest_index] = 1;
1593 }
1594 if (dest_index < static_cast<int>(bitmap_data.size()) &&
1595 bitmap_data[dest_index] != 255) {
1596 priority_buffer[dest_index] = priority;
1597 }
1598 }
1599 }
1600
1601 tile_idx++;
1602 }
1603 }
1604 };
1605
1606 auto draw_repeated_tile = [&](gfx::BackgroundBuffer& target, int start_tile_x,
1607 int start_tile_y, int width, int height,
1608 uint16_t tile_word) {
1609 auto& bitmap = target.bitmap();
1610 auto& priority_buffer = target.mutable_priority_data();
1611 auto& coverage_buffer = target.mutable_coverage_data();
1612 const int bitmap_width = bitmap.width();
1613 const auto tile_info = gfx::WordToTileInfo(tile_word);
1614
1615 for (int dx = 0; dx < width; dx++) {
1616 for (int dy = 0; dy < height; dy++) {
1617 const int pixel_x = (start_tile_x + dx) * 8;
1618 const int pixel_y = (start_tile_y + dy) * 8;
1619
1620 target.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, pixel_x, pixel_y,
1621 8, 8);
1622 DrawTileToBitmap(bitmap, tile_info, pixel_x, pixel_y, room_gfx_buffer_);
1623
1624 const uint8_t priority = tile_info.over_ ? 1 : 0;
1625 const auto& bitmap_data = bitmap.vector();
1626 for (int py = 0; py < 8; py++) {
1627 const int dest_y = pixel_y + py;
1628 if (dest_y < 0 || dest_y >= bitmap.height()) {
1629 continue;
1630 }
1631 for (int px = 0; px < 8; px++) {
1632 const int dest_x = pixel_x + px;
1633 if (dest_x < 0 || dest_x >= bitmap_width) {
1634 continue;
1635 }
1636 const int dest_index = dest_y * bitmap_width + dest_x;
1637 if (dest_index >= 0 &&
1638 dest_index < static_cast<int>(coverage_buffer.size())) {
1639 coverage_buffer[dest_index] = 1;
1640 }
1641 if (dest_index < static_cast<int>(bitmap_data.size()) &&
1642 bitmap_data[dest_index] != 255) {
1643 priority_buffer[dest_index] = priority;
1644 }
1645 }
1646 }
1647 }
1648 }
1649 };
1650
1651 auto tilemap_offset_to_tile_coords = [](uint16_t offset) {
1652 return std::pair<int, int>{static_cast<int>((offset % 0x80) / 2),
1653 static_cast<int>(offset / 0x80) - 4};
1654 };
1655 const int position_index = std::min<int>(door.position & 0x0F, 11);
1656
1657 auto resolve_render_type = [](DoorDirection render_direction,
1658 DoorType render_type) {
1659 switch (render_type) {
1661 return (render_direction == DoorDirection::North ||
1662 render_direction == DoorDirection::West)
1666 return (render_direction == DoorDirection::North ||
1667 render_direction == DoorDirection::West)
1671 return (render_direction == DoorDirection::North ||
1672 render_direction == DoorDirection::West)
1676 return (render_direction == DoorDirection::North ||
1677 render_direction == DoorDirection::West)
1680 default:
1681 return render_type;
1682 }
1683 };
1684
1685 auto draw_table_door = [&](gfx::BackgroundBuffer& target,
1686 DoorDirection render_direction, int start_tile_x,
1687 int start_tile_y, DoorType render_type) -> bool {
1688 int offset_table_addr = 0;
1689 switch (render_direction) {
1691 offset_table_addr = kDoorGfxUp;
1692 break;
1694 offset_table_addr = kDoorGfxDown;
1695 break;
1697 offset_table_addr = kDoorGfxLeft;
1698 break;
1700 offset_table_addr = kDoorGfxRight;
1701 break;
1702 }
1703
1704 const DoorType resolved_type =
1705 resolve_render_type(render_direction, render_type);
1706 const int render_type_value = static_cast<int>(resolved_type);
1707 const int type_index = render_type_value / 2;
1708 const int table_entry_addr = offset_table_addr + (type_index * 2);
1709 if (table_entry_addr + 1 >= static_cast<int>(rom_->size())) {
1710 return false;
1711 }
1712
1713 const uint16_t tile_offset =
1714 rom_data[table_entry_addr] | (rom_data[table_entry_addr + 1] << 8);
1715 const int tile_data_addr = kRoomDrawObjectDataBase + tile_offset;
1716 const auto dims = GetDoorDimensions(render_direction);
1717 const int data_size = dims.width_tiles * dims.height_tiles * 2;
1718 if (tile_data_addr < 0 ||
1719 tile_data_addr + data_size > static_cast<int>(rom_->size())) {
1720 return false;
1721 }
1722
1723 draw_from_object_data(target, start_tile_x, start_tile_y, dims.width_tiles,
1724 dims.height_tiles, tile_data_addr);
1725 return true;
1726 };
1727
1728 // USDASM has special north-door branches that do not follow the generic 4x3
1729 // ranged-door path.
1730 if (door.direction == DoorDirection::North &&
1731 door.type == DoorType::ExplodingWall && !is_door_open) {
1732 LOG_DEBUG("ObjectDrawer",
1733 "DrawDoor: closed exploding wall intentionally draws nothing");
1734 (void)bg2;
1735 return;
1736 }
1737
1738 if (door.direction == DoorDirection::North &&
1739 door.type == DoorType::CurtainDoor && !is_door_open) {
1740 const int tile_data_addr =
1741 kRoomDrawObjectDataBase + kNorthCurtainClosedOffset;
1742 const int data_size = 16 * 2; // RoomDraw_4x4 closed curtain path.
1743 if (tile_data_addr < 0 ||
1744 tile_data_addr + data_size > static_cast<int>(rom_->size())) {
1745 DrawDoorIndicator(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
1746 door.type, door.direction);
1747 return;
1748 }
1749 draw_from_object_data(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
1750 tile_data_addr);
1751 return;
1752 }
1753
1754 if (door.direction == DoorDirection::North &&
1755 door.type == DoorType::CurtainDoor && is_door_open) {
1756 const int replacement_type_addr =
1757 kDoorwayReplacementDoorGfxBase + static_cast<int>(door.type);
1758 if (replacement_type_addr < 0 ||
1759 replacement_type_addr >= static_cast<int>(rom_->size())) {
1760 DrawDoorIndicator(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
1761 door.type, door.direction);
1762 return;
1763 }
1764
1765 const int replacement_type = rom_data[replacement_type_addr];
1766 const int table_entry_addr = kDoorGfxUp + replacement_type;
1767 if (table_entry_addr < 0 ||
1768 table_entry_addr + 1 >= static_cast<int>(rom_->size())) {
1769 DrawDoorIndicator(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
1770 door.type, door.direction);
1771 return;
1772 }
1773
1774 const uint16_t tile_offset =
1775 rom_data[table_entry_addr] | (rom_data[table_entry_addr + 1] << 8);
1776 const int tile_data_addr = kRoomDrawObjectDataBase + tile_offset;
1777 const int data_size = 16 * 2; // RoomDraw_4x4 open curtain path.
1778 if (tile_data_addr < 0 ||
1779 tile_data_addr + data_size > static_cast<int>(rom_->size())) {
1780 DrawDoorIndicator(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
1781 door.type, door.direction);
1782 return;
1783 }
1784
1785 draw_from_object_data(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
1786 tile_data_addr);
1787 return;
1788 }
1789
1790 if (door.direction == DoorDirection::North &&
1791 door.type == DoorType::ExplodingWall && is_door_open) {
1792 const int position_index = std::min<int>(door.position & 0x0F, 5);
1793 const int tilemap_entry_addr =
1794 kExplodingWallTilemapPositionBase + (position_index * 2);
1795 if (tilemap_entry_addr < 0 ||
1796 tilemap_entry_addr + 1 >= static_cast<int>(rom_->size())) {
1797 DrawDoorIndicator(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
1798 door.type, door.direction);
1799 return;
1800 }
1801
1802 const uint16_t tilemap_offset =
1803 rom_data[tilemap_entry_addr] | (rom_data[tilemap_entry_addr + 1] << 8);
1804 const auto explosion_tile_coords =
1805 tilemap_offset_to_tile_coords(tilemap_offset);
1806 const int explosion_tile_x = explosion_tile_coords.first;
1807 const int explosion_tile_y = explosion_tile_coords.second;
1808
1809 auto draw_exploding_wall_segment = [&](int table_entry_addr,
1810 int segment_tile_y) -> bool {
1811 if (table_entry_addr < 0 ||
1812 table_entry_addr + 1 >= static_cast<int>(rom_->size())) {
1813 return false;
1814 }
1815
1816 const uint16_t tile_offset =
1817 rom_data[table_entry_addr] | (rom_data[table_entry_addr + 1] << 8);
1818 const int tile_data_addr = kRoomDrawObjectDataBase + tile_offset;
1819 constexpr int kFillWordIndex = 12;
1820 const int min_data_size = (kFillWordIndex + 1) * 2;
1821 if (tile_data_addr < 0 ||
1822 tile_data_addr + min_data_size > static_cast<int>(rom_->size())) {
1823 return false;
1824 }
1825
1826 draw_from_object_data(bg1, explosion_tile_x, segment_tile_y,
1827 /*width=*/2, /*height=*/6, tile_data_addr);
1828 const uint16_t fill_word =
1829 rom_data[tile_data_addr + (kFillWordIndex * 2)] |
1830 (rom_data[tile_data_addr + (kFillWordIndex * 2) + 1] << 8);
1831 draw_repeated_tile(bg1, explosion_tile_x + 2, segment_tile_y,
1832 /*width=*/18, /*height=*/6, fill_word);
1833 return true;
1834 };
1835
1836 const int south_table_entry_addr =
1837 kDoorGfxDown + kExplodingWallOpenReplacementType;
1838 const int north_table_entry_addr =
1839 kDoorGfxUp + kExplodingWallOpenReplacementType;
1840 if (!draw_exploding_wall_segment(south_table_entry_addr,
1841 explosion_tile_y) ||
1842 !draw_exploding_wall_segment(north_table_entry_addr,
1843 explosion_tile_y + 6)) {
1844 DrawDoorIndicator(bg1, explosion_tile_x, explosion_tile_y, /*width=*/20,
1845 /*height=*/12, door.type, door.direction);
1846 }
1847 return;
1848 }
1849
1850 // Door graphics use an indirect addressing scheme:
1851 // 1. kDoorGfxUp/Down/Left/Right point to offset tables (DoorGFXDataOffset_*)
1852 // 2. Each table entry is a 16-bit offset into RoomDrawObjectData
1853 // 3. RoomDrawObjectData base is at PC 0x1B52 (SNES $00:9B52)
1854 // 4. Actual tile data = 0x1B52 + offset_from_table
1855 if ((door.direction == DoorDirection::North ||
1856 door.direction == DoorDirection::West) &&
1857 position_index >= 6 && door.type != DoorType::ExplicitRoomDoor) {
1858 const DoorDirection counterpart_direction =
1861 const int counterpart_tile_x =
1862 tile_x + (counterpart_direction == DoorDirection::East ? 1 : 0);
1863 const int counterpart_tile_y =
1864 tile_y + (counterpart_direction == DoorDirection::South ? 1 : 0);
1865 (void)draw_table_door(bg1, counterpart_direction, counterpart_tile_x,
1866 counterpart_tile_y, door.type);
1867 }
1868
1869 const bool drew_current =
1870 draw_table_door(bg1, door.direction, tile_x, tile_y, door.type);
1871 if (!drew_current) {
1872 LOG_DEBUG("ObjectDrawer",
1873 "DrawDoor: INVALID ADDRESS - falling back to indicator");
1874 DrawDoorIndicator(bg1, tile_x, tile_y, door_width, door_height, door.type,
1875 door.direction);
1876 return;
1877 }
1878
1879 LOG_DEBUG("ObjectDrawer",
1880 "DrawDoor: type=%s dir=%s pos=%d at tile(%d,%d) size=%dx%d",
1881 std::string(GetDoorTypeName(door.type)).c_str(),
1882 std::string(GetDoorDirectionName(door.direction)).c_str(),
1883 door.position, tile_x, tile_y, door_width, door_height);
1884}
1885
1887 int tile_y, int width, int height,
1888 DoorType type, DoorDirection direction) {
1889 // Draw a simple colored rectangle as door indicator when graphics unavailable
1890 // Different colors for different door types using DoorType enum
1891
1892 auto& bitmap = bg.bitmap();
1893 auto& coverage_buffer = bg.mutable_coverage_data();
1894
1895 uint8_t color_idx;
1896 switch (type) {
1899 color_idx = 45; // Standard door color (brown)
1900 break;
1901
1907 color_idx = 60; // Key door - yellowish
1908 break;
1909
1912 color_idx = 58; // Big key - golden
1913 break;
1914
1918 case DoorType::DashWall:
1919 color_idx = 15; // Bombable/destructible - brownish/cracked
1920 break;
1921
1928 color_idx = 30; // Shutter - greenish
1929 break;
1930
1932 color_idx = 42; // Eye watch - lighter brown
1933 break;
1934
1936 color_idx = 35; // Curtain - special
1937 break;
1938
1939 case DoorType::CaveExit:
1944 color_idx = 25; // Cave/dungeon exit - dark
1945 break;
1946
1950 color_idx = 5; // Markers - very faint
1951 break;
1952
1953 default:
1954 color_idx = 50; // Default door color
1955 break;
1956 }
1957
1958 int pixel_x = tile_x * 8;
1959 int pixel_y = tile_y * 8;
1960 int pixel_width = width * 8;
1961 int pixel_height = height * 8;
1962
1963 int bitmap_width = bitmap.width();
1964 int bitmap_height = bitmap.height();
1965
1967 pixel_width, pixel_height);
1968
1969 // Draw filled rectangle with border
1970 for (int py = 0; py < pixel_height; py++) {
1971 for (int px = 0; px < pixel_width; px++) {
1972 int dest_x = pixel_x + px;
1973 int dest_y = pixel_y + py;
1974
1975 if (dest_x >= 0 && dest_x < bitmap_width && dest_y >= 0 &&
1976 dest_y < bitmap_height) {
1977 // Draw border (2 pixel thick) or fill
1978 bool is_border = (px < 2 || px >= pixel_width - 2 || py < 2 ||
1979 py >= pixel_height - 2);
1980 uint8_t final_color = is_border ? (color_idx + 5) : color_idx;
1981
1982 int offset = (dest_y * bitmap_width) + dest_x;
1983 bitmap.WriteToPixel(offset, final_color);
1984
1985 if (offset >= 0 && offset < static_cast<int>(coverage_buffer.size())) {
1986 coverage_buffer[offset] = 1;
1987 }
1988 }
1989 }
1990 }
1991}
1992
1994 std::span<const gfx::TileInfo> tiles,
1995 [[maybe_unused]] const DungeonState* state) {
1996 // USDASM RoomDraw_OpenChest draws F9A's fixed open graphic directly and
1997 // does not read or advance either chest/event counter.
1998 if (obj.id_ == 0xF9A) {
2000 return;
2001 }
2002
2003 // USDASM RoomDraw_Chest draws F99 as a single stateful 2x2 chest. The size
2004 // byte is not used for repetition.
2005
2006 // Determine if chest is open
2007 bool is_open = false;
2008 if (state) {
2009 is_open = state->IsChestOpen(room_id_, current_chest_index_);
2010 }
2011
2012 // RoomDraw_Chest advances the chest-only $0496 counter, then copies its next
2013 // value into the shared chest/lock $0498 counter.
2016
2017 // Draw SINGLE chest - no repetition based on size
2018 // Standard chests are 2x2 (4 tiles)
2019 // If we have extra tiles loaded, the second 4 are for open state
2020
2021 if (is_open && tiles.size() >= 8) {
2022 // Small chest open tiles (indices 4-7) - SINGLE 2x2 draw
2023 if (tiles.size() >= 8) {
2024 WriteTile8(bg, obj.x_, obj.y_, tiles[4]); // top-left
2025 WriteTile8(bg, obj.x_, obj.y_ + 1, tiles[5]); // bottom-left
2026 WriteTile8(bg, obj.x_ + 1, obj.y_, tiles[6]); // top-right
2027 WriteTile8(bg, obj.x_ + 1, obj.y_ + 1, tiles[7]); // bottom-right
2028 }
2029 return;
2030 }
2031
2032 // Draw closed chest - SINGLE 2x2 pattern (column-major order)
2033 if (tiles.size() >= 4) {
2034 WriteTile8(bg, obj.x_, obj.y_, tiles[0]); // top-left
2035 WriteTile8(bg, obj.x_, obj.y_ + 1, tiles[1]); // bottom-left
2036 WriteTile8(bg, obj.x_ + 1, obj.y_, tiles[2]); // top-right
2037 WriteTile8(bg, obj.x_ + 1, obj.y_ + 1, tiles[3]); // bottom-right
2038 }
2039}
2040
2043 std::span<const gfx::TileInfo> tiles,
2044 const DungeonState* state) {
2045 // USDASM RoomDraw_BigChest uses the chest-only $0496 slot for its room flag,
2046 // advances it once, then copies the next value into shared $0498. FB2 uses
2047 // RoomDraw_OpenBigChest directly and never reaches this stateful wrapper.
2048 bool is_open = false;
2049 if (state) {
2050 is_open = state->IsBigChestOpen(room_id_, current_chest_index_);
2051 }
2052
2055
2056 constexpr size_t kBigChestStateTileCount = 12;
2057 if (is_open && tiles.size() >= kBigChestStateTileCount * 2) {
2058 tiles = tiles.subspan(kBigChestStateTileCount, kBigChestStateTileCount);
2059 }
2061}
2062
2065 std::span<const gfx::TileInfo> tiles,
2066 const DungeonState* state) {
2067 // USDASM RoomDraw_BigKeyLock indexes $0402 through the shared $0498
2068 // chest/lock slot. An opened lock advances the slot but writes no tiles.
2069 const int room_event_index = current_room_event_index_++;
2070 if (state && state->IsBigKeyLockOpen(room_id_, room_event_index)) {
2071 return;
2072 }
2073
2075}
2076
2078 std::span<const gfx::TileInfo> tiles,
2079 [[maybe_unused]] const DungeonState* state) {
2080 // Intentionally empty - represents invisible logic objects or placeholders
2081 // ASM: RoomDraw_Nothing_A ($0190F2), RoomDraw_Nothing_B ($01932E), etc.
2082 // These routines typically just RTS.
2083 LOG_DEBUG("ObjectDrawer", "DrawNothing for object 0x%02X (logic/invisible)",
2084 obj.id_);
2085}
2086
2088 std::span<const gfx::TileInfo> tiles,
2089 [[maybe_unused]] const DungeonState* state) {
2090 // Pattern: Custom draw routine (objects 0x31-0x32)
2091 // For now, fall back to simple 1x1
2092 if (tiles.size() >= 1) {
2093 // Use first 8x8 tile from span
2094 WriteTile8(bg, obj.x_, obj.y_, tiles[0]);
2095 }
2096}
2097
2099 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2100 std::span<const gfx::TileInfo> tiles,
2101 [[maybe_unused]] const DungeonState* state) {
2102 // Pattern: 4x4 block rightward (objects 0x33, 0xBA = large ceiling, etc.)
2103 int size = obj.size_ & 0x0F;
2104
2105 // Assembly: GetSize_1to16, so count = size + 1
2106 int count = size + 1;
2107
2108 // Debug: Log large ceiling objects (0xBA)
2109 if (obj.id_ == 0xBA && tiles.size() >= 16) {
2110 LOG_DEBUG("ObjectDrawer",
2111 "Large Ceiling Draw: obj=0x%02X pos=(%d,%d) size=%d tiles=%zu",
2112 obj.id_, obj.x_, obj.y_, size, tiles.size());
2113 LOG_DEBUG("ObjectDrawer", " First 4 Tile IDs: [%d, %d, %d, %d]",
2114 tiles[0].id_, tiles[1].id_, tiles[2].id_, tiles[3].id_);
2115 LOG_DEBUG("ObjectDrawer", " First 4 Palettes: [%d, %d, %d, %d]",
2116 tiles[0].palette_, tiles[1].palette_, tiles[2].palette_,
2117 tiles[3].palette_);
2118 }
2119
2120 for (int s = 0; s < count; s++) {
2121 if (tiles.size() >= 16) {
2122 // Draw 4x4 pattern in COLUMN-MAJOR order (matching assembly)
2123 // Iterate columns (x) first, then rows (y) within each column
2124 for (int x = 0; x < 4; ++x) {
2125 for (int y = 0; y < 4; ++y) {
2126 WriteTile8(bg, obj.x_ + (s * 4) + x, obj.y_ + y, tiles[x * 4 + y]);
2127 }
2128 }
2129 }
2130 }
2131}
2132
2134 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2135 std::span<const gfx::TileInfo> tiles,
2136 [[maybe_unused]] const DungeonState* state) {
2137 // Pattern: 4x3 decoration with spacing (objects 0x3A-0x3B)
2138 // 4 columns × 3 rows = 12 tiles in COLUMN-MAJOR order
2139 // ASM: ADC #$0008 to Y = 8-byte advance = 4 tiles per iteration
2140 // Total spacing: 4 (object width) + 4 (gap) = 8 tiles between starts
2141 int size = obj.size_ & 0x0F;
2142
2143 // Assembly: GetSize_1to16, so count = size + 1
2144 int count = size + 1;
2145
2146 for (int s = 0; s < count; s++) {
2147 if (tiles.size() >= 12) {
2148 // Draw 4x3 pattern in COLUMN-MAJOR order (matching assembly)
2149 // Spacing: 8 tiles (4 object + 4 gap) per ASM ADC #$0008
2150 for (int x = 0; x < 4; ++x) {
2151 for (int y = 0; y < 3; ++y) {
2152 WriteTile8(bg, obj.x_ + (s * 8) + x, obj.y_ + y, tiles[x * 3 + y]);
2153 }
2154 }
2155 }
2156 }
2157}
2158
2159// ============================================================================
2160// Utility Methods
2161// ============================================================================
2162
2164 int start_py, int pixel_width,
2165 int pixel_height) {
2166 bg1.SetBG1RevealMaskRect(bg1_reveal_mask_source_, start_px, start_py,
2167 pixel_width, pixel_height);
2168}
2169
2171 gfx::BackgroundBuffer& bg1, const gfx::TileInfo& tile_info, int pixel_x,
2172 int pixel_y, const uint8_t* tiledata) {
2173 auto& bitmap = bg1.bitmap();
2174 if (!bitmap.is_active() || bitmap.width() == 0 || bitmap.height() == 0 ||
2175 tiledata == nullptr) {
2176 return;
2177 }
2178
2179 constexpr int kMaxTileRow = 63;
2180 const int tile_col = tile_info.id_ % 16;
2181 const int tile_row = tile_info.id_ / 16;
2182 if (tile_row > kMaxTileRow) {
2183 return;
2184 }
2185
2186 const int tile_base_x = tile_col * 8;
2187 const int tile_base_y = tile_row * 1024;
2188 auto& reveal_mask = bg1.mutable_bg1_reveal_mask_data();
2189 const uint8_t source_mask = static_cast<uint8_t>(bg1_reveal_mask_source_);
2190
2191 for (int py = 0; py < 8; ++py) {
2192 const int src_row = tile_info.vertical_mirror_ ? (7 - py) : py;
2193 const int dest_y = pixel_y + py;
2194 if (dest_y < 0 || dest_y >= bitmap.height()) {
2195 continue;
2196 }
2197
2198 for (int px = 0; px < 8; ++px) {
2199 const int src_col = tile_info.horizontal_mirror_ ? (7 - px) : px;
2200 const int src_index =
2201 (src_row * 128) + src_col + tile_base_x + tile_base_y;
2202 if (tiledata[src_index] == 0) {
2203 continue;
2204 }
2205
2206 const int dest_x = pixel_x + px;
2207 if (dest_x < 0 || dest_x >= bitmap.width()) {
2208 continue;
2209 }
2210
2211 const int dest_index = dest_y * bitmap.width() + dest_x;
2212 reveal_mask[dest_index] |= source_mask;
2213 }
2214 }
2215}
2216
2217void ObjectDrawer::WriteTile8(gfx::BackgroundBuffer& bg, int tile_x, int tile_y,
2218 const gfx::TileInfo& tile_info) {
2219 if (!IsValidTilePosition(tile_x, tile_y)) {
2220 return;
2221 }
2222 PushTrace(tile_x, tile_y, tile_info);
2223 if (trace_only_) {
2224 return;
2225 }
2226 // Draw directly to bitmap instead of tile buffer to avoid being overwritten
2227 auto& bitmap = bg.bitmap();
2228 if (!bitmap.is_active() || bitmap.width() == 0) {
2229 return; // Bitmap not ready
2230 }
2231
2232 // The room-specific graphics buffer (current_gfx16_) contains the assembled
2233 // tile graphics for the current room. Object tile IDs are relative to this
2234 // buffer.
2235 const uint8_t* gfx_data = room_gfx_buffer_;
2236
2237 if (!gfx_data) {
2238 LOG_DEBUG("ObjectDrawer", "ERROR: No graphics data available");
2239 return;
2240 }
2241
2242 // A later BG1 tilemap write supersedes an earlier reveal request from the
2243 // same logical stream, including transparent pixels in the 8x8 footprint.
2244 bg.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, tile_x * 8, tile_y * 8, 8,
2245 8);
2246
2247 const bool should_mark_bg1_mask =
2248 active_mask_source_bg_ != nullptr && (&bg == active_mask_source_bg_);
2249 // Draw single 8x8 tile directly to bitmap.
2250 DrawTileToBitmap(bitmap, tile_info, tile_x * 8, tile_y * 8, gfx_data);
2251 if (should_mark_bg1_mask && active_object_bg1_mask_ != nullptr) {
2253 tile_x * 8, tile_y * 8, gfx_data);
2254 }
2255 if (should_mark_bg1_mask && active_layout_bg1_mask_ != nullptr) {
2257 tile_x * 8, tile_y * 8, gfx_data);
2258 }
2259
2260 // Mark coverage for the full 8x8 tile region (even if pixels are transparent).
2261 //
2262 // This distinguishes "tilemap entry written but transparent" from "no write",
2263 // which is required to emulate SNES behavior where a transparent tile still
2264 // overwrites the previous tilemap entry (clearing BG1 and revealing BG2/backdrop).
2265 auto& coverage_buffer = bg.mutable_coverage_data();
2266
2267 // Also update priority buffer with tile's priority bit.
2268 // Priority (over_) affects Z-ordering in SNES Mode 1 compositing.
2269 uint8_t priority = tile_info.over_ ? 1 : 0;
2270 int pixel_x = tile_x * 8;
2271 int pixel_y = tile_y * 8;
2272 auto& priority_buffer = bg.mutable_priority_data();
2273 int width = bitmap.width();
2274
2275 // Update priority for each pixel in the 8x8 tile
2276 const auto& bitmap_data = bitmap.vector();
2277 for (int py = 0; py < 8; py++) {
2278 int dest_y = pixel_y + py;
2279 if (dest_y < 0 || dest_y >= bitmap.height())
2280 continue;
2281
2282 for (int px = 0; px < 8; px++) {
2283 int dest_x = pixel_x + px;
2284 if (dest_x < 0 || dest_x >= width)
2285 continue;
2286
2287 int dest_index = dest_y * width + dest_x;
2288
2289 // Coverage is set for all pixels in the tile footprint.
2290 if (dest_index >= 0 &&
2291 dest_index < static_cast<int>(coverage_buffer.size())) {
2292 coverage_buffer[dest_index] = 1;
2293 }
2294
2295 // Store priority only for opaque pixels; transparent writes clear stale
2296 // priority at this location.
2297 if (dest_index < static_cast<int>(bitmap_data.size()) &&
2298 bitmap_data[dest_index] != 255) {
2299 priority_buffer[dest_index] = priority;
2300 } else {
2301 priority_buffer[dest_index] = 0xFF;
2302 }
2303 }
2304 }
2305}
2306
2307bool ObjectDrawer::IsValidTilePosition(int tile_x, int tile_y) const {
2308 return tile_x >= 0 && tile_x < kMaxTilesX && tile_y >= 0 &&
2309 tile_y < kMaxTilesY;
2310}
2311
2313 const gfx::TileInfo& tile_info, int pixel_x,
2314 int pixel_y, const uint8_t* tiledata) {
2315 // Draw an 8x8 tile directly to bitmap at pixel coordinates
2316 // Graphics data is in 8BPP linear format (1 pixel per byte)
2317 if (!tiledata)
2318 return;
2319
2320 // DEBUG: Check if bitmap is valid
2321 if (!bitmap.is_active() || bitmap.width() == 0 || bitmap.height() == 0) {
2322 LOG_DEBUG("ObjectDrawer", "ERROR: Invalid bitmap - active=%d, size=%dx%d",
2323 bitmap.is_active(), bitmap.width(), bitmap.height());
2324 return;
2325 }
2326
2327 // Calculate tile position in 8BPP graphics buffer
2328 // Layout: 16 tiles per row, each tile is 8 pixels wide (8 bytes)
2329 // Row stride: 128 bytes (16 tiles * 8 bytes)
2330 // Buffer size: 0x10000 (65536 bytes) = 64 tile rows max
2331 constexpr int kGfxBufferSize = 0x10000;
2332 constexpr int kMaxTileRow = 63; // 64 rows (0-63), each 1024 bytes
2333
2334 int tile_col = tile_info.id_ % 16;
2335 int tile_row = tile_info.id_ / 16;
2336
2337 // CRITICAL: Validate tile_row to prevent index out of bounds
2338 if (tile_row > kMaxTileRow) {
2339 LOG_DEBUG("ObjectDrawer", "Tile ID 0x%03X out of bounds (row %d > %d)",
2340 tile_info.id_, tile_row, kMaxTileRow);
2341 return;
2342 }
2343
2344 int tile_base_x = tile_col * 8; // 8 bytes per tile horizontally
2345 int tile_base_y =
2346 tile_row * 1024; // 1024 bytes per tile row (8 rows * 128 bytes)
2347
2348 // DEBUG: Log first few tiles being drawn with their graphics data
2349 static int draw_debug_count = 0;
2350 if (draw_debug_count < 5) {
2351 int sample_index = tile_base_y + tile_base_x;
2352 LOG_DEBUG("ObjectDrawer",
2353 "DrawTile: id=%d (col=%d,row=%d) gfx_offset=%d (0x%04X)",
2354 tile_info.id_, tile_col, tile_row, sample_index, sample_index);
2355 draw_debug_count++;
2356 }
2357
2358 // Palette offset calculation using direct CGRAM row mirroring.
2359 //
2360 // Room::RenderRoomGraphics loads dungeon main palettes into SDL bank rows 2-7,
2361 // leaving rows 0-1 as transparent HUD placeholders. The tile palette bits are
2362 // therefore already the correct SDL bank row index.
2363 //
2364 // Drawing formula: final_color = pixel + (pal * 16)
2365 // Where pixel 0 = transparent (not written), pixel 1-15 = colors within bank.
2366 uint8_t pal = tile_info.palette_ & 0x07;
2367 const uint8_t palette_offset = static_cast<uint8_t>(pal * 16);
2368
2369 // Draw 8x8 pixels with overwrite semantics.
2370 //
2371 // Important SNES behavior: writing a tilemap entry replaces the previous
2372 // contents for the full 8x8 footprint. Source pixel 0 is transparent, but it
2373 // still clears what was there before. We model that by writing 255
2374 // (transparent key) for zero pixels.
2375 bool any_pixels_changed = false;
2376
2377 for (int py = 0; py < 8; py++) {
2378 // Source row with vertical mirroring
2379 int src_row = tile_info.vertical_mirror_ ? (7 - py) : py;
2380
2381 for (int px = 0; px < 8; px++) {
2382 // Source column with horizontal mirroring
2383 int src_col = tile_info.horizontal_mirror_ ? (7 - px) : px;
2384
2385 // Calculate source index in 8BPP buffer
2386 // Stride is 128 bytes (sheet width)
2387 int src_index = (src_row * 128) + src_col + tile_base_x + tile_base_y;
2388 uint8_t pixel = tiledata[src_index];
2389 uint8_t out_pixel = 255; // transparent/clear
2390 if (pixel != 0) {
2391 // Pixels 1-15 map into a 16-color bank chunk.
2392 out_pixel = static_cast<uint8_t>(pixel + palette_offset);
2393 }
2394
2395 int dest_x = pixel_x + px;
2396 int dest_y = pixel_y + py;
2397 if (dest_x < 0 || dest_x >= bitmap.width() || dest_y < 0 ||
2398 dest_y >= bitmap.height()) {
2399 continue;
2400 }
2401
2402 int dest_index = dest_y * bitmap.width() + dest_x;
2403 if (dest_index < 0 ||
2404 dest_index >= static_cast<int>(bitmap.mutable_data().size())) {
2405 continue;
2406 }
2407
2408 auto& dst = bitmap.mutable_data()[dest_index];
2409 if (dst != out_pixel) {
2410 dst = out_pixel;
2411 any_pixels_changed = true;
2412 }
2413 }
2414 }
2415
2416 if (any_pixels_changed) {
2417 bitmap.set_modified(true);
2418 }
2419}
2420
2422 uint16_t object_id, int tile_x, int tile_y, RoomObject::LayerType layer,
2423 uint16_t room_draw_object_data_offset, gfx::BackgroundBuffer& bg1,
2424 gfx::BackgroundBuffer& bg2) {
2425 if (!rom_ || !rom_->is_loaded()) {
2426 return absl::FailedPreconditionError("ROM not loaded");
2427 }
2428
2429 const auto& rom_data = rom_->vector();
2430 const int base =
2431 kRoomObjectTileAddress + static_cast<int>(room_draw_object_data_offset);
2432 if (base < 0 || base + 7 >= static_cast<int>(rom_data.size())) {
2433 return absl::OutOfRangeError(absl::StrFormat(
2434 "RoomDrawObjectData 2x2 out of range: base=0x%X", base));
2435 }
2436
2437 auto read_word = [&](int off) -> uint16_t {
2438 return static_cast<uint16_t>(rom_data[off]) |
2439 (static_cast<uint16_t>(rom_data[off + 1]) << 8);
2440 };
2441
2442 const uint16_t w0 = read_word(base + 0);
2443 const uint16_t w1 = read_word(base + 2);
2444 const uint16_t w2 = read_word(base + 4);
2445 const uint16_t w3 = read_word(base + 6);
2446
2447 const gfx::TileInfo t0 = gfx::WordToTileInfo(w0);
2448 const gfx::TileInfo t1 = gfx::WordToTileInfo(w1);
2449 const gfx::TileInfo t2 = gfx::WordToTileInfo(w2);
2450 const gfx::TileInfo t3 = gfx::WordToTileInfo(w3);
2451
2452 // Set trace context once; WriteTile8 will emit per-tile traces.
2453 RoomObject trace_obj(
2454 static_cast<int16_t>(object_id), static_cast<uint8_t>(tile_x),
2455 static_cast<uint8_t>(tile_y), 0, static_cast<uint8_t>(layer));
2456 SetTraceContext(trace_obj, layer);
2457
2458 gfx::BackgroundBuffer& target_bg =
2459 (layer == RoomObject::LayerType::BG2) ? bg2 : bg1;
2460
2461 // Column-major order (matches USDASM $BF/$CB/$C2/$CE writes).
2462 WriteTile8(target_bg, tile_x + 0, tile_y + 0, t0); // top-left
2463 WriteTile8(target_bg, tile_x + 0, tile_y + 1, t1); // bottom-left
2464 WriteTile8(target_bg, tile_x + 1, tile_y + 0, t2); // top-right
2465 WriteTile8(target_bg, tile_x + 1, tile_y + 1, t3); // bottom-right
2466
2467 return absl::OkStatus();
2468}
2469
2470// ============================================================================
2471// Type 3 / Special Routine Implementations
2472// ============================================================================
2473
2476 std::span<const gfx::TileInfo> tiles,
2477 int width, int height) {
2478 // Generic large object drawer
2479 if (tiles.size() >= static_cast<size_t>(width * height)) {
2480 for (int y = 0; y < height; ++y) {
2481 for (int x = 0; x < width; ++x) {
2482 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[y * width + x]);
2483 }
2484 }
2485 }
2486}
2487
2488} // namespace zelda3
2489} // namespace yaze
2490
2492 const RoomObject& object) {
2493 if (!routines_initialized_) {
2495 }
2496
2497 // Default size 16x16 (2x2 tiles)
2498 int width = 16;
2499 int height = 16;
2500
2501 int routine_id = GetDrawRoutineId(object.id_);
2502 int size = object.size_;
2503
2504 // Based on routine ID, calculate dimensions
2505 // This logic must match the draw routines
2506 switch (routine_id) {
2507 case 0: // DrawRightwards2x2_1to15or32
2508 case 4: // DrawRightwards2x2_1to16
2509 case 7: // DrawDownwards2x2_1to15or32
2510 case 11: // DrawDownwards2x2_1to16
2511 // 2x2 tiles repeated
2512 if (routine_id == 0 || routine_id == 7) {
2513 if (size == 0)
2514 size = 32;
2515 } else {
2516 size = size & 0x0F;
2517 if (size == 0)
2518 size = 16; // 0 usually means 16 for 1to16 routines
2519 }
2520
2521 if (routine_id == 0 || routine_id == 4) {
2522 // Rightwards: size * 2 tiles width, 2 tiles height
2523 width = size * 16;
2524 height = 16;
2525 } else {
2526 // Downwards: 2 tiles width, size * 2 tiles height
2527 width = 16;
2528 height = size * 16;
2529 }
2530 break;
2531
2532 case 1: // RoomDraw_Rightwards2x4_1to15or26 (layout walls 0x01-0x02)
2533 {
2534 // ASM: GetSize_1to15or26 - defaults to 26 when size is 0
2535 int effective_size = (size == 0) ? 26 : (size & 0x0F);
2536 // Draws 2x4 tiles repeated 'effective_size' times horizontally
2537 width = effective_size * 16; // 2 tiles wide per repetition
2538 height = 32; // 4 tiles tall
2539 break;
2540 }
2541 case DrawRoutineIds::kWeird2x4_1to16: { // Archery curtains (object 0xB5)
2542 const int count = (size & 0x0F) + 1;
2543 width = count * 16;
2544 height = 32;
2545 break;
2546 }
2547
2548 case 2: // RoomDraw_Rightwards2x4spaced4_1to16 (objects 0x03-0x04)
2549 case 3: // RoomDraw_Rightwards2x4spaced4_1to16_BothBG (objects 0x05-0x06)
2550 {
2551 // ASM: GetSize_1to16, so both routines repeat size + 1 times.
2552 size = size & 0x0F;
2553 int count = size + 1;
2554 width = count * 16; // 2 tiles wide per repetition (adjacent)
2555 height = 32; // 4 tiles tall
2556 break;
2557 }
2558
2559 case 5: // DrawDiagonalAcute_1to16
2560 case 6: // DrawDiagonalGrave_1to16
2561 {
2562 // ASM: RoomDraw_DiagonalAcute/Grave_1to16
2563 // Uses LDA #$0007; JSR RoomDraw_GetSize_1to16_timesA
2564 // count = size + 7
2565 // Each iteration draws 5 tiles vertically (RoomDraw_2x2and1 pattern)
2566 // Width = count tiles, Height = 5 tiles base + (count-1) diagonal offset
2567 size = size & 0x0F;
2568 int count = size + 7;
2569 width = count * 8;
2570 height = (count + 4) * 8; // 5 tiles + (count-1) = count + 4
2571 break;
2572 }
2573 case 17: // DrawDiagonalAcute_1to16_BothBG
2574 case 18: // DrawDiagonalGrave_1to16_BothBG
2575 {
2576 // ASM: RoomDraw_DiagonalAcute/Grave_1to16_BothBG
2577 // Uses LDA #$0006; JSR RoomDraw_GetSize_1to16_timesA
2578 // count = size + 6 (one less than non-BothBG)
2579 size = size & 0x0F;
2580 int count = size + 6;
2581 width = count * 8;
2582 height = (count + 4) * 8; // 5 tiles + (count-1) = count + 4
2583 break;
2584 }
2585
2586 case 8: // RoomDraw_Downwards4x2_1to15or26 (layout walls 0x61-0x62)
2587 {
2588 // ASM: GetSize_1to15or26 - defaults to 26 when size is 0
2589 int effective_size = (size == 0) ? 26 : (size & 0x0F);
2590 // Draws 4x2 tiles repeated 'effective_size' times vertically
2591 width = 32; // 4 tiles wide
2592 height = effective_size * 16; // 2 tiles tall per repetition
2593 break;
2594 }
2595 case 9: // RoomDraw_Downwards4x2_1to16_BothBG (objects 0x63-0x64)
2596 case 10: // RoomDraw_DownwardsDecor4x2spaced4_1to16 (objects 0x65-0x66)
2597 {
2598 // ASM: GetSize_1to16, draws 4x2 tiles with spacing
2599 size = size & 0x0F;
2600 int count = size + 1;
2601 width = 32; // 4 tiles wide
2602 height = count * 16; // 2 tiles tall per repetition (adjacent)
2603 break;
2604 }
2605
2606 case 12: // RoomDraw_DownwardsHasEdge1x1_1to16_plus3
2607 // ASM ($01:8EC3) uses GetSize_1to16_timesA with A=2, giving
2608 // count = size + 2 middle tiles. Total span (corner + middles + end) =
2609 // size + 4 tiles, matching the horizontal counterpart 0x22 (case 21).
2610 size = size & 0x0F;
2611 width = 8;
2612 height = (size + 4) * 8;
2613 break;
2614 case 13: // RoomDraw_DownwardsEdge1x1_1to16
2615 size = size & 0x0F;
2616 width = 8;
2617 height = (size + 1) * 8;
2618 break;
2619 case 14: // RoomDraw_DownwardsLeftCorners2x1_1to16_plus12
2620 case 15: // RoomDraw_DownwardsRightCorners2x1_1to16_plus12
2621 size = size & 0x0F;
2622 width = 16;
2623 height = (size + 14) * 8;
2624 break;
2625
2626 case 16: // DrawRightwards4x4_1to16 (Routine 16)
2627 {
2628 // 4x4 block repeated horizontally based on size
2629 // ASM: GetSize_1to16, count = (size & 0x0F) + 1
2630 int count = (size & 0x0F) + 1;
2631 width = 32 * count; // 4 tiles * 8 pixels * count
2632 height = 32; // 4 tiles * 8 pixels
2633 break;
2634 }
2637 width = 32;
2638 height = 16;
2639 break;
2641 width = 80;
2642 height = 32;
2643 break;
2644 case 19: // DrawCorner4x4 (Type 2 corners 0x100-0x103)
2645 case 34: // Water Face (4x4)
2646 case 35: // 4x4 Corner BothBG
2647 case 36: // Weird Corner Bottom
2648 case 37: // Weird Corner Top
2649 // 4x4 tiles (32x32 pixels) - fixed size, no repetition
2650 width = 32;
2651 height = 32;
2652 break;
2653 case 39: { // Chest routine (small or big)
2654 // Infer size from tile span: big chests provide >=16 tiles
2655 int tile_count = object.tiles().size();
2656 if (tile_count >= 16) {
2657 width = height = 32; // Big chest 4x4
2658 } else {
2659 width = height = 16; // Small chest 2x2
2660 }
2661 break;
2662 }
2663
2664 case 20: // Edge 1x2 (RoomDraw_Rightwards1x2_1to16_plus2)
2665 {
2666 // ZScream: width = size * 2 + 4, height = 3 tiles
2667 size = size & 0x0F;
2668 width = (size * 2 + 4) * 8;
2669 height = 24;
2670 break;
2671 }
2672
2673 case 21: // RoomDraw_RightwardsHasEdge1x1_1to16_plus3 (small rails 0x22)
2674 {
2675 // ZScream: count = size + 2 (corner + middle*count + end)
2676 size = size & 0x0F;
2677 width = (size + 4) * 8;
2678 height = 8;
2679 break;
2680 }
2681 case 22: // RoomDraw_RightwardsHasEdge1x1_1to16_plus2 (carpet trim 0x23-0x2E)
2682 {
2683 // ASM: GetSize_1to16, count = size + 1
2684 // Plus corner (1) + end (1) = count + 2 total width
2685 size = size & 0x0F;
2686 int count = size + 1;
2687 width = (count + 2) * 8; // corner + middle*count + end
2688 height = 8;
2689 break;
2690 }
2691 case 118: // RoomDraw_RightwardsHasEdge1x1_1to16_plus23 (long rails 0x5F)
2692 {
2693 size = size & 0x0F;
2694 width = (size + 23) * 8;
2695 height = 8;
2696 break;
2697 }
2699 size = size & 0x0F;
2700 width = 8;
2701 height = (size + 23) * 8;
2702 break;
2704 size = size & 0x0F;
2705 width = 8;
2706 height = (size + 8) * 8;
2707 break;
2708 case 25: // RoomDraw_Rightwards1x1Solid_1to16_plus3
2709 {
2710 // ASM: GetSize_1to16_timesA(4), so count = size + 4
2711 size = size & 0x0F;
2712 width = (size + 4) * 8;
2713 height = 8;
2714 break;
2715 }
2716
2717 case 23: // RightwardsTopCorners1x2_1to16_plus13
2718 case 24: // RightwardsBottomCorners1x2_1to16_plus13
2719 size = size & 0x0F;
2720 width = 8 + size * 8;
2721 height = 16;
2722 break;
2723
2724 case 26: // Door Switcher
2725 width = 32;
2726 height = 32;
2727 break;
2728
2729 case 27: // RoomDraw_RightwardsDecor4x4spaced2_1to16
2730 {
2731 // 4x4 tiles with 6-tile X spacing per repetition
2732 // ASM: s * 6 spacing, count = size + 1
2733 size = size & 0x0F;
2734 int count = size + 1;
2735 // Total width = (count - 1) * 6 (spacing) + 4 (last block)
2736 width = ((count - 1) * 6 + 4) * 8;
2737 height = 32; // 4 tiles
2738 break;
2739 }
2740
2741 case 28: // RoomDraw_RightwardsStatue2x3spaced2_1to16
2742 {
2743 // 2x3 tiles with 4-tile X spacing per repetition
2744 // ASM: s * 4 spacing, count = size + 1
2745 size = size & 0x0F;
2746 int count = size + 1;
2747 // Total width = (count - 1) * 4 (spacing) + 2 (last block)
2748 width = ((count - 1) * 4 + 2) * 8;
2749 height = 24; // 3 tiles
2750 break;
2751 }
2752
2753 case 29: // RoomDraw_RightwardsPillar2x4spaced4_1to16
2754 {
2755 // 2x4 tiles with 4-tile X spacing per repetition
2756 // ASM: ADC #$0008 = 4 tiles between starts
2757 size = size & 0x0F;
2758 int count = size + 1;
2759 // Total width = (count - 1) * 4 (spacing) + 2 (last block)
2760 width = ((count - 1) * 4 + 2) * 8;
2761 height = 32; // 4 tiles
2762 break;
2763 }
2764
2765 case 30: // RoomDraw_RightwardsDecor4x3spaced4_1to16
2766 {
2767 // 4x3 tiles with 8-tile X spacing per repetition
2768 // ASM: ADC #$0008 = 8-byte advance = 4 tiles gap between 4-tile objects
2769 size = size & 0x0F;
2770 int count = size + 1;
2771 // Total width = (count - 1) * 8 (spacing) + 4 (last block)
2772 width = ((count - 1) * 8 + 4) * 8;
2773 height = 24; // 3 tiles
2774 break;
2775 }
2776
2777 case 31: // RoomDraw_RightwardsDoubled2x2spaced2_1to16
2778 {
2779 // 4x2 tiles (doubled 2x2) with 6-tile X spacing
2780 // ASM: s * 6 spacing, count = size + 1
2781 size = size & 0x0F;
2782 int count = size + 1;
2783 // Total width = (count - 1) * 6 (spacing) + 4 (last block)
2784 width = ((count - 1) * 6 + 4) * 8;
2785 height = 16; // 2 tiles
2786 break;
2787 }
2788 case 32: // RoomDraw_RightwardsDecor2x2spaced12_1to16
2789 {
2790 // 2x2 tiles with 14-tile X spacing per repetition
2791 // ASM: s * 14 spacing, count = size + 1
2792 size = size & 0x0F;
2793 int count = size + 1;
2794 // Total width = (count - 1) * 14 (spacing) + 2 (last block)
2795 width = ((count - 1) * 14 + 2) * 8;
2796 height = 16; // 2 tiles
2797 break;
2798 }
2799
2800 case 33: // Somaria Line
2801 // Each subtype-3 path piece is one 8x8 tile.
2802 width = 8;
2803 height = 8;
2804 break;
2805
2806 case 38: // Nothing (RoomDraw_Nothing)
2807 width = 8;
2808 height = 8;
2809 break;
2810
2811 case 40: // Rightwards 4x2 (FloorTile)
2812 {
2813 // 4 cols x 2 rows, GetSize_1to16
2814 size = size & 0x0F;
2815 int count = size + 1;
2816 width = count * 4 * 8; // 4 tiles per repetition
2817 height = 16; // 2 tiles
2818 break;
2819 }
2820
2821 case 41: // Rightwards Decor 4x2 spaced 12 (wall torches 0x55-0x56)
2822 {
2823 // ASM: 4 columns x 2 rows with 12-tile horizontal spacing.
2824 size = size & 0x0F;
2825 int count = size + 1;
2826 width = ((count - 1) * 12 + 4) * 8;
2827 height = 16;
2828 break;
2829 }
2830
2831 case 42: // Rightwards Cannon Hole 4x3
2832 {
2833 // 4x3 tiles, GetSize_1to16
2834 size = size & 0x0F;
2835 int count = size + 1;
2836 width = count * 4 * 8;
2837 height = 24;
2838 break;
2839 }
2840
2841 case 43: // Downwards Floor 4x4
2842 {
2843 // 4x4 tiles, GetSize_1to16
2844 size = size & 0x0F;
2845 int count = size + 1;
2846 width = 32;
2847 height = count * 4 * 8;
2848 break;
2849 }
2850
2851 case 44: // Downwards 1x1 Solid +3
2852 {
2853 size = size & 0x0F;
2854 width = 8;
2855 height = (size + 4) * 8;
2856 break;
2857 }
2858
2859 case 45: // Downwards Decor 4x4 spaced 2
2860 {
2861 size = size & 0x0F;
2862 int count = size + 1;
2863 width = 32;
2864 height = ((count - 1) * 6 + 4) * 8;
2865 break;
2866 }
2867
2868 case 46: // Downwards Pillar 2x4 spaced 2
2869 {
2870 size = size & 0x0F;
2871 int count = size + 1;
2872 width = 16;
2873 height = ((count - 1) * 6 + 4) * 8;
2874 break;
2875 }
2876
2877 case 47: // Downwards Decor 3x4 spaced 4
2878 {
2879 size = size & 0x0F;
2880 int count = size + 1;
2881 width = 24;
2882 height = ((count - 1) * 6 + 4) * 8;
2883 break;
2884 }
2885
2886 case 48: // Downwards Decor 2x2 spaced 12
2887 {
2888 size = size & 0x0F;
2889 int count = size + 1;
2890 width = 16;
2891 height = ((count - 1) * 14 + 2) * 8;
2892 break;
2893 }
2894
2895 case 49: // Downwards Line 1x1 +1
2896 {
2897 size = size & 0x0F;
2898 width = 8;
2899 height = (size + 2) * 8;
2900 break;
2901 }
2902
2903 case 50: // Downwards Decor 2x4 spaced 8
2904 {
2905 size = size & 0x0F;
2906 int count = size + 1;
2907 width = 16;
2908 height = ((count - 1) * 12 + 4) * 8;
2909 break;
2910 }
2911
2912 case 51: // Rightwards Line 1x1 +1
2913 {
2914 size = size & 0x0F;
2915 width = (size + 2) * 8;
2916 height = 8;
2917 break;
2918 }
2919
2920 case 52: // Rightwards Bar 4x3
2921 {
2922 size = size & 0x0F;
2923 int count = size + 1;
2924 width = ((count - 1) * 6 + 4) * 8;
2925 height = 24;
2926 break;
2927 }
2928
2929 case 53: // Rightwards Shelf 4x4
2930 {
2931 size = size & 0x0F;
2932 int count = size + 1;
2933 width = ((count - 1) * 6 + 4) * 8;
2934 height = 32;
2935 break;
2936 }
2937
2938 case 54: // Rightwards Big Rail 1x3 +5
2939 {
2940 size = size & 0x0F;
2941 width = (size + 6) * 8;
2942 height = 24;
2943 break;
2944 }
2945
2946 case 55: // Rightwards Block 2x2 spaced 2
2947 {
2948 size = size & 0x0F;
2949 int count = size + 1;
2950 width = ((count - 1) * 4 + 2) * 8;
2951 height = 16;
2952 break;
2953 }
2954
2955 // Routines 56-64: SuperSquare patterns
2956 // ASM: Type1/Type3 objects pack 2-bit X/Y sizes into a 4-bit size:
2957 // size = (x_size << 2) | y_size, where x_size/y_size are 0..3 (meaning 1..4).
2958 // Each super square unit is 4 tiles (32 pixels) in each dimension.
2959 case 56: // 4x4BlocksIn4x4SuperSquare
2960 case 57: // 3x3FloorIn4x4SuperSquare
2961 case 58: // 4x4FloorIn4x4SuperSquare
2962 case 59: // 4x4FloorOneIn4x4SuperSquare
2963 case 60: // 4x4FloorTwoIn4x4SuperSquare
2964 case 62: // Spike2x2In4x4SuperSquare
2965 {
2966 int size_x = ((size >> 2) & 0x03) + 1;
2967 int size_y = (size & 0x03) + 1;
2968 width = size_x * 32; // 4 tiles per super square
2969 height = size_y * 32; // 4 tiles per super square
2970 break;
2971 }
2972 case 61: // BigHole4x4
2973 case 63: // TableRock4x4
2974 case 64: // WaterOverlay8x8
2975 width = 32;
2976 height = 32;
2977 break;
2978
2979 // Routines 65-74: Various downwards/rightwards patterns
2980 case 65: // DownwardsDecor3x4spaced2
2981 {
2982 size = size & 0x0F;
2983 int count = size + 1;
2984 width = 24;
2985 height = ((count - 1) * 5 + 4) * 8;
2986 break;
2987 }
2988
2989 case 66: // DownwardsBigRail3x1 +5
2990 {
2991 // Top cap (2x2) + Middle (2x1 x count) + Bottom cap (2x3)
2992 // Total: 2 tiles wide, 2 + (size+1) + 3 = size + 6 tiles tall
2993 size = size & 0x0F;
2994 width = 16; // 2 tiles wide
2995 height = (size + 6) * 8;
2996 break;
2997 }
2998
2999 case 67: // DownwardsBlock2x2spaced2
3000 {
3001 size = size & 0x0F;
3002 int count = size + 1;
3003 width = 16;
3004 height = ((count - 1) * 4 + 2) * 8;
3005 break;
3006 }
3007
3008 case 68: // DownwardsCannonHole3x4
3009 {
3010 size = size & 0x0F;
3011 width = 24;
3012 // Height = repeated 3x2 segment (size+1) + final 3x2 edge segment.
3013 // => (2 * (size + 2)) tiles.
3014 height = (2 * (size + 2)) * 8;
3015 break;
3016 }
3017
3018 case 69: // DownwardsBar2x5
3019 {
3020 size = size & 0x0F;
3021 width = 16;
3022 // 1 top row + 2*(size+2) body rows.
3023 height = (2 * size + 5) * 8;
3024 break;
3025 }
3026
3027 case 70: // DownwardsPots2x2
3028 case 71: // DownwardsHammerPegs2x2
3029 {
3030 size = size & 0x0F;
3031 int count = size + 1;
3032 width = 16;
3033 height = count * 2 * 8;
3034 break;
3035 }
3036
3037 case 72: // RightwardsEdge1x1 +7
3038 {
3039 size = size & 0x0F;
3040 width = (size + 8) * 8;
3041 height = 8;
3042 break;
3043 }
3044
3045 case 73: // RightwardsPots2x2
3046 case 74: // RightwardsHammerPegs2x2
3047 {
3048 size = size & 0x0F;
3049 int count = size + 1;
3050 width = count * 2 * 8;
3051 height = 16;
3052 break;
3053 }
3054
3055 // Diagonal ceilings (75-78) - TRIANGLE shapes
3056 // Draw uses count = (size & 0x0F) + 4
3057 // Outline uses smaller size since triangle only fills half the square area
3058 case 75: // DiagonalCeilingTopLeft - triangle at origin
3059 case 76: // DiagonalCeilingBottomLeft - triangle at origin
3060 {
3061 // Smaller outline for triangle - use half the drawn area
3062 int count = (size & 0x0F) + 2;
3063 width = count * 8;
3064 height = count * 8;
3065 break;
3066 }
3067 case 77: // DiagonalCeilingTopRight - triangle shifts diagonally
3068 case 78: // DiagonalCeilingBottomRight - triangle shifts diagonally
3069 {
3070 // Smaller outline for diagonal triangles
3071 int count = (size & 0x0F) + 2;
3072 width = count * 8;
3073 height = count * 8;
3074 break;
3075 }
3076
3077 case 79: { // ClosedChestPlatform
3078 int size_x = (size >> 2) & 0x03;
3079 int size_y = size & 0x03;
3080 width = (size_x * 2 + 14) * 8;
3081 height = (size_y * 2 + 8) * 8;
3082 break;
3083 }
3084
3085 // Special platform routines (80-82)
3086 case 80: // MovingWallWest
3087 case 81: // MovingWallEast
3089
3090 case 82: { // OpenChestPlatform
3091 int size_x = (size >> 2) & 0x03;
3092 int size_y = size & 0x03;
3093 width = (size_x * 2 + 10) * 8;
3094 height = (size_y * 2 + 7) * 8;
3095 break;
3096 }
3097
3098 // Stair routines - different sizes for different types
3099
3100 // 4x4 stair patterns (32x32 pixels)
3101 case 83: // InterRoomFatStairsUp (0x12D)
3102 case 84: // InterRoomFatStairsDownA (0x12E)
3103 case 85: // InterRoomFatStairsDownB (0x12F)
3104 case 86: // AutoStairs (0x130-0x133)
3105 case 87: // StraightInterroomStairs (0xF9E-0xFA9)
3106 width = 32; // 4 tiles
3107 height = 32; // 4 tiles (4x4 pattern)
3108 break;
3109
3110 // 4x3 stair patterns (32x24 pixels)
3111 case 88: // SpiralStairsGoingUpUpper (0x138)
3112 case 89: // SpiralStairsGoingDownUpper (0x139)
3113 case 90: // SpiralStairsGoingUpLower (0x13A)
3114 case 91: // SpiralStairsGoingDownLower (0x13B)
3115 // ASM: RoomDraw_1x3N_rightwards with A=4 -> 4 columns x 3 rows
3116 width = 32; // 4 tiles
3117 height = 24; // 3 tiles
3118 break;
3119
3120 case 92: // BigKeyLock
3121 width = 16;
3122 height = 16;
3123 break;
3124
3125 case 93: // BombableFloor
3126 width = 32;
3127 height = 32;
3128 break;
3129
3130 case 94: // EmptyWaterFace
3131 width = 32;
3132 // Report the larger stateful footprint so editor bounds do not
3133 // undershoot the active 4x5 branch.
3134 height = 40;
3135 break;
3136
3137 case 95: // SpittingWaterFace
3138 width = 32;
3139 height = 40;
3140 break;
3141
3142 case 96: // DrenchingWaterFace
3143 width = 32;
3144 height = 56;
3145 break;
3146
3147 case 97: // PrisonCell
3148 width = 128; // 16 tiles
3149 height = 32; // 4 tiles
3150 break;
3151
3152 case 98: // Bed4x5
3153 width = 32;
3154 height = 40;
3155 break;
3156
3157 case 99: // Rightwards3x6
3158 width = 48; // 6 tiles
3159 height = 24; // 3 tiles
3160 break;
3161
3162 case 100: // Utility6x3
3163 width = 48;
3164 height = 24;
3165 break;
3166
3167 case 101: // Utility3x5
3168 width = 24;
3169 height = 40;
3170 break;
3171
3172 case 102: // VerticalTurtleRockPipe
3173 width = 32;
3174 height = 48;
3175 break;
3176
3177 case 103: // HorizontalTurtleRockPipe
3178 width = 48;
3179 height = 32;
3180 break;
3181
3182 case 104: // LightBeam
3183 width = 32; // 4 tiles
3184 height = 80;
3185 break;
3186
3187 case 105: // BigLightBeam
3188 width = 64;
3189 height = 64;
3190 break;
3191
3193 width = 64;
3194 height = 64;
3195 break;
3196
3197 case 106: // BossShell4x4
3198 width = 32;
3199 height = 32;
3200 break;
3201
3203 width = 160;
3204 height = 64;
3205 break;
3206
3207 case 107: // SolidWallDecor3x4
3208 width = 24;
3209 height = 32;
3210 break;
3211
3212 case 108: // ArcheryGameTargetDoor
3213 width = 24;
3214 height = 48;
3215 break;
3216
3217 case 109: // GanonTriforceFloorDecor
3218 width = 64;
3219 height = 64;
3220 break;
3221
3222 case 110: // Single2x2
3223 width = 16;
3224 height = 16;
3225 break;
3226
3227 case 111: // Waterfall47 (object 0x47)
3228 {
3229 // ASM: count = (size+1)*2, draws 1x5 columns
3230 // Width = first column + middle columns + last column = 2 + count tiles
3231 size = size & 0x0F;
3232 int count = (size + 1) * 2;
3233 width = (2 + count) * 8;
3234 height = 40; // 5 tiles
3235 break;
3236 }
3237 case 112: // Waterfall48 (object 0x48)
3238 {
3239 // ASM: count = (size+1)*2, draws 1x3 columns
3240 // Width = first column + middle columns + last column = 2 + count tiles
3241 size = size & 0x0F;
3242 int count = (size + 1) * 2;
3243 width = (2 + count) * 8;
3244 height = 24; // 3 tiles
3245 break;
3246 }
3247
3248 case 113: // Single4x4 (no repetition) - 4x4 TILE16 = 8x8 TILE8
3249 // ASM RoomDraw_4x4 = 4x4 tile8.
3250 width = 32;
3251 height = 32;
3252 break;
3253
3254 case 114: // Single4x3 (no repetition)
3255 // 4 tiles wide x 3 tiles tall = 32x24 pixels
3256 width = 32;
3257 height = 24;
3258 break;
3259
3260 case 115: // RupeeFloor (special pattern)
3261 // Columns at x + 0, +2, +4 bound a 5x8-tile area = 40x64 pixels.
3262 width = 40;
3263 height = 64;
3264 break;
3265
3266 case 116: // Actual4x4 (true 4x4 tile8 pattern, no repetition)
3267 // 4 tile8s x 4 tile8s = 32x32 pixels
3268 width = 32;
3269 height = 32;
3270 break;
3271
3273 width = 64;
3274 height = 24;
3275 break;
3276
3278 width = 32;
3279 height = 16;
3280 break;
3281
3283 width = 48;
3284 height = 64;
3285 break;
3286
3288 width = 32;
3289 height = 32;
3290 break;
3291
3293 width = 112;
3294 height = 112;
3295 break;
3296
3298 width = 112;
3299 height = 112;
3300 break;
3301
3303 width = 64;
3304 height = 56;
3305 break;
3306
3307 default:
3308 // Fallback to naive calculation if not handled
3309 // Matches DungeonCanvasViewer::DrawRoomObjects logic
3310 {
3311 int size_h = (object.size_ & 0x0F);
3312 int size_v = (object.size_ >> 4) & 0x0F;
3313 width = (size_h + 1) * 8;
3314 height = (size_v + 1) * 8;
3315 }
3316 break;
3317 }
3318
3319 return {width, height};
3320}
3321
3323 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3324 [[maybe_unused]] std::span<const gfx::TileInfo> tiles,
3325 [[maybe_unused]] const DungeonState* state) {
3326 // CustomObjectManager should be initialized by DungeonEditorV2 with the
3327 // project's custom_objects_folder path before any objects are drawn
3328 auto& manager = CustomObjectManager::Get();
3329
3330 int subtype = obj.size_ & 0x1F;
3331 const std::string filename = manager.ResolveFilename(obj.id_, subtype);
3332 auto result = manager.GetObjectInternal(obj.id_, subtype);
3333 if (!result.ok()) {
3334 DrawMissingCustomObjectPlaceholder(bg, obj.x_, obj.y_);
3335 LOG_DEBUG("ObjectDrawer",
3336 "Custom object 0x%03X subtype %d (%s) not found: %s", obj.id_,
3337 subtype, filename.empty() ? "<unmapped>" : filename.c_str(),
3338 result.status().message().data());
3339 return;
3340 }
3341
3342 auto custom_obj = result.value();
3343 if (!custom_obj || custom_obj->IsEmpty())
3344 return;
3345
3346 int tile_x = obj.x_;
3347 int tile_y = obj.y_;
3348
3349 for (const auto& entry : custom_obj->tiles) {
3350 // entry.tile_data is vhopppcc cccccccc (SNES tilemap word format)
3351 // Convert to TileInfo and render using WriteTile8 (not SetTileAt which
3352 // only stores to buffer without rendering)
3353 gfx::TileInfo tile_info = gfx::WordToTileInfo(entry.tile_data);
3354 WriteTile8(bg, tile_x + entry.rel_x, tile_y + entry.rel_y, tile_info);
3355 }
3356}
3357
3359 gfx::BackgroundBuffer& bg, int tile_x, int tile_y) {
3360 if (trace_only_) {
3361 return;
3362 }
3363
3364 auto& bitmap = bg.bitmap();
3365 if (!bitmap.is_active() || bitmap.width() <= 0 || bitmap.height() <= 0) {
3366 return;
3367 }
3368
3369 auto& pixels = bitmap.mutable_data();
3370 auto& coverage = bg.mutable_coverage_data();
3371 auto& priority = bg.mutable_priority_data();
3372
3373 constexpr int kPlaceholderSizePx = 16;
3374 constexpr uint8_t kFillColor = 33;
3375 constexpr uint8_t kAccentColor = 47;
3376
3377 const int start_x = tile_x * 8;
3378 const int start_y = tile_y * 8;
3379 const int width = bitmap.width();
3380 const int height = bitmap.height();
3381
3382 bg.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, start_x, start_y,
3383 kPlaceholderSizePx, kPlaceholderSizePx);
3384
3385 for (int py = 0; py < kPlaceholderSizePx; ++py) {
3386 const int dest_y = start_y + py;
3387 if (dest_y < 0 || dest_y >= height)
3388 continue;
3389 for (int px = 0; px < kPlaceholderSizePx; ++px) {
3390 const int dest_x = start_x + px;
3391 if (dest_x < 0 || dest_x >= width)
3392 continue;
3393 const bool border = (px == 0 || py == 0 || px == kPlaceholderSizePx - 1 ||
3394 py == kPlaceholderSizePx - 1);
3395 const bool diagonal = (px == py) || (px + py == kPlaceholderSizePx - 1);
3396 const int dest_index = dest_y * width + dest_x;
3397 pixels[dest_index] = (border || diagonal) ? kAccentColor : kFillColor;
3398 if (dest_index < static_cast<int>(coverage.size()))
3399 coverage[dest_index] = 1;
3400 if (dest_index < static_cast<int>(priority.size()))
3401 priority[dest_index] = 0;
3402 }
3403 }
3404 bitmap.set_modified(true);
3405}
3406
3407void yaze::zelda3::ObjectDrawer::DrawPotItem(uint8_t item_id, int x, int y,
3409 // Draw a small colored indicator for pot items
3410 // Item types from ZELDA3_DUNGEON_SPEC.md Section 7.2
3411 // Uses palette indices that map to recognizable colors
3412
3413 if (item_id == 0)
3414 return; // Nothing - skip
3415
3416 auto& bitmap = bg.bitmap();
3417 auto& coverage_buffer = bg.mutable_coverage_data();
3418 if (!bitmap.is_active() || bitmap.width() == 0)
3419 return;
3420
3421 // Convert tile coordinates to pixel coordinates
3422 // Items are drawn offset from pot position (centered on pot)
3423 int pixel_x = (x * 8) + 2; // Offset 2 pixels into the pot tile
3424 int pixel_y = (y * 8) + 2;
3425
3426 // Choose color based on item category
3427 // Using palette indices that should be visible in dungeon palettes
3428 uint8_t color_idx;
3429 switch (item_id) {
3430 // Rupees (green/blue/red tones)
3431 case 1: // Green rupee
3432 case 7: // Blue rupee
3433 case 12: // Blue rupee variant
3434 color_idx = 30; // Greenish (palette 2, index 0)
3435 break;
3436
3437 // Hearts (red tones)
3438 case 6: // Heart
3439 case 11: // Heart
3440 case 13: // Heart variant
3441 // NOTE: Avoid palette indices 0/16/32/.. which are transparent in SNES
3442 // CGRAM rows. Using 5 gives a consistently visible indicator.
3443 color_idx = 5;
3444 break;
3445
3446 // Keys (yellow/gold)
3447 case 8: // Key*8
3448 case 19: // Key
3449 color_idx = 45; // Yellowish (palette 3)
3450 break;
3451
3452 // Bombs (dark/black)
3453 case 5: // Bomb
3454 case 10: // 1 bomb
3455 case 16: // Bomb refill
3456 color_idx = 60; // Darker color (palette 4)
3457 break;
3458
3459 // Arrows (brown/wood)
3460 case 9: // Arrow
3461 case 17: // Arrow refill
3462 color_idx = 15; // Brownish (palette 1)
3463 break;
3464
3465 // Magic (blue/purple)
3466 case 14: // Small magic
3467 case 15: // Big magic
3468 color_idx = 75; // Bluish (palette 5)
3469 break;
3470
3471 // Fairy (pink/light)
3472 case 18: // Fairy
3473 case 20: // Fairy*8
3474 color_idx = 5; // Pinkish
3475 break;
3476
3477 // Special/Traps (distinct colors)
3478 case 2: // Rock crab
3479 case 3: // Bee
3480 color_idx = 20; // Enemy indicator
3481 break;
3482
3483 case 23: // Hole
3484 case 24: // Warp
3485 case 25: // Staircase
3486 color_idx = 10; // Transport indicator
3487 break;
3488
3489 case 26: // Bombable
3490 case 27: // Switch
3491 color_idx = 35; // Interactive indicator
3492 break;
3493
3494 case 4: // Random
3495 default:
3496 color_idx = 50; // Default/random indicator
3497 break;
3498 }
3499
3500 // Safety: never use CGRAM transparent slots (0,16,32,...) for the indicator.
3501 // In the editor these would appear invisible or "missing" depending on
3502 // compositing.
3503 if (color_idx != 255 && (color_idx % 16) == 0) {
3504 color_idx++;
3505 }
3506
3507 // Draw a 4x4 colored square as item indicator
3508 int bitmap_width = bitmap.width();
3509 int bitmap_height = bitmap.height();
3510
3511 bg.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, pixel_x, pixel_y, 4, 4);
3512
3513 for (int py = 0; py < 4; py++) {
3514 for (int px = 0; px < 4; px++) {
3515 int dest_x = pixel_x + px;
3516 int dest_y = pixel_y + py;
3517
3518 // Bounds check
3519 if (dest_x >= 0 && dest_x < bitmap_width && dest_y >= 0 &&
3520 dest_y < bitmap_height) {
3521 int offset = (dest_y * bitmap_width) + dest_x;
3522 bitmap.WriteToPixel(offset, color_idx);
3523 if (offset >= 0 && offset < static_cast<int>(coverage_buffer.size())) {
3524 coverage_buffer[offset] = 1;
3525 }
3526 }
3527 }
3528 }
3529}
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
const auto & vector() const
Definition rom.h:173
auto data() const
Definition rom.h:169
auto size() const
Definition rom.h:168
bool is_loaded() const
Definition rom.h:155
static Flags & get()
Definition features.h:119
void SetBG1RevealMaskRect(BG1RevealMaskSource source, int start_x, int start_y, int width, int height)
std::vector< uint8_t > & mutable_bg1_reveal_mask_data()
std::vector< uint8_t > & mutable_priority_data()
std::vector< uint8_t > & mutable_coverage_data()
void ClearBG1RevealMaskRect(BG1RevealMaskSource source, int start_x, int start_y, int width, int height)
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
const uint8_t * data() const
Definition bitmap.h:398
auto size() const
Definition bitmap.h:397
bool is_active() const
Definition bitmap.h:405
void set_modified(bool modified)
Definition bitmap.h:409
int height() const
Definition bitmap.h:395
int width() const
Definition bitmap.h:394
int depth() const
Definition bitmap.h:396
std::vector< uint8_t > & mutable_data()
Definition bitmap.h:399
SDL_Surface * surface() const
Definition bitmap.h:400
bool modified() const
Definition bitmap.h:404
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)
static DimensionService & Get()
std::tuple< int, int, int, int > GetSelectionBoundsPixels(const RoomObject &obj) const
std::pair< int, int > GetPixelDimensions(const RoomObject &obj) const
const DrawRoutineInfo * GetRoutineInfo(int routine_id) const
bool RoutineDrawsToBothBGs(int routine_id) const
int GetRoutineIdForObject(int16_t object_id) const
static DrawRoutineRegistry & Get()
Interface for accessing dungeon game state.
virtual bool IsDoorOpen(int room_id, int door_index) const =0
virtual bool IsBigKeyLockOpen(int room_id, int room_event_index) const
virtual bool IsBigChestOpen() const =0
static ObjectDimensionTable & Get()
Draws dungeon objects to background buffers using game patterns.
int GetDrawRoutineId(int16_t object_id) const
Get draw routine ID for an object.
void WriteTile8(gfx::BackgroundBuffer &bg, int tile_x, int tile_y, const gfx::TileInfo &tile_info)
void DrawTileToBitmap(gfx::Bitmap &bitmap, const gfx::TileInfo &tile_info, int pixel_x, int pixel_y, const uint8_t *tiledata)
Draw a single tile directly to bitmap.
void InitializeDrawRoutines()
Initialize draw routine registry Must be called before drawing objects.
void DrawRightwards4x4_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
gfx::BackgroundBuffer * active_mask_source_bg_
void DrawBigChest(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
std::vector< TileTrace > * trace_collector_
gfx::BackgroundBuffer * active_layout_bg1_mask_
std::vector< DrawRoutine > draw_routines_
void DrawUsingRegistryRoutine(int routine_id, const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state)
gfx::BackgroundBuffer * registry_secondary_bg_
static bool RequiresRectangularBg1Mask(const RoomObject &object)
static void TraceHookThunk(gfx::BackgroundBuffer *bg, int tile_x, int tile_y, const gfx::TileInfo &tile_info, void *user_data)
void MarkBg1OpaqueTilePixelsRevealed(gfx::BackgroundBuffer &bg1, const gfx::TileInfo &tile_info, int pixel_x, int pixel_y, const uint8_t *tiledata)
std::pair< int, int > CalculateObjectDimensions(const RoomObject &object)
Calculate the dimensions (width, height) of an object in pixels.
void SetTraceContext(const RoomObject &object, RoomObject::LayerType layer)
void MarkBg1RectRevealed(gfx::BackgroundBuffer &bg1, int start_px, int start_py, int pixel_width, int pixel_height)
gfx::BackgroundBuffer * active_object_bg1_mask_
const uint8_t * room_gfx_buffer_
static bool RoutineDrawsToBothBGs(int routine_id)
void DrawNothing(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDoor(const DoorDef &door, int door_index, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2, const DungeonState *state=nullptr)
Draw a door to background buffers.
void DrawBigKeyLock(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
RoomObject::LayerType registry_primary_layer_
void DrawPotItem(uint8_t item_id, int x, int y, gfx::BackgroundBuffer &bg)
Draw a pot item visualization.
void DrawDoorIndicator(gfx::BackgroundBuffer &bg, int tile_x, int tile_y, int width, int height, DoorType type, DoorDirection direction)
void DrawLargeCanvasObject(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, int width, int height)
void PushTrace(int tile_x, int tile_y, const gfx::TileInfo &tile_info)
void DrawCustomObject(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwardsDecor4x3spaced4_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawChest(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
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)
Draw all objects in a room.
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)
ObjectDrawer(Rom *rom, int room_id, const uint8_t *room_gfx_buffer=nullptr)
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)
Draw a room object to background buffers.
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_
RoomObject::LayerType registry_secondary_layer_
const std::vector< gfx::TileInfo > & tiles() const
void SetRom(Rom *rom)
Definition room_object.h:80
#define LOG_DEBUG(category, format,...)
Definition log.h:103
#define LOG_WARN(category, format,...)
Definition log.h:107
TileInfo WordToTileInfo(uint16_t word)
Definition snes_tile.cc:378
void SetTraceHook(TraceHookFn hook, void *user_data, bool trace_only)
void SyncModifiedBitmapToSurface(gfx::Bitmap &bitmap, const char *layer_name)
constexpr DoorDimensions GetDoorDimensions(DoorDirection dir)
Get door dimensions based on direction.
Definition door_types.h:235
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.
@ SmallKeyStairsDown
Small key stairs (downwards)
@ BombableCaveExit
Bombable cave exit.
@ SmallKeyStairsUp
Small key stairs (upwards)
@ 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 std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
Definition door_types.h:204
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
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
Room transition destination.
Definition zelda.h:448
Represents a group of palettes.
int width_tiles
Width in 8x8 tiles.
Definition door_types.h:222
Context passed to draw routines containing all necessary state.
gfx::BackgroundBuffer & target_bg
Metadata about a draw routine.
std::pair< int, int > GetTileCoords() const
DoorDimensions GetDimensions() const