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 (Type 3 objects 0x272, 0x27B, 0xF95)
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 // Routine 130 - Custom Object (Oracle of Secrets 0x31, 0x32)
1474 // Uses external binary files instead of ROM tile data.
1475 // Requires CustomObjectManager initialization and enable_custom_objects flag.
1476 ensure_index(130);
1477 draw_routines_[130] = [](ObjectDrawer* self, const RoomObject& obj,
1479 std::span<const gfx::TileInfo> tiles,
1480 [[maybe_unused]] const DungeonState* state) {
1481 self->DrawCustomObject(obj, bg, tiles, state);
1482 };
1483
1484 routines_initialized_ = true;
1485}
1486
1487int ObjectDrawer::GetDrawRoutineId(int16_t object_id) const {
1488 // Delegate to the unified registry for the canonical mapping
1490}
1491
1492// ============================================================================
1493// Draw Routine Implementations (Based on ZScream patterns)
1494// ============================================================================
1495
1496void ObjectDrawer::DrawDoor(const DoorDef& door, int door_index,
1499 const DungeonState* state) {
1500 // Door rendering based on ZELDA3_DUNGEON_SPEC.md Section 5 and disassembly
1501 // Uses DoorType and DoorDirection enums for type safety
1502 // Position calculations via DoorPositionManager
1503
1504 LOG_DEBUG("ObjectDrawer", "DrawDoor: idx=%d type=%d dir=%d pos=%d",
1505 door_index, static_cast<int>(door.type),
1506 static_cast<int>(door.direction), door.position);
1507
1508 if (!rom_ || !rom_->is_loaded() || !room_gfx_buffer_) {
1509 LOG_DEBUG("ObjectDrawer", "DrawDoor: SKIPPED - rom=%p loaded=%d gfx=%p",
1510 (void*)rom_, rom_ ? rom_->is_loaded() : 0,
1511 (void*)room_gfx_buffer_);
1512 return;
1513 }
1514
1515 auto& bitmap = bg1.bitmap();
1516 if (!bitmap.is_active() || bitmap.width() == 0) {
1517 LOG_DEBUG("ObjectDrawer",
1518 "DrawDoor: SKIPPED - bitmap not active or zero width");
1519 return;
1520 }
1521
1522 const bool is_door_open = state && state->IsDoorOpen(room_id_, door_index);
1523
1524 // Get door position from DoorPositionManager
1525 auto [tile_x, tile_y] = door.GetTileCoords();
1526 auto dims = door.GetDimensions();
1527 int door_width = dims.width_tiles;
1528 int door_height = dims.height_tiles;
1529
1530 LOG_DEBUG("ObjectDrawer", "DrawDoor: tile_pos=(%d,%d) dims=%dx%d", tile_x,
1531 tile_y, door_width, door_height);
1532
1533 constexpr int kRoomDrawObjectDataBase = 0x1B52;
1534 constexpr int kDoorwayReplacementDoorGfxBase = 0x1A02;
1535 constexpr int kExplodingWallTilemapPositionBase = 0x19DE;
1536 constexpr int kExplodingWallOpenReplacementType = 0x54;
1537 constexpr int kNorthCurtainClosedOffset = 0x078A;
1538 const auto& rom_data = rom_->data();
1539
1540 auto draw_from_object_data = [&](gfx::BackgroundBuffer& target,
1541 int start_tile_x, int start_tile_y,
1542 int width, int height, int tile_data_addr) {
1543 auto& bitmap = target.bitmap();
1544 auto& priority_buffer = target.mutable_priority_data();
1545 auto& coverage_buffer = target.mutable_coverage_data();
1546 const int bitmap_width = bitmap.width();
1547 int tile_idx = 0;
1548
1549 for (int dx = 0; dx < width; dx++) {
1550 for (int dy = 0; dy < height; dy++) {
1551 const int addr = tile_data_addr + (tile_idx * 2);
1552 const uint16_t tile_word = rom_data[addr] | (rom_data[addr + 1] << 8);
1553 const auto tile_info = gfx::WordToTileInfo(tile_word);
1554 const int pixel_x = (start_tile_x + dx) * 8;
1555 const int pixel_y = (start_tile_y + dy) * 8;
1556
1557 target.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, pixel_x, pixel_y,
1558 8, 8);
1559 DrawTileToBitmap(bitmap, tile_info, pixel_x, pixel_y, room_gfx_buffer_);
1560
1561 const uint8_t priority = tile_info.over_ ? 1 : 0;
1562 const auto& bitmap_data = bitmap.vector();
1563 for (int py = 0; py < 8; py++) {
1564 const int dest_y = pixel_y + py;
1565 if (dest_y < 0 || dest_y >= bitmap.height()) {
1566 continue;
1567 }
1568 for (int px = 0; px < 8; px++) {
1569 const int dest_x = pixel_x + px;
1570 if (dest_x < 0 || dest_x >= bitmap_width) {
1571 continue;
1572 }
1573 const int dest_index = dest_y * bitmap_width + dest_x;
1574 if (dest_index >= 0 &&
1575 dest_index < static_cast<int>(coverage_buffer.size())) {
1576 coverage_buffer[dest_index] = 1;
1577 }
1578 if (dest_index < static_cast<int>(bitmap_data.size()) &&
1579 bitmap_data[dest_index] != 255) {
1580 priority_buffer[dest_index] = priority;
1581 }
1582 }
1583 }
1584
1585 tile_idx++;
1586 }
1587 }
1588 };
1589
1590 auto draw_repeated_tile = [&](gfx::BackgroundBuffer& target, int start_tile_x,
1591 int start_tile_y, int width, int height,
1592 uint16_t tile_word) {
1593 auto& bitmap = target.bitmap();
1594 auto& priority_buffer = target.mutable_priority_data();
1595 auto& coverage_buffer = target.mutable_coverage_data();
1596 const int bitmap_width = bitmap.width();
1597 const auto tile_info = gfx::WordToTileInfo(tile_word);
1598
1599 for (int dx = 0; dx < width; dx++) {
1600 for (int dy = 0; dy < height; dy++) {
1601 const int pixel_x = (start_tile_x + dx) * 8;
1602 const int pixel_y = (start_tile_y + dy) * 8;
1603
1604 target.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, pixel_x, pixel_y,
1605 8, 8);
1606 DrawTileToBitmap(bitmap, tile_info, pixel_x, pixel_y, room_gfx_buffer_);
1607
1608 const uint8_t priority = tile_info.over_ ? 1 : 0;
1609 const auto& bitmap_data = bitmap.vector();
1610 for (int py = 0; py < 8; py++) {
1611 const int dest_y = pixel_y + py;
1612 if (dest_y < 0 || dest_y >= bitmap.height()) {
1613 continue;
1614 }
1615 for (int px = 0; px < 8; px++) {
1616 const int dest_x = pixel_x + px;
1617 if (dest_x < 0 || dest_x >= bitmap_width) {
1618 continue;
1619 }
1620 const int dest_index = dest_y * bitmap_width + dest_x;
1621 if (dest_index >= 0 &&
1622 dest_index < static_cast<int>(coverage_buffer.size())) {
1623 coverage_buffer[dest_index] = 1;
1624 }
1625 if (dest_index < static_cast<int>(bitmap_data.size()) &&
1626 bitmap_data[dest_index] != 255) {
1627 priority_buffer[dest_index] = priority;
1628 }
1629 }
1630 }
1631 }
1632 }
1633 };
1634
1635 auto tilemap_offset_to_tile_coords = [](uint16_t offset) {
1636 return std::pair<int, int>{static_cast<int>((offset % 0x80) / 2),
1637 static_cast<int>(offset / 0x80) - 4};
1638 };
1639 const int position_index = std::min<int>(door.position & 0x0F, 11);
1640
1641 auto resolve_render_type = [](DoorDirection render_direction,
1642 DoorType render_type) {
1643 switch (render_type) {
1645 return (render_direction == DoorDirection::North ||
1646 render_direction == DoorDirection::West)
1650 return (render_direction == DoorDirection::North ||
1651 render_direction == DoorDirection::West)
1655 return (render_direction == DoorDirection::North ||
1656 render_direction == DoorDirection::West)
1660 return (render_direction == DoorDirection::North ||
1661 render_direction == DoorDirection::West)
1664 default:
1665 return render_type;
1666 }
1667 };
1668
1669 auto draw_table_door = [&](gfx::BackgroundBuffer& target,
1670 DoorDirection render_direction, int start_tile_x,
1671 int start_tile_y, DoorType render_type) -> bool {
1672 int offset_table_addr = 0;
1673 switch (render_direction) {
1675 offset_table_addr = kDoorGfxUp;
1676 break;
1678 offset_table_addr = kDoorGfxDown;
1679 break;
1681 offset_table_addr = kDoorGfxLeft;
1682 break;
1684 offset_table_addr = kDoorGfxRight;
1685 break;
1686 }
1687
1688 const DoorType resolved_type =
1689 resolve_render_type(render_direction, render_type);
1690 const int render_type_value = static_cast<int>(resolved_type);
1691 const int type_index = render_type_value / 2;
1692 const int table_entry_addr = offset_table_addr + (type_index * 2);
1693 if (table_entry_addr + 1 >= static_cast<int>(rom_->size())) {
1694 return false;
1695 }
1696
1697 const uint16_t tile_offset =
1698 rom_data[table_entry_addr] | (rom_data[table_entry_addr + 1] << 8);
1699 const int tile_data_addr = kRoomDrawObjectDataBase + tile_offset;
1700 const auto dims = GetDoorDimensions(render_direction);
1701 const int data_size = dims.width_tiles * dims.height_tiles * 2;
1702 if (tile_data_addr < 0 ||
1703 tile_data_addr + data_size > static_cast<int>(rom_->size())) {
1704 return false;
1705 }
1706
1707 draw_from_object_data(target, start_tile_x, start_tile_y, dims.width_tiles,
1708 dims.height_tiles, tile_data_addr);
1709 return true;
1710 };
1711
1712 // USDASM has special north-door branches that do not follow the generic 4x3
1713 // ranged-door path.
1714 if (door.direction == DoorDirection::North &&
1715 door.type == DoorType::ExplodingWall && !is_door_open) {
1716 LOG_DEBUG("ObjectDrawer",
1717 "DrawDoor: closed exploding wall intentionally draws nothing");
1718 (void)bg2;
1719 return;
1720 }
1721
1722 if (door.direction == DoorDirection::North &&
1723 door.type == DoorType::CurtainDoor && !is_door_open) {
1724 const int tile_data_addr =
1725 kRoomDrawObjectDataBase + kNorthCurtainClosedOffset;
1726 const int data_size = 16 * 2; // RoomDraw_4x4 closed curtain path.
1727 if (tile_data_addr < 0 ||
1728 tile_data_addr + data_size > static_cast<int>(rom_->size())) {
1729 DrawDoorIndicator(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
1730 door.type, door.direction);
1731 return;
1732 }
1733 draw_from_object_data(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
1734 tile_data_addr);
1735 return;
1736 }
1737
1738 if (door.direction == DoorDirection::North &&
1739 door.type == DoorType::CurtainDoor && is_door_open) {
1740 const int replacement_type_addr =
1741 kDoorwayReplacementDoorGfxBase + static_cast<int>(door.type);
1742 if (replacement_type_addr < 0 ||
1743 replacement_type_addr >= static_cast<int>(rom_->size())) {
1744 DrawDoorIndicator(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
1745 door.type, door.direction);
1746 return;
1747 }
1748
1749 const int replacement_type = rom_data[replacement_type_addr];
1750 const int table_entry_addr = kDoorGfxUp + replacement_type;
1751 if (table_entry_addr < 0 ||
1752 table_entry_addr + 1 >= static_cast<int>(rom_->size())) {
1753 DrawDoorIndicator(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
1754 door.type, door.direction);
1755 return;
1756 }
1757
1758 const uint16_t tile_offset =
1759 rom_data[table_entry_addr] | (rom_data[table_entry_addr + 1] << 8);
1760 const int tile_data_addr = kRoomDrawObjectDataBase + tile_offset;
1761 const int data_size = 16 * 2; // RoomDraw_4x4 open curtain path.
1762 if (tile_data_addr < 0 ||
1763 tile_data_addr + data_size > static_cast<int>(rom_->size())) {
1764 DrawDoorIndicator(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
1765 door.type, door.direction);
1766 return;
1767 }
1768
1769 draw_from_object_data(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
1770 tile_data_addr);
1771 return;
1772 }
1773
1774 if (door.direction == DoorDirection::North &&
1775 door.type == DoorType::ExplodingWall && is_door_open) {
1776 const int position_index = std::min<int>(door.position & 0x0F, 5);
1777 const int tilemap_entry_addr =
1778 kExplodingWallTilemapPositionBase + (position_index * 2);
1779 if (tilemap_entry_addr < 0 ||
1780 tilemap_entry_addr + 1 >= static_cast<int>(rom_->size())) {
1781 DrawDoorIndicator(bg1, tile_x, tile_y, /*width=*/4, /*height=*/4,
1782 door.type, door.direction);
1783 return;
1784 }
1785
1786 const uint16_t tilemap_offset =
1787 rom_data[tilemap_entry_addr] | (rom_data[tilemap_entry_addr + 1] << 8);
1788 const auto explosion_tile_coords =
1789 tilemap_offset_to_tile_coords(tilemap_offset);
1790 const int explosion_tile_x = explosion_tile_coords.first;
1791 const int explosion_tile_y = explosion_tile_coords.second;
1792
1793 auto draw_exploding_wall_segment = [&](int table_entry_addr,
1794 int segment_tile_y) -> bool {
1795 if (table_entry_addr < 0 ||
1796 table_entry_addr + 1 >= static_cast<int>(rom_->size())) {
1797 return false;
1798 }
1799
1800 const uint16_t tile_offset =
1801 rom_data[table_entry_addr] | (rom_data[table_entry_addr + 1] << 8);
1802 const int tile_data_addr = kRoomDrawObjectDataBase + tile_offset;
1803 constexpr int kFillWordIndex = 12;
1804 const int min_data_size = (kFillWordIndex + 1) * 2;
1805 if (tile_data_addr < 0 ||
1806 tile_data_addr + min_data_size > static_cast<int>(rom_->size())) {
1807 return false;
1808 }
1809
1810 draw_from_object_data(bg1, explosion_tile_x, segment_tile_y,
1811 /*width=*/2, /*height=*/6, tile_data_addr);
1812 const uint16_t fill_word =
1813 rom_data[tile_data_addr + (kFillWordIndex * 2)] |
1814 (rom_data[tile_data_addr + (kFillWordIndex * 2) + 1] << 8);
1815 draw_repeated_tile(bg1, explosion_tile_x + 2, segment_tile_y,
1816 /*width=*/18, /*height=*/6, fill_word);
1817 return true;
1818 };
1819
1820 const int south_table_entry_addr =
1821 kDoorGfxDown + kExplodingWallOpenReplacementType;
1822 const int north_table_entry_addr =
1823 kDoorGfxUp + kExplodingWallOpenReplacementType;
1824 if (!draw_exploding_wall_segment(south_table_entry_addr,
1825 explosion_tile_y) ||
1826 !draw_exploding_wall_segment(north_table_entry_addr,
1827 explosion_tile_y + 6)) {
1828 DrawDoorIndicator(bg1, explosion_tile_x, explosion_tile_y, /*width=*/20,
1829 /*height=*/12, door.type, door.direction);
1830 }
1831 return;
1832 }
1833
1834 // Door graphics use an indirect addressing scheme:
1835 // 1. kDoorGfxUp/Down/Left/Right point to offset tables (DoorGFXDataOffset_*)
1836 // 2. Each table entry is a 16-bit offset into RoomDrawObjectData
1837 // 3. RoomDrawObjectData base is at PC 0x1B52 (SNES $00:9B52)
1838 // 4. Actual tile data = 0x1B52 + offset_from_table
1839 if ((door.direction == DoorDirection::North ||
1840 door.direction == DoorDirection::West) &&
1841 position_index >= 6 && door.type != DoorType::ExplicitRoomDoor) {
1842 const DoorDirection counterpart_direction =
1845 const int counterpart_tile_x =
1846 tile_x + (counterpart_direction == DoorDirection::East ? 1 : 0);
1847 const int counterpart_tile_y =
1848 tile_y + (counterpart_direction == DoorDirection::South ? 1 : 0);
1849 (void)draw_table_door(bg1, counterpart_direction, counterpart_tile_x,
1850 counterpart_tile_y, door.type);
1851 }
1852
1853 const bool drew_current =
1854 draw_table_door(bg1, door.direction, tile_x, tile_y, door.type);
1855 if (!drew_current) {
1856 LOG_DEBUG("ObjectDrawer",
1857 "DrawDoor: INVALID ADDRESS - falling back to indicator");
1858 DrawDoorIndicator(bg1, tile_x, tile_y, door_width, door_height, door.type,
1859 door.direction);
1860 return;
1861 }
1862
1863 LOG_DEBUG("ObjectDrawer",
1864 "DrawDoor: type=%s dir=%s pos=%d at tile(%d,%d) size=%dx%d",
1865 std::string(GetDoorTypeName(door.type)).c_str(),
1866 std::string(GetDoorDirectionName(door.direction)).c_str(),
1867 door.position, tile_x, tile_y, door_width, door_height);
1868}
1869
1871 int tile_y, int width, int height,
1872 DoorType type, DoorDirection direction) {
1873 // Draw a simple colored rectangle as door indicator when graphics unavailable
1874 // Different colors for different door types using DoorType enum
1875
1876 auto& bitmap = bg.bitmap();
1877 auto& coverage_buffer = bg.mutable_coverage_data();
1878
1879 uint8_t color_idx;
1880 switch (type) {
1883 color_idx = 45; // Standard door color (brown)
1884 break;
1885
1891 color_idx = 60; // Key door - yellowish
1892 break;
1893
1896 color_idx = 58; // Big key - golden
1897 break;
1898
1902 case DoorType::DashWall:
1903 color_idx = 15; // Bombable/destructible - brownish/cracked
1904 break;
1905
1912 color_idx = 30; // Shutter - greenish
1913 break;
1914
1916 color_idx = 42; // Eye watch - lighter brown
1917 break;
1918
1920 color_idx = 35; // Curtain - special
1921 break;
1922
1923 case DoorType::CaveExit:
1928 color_idx = 25; // Cave/dungeon exit - dark
1929 break;
1930
1934 color_idx = 5; // Markers - very faint
1935 break;
1936
1937 default:
1938 color_idx = 50; // Default door color
1939 break;
1940 }
1941
1942 int pixel_x = tile_x * 8;
1943 int pixel_y = tile_y * 8;
1944 int pixel_width = width * 8;
1945 int pixel_height = height * 8;
1946
1947 int bitmap_width = bitmap.width();
1948 int bitmap_height = bitmap.height();
1949
1951 pixel_width, pixel_height);
1952
1953 // Draw filled rectangle with border
1954 for (int py = 0; py < pixel_height; py++) {
1955 for (int px = 0; px < pixel_width; px++) {
1956 int dest_x = pixel_x + px;
1957 int dest_y = pixel_y + py;
1958
1959 if (dest_x >= 0 && dest_x < bitmap_width && dest_y >= 0 &&
1960 dest_y < bitmap_height) {
1961 // Draw border (2 pixel thick) or fill
1962 bool is_border = (px < 2 || px >= pixel_width - 2 || py < 2 ||
1963 py >= pixel_height - 2);
1964 uint8_t final_color = is_border ? (color_idx + 5) : color_idx;
1965
1966 int offset = (dest_y * bitmap_width) + dest_x;
1967 bitmap.WriteToPixel(offset, final_color);
1968
1969 if (offset >= 0 && offset < static_cast<int>(coverage_buffer.size())) {
1970 coverage_buffer[offset] = 1;
1971 }
1972 }
1973 }
1974 }
1975}
1976
1978 std::span<const gfx::TileInfo> tiles,
1979 [[maybe_unused]] const DungeonState* state) {
1980 // USDASM RoomDraw_OpenChest draws F9A's fixed open graphic directly and
1981 // does not read or advance either chest/event counter.
1982 if (obj.id_ == 0xF9A) {
1984 return;
1985 }
1986
1987 // USDASM RoomDraw_Chest draws F99 as a single stateful 2x2 chest. The size
1988 // byte is not used for repetition.
1989
1990 // Determine if chest is open
1991 bool is_open = false;
1992 if (state) {
1993 is_open = state->IsChestOpen(room_id_, current_chest_index_);
1994 }
1995
1996 // RoomDraw_Chest advances the chest-only $0496 counter, then copies its next
1997 // value into the shared chest/lock $0498 counter.
2000
2001 // Draw SINGLE chest - no repetition based on size
2002 // Standard chests are 2x2 (4 tiles)
2003 // If we have extra tiles loaded, the second 4 are for open state
2004
2005 if (is_open && tiles.size() >= 8) {
2006 // Small chest open tiles (indices 4-7) - SINGLE 2x2 draw
2007 if (tiles.size() >= 8) {
2008 WriteTile8(bg, obj.x_, obj.y_, tiles[4]); // top-left
2009 WriteTile8(bg, obj.x_, obj.y_ + 1, tiles[5]); // bottom-left
2010 WriteTile8(bg, obj.x_ + 1, obj.y_, tiles[6]); // top-right
2011 WriteTile8(bg, obj.x_ + 1, obj.y_ + 1, tiles[7]); // bottom-right
2012 }
2013 return;
2014 }
2015
2016 // Draw closed chest - SINGLE 2x2 pattern (column-major order)
2017 if (tiles.size() >= 4) {
2018 WriteTile8(bg, obj.x_, obj.y_, tiles[0]); // top-left
2019 WriteTile8(bg, obj.x_, obj.y_ + 1, tiles[1]); // bottom-left
2020 WriteTile8(bg, obj.x_ + 1, obj.y_, tiles[2]); // top-right
2021 WriteTile8(bg, obj.x_ + 1, obj.y_ + 1, tiles[3]); // bottom-right
2022 }
2023}
2024
2027 std::span<const gfx::TileInfo> tiles,
2028 const DungeonState* state) {
2029 // USDASM RoomDraw_BigChest uses the chest-only $0496 slot for its room flag,
2030 // advances it once, then copies the next value into shared $0498. FB2 uses
2031 // RoomDraw_OpenBigChest directly and never reaches this stateful wrapper.
2032 bool is_open = false;
2033 if (state) {
2034 is_open = state->IsBigChestOpen(room_id_, current_chest_index_);
2035 }
2036
2039
2040 constexpr size_t kBigChestStateTileCount = 12;
2041 if (is_open && tiles.size() >= kBigChestStateTileCount * 2) {
2042 tiles = tiles.subspan(kBigChestStateTileCount, kBigChestStateTileCount);
2043 }
2045}
2046
2049 std::span<const gfx::TileInfo> tiles,
2050 const DungeonState* state) {
2051 // USDASM RoomDraw_BigKeyLock indexes $0402 through the shared $0498
2052 // chest/lock slot. An opened lock advances the slot but writes no tiles.
2053 const int room_event_index = current_room_event_index_++;
2054 if (state && state->IsBigKeyLockOpen(room_id_, room_event_index)) {
2055 return;
2056 }
2057
2059}
2060
2062 std::span<const gfx::TileInfo> tiles,
2063 [[maybe_unused]] const DungeonState* state) {
2064 // Intentionally empty - represents invisible logic objects or placeholders
2065 // ASM: RoomDraw_Nothing_A ($0190F2), RoomDraw_Nothing_B ($01932E), etc.
2066 // These routines typically just RTS.
2067 LOG_DEBUG("ObjectDrawer", "DrawNothing for object 0x%02X (logic/invisible)",
2068 obj.id_);
2069}
2070
2072 std::span<const gfx::TileInfo> tiles,
2073 [[maybe_unused]] const DungeonState* state) {
2074 // Pattern: Custom draw routine (objects 0x31-0x32)
2075 // For now, fall back to simple 1x1
2076 if (tiles.size() >= 1) {
2077 // Use first 8x8 tile from span
2078 WriteTile8(bg, obj.x_, obj.y_, tiles[0]);
2079 }
2080}
2081
2083 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2084 std::span<const gfx::TileInfo> tiles,
2085 [[maybe_unused]] const DungeonState* state) {
2086 // Pattern: 4x4 block rightward (objects 0x33, 0xBA = large ceiling, etc.)
2087 int size = obj.size_ & 0x0F;
2088
2089 // Assembly: GetSize_1to16, so count = size + 1
2090 int count = size + 1;
2091
2092 // Debug: Log large ceiling objects (0xBA)
2093 if (obj.id_ == 0xBA && tiles.size() >= 16) {
2094 LOG_DEBUG("ObjectDrawer",
2095 "Large Ceiling Draw: obj=0x%02X pos=(%d,%d) size=%d tiles=%zu",
2096 obj.id_, obj.x_, obj.y_, size, tiles.size());
2097 LOG_DEBUG("ObjectDrawer", " First 4 Tile IDs: [%d, %d, %d, %d]",
2098 tiles[0].id_, tiles[1].id_, tiles[2].id_, tiles[3].id_);
2099 LOG_DEBUG("ObjectDrawer", " First 4 Palettes: [%d, %d, %d, %d]",
2100 tiles[0].palette_, tiles[1].palette_, tiles[2].palette_,
2101 tiles[3].palette_);
2102 }
2103
2104 for (int s = 0; s < count; s++) {
2105 if (tiles.size() >= 16) {
2106 // Draw 4x4 pattern in COLUMN-MAJOR order (matching assembly)
2107 // Iterate columns (x) first, then rows (y) within each column
2108 for (int x = 0; x < 4; ++x) {
2109 for (int y = 0; y < 4; ++y) {
2110 WriteTile8(bg, obj.x_ + (s * 4) + x, obj.y_ + y, tiles[x * 4 + y]);
2111 }
2112 }
2113 }
2114 }
2115}
2116
2118 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2119 std::span<const gfx::TileInfo> tiles,
2120 [[maybe_unused]] const DungeonState* state) {
2121 // Pattern: 4x3 decoration with spacing (objects 0x3A-0x3B)
2122 // 4 columns × 3 rows = 12 tiles in COLUMN-MAJOR order
2123 // ASM: ADC #$0008 to Y = 8-byte advance = 4 tiles per iteration
2124 // Total spacing: 4 (object width) + 4 (gap) = 8 tiles between starts
2125 int size = obj.size_ & 0x0F;
2126
2127 // Assembly: GetSize_1to16, so count = size + 1
2128 int count = size + 1;
2129
2130 for (int s = 0; s < count; s++) {
2131 if (tiles.size() >= 12) {
2132 // Draw 4x3 pattern in COLUMN-MAJOR order (matching assembly)
2133 // Spacing: 8 tiles (4 object + 4 gap) per ASM ADC #$0008
2134 for (int x = 0; x < 4; ++x) {
2135 for (int y = 0; y < 3; ++y) {
2136 WriteTile8(bg, obj.x_ + (s * 8) + x, obj.y_ + y, tiles[x * 3 + y]);
2137 }
2138 }
2139 }
2140 }
2141}
2142
2143// ============================================================================
2144// Utility Methods
2145// ============================================================================
2146
2148 int start_py, int pixel_width,
2149 int pixel_height) {
2150 bg1.SetBG1RevealMaskRect(bg1_reveal_mask_source_, start_px, start_py,
2151 pixel_width, pixel_height);
2152}
2153
2155 gfx::BackgroundBuffer& bg1, const gfx::TileInfo& tile_info, int pixel_x,
2156 int pixel_y, const uint8_t* tiledata) {
2157 auto& bitmap = bg1.bitmap();
2158 if (!bitmap.is_active() || bitmap.width() == 0 || bitmap.height() == 0 ||
2159 tiledata == nullptr) {
2160 return;
2161 }
2162
2163 constexpr int kMaxTileRow = 63;
2164 const int tile_col = tile_info.id_ % 16;
2165 const int tile_row = tile_info.id_ / 16;
2166 if (tile_row > kMaxTileRow) {
2167 return;
2168 }
2169
2170 const int tile_base_x = tile_col * 8;
2171 const int tile_base_y = tile_row * 1024;
2172 auto& reveal_mask = bg1.mutable_bg1_reveal_mask_data();
2173 const uint8_t source_mask = static_cast<uint8_t>(bg1_reveal_mask_source_);
2174
2175 for (int py = 0; py < 8; ++py) {
2176 const int src_row = tile_info.vertical_mirror_ ? (7 - py) : py;
2177 const int dest_y = pixel_y + py;
2178 if (dest_y < 0 || dest_y >= bitmap.height()) {
2179 continue;
2180 }
2181
2182 for (int px = 0; px < 8; ++px) {
2183 const int src_col = tile_info.horizontal_mirror_ ? (7 - px) : px;
2184 const int src_index =
2185 (src_row * 128) + src_col + tile_base_x + tile_base_y;
2186 if (tiledata[src_index] == 0) {
2187 continue;
2188 }
2189
2190 const int dest_x = pixel_x + px;
2191 if (dest_x < 0 || dest_x >= bitmap.width()) {
2192 continue;
2193 }
2194
2195 const int dest_index = dest_y * bitmap.width() + dest_x;
2196 reveal_mask[dest_index] |= source_mask;
2197 }
2198 }
2199}
2200
2201void ObjectDrawer::WriteTile8(gfx::BackgroundBuffer& bg, int tile_x, int tile_y,
2202 const gfx::TileInfo& tile_info) {
2203 if (!IsValidTilePosition(tile_x, tile_y)) {
2204 return;
2205 }
2206 PushTrace(tile_x, tile_y, tile_info);
2207 if (trace_only_) {
2208 return;
2209 }
2210 // Draw directly to bitmap instead of tile buffer to avoid being overwritten
2211 auto& bitmap = bg.bitmap();
2212 if (!bitmap.is_active() || bitmap.width() == 0) {
2213 return; // Bitmap not ready
2214 }
2215
2216 // The room-specific graphics buffer (current_gfx16_) contains the assembled
2217 // tile graphics for the current room. Object tile IDs are relative to this
2218 // buffer.
2219 const uint8_t* gfx_data = room_gfx_buffer_;
2220
2221 if (!gfx_data) {
2222 LOG_DEBUG("ObjectDrawer", "ERROR: No graphics data available");
2223 return;
2224 }
2225
2226 // A later BG1 tilemap write supersedes an earlier reveal request from the
2227 // same logical stream, including transparent pixels in the 8x8 footprint.
2228 bg.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, tile_x * 8, tile_y * 8, 8,
2229 8);
2230
2231 const bool should_mark_bg1_mask =
2232 active_mask_source_bg_ != nullptr && (&bg == active_mask_source_bg_);
2233 // Draw single 8x8 tile directly to bitmap.
2234 DrawTileToBitmap(bitmap, tile_info, tile_x * 8, tile_y * 8, gfx_data);
2235 if (should_mark_bg1_mask && active_object_bg1_mask_ != nullptr) {
2237 tile_x * 8, tile_y * 8, gfx_data);
2238 }
2239 if (should_mark_bg1_mask && active_layout_bg1_mask_ != nullptr) {
2241 tile_x * 8, tile_y * 8, gfx_data);
2242 }
2243
2244 // Mark coverage for the full 8x8 tile region (even if pixels are transparent).
2245 //
2246 // This distinguishes "tilemap entry written but transparent" from "no write",
2247 // which is required to emulate SNES behavior where a transparent tile still
2248 // overwrites the previous tilemap entry (clearing BG1 and revealing BG2/backdrop).
2249 auto& coverage_buffer = bg.mutable_coverage_data();
2250
2251 // Also update priority buffer with tile's priority bit.
2252 // Priority (over_) affects Z-ordering in SNES Mode 1 compositing.
2253 uint8_t priority = tile_info.over_ ? 1 : 0;
2254 int pixel_x = tile_x * 8;
2255 int pixel_y = tile_y * 8;
2256 auto& priority_buffer = bg.mutable_priority_data();
2257 int width = bitmap.width();
2258
2259 // Update priority for each pixel in the 8x8 tile
2260 const auto& bitmap_data = bitmap.vector();
2261 for (int py = 0; py < 8; py++) {
2262 int dest_y = pixel_y + py;
2263 if (dest_y < 0 || dest_y >= bitmap.height())
2264 continue;
2265
2266 for (int px = 0; px < 8; px++) {
2267 int dest_x = pixel_x + px;
2268 if (dest_x < 0 || dest_x >= width)
2269 continue;
2270
2271 int dest_index = dest_y * width + dest_x;
2272
2273 // Coverage is set for all pixels in the tile footprint.
2274 if (dest_index >= 0 &&
2275 dest_index < static_cast<int>(coverage_buffer.size())) {
2276 coverage_buffer[dest_index] = 1;
2277 }
2278
2279 // Store priority only for opaque pixels; transparent writes clear stale
2280 // priority at this location.
2281 if (dest_index < static_cast<int>(bitmap_data.size()) &&
2282 bitmap_data[dest_index] != 255) {
2283 priority_buffer[dest_index] = priority;
2284 } else {
2285 priority_buffer[dest_index] = 0xFF;
2286 }
2287 }
2288 }
2289}
2290
2291bool ObjectDrawer::IsValidTilePosition(int tile_x, int tile_y) const {
2292 return tile_x >= 0 && tile_x < kMaxTilesX && tile_y >= 0 &&
2293 tile_y < kMaxTilesY;
2294}
2295
2297 const gfx::TileInfo& tile_info, int pixel_x,
2298 int pixel_y, const uint8_t* tiledata) {
2299 // Draw an 8x8 tile directly to bitmap at pixel coordinates
2300 // Graphics data is in 8BPP linear format (1 pixel per byte)
2301 if (!tiledata)
2302 return;
2303
2304 // DEBUG: Check if bitmap is valid
2305 if (!bitmap.is_active() || bitmap.width() == 0 || bitmap.height() == 0) {
2306 LOG_DEBUG("ObjectDrawer", "ERROR: Invalid bitmap - active=%d, size=%dx%d",
2307 bitmap.is_active(), bitmap.width(), bitmap.height());
2308 return;
2309 }
2310
2311 // Calculate tile position in 8BPP graphics buffer
2312 // Layout: 16 tiles per row, each tile is 8 pixels wide (8 bytes)
2313 // Row stride: 128 bytes (16 tiles * 8 bytes)
2314 // Buffer size: 0x10000 (65536 bytes) = 64 tile rows max
2315 constexpr int kGfxBufferSize = 0x10000;
2316 constexpr int kMaxTileRow = 63; // 64 rows (0-63), each 1024 bytes
2317
2318 int tile_col = tile_info.id_ % 16;
2319 int tile_row = tile_info.id_ / 16;
2320
2321 // CRITICAL: Validate tile_row to prevent index out of bounds
2322 if (tile_row > kMaxTileRow) {
2323 LOG_DEBUG("ObjectDrawer", "Tile ID 0x%03X out of bounds (row %d > %d)",
2324 tile_info.id_, tile_row, kMaxTileRow);
2325 return;
2326 }
2327
2328 int tile_base_x = tile_col * 8; // 8 bytes per tile horizontally
2329 int tile_base_y =
2330 tile_row * 1024; // 1024 bytes per tile row (8 rows * 128 bytes)
2331
2332 // DEBUG: Log first few tiles being drawn with their graphics data
2333 static int draw_debug_count = 0;
2334 if (draw_debug_count < 5) {
2335 int sample_index = tile_base_y + tile_base_x;
2336 LOG_DEBUG("ObjectDrawer",
2337 "DrawTile: id=%d (col=%d,row=%d) gfx_offset=%d (0x%04X)",
2338 tile_info.id_, tile_col, tile_row, sample_index, sample_index);
2339 draw_debug_count++;
2340 }
2341
2342 // Palette offset calculation using direct CGRAM row mirroring.
2343 //
2344 // Room::RenderRoomGraphics loads dungeon main palettes into SDL bank rows 2-7,
2345 // leaving rows 0-1 as transparent HUD placeholders. The tile palette bits are
2346 // therefore already the correct SDL bank row index.
2347 //
2348 // Drawing formula: final_color = pixel + (pal * 16)
2349 // Where pixel 0 = transparent (not written), pixel 1-15 = colors within bank.
2350 uint8_t pal = tile_info.palette_ & 0x07;
2351 const uint8_t palette_offset = static_cast<uint8_t>(pal * 16);
2352
2353 // Draw 8x8 pixels with overwrite semantics.
2354 //
2355 // Important SNES behavior: writing a tilemap entry replaces the previous
2356 // contents for the full 8x8 footprint. Source pixel 0 is transparent, but it
2357 // still clears what was there before. We model that by writing 255
2358 // (transparent key) for zero pixels.
2359 bool any_pixels_changed = false;
2360
2361 for (int py = 0; py < 8; py++) {
2362 // Source row with vertical mirroring
2363 int src_row = tile_info.vertical_mirror_ ? (7 - py) : py;
2364
2365 for (int px = 0; px < 8; px++) {
2366 // Source column with horizontal mirroring
2367 int src_col = tile_info.horizontal_mirror_ ? (7 - px) : px;
2368
2369 // Calculate source index in 8BPP buffer
2370 // Stride is 128 bytes (sheet width)
2371 int src_index = (src_row * 128) + src_col + tile_base_x + tile_base_y;
2372 uint8_t pixel = tiledata[src_index];
2373 uint8_t out_pixel = 255; // transparent/clear
2374 if (pixel != 0) {
2375 // Pixels 1-15 map into a 16-color bank chunk.
2376 out_pixel = static_cast<uint8_t>(pixel + palette_offset);
2377 }
2378
2379 int dest_x = pixel_x + px;
2380 int dest_y = pixel_y + py;
2381 if (dest_x < 0 || dest_x >= bitmap.width() || dest_y < 0 ||
2382 dest_y >= bitmap.height()) {
2383 continue;
2384 }
2385
2386 int dest_index = dest_y * bitmap.width() + dest_x;
2387 if (dest_index < 0 ||
2388 dest_index >= static_cast<int>(bitmap.mutable_data().size())) {
2389 continue;
2390 }
2391
2392 auto& dst = bitmap.mutable_data()[dest_index];
2393 if (dst != out_pixel) {
2394 dst = out_pixel;
2395 any_pixels_changed = true;
2396 }
2397 }
2398 }
2399
2400 if (any_pixels_changed) {
2401 bitmap.set_modified(true);
2402 }
2403}
2404
2406 uint16_t object_id, int tile_x, int tile_y, RoomObject::LayerType layer,
2407 uint16_t room_draw_object_data_offset, gfx::BackgroundBuffer& bg1,
2408 gfx::BackgroundBuffer& bg2) {
2409 if (!rom_ || !rom_->is_loaded()) {
2410 return absl::FailedPreconditionError("ROM not loaded");
2411 }
2412
2413 const auto& rom_data = rom_->vector();
2414 const int base =
2415 kRoomObjectTileAddress + static_cast<int>(room_draw_object_data_offset);
2416 if (base < 0 || base + 7 >= static_cast<int>(rom_data.size())) {
2417 return absl::OutOfRangeError(absl::StrFormat(
2418 "RoomDrawObjectData 2x2 out of range: base=0x%X", base));
2419 }
2420
2421 auto read_word = [&](int off) -> uint16_t {
2422 return static_cast<uint16_t>(rom_data[off]) |
2423 (static_cast<uint16_t>(rom_data[off + 1]) << 8);
2424 };
2425
2426 const uint16_t w0 = read_word(base + 0);
2427 const uint16_t w1 = read_word(base + 2);
2428 const uint16_t w2 = read_word(base + 4);
2429 const uint16_t w3 = read_word(base + 6);
2430
2431 const gfx::TileInfo t0 = gfx::WordToTileInfo(w0);
2432 const gfx::TileInfo t1 = gfx::WordToTileInfo(w1);
2433 const gfx::TileInfo t2 = gfx::WordToTileInfo(w2);
2434 const gfx::TileInfo t3 = gfx::WordToTileInfo(w3);
2435
2436 // Set trace context once; WriteTile8 will emit per-tile traces.
2437 RoomObject trace_obj(
2438 static_cast<int16_t>(object_id), static_cast<uint8_t>(tile_x),
2439 static_cast<uint8_t>(tile_y), 0, static_cast<uint8_t>(layer));
2440 SetTraceContext(trace_obj, layer);
2441
2442 gfx::BackgroundBuffer& target_bg =
2443 (layer == RoomObject::LayerType::BG2) ? bg2 : bg1;
2444
2445 // Column-major order (matches USDASM $BF/$CB/$C2/$CE writes).
2446 WriteTile8(target_bg, tile_x + 0, tile_y + 0, t0); // top-left
2447 WriteTile8(target_bg, tile_x + 0, tile_y + 1, t1); // bottom-left
2448 WriteTile8(target_bg, tile_x + 1, tile_y + 0, t2); // top-right
2449 WriteTile8(target_bg, tile_x + 1, tile_y + 1, t3); // bottom-right
2450
2451 return absl::OkStatus();
2452}
2453
2454// ============================================================================
2455// Type 3 / Special Routine Implementations
2456// ============================================================================
2457
2460 std::span<const gfx::TileInfo> tiles,
2461 int width, int height) {
2462 // Generic large object drawer
2463 if (tiles.size() >= static_cast<size_t>(width * height)) {
2464 for (int y = 0; y < height; ++y) {
2465 for (int x = 0; x < width; ++x) {
2466 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[y * width + x]);
2467 }
2468 }
2469 }
2470}
2471
2472} // namespace zelda3
2473} // namespace yaze
2474
2476 const RoomObject& object) {
2477 if (!routines_initialized_) {
2479 }
2480
2481 // Default size 16x16 (2x2 tiles)
2482 int width = 16;
2483 int height = 16;
2484
2485 int routine_id = GetDrawRoutineId(object.id_);
2486 int size = object.size_;
2487
2488 // Based on routine ID, calculate dimensions
2489 // This logic must match the draw routines
2490 switch (routine_id) {
2491 case 0: // DrawRightwards2x2_1to15or32
2492 case 4: // DrawRightwards2x2_1to16
2493 case 7: // DrawDownwards2x2_1to15or32
2494 case 11: // DrawDownwards2x2_1to16
2495 // 2x2 tiles repeated
2496 if (routine_id == 0 || routine_id == 7) {
2497 if (size == 0)
2498 size = 32;
2499 } else {
2500 size = size & 0x0F;
2501 if (size == 0)
2502 size = 16; // 0 usually means 16 for 1to16 routines
2503 }
2504
2505 if (routine_id == 0 || routine_id == 4) {
2506 // Rightwards: size * 2 tiles width, 2 tiles height
2507 width = size * 16;
2508 height = 16;
2509 } else {
2510 // Downwards: 2 tiles width, size * 2 tiles height
2511 width = 16;
2512 height = size * 16;
2513 }
2514 break;
2515
2516 case 1: // RoomDraw_Rightwards2x4_1to15or26 (layout walls 0x01-0x02)
2517 {
2518 // ASM: GetSize_1to15or26 - defaults to 26 when size is 0
2519 int effective_size = (size == 0) ? 26 : (size & 0x0F);
2520 // Draws 2x4 tiles repeated 'effective_size' times horizontally
2521 width = effective_size * 16; // 2 tiles wide per repetition
2522 height = 32; // 4 tiles tall
2523 break;
2524 }
2525 case DrawRoutineIds::kWeird2x4_1to16: { // Archery curtains (object 0xB5)
2526 const int count = (size & 0x0F) + 1;
2527 width = count * 16;
2528 height = 32;
2529 break;
2530 }
2531
2532 case 2: // RoomDraw_Rightwards2x4spaced4_1to16 (objects 0x03-0x04)
2533 case 3: // RoomDraw_Rightwards2x4spaced4_1to16_BothBG (objects 0x05-0x06)
2534 {
2535 // ASM: GetSize_1to16, so both routines repeat size + 1 times.
2536 size = size & 0x0F;
2537 int count = size + 1;
2538 width = count * 16; // 2 tiles wide per repetition (adjacent)
2539 height = 32; // 4 tiles tall
2540 break;
2541 }
2542
2543 case 5: // DrawDiagonalAcute_1to16
2544 case 6: // DrawDiagonalGrave_1to16
2545 {
2546 // ASM: RoomDraw_DiagonalAcute/Grave_1to16
2547 // Uses LDA #$0007; JSR RoomDraw_GetSize_1to16_timesA
2548 // count = size + 7
2549 // Each iteration draws 5 tiles vertically (RoomDraw_2x2and1 pattern)
2550 // Width = count tiles, Height = 5 tiles base + (count-1) diagonal offset
2551 size = size & 0x0F;
2552 int count = size + 7;
2553 width = count * 8;
2554 height = (count + 4) * 8; // 5 tiles + (count-1) = count + 4
2555 break;
2556 }
2557 case 17: // DrawDiagonalAcute_1to16_BothBG
2558 case 18: // DrawDiagonalGrave_1to16_BothBG
2559 {
2560 // ASM: RoomDraw_DiagonalAcute/Grave_1to16_BothBG
2561 // Uses LDA #$0006; JSR RoomDraw_GetSize_1to16_timesA
2562 // count = size + 6 (one less than non-BothBG)
2563 size = size & 0x0F;
2564 int count = size + 6;
2565 width = count * 8;
2566 height = (count + 4) * 8; // 5 tiles + (count-1) = count + 4
2567 break;
2568 }
2569
2570 case 8: // RoomDraw_Downwards4x2_1to15or26 (layout walls 0x61-0x62)
2571 {
2572 // ASM: GetSize_1to15or26 - defaults to 26 when size is 0
2573 int effective_size = (size == 0) ? 26 : (size & 0x0F);
2574 // Draws 4x2 tiles repeated 'effective_size' times vertically
2575 width = 32; // 4 tiles wide
2576 height = effective_size * 16; // 2 tiles tall per repetition
2577 break;
2578 }
2579 case 9: // RoomDraw_Downwards4x2_1to16_BothBG (objects 0x63-0x64)
2580 case 10: // RoomDraw_DownwardsDecor4x2spaced4_1to16 (objects 0x65-0x66)
2581 {
2582 // ASM: GetSize_1to16, draws 4x2 tiles with spacing
2583 size = size & 0x0F;
2584 int count = size + 1;
2585 width = 32; // 4 tiles wide
2586 height = count * 16; // 2 tiles tall per repetition (adjacent)
2587 break;
2588 }
2589
2590 case 12: // RoomDraw_DownwardsHasEdge1x1_1to16_plus3
2591 // ASM ($01:8EC3) uses GetSize_1to16_timesA with A=2, giving
2592 // count = size + 2 middle tiles. Total span (corner + middles + end) =
2593 // size + 4 tiles, matching the horizontal counterpart 0x22 (case 21).
2594 size = size & 0x0F;
2595 width = 8;
2596 height = (size + 4) * 8;
2597 break;
2598 case 13: // RoomDraw_DownwardsEdge1x1_1to16
2599 size = size & 0x0F;
2600 width = 8;
2601 height = (size + 1) * 8;
2602 break;
2603 case 14: // RoomDraw_DownwardsLeftCorners2x1_1to16_plus12
2604 case 15: // RoomDraw_DownwardsRightCorners2x1_1to16_plus12
2605 size = size & 0x0F;
2606 width = 16;
2607 height = (size + 14) * 8;
2608 break;
2609
2610 case 16: // DrawRightwards4x4_1to16 (Routine 16)
2611 {
2612 // 4x4 block repeated horizontally based on size
2613 // ASM: GetSize_1to16, count = (size & 0x0F) + 1
2614 int count = (size & 0x0F) + 1;
2615 width = 32 * count; // 4 tiles * 8 pixels * count
2616 height = 32; // 4 tiles * 8 pixels
2617 break;
2618 }
2621 width = 32;
2622 height = 16;
2623 break;
2625 width = 80;
2626 height = 32;
2627 break;
2628 case 19: // DrawCorner4x4 (Type 2 corners 0x100-0x103)
2629 case 34: // Water Face (4x4)
2630 case 35: // 4x4 Corner BothBG
2631 case 36: // Weird Corner Bottom
2632 case 37: // Weird Corner Top
2633 // 4x4 tiles (32x32 pixels) - fixed size, no repetition
2634 width = 32;
2635 height = 32;
2636 break;
2637 case 39: { // Chest routine (small or big)
2638 // Infer size from tile span: big chests provide >=16 tiles
2639 int tile_count = object.tiles().size();
2640 if (tile_count >= 16) {
2641 width = height = 32; // Big chest 4x4
2642 } else {
2643 width = height = 16; // Small chest 2x2
2644 }
2645 break;
2646 }
2647
2648 case 20: // Edge 1x2 (RoomDraw_Rightwards1x2_1to16_plus2)
2649 {
2650 // ZScream: width = size * 2 + 4, height = 3 tiles
2651 size = size & 0x0F;
2652 width = (size * 2 + 4) * 8;
2653 height = 24;
2654 break;
2655 }
2656
2657 case 21: // RoomDraw_RightwardsHasEdge1x1_1to16_plus3 (small rails 0x22)
2658 {
2659 // ZScream: count = size + 2 (corner + middle*count + end)
2660 size = size & 0x0F;
2661 width = (size + 4) * 8;
2662 height = 8;
2663 break;
2664 }
2665 case 22: // RoomDraw_RightwardsHasEdge1x1_1to16_plus2 (carpet trim 0x23-0x2E)
2666 {
2667 // ASM: GetSize_1to16, count = size + 1
2668 // Plus corner (1) + end (1) = count + 2 total width
2669 size = size & 0x0F;
2670 int count = size + 1;
2671 width = (count + 2) * 8; // corner + middle*count + end
2672 height = 8;
2673 break;
2674 }
2675 case 118: // RoomDraw_RightwardsHasEdge1x1_1to16_plus23 (long rails 0x5F)
2676 {
2677 size = size & 0x0F;
2678 width = (size + 23) * 8;
2679 height = 8;
2680 break;
2681 }
2683 size = size & 0x0F;
2684 width = 8;
2685 height = (size + 23) * 8;
2686 break;
2688 size = size & 0x0F;
2689 width = 8;
2690 height = (size + 8) * 8;
2691 break;
2692 case 25: // RoomDraw_Rightwards1x1Solid_1to16_plus3
2693 {
2694 // ASM: GetSize_1to16_timesA(4), so count = size + 4
2695 size = size & 0x0F;
2696 width = (size + 4) * 8;
2697 height = 8;
2698 break;
2699 }
2700
2701 case 23: // RightwardsTopCorners1x2_1to16_plus13
2702 case 24: // RightwardsBottomCorners1x2_1to16_plus13
2703 size = size & 0x0F;
2704 width = 8 + size * 8;
2705 height = 16;
2706 break;
2707
2708 case 26: // Door Switcher
2709 width = 32;
2710 height = 32;
2711 break;
2712
2713 case 27: // RoomDraw_RightwardsDecor4x4spaced2_1to16
2714 {
2715 // 4x4 tiles with 6-tile X spacing per repetition
2716 // ASM: s * 6 spacing, count = size + 1
2717 size = size & 0x0F;
2718 int count = size + 1;
2719 // Total width = (count - 1) * 6 (spacing) + 4 (last block)
2720 width = ((count - 1) * 6 + 4) * 8;
2721 height = 32; // 4 tiles
2722 break;
2723 }
2724
2725 case 28: // RoomDraw_RightwardsStatue2x3spaced2_1to16
2726 {
2727 // 2x3 tiles with 4-tile X spacing per repetition
2728 // ASM: s * 4 spacing, count = size + 1
2729 size = size & 0x0F;
2730 int count = size + 1;
2731 // Total width = (count - 1) * 4 (spacing) + 2 (last block)
2732 width = ((count - 1) * 4 + 2) * 8;
2733 height = 24; // 3 tiles
2734 break;
2735 }
2736
2737 case 29: // RoomDraw_RightwardsPillar2x4spaced4_1to16
2738 {
2739 // 2x4 tiles with 4-tile X spacing per repetition
2740 // ASM: ADC #$0008 = 4 tiles between starts
2741 size = size & 0x0F;
2742 int count = size + 1;
2743 // Total width = (count - 1) * 4 (spacing) + 2 (last block)
2744 width = ((count - 1) * 4 + 2) * 8;
2745 height = 32; // 4 tiles
2746 break;
2747 }
2748
2749 case 30: // RoomDraw_RightwardsDecor4x3spaced4_1to16
2750 {
2751 // 4x3 tiles with 8-tile X spacing per repetition
2752 // ASM: ADC #$0008 = 8-byte advance = 4 tiles gap between 4-tile objects
2753 size = size & 0x0F;
2754 int count = size + 1;
2755 // Total width = (count - 1) * 8 (spacing) + 4 (last block)
2756 width = ((count - 1) * 8 + 4) * 8;
2757 height = 24; // 3 tiles
2758 break;
2759 }
2760
2761 case 31: // RoomDraw_RightwardsDoubled2x2spaced2_1to16
2762 {
2763 // 4x2 tiles (doubled 2x2) with 6-tile X spacing
2764 // ASM: s * 6 spacing, count = size + 1
2765 size = size & 0x0F;
2766 int count = size + 1;
2767 // Total width = (count - 1) * 6 (spacing) + 4 (last block)
2768 width = ((count - 1) * 6 + 4) * 8;
2769 height = 16; // 2 tiles
2770 break;
2771 }
2772 case 32: // RoomDraw_RightwardsDecor2x2spaced12_1to16
2773 {
2774 // 2x2 tiles with 14-tile X spacing per repetition
2775 // ASM: s * 14 spacing, count = size + 1
2776 size = size & 0x0F;
2777 int count = size + 1;
2778 // Total width = (count - 1) * 14 (spacing) + 2 (last block)
2779 width = ((count - 1) * 14 + 2) * 8;
2780 height = 16; // 2 tiles
2781 break;
2782 }
2783
2784 case 33: // Somaria Line
2785 // Each subtype-3 path piece is one 8x8 tile.
2786 width = 8;
2787 height = 8;
2788 break;
2789
2790 case 38: // Nothing (RoomDraw_Nothing)
2791 width = 8;
2792 height = 8;
2793 break;
2794
2795 case 40: // Rightwards 4x2 (FloorTile)
2796 {
2797 // 4 cols x 2 rows, GetSize_1to16
2798 size = size & 0x0F;
2799 int count = size + 1;
2800 width = count * 4 * 8; // 4 tiles per repetition
2801 height = 16; // 2 tiles
2802 break;
2803 }
2804
2805 case 41: // Rightwards Decor 4x2 spaced 12 (wall torches 0x55-0x56)
2806 {
2807 // ASM: 4 columns x 2 rows with 12-tile horizontal spacing.
2808 size = size & 0x0F;
2809 int count = size + 1;
2810 width = ((count - 1) * 12 + 4) * 8;
2811 height = 16;
2812 break;
2813 }
2814
2815 case 42: // Rightwards Cannon Hole 4x3
2816 {
2817 // 4x3 tiles, GetSize_1to16
2818 size = size & 0x0F;
2819 int count = size + 1;
2820 width = count * 4 * 8;
2821 height = 24;
2822 break;
2823 }
2824
2825 case 43: // Downwards Floor 4x4
2826 {
2827 // 4x4 tiles, GetSize_1to16
2828 size = size & 0x0F;
2829 int count = size + 1;
2830 width = 32;
2831 height = count * 4 * 8;
2832 break;
2833 }
2834
2835 case 44: // Downwards 1x1 Solid +3
2836 {
2837 size = size & 0x0F;
2838 width = 8;
2839 height = (size + 4) * 8;
2840 break;
2841 }
2842
2843 case 45: // Downwards Decor 4x4 spaced 2
2844 {
2845 size = size & 0x0F;
2846 int count = size + 1;
2847 width = 32;
2848 height = ((count - 1) * 6 + 4) * 8;
2849 break;
2850 }
2851
2852 case 46: // Downwards Pillar 2x4 spaced 2
2853 {
2854 size = size & 0x0F;
2855 int count = size + 1;
2856 width = 16;
2857 height = ((count - 1) * 6 + 4) * 8;
2858 break;
2859 }
2860
2861 case 47: // Downwards Decor 3x4 spaced 4
2862 {
2863 size = size & 0x0F;
2864 int count = size + 1;
2865 width = 24;
2866 height = ((count - 1) * 6 + 4) * 8;
2867 break;
2868 }
2869
2870 case 48: // Downwards Decor 2x2 spaced 12
2871 {
2872 size = size & 0x0F;
2873 int count = size + 1;
2874 width = 16;
2875 height = ((count - 1) * 14 + 2) * 8;
2876 break;
2877 }
2878
2879 case 49: // Downwards Line 1x1 +1
2880 {
2881 size = size & 0x0F;
2882 width = 8;
2883 height = (size + 2) * 8;
2884 break;
2885 }
2886
2887 case 50: // Downwards Decor 2x4 spaced 8
2888 {
2889 size = size & 0x0F;
2890 int count = size + 1;
2891 width = 16;
2892 height = ((count - 1) * 12 + 4) * 8;
2893 break;
2894 }
2895
2896 case 51: // Rightwards Line 1x1 +1
2897 {
2898 size = size & 0x0F;
2899 width = (size + 2) * 8;
2900 height = 8;
2901 break;
2902 }
2903
2904 case 52: // Rightwards Bar 4x3
2905 {
2906 size = size & 0x0F;
2907 int count = size + 1;
2908 width = ((count - 1) * 6 + 4) * 8;
2909 height = 24;
2910 break;
2911 }
2912
2913 case 53: // Rightwards Shelf 4x4
2914 {
2915 size = size & 0x0F;
2916 int count = size + 1;
2917 width = ((count - 1) * 6 + 4) * 8;
2918 height = 32;
2919 break;
2920 }
2921
2922 case 54: // Rightwards Big Rail 1x3 +5
2923 {
2924 size = size & 0x0F;
2925 width = (size + 6) * 8;
2926 height = 24;
2927 break;
2928 }
2929
2930 case 55: // Rightwards Block 2x2 spaced 2
2931 {
2932 size = size & 0x0F;
2933 int count = size + 1;
2934 width = ((count - 1) * 4 + 2) * 8;
2935 height = 16;
2936 break;
2937 }
2938
2939 // Routines 56-64: SuperSquare patterns
2940 // ASM: Type1/Type3 objects pack 2-bit X/Y sizes into a 4-bit size:
2941 // size = (x_size << 2) | y_size, where x_size/y_size are 0..3 (meaning 1..4).
2942 // Each super square unit is 4 tiles (32 pixels) in each dimension.
2943 case 56: // 4x4BlocksIn4x4SuperSquare
2944 case 57: // 3x3FloorIn4x4SuperSquare
2945 case 58: // 4x4FloorIn4x4SuperSquare
2946 case 59: // 4x4FloorOneIn4x4SuperSquare
2947 case 60: // 4x4FloorTwoIn4x4SuperSquare
2948 case 62: // Spike2x2In4x4SuperSquare
2949 {
2950 int size_x = ((size >> 2) & 0x03) + 1;
2951 int size_y = (size & 0x03) + 1;
2952 width = size_x * 32; // 4 tiles per super square
2953 height = size_y * 32; // 4 tiles per super square
2954 break;
2955 }
2956 case 61: // BigHole4x4
2957 case 63: // TableRock4x4
2958 case 64: // WaterOverlay8x8
2959 width = 32;
2960 height = 32;
2961 break;
2962
2963 // Routines 65-74: Various downwards/rightwards patterns
2964 case 65: // DownwardsDecor3x4spaced2
2965 {
2966 size = size & 0x0F;
2967 int count = size + 1;
2968 width = 24;
2969 height = ((count - 1) * 5 + 4) * 8;
2970 break;
2971 }
2972
2973 case 66: // DownwardsBigRail3x1 +5
2974 {
2975 // Top cap (2x2) + Middle (2x1 x count) + Bottom cap (2x3)
2976 // Total: 2 tiles wide, 2 + (size+1) + 3 = size + 6 tiles tall
2977 size = size & 0x0F;
2978 width = 16; // 2 tiles wide
2979 height = (size + 6) * 8;
2980 break;
2981 }
2982
2983 case 67: // DownwardsBlock2x2spaced2
2984 {
2985 size = size & 0x0F;
2986 int count = size + 1;
2987 width = 16;
2988 height = ((count - 1) * 4 + 2) * 8;
2989 break;
2990 }
2991
2992 case 68: // DownwardsCannonHole3x4
2993 {
2994 size = size & 0x0F;
2995 width = 24;
2996 // Height = repeated 3x2 segment (size+1) + final 3x2 edge segment.
2997 // => (2 * (size + 2)) tiles.
2998 height = (2 * (size + 2)) * 8;
2999 break;
3000 }
3001
3002 case 69: // DownwardsBar2x5
3003 {
3004 size = size & 0x0F;
3005 width = 16;
3006 // 1 top row + 2*(size+2) body rows.
3007 height = (2 * size + 5) * 8;
3008 break;
3009 }
3010
3011 case 70: // DownwardsPots2x2
3012 case 71: // DownwardsHammerPegs2x2
3013 {
3014 size = size & 0x0F;
3015 int count = size + 1;
3016 width = 16;
3017 height = count * 2 * 8;
3018 break;
3019 }
3020
3021 case 72: // RightwardsEdge1x1 +7
3022 {
3023 size = size & 0x0F;
3024 width = (size + 8) * 8;
3025 height = 8;
3026 break;
3027 }
3028
3029 case 73: // RightwardsPots2x2
3030 case 74: // RightwardsHammerPegs2x2
3031 {
3032 size = size & 0x0F;
3033 int count = size + 1;
3034 width = count * 2 * 8;
3035 height = 16;
3036 break;
3037 }
3038
3039 // Diagonal ceilings (75-78) - TRIANGLE shapes
3040 // Draw uses count = (size & 0x0F) + 4
3041 // Outline uses smaller size since triangle only fills half the square area
3042 case 75: // DiagonalCeilingTopLeft - triangle at origin
3043 case 76: // DiagonalCeilingBottomLeft - triangle at origin
3044 {
3045 // Smaller outline for triangle - use half the drawn area
3046 int count = (size & 0x0F) + 2;
3047 width = count * 8;
3048 height = count * 8;
3049 break;
3050 }
3051 case 77: // DiagonalCeilingTopRight - triangle shifts diagonally
3052 case 78: // DiagonalCeilingBottomRight - triangle shifts diagonally
3053 {
3054 // Smaller outline for diagonal triangles
3055 int count = (size & 0x0F) + 2;
3056 width = count * 8;
3057 height = count * 8;
3058 break;
3059 }
3060
3061 case 79: { // ClosedChestPlatform
3062 int size_x = (size >> 2) & 0x03;
3063 int size_y = size & 0x03;
3064 width = (size_x * 2 + 14) * 8;
3065 height = (size_y * 2 + 8) * 8;
3066 break;
3067 }
3068
3069 // Special platform routines (80-82)
3070 case 80: // MovingWallWest
3071 case 81: // MovingWallEast
3073
3074 case 82: { // OpenChestPlatform
3075 int size_x = (size >> 2) & 0x03;
3076 int size_y = size & 0x03;
3077 width = (size_x * 2 + 10) * 8;
3078 height = (size_y * 2 + 7) * 8;
3079 break;
3080 }
3081
3082 // Stair routines - different sizes for different types
3083
3084 // 4x4 stair patterns (32x32 pixels)
3085 case 83: // InterRoomFatStairsUp (0x12D)
3086 case 84: // InterRoomFatStairsDownA (0x12E)
3087 case 85: // InterRoomFatStairsDownB (0x12F)
3088 case 86: // AutoStairs (0x130-0x133)
3089 case 87: // StraightInterroomStairs (0xF9E-0xFA9)
3090 width = 32; // 4 tiles
3091 height = 32; // 4 tiles (4x4 pattern)
3092 break;
3093
3094 // 4x3 stair patterns (32x24 pixels)
3095 case 88: // SpiralStairsGoingUpUpper (0x138)
3096 case 89: // SpiralStairsGoingDownUpper (0x139)
3097 case 90: // SpiralStairsGoingUpLower (0x13A)
3098 case 91: // SpiralStairsGoingDownLower (0x13B)
3099 // ASM: RoomDraw_1x3N_rightwards with A=4 -> 4 columns x 3 rows
3100 width = 32; // 4 tiles
3101 height = 24; // 3 tiles
3102 break;
3103
3104 case 92: // BigKeyLock
3105 width = 16;
3106 height = 16;
3107 break;
3108
3109 case 93: // BombableFloor
3110 width = 32;
3111 height = 32;
3112 break;
3113
3114 case 94: // EmptyWaterFace
3115 width = 32;
3116 // Report the larger stateful footprint so editor bounds do not
3117 // undershoot the active 4x5 branch.
3118 height = 40;
3119 break;
3120
3121 case 95: // SpittingWaterFace
3122 width = 32;
3123 height = 40;
3124 break;
3125
3126 case 96: // DrenchingWaterFace
3127 width = 32;
3128 height = 56;
3129 break;
3130
3131 case 97: // PrisonCell
3132 width = 128; // 16 tiles
3133 height = 32; // 4 tiles
3134 break;
3135
3136 case 98: // Bed4x5
3137 width = 32;
3138 height = 40;
3139 break;
3140
3141 case 99: // Rightwards3x6
3142 width = 48; // 6 tiles
3143 height = 24; // 3 tiles
3144 break;
3145
3146 case 100: // Utility6x3
3147 width = 48;
3148 height = 24;
3149 break;
3150
3151 case 101: // Utility3x5
3152 width = 24;
3153 height = 40;
3154 break;
3155
3156 case 102: // VerticalTurtleRockPipe
3157 width = 32;
3158 height = 48;
3159 break;
3160
3161 case 103: // HorizontalTurtleRockPipe
3162 width = 48;
3163 height = 32;
3164 break;
3165
3166 case 104: // LightBeam
3167 width = 32; // 4 tiles
3168 height = 80;
3169 break;
3170
3171 case 105: // BigLightBeam
3172 width = 64;
3173 height = 64;
3174 break;
3175
3177 width = 64;
3178 height = 64;
3179 break;
3180
3181 case 106: // BossShell4x4
3182 width = 32;
3183 height = 32;
3184 break;
3185
3186 case 107: // SolidWallDecor3x4
3187 width = 24;
3188 height = 32;
3189 break;
3190
3191 case 108: // ArcheryGameTargetDoor
3192 width = 24;
3193 height = 48;
3194 break;
3195
3196 case 109: // GanonTriforceFloorDecor
3197 width = 64;
3198 height = 64;
3199 break;
3200
3201 case 110: // Single2x2
3202 width = 16;
3203 height = 16;
3204 break;
3205
3206 case 111: // Waterfall47 (object 0x47)
3207 {
3208 // ASM: count = (size+1)*2, draws 1x5 columns
3209 // Width = first column + middle columns + last column = 2 + count tiles
3210 size = size & 0x0F;
3211 int count = (size + 1) * 2;
3212 width = (2 + count) * 8;
3213 height = 40; // 5 tiles
3214 break;
3215 }
3216 case 112: // Waterfall48 (object 0x48)
3217 {
3218 // ASM: count = (size+1)*2, draws 1x3 columns
3219 // Width = first column + middle columns + last column = 2 + count tiles
3220 size = size & 0x0F;
3221 int count = (size + 1) * 2;
3222 width = (2 + count) * 8;
3223 height = 24; // 3 tiles
3224 break;
3225 }
3226
3227 case 113: // Single4x4 (no repetition) - 4x4 TILE16 = 8x8 TILE8
3228 // ASM RoomDraw_4x4 = 4x4 tile8.
3229 width = 32;
3230 height = 32;
3231 break;
3232
3233 case 114: // Single4x3 (no repetition)
3234 // 4 tiles wide x 3 tiles tall = 32x24 pixels
3235 width = 32;
3236 height = 24;
3237 break;
3238
3239 case 115: // RupeeFloor (special pattern)
3240 // Columns at x + 0, +2, +4 bound a 5x8-tile area = 40x64 pixels.
3241 width = 40;
3242 height = 64;
3243 break;
3244
3245 case 116: // Actual4x4 (true 4x4 tile8 pattern, no repetition)
3246 // 4 tile8s x 4 tile8s = 32x32 pixels
3247 width = 32;
3248 height = 32;
3249 break;
3250
3252 width = 64;
3253 height = 24;
3254 break;
3255
3257 width = 32;
3258 height = 16;
3259 break;
3260
3262 width = 48;
3263 height = 64;
3264 break;
3265
3267 width = 32;
3268 height = 32;
3269 break;
3270
3272 width = 112;
3273 height = 112;
3274 break;
3275
3277 width = 112;
3278 height = 112;
3279 break;
3280
3281 default:
3282 // Fallback to naive calculation if not handled
3283 // Matches DungeonCanvasViewer::DrawRoomObjects logic
3284 {
3285 int size_h = (object.size_ & 0x0F);
3286 int size_v = (object.size_ >> 4) & 0x0F;
3287 width = (size_h + 1) * 8;
3288 height = (size_v + 1) * 8;
3289 }
3290 break;
3291 }
3292
3293 return {width, height};
3294}
3295
3297 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3298 [[maybe_unused]] std::span<const gfx::TileInfo> tiles,
3299 [[maybe_unused]] const DungeonState* state) {
3300 // CustomObjectManager should be initialized by DungeonEditorV2 with the
3301 // project's custom_objects_folder path before any objects are drawn
3302 auto& manager = CustomObjectManager::Get();
3303
3304 int subtype = obj.size_ & 0x1F;
3305 const std::string filename = manager.ResolveFilename(obj.id_, subtype);
3306 auto result = manager.GetObjectInternal(obj.id_, subtype);
3307 if (!result.ok()) {
3308 DrawMissingCustomObjectPlaceholder(bg, obj.x_, obj.y_);
3309 LOG_DEBUG("ObjectDrawer",
3310 "Custom object 0x%03X subtype %d (%s) not found: %s", obj.id_,
3311 subtype, filename.empty() ? "<unmapped>" : filename.c_str(),
3312 result.status().message().data());
3313 return;
3314 }
3315
3316 auto custom_obj = result.value();
3317 if (!custom_obj || custom_obj->IsEmpty())
3318 return;
3319
3320 int tile_x = obj.x_;
3321 int tile_y = obj.y_;
3322
3323 for (const auto& entry : custom_obj->tiles) {
3324 // entry.tile_data is vhopppcc cccccccc (SNES tilemap word format)
3325 // Convert to TileInfo and render using WriteTile8 (not SetTileAt which
3326 // only stores to buffer without rendering)
3327 gfx::TileInfo tile_info = gfx::WordToTileInfo(entry.tile_data);
3328 WriteTile8(bg, tile_x + entry.rel_x, tile_y + entry.rel_y, tile_info);
3329 }
3330}
3331
3333 gfx::BackgroundBuffer& bg, int tile_x, int tile_y) {
3334 if (trace_only_) {
3335 return;
3336 }
3337
3338 auto& bitmap = bg.bitmap();
3339 if (!bitmap.is_active() || bitmap.width() <= 0 || bitmap.height() <= 0) {
3340 return;
3341 }
3342
3343 auto& pixels = bitmap.mutable_data();
3344 auto& coverage = bg.mutable_coverage_data();
3345 auto& priority = bg.mutable_priority_data();
3346
3347 constexpr int kPlaceholderSizePx = 16;
3348 constexpr uint8_t kFillColor = 33;
3349 constexpr uint8_t kAccentColor = 47;
3350
3351 const int start_x = tile_x * 8;
3352 const int start_y = tile_y * 8;
3353 const int width = bitmap.width();
3354 const int height = bitmap.height();
3355
3356 bg.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, start_x, start_y,
3357 kPlaceholderSizePx, kPlaceholderSizePx);
3358
3359 for (int py = 0; py < kPlaceholderSizePx; ++py) {
3360 const int dest_y = start_y + py;
3361 if (dest_y < 0 || dest_y >= height)
3362 continue;
3363 for (int px = 0; px < kPlaceholderSizePx; ++px) {
3364 const int dest_x = start_x + px;
3365 if (dest_x < 0 || dest_x >= width)
3366 continue;
3367 const bool border = (px == 0 || py == 0 || px == kPlaceholderSizePx - 1 ||
3368 py == kPlaceholderSizePx - 1);
3369 const bool diagonal = (px == py) || (px + py == kPlaceholderSizePx - 1);
3370 const int dest_index = dest_y * width + dest_x;
3371 pixels[dest_index] = (border || diagonal) ? kAccentColor : kFillColor;
3372 if (dest_index < static_cast<int>(coverage.size()))
3373 coverage[dest_index] = 1;
3374 if (dest_index < static_cast<int>(priority.size()))
3375 priority[dest_index] = 0;
3376 }
3377 }
3378 bitmap.set_modified(true);
3379}
3380
3381void yaze::zelda3::ObjectDrawer::DrawPotItem(uint8_t item_id, int x, int y,
3383 // Draw a small colored indicator for pot items
3384 // Item types from ZELDA3_DUNGEON_SPEC.md Section 7.2
3385 // Uses palette indices that map to recognizable colors
3386
3387 if (item_id == 0)
3388 return; // Nothing - skip
3389
3390 auto& bitmap = bg.bitmap();
3391 auto& coverage_buffer = bg.mutable_coverage_data();
3392 if (!bitmap.is_active() || bitmap.width() == 0)
3393 return;
3394
3395 // Convert tile coordinates to pixel coordinates
3396 // Items are drawn offset from pot position (centered on pot)
3397 int pixel_x = (x * 8) + 2; // Offset 2 pixels into the pot tile
3398 int pixel_y = (y * 8) + 2;
3399
3400 // Choose color based on item category
3401 // Using palette indices that should be visible in dungeon palettes
3402 uint8_t color_idx;
3403 switch (item_id) {
3404 // Rupees (green/blue/red tones)
3405 case 1: // Green rupee
3406 case 7: // Blue rupee
3407 case 12: // Blue rupee variant
3408 color_idx = 30; // Greenish (palette 2, index 0)
3409 break;
3410
3411 // Hearts (red tones)
3412 case 6: // Heart
3413 case 11: // Heart
3414 case 13: // Heart variant
3415 // NOTE: Avoid palette indices 0/16/32/.. which are transparent in SNES
3416 // CGRAM rows. Using 5 gives a consistently visible indicator.
3417 color_idx = 5;
3418 break;
3419
3420 // Keys (yellow/gold)
3421 case 8: // Key*8
3422 case 19: // Key
3423 color_idx = 45; // Yellowish (palette 3)
3424 break;
3425
3426 // Bombs (dark/black)
3427 case 5: // Bomb
3428 case 10: // 1 bomb
3429 case 16: // Bomb refill
3430 color_idx = 60; // Darker color (palette 4)
3431 break;
3432
3433 // Arrows (brown/wood)
3434 case 9: // Arrow
3435 case 17: // Arrow refill
3436 color_idx = 15; // Brownish (palette 1)
3437 break;
3438
3439 // Magic (blue/purple)
3440 case 14: // Small magic
3441 case 15: // Big magic
3442 color_idx = 75; // Bluish (palette 5)
3443 break;
3444
3445 // Fairy (pink/light)
3446 case 18: // Fairy
3447 case 20: // Fairy*8
3448 color_idx = 5; // Pinkish
3449 break;
3450
3451 // Special/Traps (distinct colors)
3452 case 2: // Rock crab
3453 case 3: // Bee
3454 color_idx = 20; // Enemy indicator
3455 break;
3456
3457 case 23: // Hole
3458 case 24: // Warp
3459 case 25: // Staircase
3460 color_idx = 10; // Transport indicator
3461 break;
3462
3463 case 26: // Bombable
3464 case 27: // Switch
3465 color_idx = 35; // Interactive indicator
3466 break;
3467
3468 case 4: // Random
3469 default:
3470 color_idx = 50; // Default/random indicator
3471 break;
3472 }
3473
3474 // Safety: never use CGRAM transparent slots (0,16,32,...) for the indicator.
3475 // In the editor these would appear invisible or "missing" depending on
3476 // compositing.
3477 if (color_idx != 255 && (color_idx % 16) == 0) {
3478 color_idx++;
3479 }
3480
3481 // Draw a 4x4 colored square as item indicator
3482 int bitmap_width = bitmap.width();
3483 int bitmap_height = bitmap.height();
3484
3485 bg.ClearBG1RevealMaskRect(bg1_reveal_mask_source_, pixel_x, pixel_y, 4, 4);
3486
3487 for (int py = 0; py < 4; py++) {
3488 for (int px = 0; px < 4; px++) {
3489 int dest_x = pixel_x + px;
3490 int dest_y = pixel_y + py;
3491
3492 // Bounds check
3493 if (dest_x >= 0 && dest_x < bitmap_width && dest_y >= 0 &&
3494 dest_y < bitmap_height) {
3495 int offset = (dest_y * bitmap_width) + dest_x;
3496 bitmap.WriteToPixel(offset, color_idx);
3497 if (offset >= 0 && offset < static_cast<int>(coverage_buffer.size())) {
3498 coverage_buffer[offset] = 1;
3499 }
3500 }
3501 }
3502 }
3503}
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:155
auto data() const
Definition rom.h:151
auto size() const
Definition rom.h:150
bool is_loaded() const
Definition rom.h:144
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