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