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