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
79void ObjectDrawer::DrawUsingRegistryRoutine(int routine_id, const RoomObject& obj,
81 std::span<const gfx::TileInfo> tiles,
82 const DungeonState* state) {
83 // Many DrawRoutineRegistry routines are implemented as pure functions that
84 // call DrawRoutineUtils::WriteTile8(), which only writes to BackgroundBuffer's
85 // tile buffer (not the bitmap). Runtime rendering/compositing uses the
86 // bitmap-backed buffers, so we capture tile writes from the pure routine and
87 // replay them via ObjectDrawer::WriteTile8().
88 const auto* info = DrawRoutineRegistry::Get().GetRoutineInfo(routine_id);
89 if (info == nullptr) {
90 LOG_DEBUG("ObjectDrawer", "DrawUsingRegistryRoutine: unknown routine %d",
91 routine_id);
92 return;
93 }
94
95 struct CapturedWrite {
96 int x = 0;
97 int y = 0;
98 gfx::TileInfo tile{};
99 };
100
101 std::vector<CapturedWrite> writes;
102 writes.reserve(256);
103
105 [](int tile_x, int tile_y, const gfx::TileInfo& tile_info,
106 void* user_data) {
107 auto* out = static_cast<std::vector<CapturedWrite>*>(user_data);
108 if (!out) {
109 return;
110 }
111 out->push_back(CapturedWrite{tile_x, tile_y, tile_info});
112 },
113 &writes,
114 /*trace_only=*/true);
115
116 DrawContext ctx{
117 .target_bg = bg,
118 .object = obj,
119 .tiles = tiles,
120 .state = state,
121 .rom = rom_,
122 .room_id = room_id_,
123 .room_gfx_buffer = room_gfx_buffer_,
124 .secondary_bg = nullptr,
125 };
126 info->function(ctx);
127
129
130 for (const auto& w : writes) {
131 WriteTile8(bg, w.x, w.y, w.tile);
132 }
133}
134
136 const RoomObject& object, gfx::BackgroundBuffer& bg1,
137 gfx::BackgroundBuffer& bg2, const gfx::PaletteGroup& palette_group,
138 [[maybe_unused]] const DungeonState* state,
139 gfx::BackgroundBuffer* layout_bg1) {
140 if (!rom_ || !rom_->is_loaded()) {
141 return absl::FailedPreconditionError("ROM not loaded");
142 }
143
145 return absl::FailedPreconditionError("Draw routines not initialized");
146 }
147
148 // Ensure object has tiles loaded
149 auto mutable_obj = const_cast<RoomObject&>(object);
150 mutable_obj.SetRom(rom_);
151 mutable_obj.EnsureTilesLoaded();
152
153 // Select buffer based on layer
154 // Layer 0 (BG1): Main objects - drawn to BG1_Objects (on top of layout)
155 // Layer 1 (BG2): Overlay objects - drawn to BG2_Objects (behind layout)
156 // Layer 2 (BG3): Priority objects (torches) - drawn to BG1_Objects (on top)
157 bool use_bg2 = (object.layer_ == RoomObject::LayerType::BG2);
158 auto& target_bg = use_bg2 ? bg2 : bg1;
159
160 // Log buffer selection for debugging layer routing
161 LOG_DEBUG("ObjectDrawer", "Object 0x%03X layer=%d -> drawing to %s buffer",
162 object.id_, static_cast<int>(object.layer_),
163 use_bg2 ? "BG2 (behind layout)" : "BG1 (on top of layout)");
164
165 // Check for custom object override first (guarded by feature flag).
166 // We check this BEFORE routine lookup to allow overriding vanilla objects.
167 int subtype = object.size_ & 0x1F;
168 bool is_custom_object = false;
170 CustomObjectManager::Get().GetObjectInternal(object.id_, subtype).ok()) {
171 is_custom_object = true;
172 // Custom objects default to drawing on the target layer only, unless all_bgs_ is set
173 // Mask propagation is difficult without dimensions, so we rely on explicit transparency in the custom object tiles if needed
174
175 // Draw to target layer
178 DrawCustomObject(object, target_bg, mutable_obj.tiles(), state);
179
180 // If marked for both BGs, draw to the other layer too
181 if (object.all_bgs_) {
182 auto& other_bg = (object.layer_ == RoomObject::LayerType::BG2 ||
183 object.layer_ == RoomObject::LayerType::BG3)
184 ? bg1
185 : bg2;
186 SetTraceContext(object, (&other_bg == &bg1) ? RoomObject::LayerType::BG1
188 DrawCustomObject(object, other_bg, mutable_obj.tiles(), state);
189 }
190 return absl::OkStatus();
191 }
192
193 // Skip objects that don't have tiles loaded
194 if (!is_custom_object && mutable_obj.tiles().empty()) {
195 LOG_DEBUG("ObjectDrawer",
196 "Object 0x%03X at (%d,%d) has NO TILES - skipping", object.id_,
197 object.x_, object.y_);
198 return absl::OkStatus();
199 }
200
201 // Look up draw routine for this object
202 int routine_id = GetDrawRoutineId(object.id_);
203
204 // Log draw routine lookup with tile info
205 LOG_DEBUG("ObjectDrawer",
206 "Object 0x%03X at (%d,%d) size=%d -> routine=%d tiles=%zu",
207 object.id_, object.x_, object.y_, object.size_, routine_id,
208 mutable_obj.tiles().size());
209
210 if (routine_id < 0 || routine_id >= static_cast<int>(draw_routines_.size())) {
211 LOG_DEBUG("ObjectDrawer",
212 "Object 0x%03X: NO ROUTINE (id=%d, max=%zu) - using fallback 1x1",
213 object.id_, routine_id, draw_routines_.size());
214 // Fallback to simple 1x1 drawing using first 8x8 tile
215 if (!mutable_obj.tiles().empty()) {
216 const auto& tile_info = mutable_obj.tiles()[0];
219 WriteTile8(target_bg, object.x_, object.y_, tile_info);
220 }
221 return absl::OkStatus();
222 }
223
224 // Null-tile guard: skip routines whose tile payload is too small.
225 // Hack ROMs with abbreviated tile tables would otherwise cause
226 // out-of-bounds access in fixed-size draw patterns.
227 const DrawRoutineInfo* routine_info =
229 if (routine_info && routine_info->min_tiles > 0 &&
230 static_cast<int>(mutable_obj.tiles().size()) <
231 routine_info->min_tiles) {
232 LOG_WARN("ObjectDrawer",
233 "Object 0x%03X at (%d,%d): tile payload too small "
234 "(%zu < %d required by routine '%s') - skipping",
235 object.id_, object.x_, object.y_,
236 mutable_obj.tiles().size(), routine_info->min_tiles,
237 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) {
330 absl::Status status = absl::OkStatus();
331
332 // DEBUG: Count objects routed to each buffer
333 int to_bg1 = 0, to_bg2 = 0, both_bgs = 0;
334
335 for (const auto& object : objects) {
336 // Track buffer routing for summary
337 bool use_bg2 = (object.layer_ == RoomObject::LayerType::BG2);
338 int routine_id = GetDrawRoutineId(object.id_);
339 bool is_both_bg = (object.all_bgs_ || RoutineDrawsToBothBGs(routine_id));
340
341 if (is_both_bg) {
342 both_bgs++;
343 } else if (use_bg2) {
344 to_bg2++;
345 } else {
346 to_bg1++;
347 }
348
349 auto s = DrawObject(object, bg1, bg2, palette_group, state, layout_bg1);
350 if (!s.ok() && status.ok()) {
351 status = s;
352 }
353 }
354
355 LOG_DEBUG("ObjectDrawer", "Buffer routing: to_BG1=%d, to_BG2=%d, BothBGs=%d",
356 to_bg1, to_bg2, both_bgs);
357
358 // NOTE: Palette is already set in Room::RenderRoomGraphics() before calling
359 // this function. We just need to sync the pixel data to the SDL surface.
360 auto& bg1_bmp = bg1.bitmap();
361 auto& bg2_bmp = bg2.bitmap();
362
363 // Sync bitmap data to SDL surfaces (palette already applied)
364 if (bg1_bmp.modified() && bg1_bmp.surface() &&
365 bg1_bmp.mutable_data().size() > 0) {
366 SDL_LockSurface(bg1_bmp.surface());
367 // Safety check: ensure surface can hold the data
368 // Note: This assumes 8bpp surface where pitch >= width
369 size_t surface_size = bg1_bmp.surface()->h * bg1_bmp.surface()->pitch;
370 size_t buffer_size = bg1_bmp.mutable_data().size();
371
372 if (surface_size >= buffer_size) {
373 // TODO: Handle pitch mismatch properly (copy row by row)
374 // For now, just ensure we don't overflow
375 memcpy(bg1_bmp.surface()->pixels, bg1_bmp.mutable_data().data(),
376 buffer_size);
377 } else {
378 LOG_DEBUG("ObjectDrawer", "BG1 Surface too small: surf=%zu buf=%zu",
379 surface_size, buffer_size);
380 }
381 SDL_UnlockSurface(bg1_bmp.surface());
382 }
383
384 if (bg2_bmp.modified() && bg2_bmp.surface() &&
385 bg2_bmp.mutable_data().size() > 0) {
386 SDL_LockSurface(bg2_bmp.surface());
387 size_t surface_size = bg2_bmp.surface()->h * bg2_bmp.surface()->pitch;
388 size_t buffer_size = bg2_bmp.mutable_data().size();
389
390 if (surface_size >= buffer_size) {
391 memcpy(bg2_bmp.surface()->pixels, bg2_bmp.mutable_data().data(),
392 buffer_size);
393 } else {
394 LOG_DEBUG("ObjectDrawer", "BG2 Surface too small: surf=%zu buf=%zu",
395 surface_size, buffer_size);
396 }
397 SDL_UnlockSurface(bg2_bmp.surface());
398 }
399
400 return status;
401}
402
403// ============================================================================
404// Metadata-based BothBG Detection
405// ============================================================================
406
408 // Use DrawRoutineRegistry as the single source of truth for BothBG metadata.
410}
411
412// ============================================================================
413// Draw Routine Registry Initialization
414// ============================================================================
415
417 // This function maps object IDs to their corresponding draw routines.
418 // The mapping is based on ZScream's DungeonObjectData.cs and the game's
419 // assembly code. The order of functions in draw_routines_ MUST match the
420 // indices used here.
421 //
422 // ASM Reference (Bank 01):
423 // Subtype 1 Data Offset: $018000 (DrawObjects.type1_subtype_1_data_offset)
424 // Subtype 1 Routine Ptr: $018200 (DrawObjects.type1_subtype_1_routine)
425 // Subtype 2 Data Offset: $0183F0 (DrawObjects.type1_subtype_2_data_offset)
426 // Subtype 2 Routine Ptr: $018470 (DrawObjects.type1_subtype_2_routine)
427 // Subtype 3 Data Offset: $0184F0 (DrawObjects.type1_subtype_3_data_offset)
428 // Subtype 3 Routine Ptr: $0185F0 (DrawObjects.type1_subtype_3_routine)
429
430 draw_routines_.clear();
431
432 // Object-to-routine mapping now lives in DrawRoutineRegistry::BuildObjectMapping().
433 // ObjectDrawer::GetDrawRoutineId() delegates to the registry singleton.
434 // Initialize draw routine function array in the correct order
435 // Routines 0-82 (existing), 80-98 (new special routines for stairs, locks, etc.)
436 draw_routines_.reserve(100);
437
438 // Routine 0
439 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
441 std::span<const gfx::TileInfo> tiles,
442 [[maybe_unused]] const DungeonState* state) {
443 self->DrawRightwards2x2_1to15or32(obj, bg, tiles);
444 });
445 // Routine 1
446 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
448 std::span<const gfx::TileInfo> tiles,
449 [[maybe_unused]] const DungeonState* state) {
450 self->DrawRightwards2x4_1to15or26(obj, bg, tiles);
451 });
452 // Routine 2 - 2x4 tiles with adjacent spacing (s * 2), count = size + 1
453 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
455 std::span<const gfx::TileInfo> tiles,
456 [[maybe_unused]] const DungeonState* state) {
457 self->DrawRightwards2x4_1to16(obj, bg, tiles);
458 });
459 // Routine 3 - Same as routine 2 but draws to both BG1 and BG2
460 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
462 std::span<const gfx::TileInfo> tiles,
463 [[maybe_unused]] const DungeonState* state) {
464 self->DrawRightwards2x4_1to16_BothBG(obj, bg, tiles);
465 });
466 // Routine 4
467 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
469 std::span<const gfx::TileInfo> tiles,
470 [[maybe_unused]] const DungeonState* state) {
471 self->DrawRightwards2x2_1to16(obj, bg, tiles);
472 });
473 // Routine 5
474 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
476 std::span<const gfx::TileInfo> tiles,
477 [[maybe_unused]] const DungeonState* state) {
478 self->DrawDiagonalAcute_1to16(obj, bg, tiles);
479 });
480 // Routine 6
481 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
483 std::span<const gfx::TileInfo> tiles,
484 [[maybe_unused]] const DungeonState* state) {
485 self->DrawDiagonalGrave_1to16(obj, bg, tiles);
486 });
487 // Routine 7
488 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
490 std::span<const gfx::TileInfo> tiles,
491 [[maybe_unused]] const DungeonState* state) {
492 self->DrawDownwards2x2_1to15or32(obj, bg, tiles);
493 });
494 // Routine 8
495 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
497 std::span<const gfx::TileInfo> tiles,
498 [[maybe_unused]] const DungeonState* state) {
499 self->DrawDownwards4x2_1to15or26(obj, bg, tiles);
500 });
501 // Routine 9
502 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
504 std::span<const gfx::TileInfo> tiles,
505 [[maybe_unused]] const DungeonState* state) {
506 self->DrawDownwards4x2_1to16_BothBG(obj, bg, tiles);
507 });
508 // Routine 10
509 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
511 std::span<const gfx::TileInfo> tiles,
512 [[maybe_unused]] const DungeonState* state) {
513 self->DrawDownwardsDecor4x2spaced4_1to16(obj, bg, tiles);
514 });
515 // Routine 11
516 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
518 std::span<const gfx::TileInfo> tiles,
519 [[maybe_unused]] const DungeonState* state) {
520 self->DrawDownwards2x2_1to16(obj, bg, tiles);
521 });
522 // Routine 12
523 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
525 std::span<const gfx::TileInfo> tiles,
526 [[maybe_unused]] const DungeonState* state) {
527 self->DrawDownwardsHasEdge1x1_1to16_plus3(obj, bg, tiles);
528 });
529 // Routine 13
530 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
532 std::span<const gfx::TileInfo> tiles,
533 [[maybe_unused]] const DungeonState* state) {
534 self->DrawDownwardsEdge1x1_1to16(obj, bg, tiles);
535 });
536 // Routine 14
537 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
539 std::span<const gfx::TileInfo> tiles,
540 [[maybe_unused]] const DungeonState* state) {
541 self->DrawDownwardsLeftCorners2x1_1to16_plus12(obj, bg, tiles);
542 });
543 // Routine 15
544 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
546 std::span<const gfx::TileInfo> tiles,
547 [[maybe_unused]] const DungeonState* state) {
548 self->DrawDownwardsRightCorners2x1_1to16_plus12(obj, bg, tiles);
549 });
550 // Routine 16
551 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
553 std::span<const gfx::TileInfo> tiles,
554 [[maybe_unused]] const DungeonState* state) {
555 self->DrawRightwards4x4_1to16(obj, bg, tiles);
556 });
557 // Routine 17 - Diagonal Acute BothBG
558 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
560 std::span<const gfx::TileInfo> tiles,
561 [[maybe_unused]] const DungeonState* state) {
562 self->DrawDiagonalAcute_1to16_BothBG(obj, bg, tiles);
563 });
564 // Routine 18 - Diagonal Grave BothBG
565 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
567 std::span<const gfx::TileInfo> tiles,
568 [[maybe_unused]] const DungeonState* state) {
569 self->DrawDiagonalGrave_1to16_BothBG(obj, bg, tiles);
570 });
571 // Routine 19 - 4x4 Corner (Type 2 corners)
572 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
574 std::span<const gfx::TileInfo> tiles,
575 [[maybe_unused]] const DungeonState* state) {
576 self->DrawCorner4x4(obj, bg, tiles);
577 });
578
579 // Routine 20 - Edge objects 1x2 +2
580 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
582 std::span<const gfx::TileInfo> tiles,
583 [[maybe_unused]] const DungeonState* state) {
584 self->DrawRightwards1x2_1to16_plus2(obj, bg, tiles);
585 });
586 // Routine 21 - Edge with perimeter 1x1 +3
587 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
589 std::span<const gfx::TileInfo> tiles,
590 [[maybe_unused]] const DungeonState* state) {
591 self->DrawRightwardsHasEdge1x1_1to16_plus3(obj, bg, tiles);
592 });
593 // Routine 22 - Edge variant 1x1 +2
594 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
596 std::span<const gfx::TileInfo> tiles,
597 [[maybe_unused]] const DungeonState* state) {
598 self->DrawRightwardsHasEdge1x1_1to16_plus2(obj, bg, tiles);
599 });
600 // Routine 23 - Top corners 1x2 +13
601 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
603 std::span<const gfx::TileInfo> tiles,
604 [[maybe_unused]] const DungeonState* state) {
605 self->DrawRightwardsTopCorners1x2_1to16_plus13(obj, bg, tiles);
606 });
607 // Routine 24 - Bottom corners 1x2 +13
608 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
610 std::span<const gfx::TileInfo> tiles,
611 [[maybe_unused]] const DungeonState* state) {
613 });
614 // Routine 25 - Solid fill 1x1 +3 (floor patterns)
615 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
617 std::span<const gfx::TileInfo> tiles,
618 [[maybe_unused]] const DungeonState* state) {
619 self->DrawRightwards1x1Solid_1to16_plus3(obj, bg, tiles);
620 });
621 // Routine 26 - Door switcherer
622 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
624 std::span<const gfx::TileInfo> tiles,
625 [[maybe_unused]] const DungeonState* state) {
626 self->DrawDoorSwitcherer(obj, bg, tiles);
627 });
628 // Routine 27 - Decorations 4x4 spaced 2
629 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
631 std::span<const gfx::TileInfo> tiles,
632 [[maybe_unused]] const DungeonState* state) {
633 self->DrawRightwardsDecor4x4spaced2_1to16(obj, bg, tiles);
634 });
635 // Routine 28 - Statues 2x3 spaced 2
636 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
638 std::span<const gfx::TileInfo> tiles,
639 [[maybe_unused]] const DungeonState* state) {
640 self->DrawRightwardsStatue2x3spaced2_1to16(obj, bg, tiles);
641 });
642 // Routine 29 - Pillars 2x4 spaced 4
643 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
645 std::span<const gfx::TileInfo> tiles,
646 [[maybe_unused]] const DungeonState* state) {
647 self->DrawRightwardsPillar2x4spaced4_1to16(obj, bg, tiles);
648 });
649 // Routine 30 - Decorations 4x3 spaced 4
650 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
652 std::span<const gfx::TileInfo> tiles,
653 [[maybe_unused]] const DungeonState* state) {
654 self->DrawRightwardsDecor4x3spaced4_1to16(obj, bg, tiles);
655 });
656 // Routine 31 - Doubled 2x2 spaced 2
657 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
659 std::span<const gfx::TileInfo> tiles,
660 [[maybe_unused]] const DungeonState* state) {
661 self->DrawRightwardsDoubled2x2spaced2_1to16(obj, bg, tiles);
662 });
663 // Routine 32 - Decorations 2x2 spaced 12
664 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
666 std::span<const gfx::TileInfo> tiles,
667 [[maybe_unused]] const DungeonState* state) {
668 self->DrawRightwardsDecor2x2spaced12_1to16(obj, bg, tiles);
669 });
670 // Routine 33 - Somaria Line
671 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
673 std::span<const gfx::TileInfo> tiles,
674 [[maybe_unused]] const DungeonState* state) {
675 self->DrawSomariaLine(obj, bg, tiles);
676 });
677 // Routine 34 - Water Face
678 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
680 std::span<const gfx::TileInfo> tiles,
681 [[maybe_unused]] const DungeonState* state) {
682 self->DrawWaterFace(obj, bg, tiles);
683 });
684 // Routine 35 - 4x4 Corner BothBG
685 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
687 std::span<const gfx::TileInfo> tiles,
688 [[maybe_unused]] const DungeonState* state) {
689 self->Draw4x4Corner_BothBG(obj, bg, tiles);
690 });
691 // Routine 36 - Weird Corner Bottom BothBG
692 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
694 std::span<const gfx::TileInfo> tiles,
695 [[maybe_unused]] const DungeonState* state) {
696 self->DrawWeirdCornerBottom_BothBG(obj, bg, tiles);
697 });
698 // Routine 37 - Weird Corner Top BothBG
699 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
701 std::span<const gfx::TileInfo> tiles,
702 [[maybe_unused]] const DungeonState* state) {
703 self->DrawWeirdCornerTop_BothBG(obj, bg, tiles);
704 });
705 // Routine 38 - Nothing
706 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
708 std::span<const gfx::TileInfo> tiles,
709 [[maybe_unused]] const DungeonState* state) {
710 self->DrawNothing(obj, bg, tiles);
711 });
712 // Routine 39 - Chest rendering (small + big)
713 draw_routines_.push_back(
714 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
715 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
716 self->DrawChest(obj, bg, tiles, state);
717 });
718 // Routine 40 - Rightwards 4x2 (Floor Tile)
719 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
721 std::span<const gfx::TileInfo> tiles,
722 [[maybe_unused]] const DungeonState* state) {
723 self->DrawRightwards4x2_1to16(obj, bg, tiles);
724 });
725 // Routine 41 - Rightwards Decor 4x2 spaced 8 (12-column spacing)
726 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
728 std::span<const gfx::TileInfo> tiles,
729 [[maybe_unused]] const DungeonState* state) {
730 self->DrawRightwardsDecor4x2spaced8_1to16(obj, bg, tiles);
731 });
732 // Routine 42 - Rightwards Cannon Hole 4x3
733 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
735 std::span<const gfx::TileInfo> tiles,
736 [[maybe_unused]] const DungeonState* state) {
737 self->DrawRightwardsCannonHole4x3_1to16(obj, bg, tiles);
738 });
739 // Routine 43 - Downwards Floor 4x4 (object 0x70)
740 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
742 std::span<const gfx::TileInfo> tiles,
743 [[maybe_unused]] const DungeonState* state) {
744 self->DrawDownwardsFloor4x4_1to16(obj, bg, tiles);
745 });
746 // Routine 44 - Downwards 1x1 Solid +3 (object 0x71)
747 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
749 std::span<const gfx::TileInfo> tiles,
750 [[maybe_unused]] const DungeonState* state) {
751 self->DrawDownwards1x1Solid_1to16_plus3(obj, bg, tiles);
752 });
753 // Routine 45 - Downwards Decor 4x4 spaced 2 (objects 0x73-0x74)
754 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
756 std::span<const gfx::TileInfo> tiles,
757 [[maybe_unused]] const DungeonState* state) {
758 self->DrawDownwardsDecor4x4spaced2_1to16(obj, bg, tiles);
759 });
760 // Routine 46 - Downwards Pillar 2x4 spaced 2 (objects 0x75, 0x87)
761 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
763 std::span<const gfx::TileInfo> tiles,
764 [[maybe_unused]] const DungeonState* state) {
765 self->DrawDownwardsPillar2x4spaced2_1to16(obj, bg, tiles);
766 });
767 // Routine 47 - Downwards Decor 3x4 spaced 4 (objects 0x76-0x77)
768 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
770 std::span<const gfx::TileInfo> tiles,
771 [[maybe_unused]] const DungeonState* state) {
772 self->DrawDownwardsDecor3x4spaced4_1to16(obj, bg, tiles);
773 });
774 // Routine 48 - Downwards Decor 2x2 spaced 12 (objects 0x78, 0x7B)
775 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
777 std::span<const gfx::TileInfo> tiles,
778 [[maybe_unused]] const DungeonState* state) {
779 self->DrawDownwardsDecor2x2spaced12_1to16(obj, bg, tiles);
780 });
781 // Routine 49 - Downwards Line 1x1 +1 (object 0x7C)
782 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
784 std::span<const gfx::TileInfo> tiles,
785 [[maybe_unused]] const DungeonState* state) {
786 self->DrawDownwardsLine1x1_1to16plus1(obj, bg, tiles);
787 });
788 // Routine 50 - Downwards Decor 2x4 spaced 8 (objects 0x7F, 0x80)
789 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
791 std::span<const gfx::TileInfo> tiles,
792 [[maybe_unused]] const DungeonState* state) {
793 self->DrawDownwardsDecor2x4spaced8_1to16(obj, bg, tiles);
794 });
795 // Routine 51 - Rightwards Line 1x1 +1 (object 0x50)
796 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
798 std::span<const gfx::TileInfo> tiles,
799 [[maybe_unused]] const DungeonState* state) {
800 self->DrawRightwardsLine1x1_1to16plus1(obj, bg, tiles);
801 });
802 // Routine 52 - Rightwards Bar 4x3 (object 0x4C)
803 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
805 std::span<const gfx::TileInfo> tiles,
806 [[maybe_unused]] const DungeonState* state) {
807 self->DrawRightwardsBar4x3_1to16(obj, bg, tiles);
808 });
809 // Routine 53 - Rightwards Shelf 4x4 (objects 0x4D-0x4F)
810 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
812 std::span<const gfx::TileInfo> tiles,
813 [[maybe_unused]] const DungeonState* state) {
814 self->DrawRightwardsShelf4x4_1to16(obj, bg, tiles);
815 });
816 // Routine 54 - Rightwards Big Rail 1x3 +5 (object 0x5D)
817 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
819 std::span<const gfx::TileInfo> tiles,
820 [[maybe_unused]] const DungeonState* state) {
821 self->DrawRightwardsBigRail1x3_1to16plus5(obj, bg, tiles);
822 });
823 // Routine 55 - Rightwards Block 2x2 spaced 2 (object 0x5E)
824 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
826 std::span<const gfx::TileInfo> tiles,
827 [[maybe_unused]] const DungeonState* state) {
828 self->DrawRightwardsBlock2x2spaced2_1to16(obj, bg, tiles);
829 });
830
831 // ============================================================================
832 // Phase 4: SuperSquare Routines (routines 56-64)
833 // ============================================================================
834
835 // Routine 56 - 4x4 Blocks in 4x4 SuperSquare (objects 0xC0, 0xC2)
836 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
838 std::span<const gfx::TileInfo> tiles,
839 const DungeonState* state) {
840 self->DrawUsingRegistryRoutine(56, obj, bg, tiles, state);
841 });
842
843 // Routine 57 - 3x3 Floor in 4x4 SuperSquare (objects 0xC3, 0xD7)
844 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
846 std::span<const gfx::TileInfo> tiles,
847 const DungeonState* state) {
848 self->DrawUsingRegistryRoutine(57, obj, bg, tiles, state);
849 });
850
851 // Routine 58 - 4x4 Floor in 4x4 SuperSquare (objects 0xC5-0xCA, 0xD1-0xD2,
852 // 0xD9, 0xDF-0xE8)
853 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
855 std::span<const gfx::TileInfo> tiles,
856 const DungeonState* state) {
857 self->DrawUsingRegistryRoutine(58, obj, bg, tiles, state);
858 });
859
860 // Routine 59 - 4x4 Floor One in 4x4 SuperSquare (object 0xC4)
861 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
863 std::span<const gfx::TileInfo> tiles,
864 const DungeonState* state) {
865 self->DrawUsingRegistryRoutine(59, obj, bg, tiles, state);
866 });
867
868 // Routine 60 - 4x4 Floor Two in 4x4 SuperSquare (object 0xDB)
869 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
871 std::span<const gfx::TileInfo> tiles,
872 const DungeonState* state) {
873 self->DrawUsingRegistryRoutine(60, obj, bg, tiles, state);
874 });
875
876 // Routine 61 - Big Hole 4x4 (object 0xA4)
877 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
879 std::span<const gfx::TileInfo> tiles,
880 const DungeonState* state) {
881 self->DrawUsingRegistryRoutine(61, obj, bg, tiles, state);
882 });
883
884 // Routine 62 - Spike 2x2 in 4x4 SuperSquare (object 0xDE)
885 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
887 std::span<const gfx::TileInfo> tiles,
888 const DungeonState* state) {
889 self->DrawUsingRegistryRoutine(62, obj, bg, tiles, state);
890 });
891
892 // Routine 63 - Table Rock 4x4 (object 0xDD)
893 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
895 std::span<const gfx::TileInfo> tiles,
896 const DungeonState* state) {
897 self->DrawUsingRegistryRoutine(63, obj, bg, tiles, state);
898 });
899
900 // Routine 64 - Water Overlay 8x8 (objects 0xD8, 0xDA)
901 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
903 std::span<const gfx::TileInfo> tiles,
904 const DungeonState* state) {
905 self->DrawUsingRegistryRoutine(64, obj, bg, tiles, state);
906 });
907
908 // ============================================================================
909 // Phase 4 Step 2: Simple Variant Routines (routines 65-74)
910 // ============================================================================
911
912 // Routine 65 - Downwards Decor 3x4 spaced 2 (objects 0x81-0x84)
913 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
915 std::span<const gfx::TileInfo> tiles,
916 [[maybe_unused]] const DungeonState* state) {
917 self->DrawDownwardsDecor3x4spaced2_1to16(obj, bg, tiles);
918 });
919
920 // Routine 66 - Downwards Big Rail 3x1 plus 5 (object 0x88)
921 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
923 std::span<const gfx::TileInfo> tiles,
924 [[maybe_unused]] const DungeonState* state) {
925 self->DrawDownwardsBigRail3x1_1to16plus5(obj, bg, tiles);
926 });
927
928 // Routine 67 - Downwards Block 2x2 spaced 2 (object 0x89)
929 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
931 std::span<const gfx::TileInfo> tiles,
932 [[maybe_unused]] const DungeonState* state) {
933 self->DrawDownwardsBlock2x2spaced2_1to16(obj, bg, tiles);
934 });
935
936 // Routine 68 - Downwards Cannon Hole 3x6 (objects 0x85-0x86)
937 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
939 std::span<const gfx::TileInfo> tiles,
940 [[maybe_unused]] const DungeonState* state) {
941 self->DrawDownwardsCannonHole3x6_1to16(obj, bg, tiles);
942 });
943
944 // Routine 69 - Downwards Bar 2x3 (object 0x8F)
945 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
947 std::span<const gfx::TileInfo> tiles,
948 [[maybe_unused]] const DungeonState* state) {
949 self->DrawDownwardsBar2x3_1to16(obj, bg, tiles);
950 });
951
952 // Routine 70 - Downwards Pots 2x2 (object 0x95)
953 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
955 std::span<const gfx::TileInfo> tiles,
956 [[maybe_unused]] const DungeonState* state) {
957 self->DrawDownwardsPots2x2_1to16(obj, bg, tiles);
958 });
959
960 // Routine 71 - Downwards Hammer Pegs 2x2 (object 0x96)
961 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
963 std::span<const gfx::TileInfo> tiles,
964 [[maybe_unused]] const DungeonState* state) {
965 self->DrawDownwardsHammerPegs2x2_1to16(obj, bg, tiles);
966 });
967
968 // Routine 72 - Rightwards Edge 1x1 plus 7 (objects 0xB0-0xB1)
969 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
971 std::span<const gfx::TileInfo> tiles,
972 [[maybe_unused]] const DungeonState* state) {
973 self->DrawRightwardsEdge1x1_1to16plus7(obj, bg, tiles);
974 });
975
976 // Routine 73 - Rightwards Pots 2x2 (object 0xBC)
977 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
979 std::span<const gfx::TileInfo> tiles,
980 [[maybe_unused]] const DungeonState* state) {
981 self->DrawRightwardsPots2x2_1to16(obj, bg, tiles);
982 });
983
984 // Routine 74 - Rightwards Hammer Pegs 2x2 (object 0xBD)
985 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
987 std::span<const gfx::TileInfo> tiles,
988 [[maybe_unused]] const DungeonState* state) {
989 self->DrawRightwardsHammerPegs2x2_1to16(obj, bg, tiles);
990 });
991
992 // ============================================================================
993 // Phase 4 Step 3: Diagonal Ceiling Routines (routines 75-78)
994 // ============================================================================
995
996 // Routine 75 - Diagonal Ceiling Top Left (objects 0xA0, 0xA5, 0xA9)
997 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
999 std::span<const gfx::TileInfo> tiles,
1000 [[maybe_unused]] const DungeonState* state) {
1001 self->DrawDiagonalCeilingTopLeft(obj, bg, tiles);
1002 });
1003
1004 // Routine 76 - Diagonal Ceiling Bottom Left (objects 0xA1, 0xA6, 0xAA)
1005 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1007 std::span<const gfx::TileInfo> tiles,
1008 [[maybe_unused]] const DungeonState* state) {
1009 self->DrawDiagonalCeilingBottomLeft(obj, bg, tiles);
1010 });
1011
1012 // Routine 77 - Diagonal Ceiling Top Right (objects 0xA2, 0xA7, 0xAB)
1013 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1015 std::span<const gfx::TileInfo> tiles,
1016 [[maybe_unused]] const DungeonState* state) {
1017 self->DrawDiagonalCeilingTopRight(obj, bg, tiles);
1018 });
1019
1020 // Routine 78 - Diagonal Ceiling Bottom Right (objects 0xA3, 0xA8, 0xAC)
1021 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1023 std::span<const gfx::TileInfo> tiles,
1024 [[maybe_unused]] const DungeonState* state) {
1025 self->DrawDiagonalCeilingBottomRight(obj, bg, tiles);
1026 });
1027
1028 // ============================================================================
1029 // Phase 4 Step 5: Special Routines (routines 79-82)
1030 // ============================================================================
1031
1032 // Routine 79 - Closed Chest Platform (object 0xC1, 68 tiles)
1033 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1035 std::span<const gfx::TileInfo> tiles,
1036 [[maybe_unused]] const DungeonState* state) {
1037 self->DrawClosedChestPlatform(obj, bg, tiles);
1038 });
1039
1040 // Routine 80 - Moving Wall West (object 0xCD, 28 tiles)
1041 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1043 std::span<const gfx::TileInfo> tiles,
1044 [[maybe_unused]] const DungeonState* state) {
1045 self->DrawMovingWallWest(obj, bg, tiles);
1046 });
1047
1048 // Routine 81 - Moving Wall East (object 0xCE, 28 tiles)
1049 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1051 std::span<const gfx::TileInfo> tiles,
1052 [[maybe_unused]] const DungeonState* state) {
1053 self->DrawMovingWallEast(obj, bg, tiles);
1054 });
1055
1056 // Routine 82 - Open Chest Platform (object 0xDC, 21 tiles)
1057 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1059 std::span<const gfx::TileInfo> tiles,
1060 [[maybe_unused]] const DungeonState* state) {
1061 self->DrawOpenChestPlatform(obj, bg, tiles);
1062 });
1063
1064 // ============================================================================
1065 // New Special Routines (Phase 5) - Stairs, Locks, Interactive Objects
1066 // ============================================================================
1067
1068 // Routine 83 - InterRoom Fat Stairs Up (object 0x12D)
1069 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1071 std::span<const gfx::TileInfo> tiles,
1072 const DungeonState* state) {
1073 self->DrawUsingRegistryRoutine(83, obj, bg, tiles, state);
1074 });
1075
1076 // Routine 84 - InterRoom Fat Stairs Down A (object 0x12E)
1077 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1079 std::span<const gfx::TileInfo> tiles,
1080 const DungeonState* state) {
1081 self->DrawUsingRegistryRoutine(84, obj, bg, tiles, state);
1082 });
1083
1084 // Routine 85 - InterRoom Fat Stairs Down B (object 0x12F)
1085 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1087 std::span<const gfx::TileInfo> tiles,
1088 const DungeonState* state) {
1089 self->DrawUsingRegistryRoutine(85, obj, bg, tiles, state);
1090 });
1091
1092 // Routine 86 - Auto Stairs (objects 0x130-0x133)
1093 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1095 std::span<const gfx::TileInfo> tiles,
1096 const DungeonState* state) {
1097 self->DrawUsingRegistryRoutine(86, obj, bg, tiles, state);
1098 });
1099
1100 // Routine 87 - Straight InterRoom Stairs (Type 3 objects 0x21E-0x229)
1101 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1103 std::span<const gfx::TileInfo> tiles,
1104 const DungeonState* state) {
1105 self->DrawUsingRegistryRoutine(87, obj, bg, tiles, state);
1106 });
1107
1108 // Routine 88 - Spiral Stairs Going Up Upper (object 0x138)
1109 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1111 std::span<const gfx::TileInfo> tiles,
1112 const DungeonState* state) {
1113 self->DrawUsingRegistryRoutine(88, obj, bg, tiles, state);
1114 });
1115
1116 // Routine 89 - Spiral Stairs Going Down Upper (object 0x139)
1117 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1119 std::span<const gfx::TileInfo> tiles,
1120 const DungeonState* state) {
1121 self->DrawUsingRegistryRoutine(89, obj, bg, tiles, state);
1122 });
1123
1124 // Routine 90 - Spiral Stairs Going Up Lower (object 0x13A)
1125 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1127 std::span<const gfx::TileInfo> tiles,
1128 const DungeonState* state) {
1129 self->DrawUsingRegistryRoutine(90, obj, bg, tiles, state);
1130 });
1131
1132 // Routine 91 - Spiral Stairs Going Down Lower (object 0x13B)
1133 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1135 std::span<const gfx::TileInfo> tiles,
1136 const DungeonState* state) {
1137 self->DrawUsingRegistryRoutine(91, obj, bg, tiles, state);
1138 });
1139
1140 // Routine 92 - Big Key Lock (Type 3 object 0x218)
1141 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1143 std::span<const gfx::TileInfo> tiles,
1144 const DungeonState* state) {
1145 self->DrawUsingRegistryRoutine(92, obj, bg, tiles, state);
1146 });
1147
1148 // Routine 93 - Bombable Floor (Type 3 object 0x247)
1149 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1151 std::span<const gfx::TileInfo> tiles,
1152 const DungeonState* state) {
1153 self->DrawUsingRegistryRoutine(93, obj, bg, tiles, state);
1154 });
1155
1156 // Routine 94 - Empty Water Face (Type 3 object 0x200)
1157 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1159 std::span<const gfx::TileInfo> tiles,
1160 const DungeonState* state) {
1161 self->DrawUsingRegistryRoutine(94, obj, bg, tiles, state);
1162 });
1163
1164 // Routine 95 - Spitting Water Face (Type 3 object 0x201)
1165 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1167 std::span<const gfx::TileInfo> tiles,
1168 const DungeonState* state) {
1169 self->DrawUsingRegistryRoutine(95, obj, bg, tiles, state);
1170 });
1171
1172 // Routine 96 - Drenching Water Face (Type 3 object 0x202)
1173 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1175 std::span<const gfx::TileInfo> tiles,
1176 const DungeonState* state) {
1177 self->DrawUsingRegistryRoutine(96, obj, bg, tiles, state);
1178 });
1179
1180 // Routine 97 - Prison Cell (Type 3 objects 0x20D, 0x217)
1181 // This routine draws to both BG1 and BG2 with horizontal flip symmetry
1182 // Note: secondary_bg is set in DrawObject() for dual-BG objects
1183 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1185 std::span<const gfx::TileInfo> tiles,
1186 const DungeonState* state) {
1187 self->DrawUsingRegistryRoutine(97, obj, bg, tiles, state);
1188 });
1189
1190 // Routine 98 - Bed 4x5 (Type 2 objects 0x122, 0x128)
1191 draw_routines_.push_back(
1192 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1193 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1194 self->DrawBed4x5(obj, bg, tiles, state);
1195 });
1196
1197 // Routine 99 - Rightwards 3x6 (Type 2 object 0x12C, Type 3 0x236-0x237)
1198 draw_routines_.push_back(
1199 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1200 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1201 self->DrawRightwards3x6(obj, bg, tiles, state);
1202 });
1203
1204 // Routine 100 - Utility 6x3 (Type 2 object 0x13E, Type 3 0x24D, 0x25D)
1205 draw_routines_.push_back(
1206 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1207 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1208 self->DrawUtility6x3(obj, bg, tiles, state);
1209 });
1210
1211 // Routine 101 - Utility 3x5 (Type 3 objects 0x255, 0x25B)
1212 draw_routines_.push_back(
1213 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1214 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1215 self->DrawUtility3x5(obj, bg, tiles, state);
1216 });
1217
1218 // Routine 102 - Vertical Turtle Rock Pipe (Type 3 objects 0x23A, 0x23B)
1219 draw_routines_.push_back(
1220 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1221 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1222 self->DrawVerticalTurtleRockPipe(obj, bg, tiles, state);
1223 });
1224
1225 // Routine 103 - Horizontal Turtle Rock Pipe (Type 3 objects 0x23C, 0x23D, 0x25C)
1226 draw_routines_.push_back(
1227 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1228 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1229 self->DrawHorizontalTurtleRockPipe(obj, bg, tiles, state);
1230 });
1231
1232 // Routine 104 - Light Beam on Floor (Type 3 object 0x270)
1233 draw_routines_.push_back(
1234 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1235 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1236 self->DrawLightBeam(obj, bg, tiles, state);
1237 });
1238
1239 // Routine 105 - Big Light Beam on Floor (Type 3 object 0x271)
1240 draw_routines_.push_back(
1241 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1242 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1243 self->DrawBigLightBeam(obj, bg, tiles, state);
1244 });
1245
1246 // Routine 106 - Boss Shell 4x4 (Type 3 objects 0x272, 0x27B, 0xF95)
1247 draw_routines_.push_back(
1248 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1249 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1250 self->DrawBossShell4x4(obj, bg, tiles, state);
1251 });
1252
1253 // Routine 107 - Solid Wall Decor 3x4 (Type 3 objects 0x269-0x26A, 0x26E-0x26F)
1254 draw_routines_.push_back(
1255 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1256 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1257 self->DrawSolidWallDecor3x4(obj, bg, tiles, state);
1258 });
1259
1260 // Routine 108 - Archery Game Target Door (Type 3 objects 0x260-0x261)
1261 draw_routines_.push_back(
1262 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1263 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1264 self->DrawArcheryGameTargetDoor(obj, bg, tiles, state);
1265 });
1266
1267 // Routine 109 - Ganon Triforce Floor Decor (Type 3 object 0x278)
1268 draw_routines_.push_back(
1269 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1270 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1271 self->DrawGanonTriforceFloorDecor(obj, bg, tiles, state);
1272 });
1273
1274 // Routine 110 - Single 2x2 (pots, statues, single-instance 2x2 objects)
1275 draw_routines_.push_back(
1276 [](ObjectDrawer* self, const RoomObject& obj, gfx::BackgroundBuffer& bg,
1277 std::span<const gfx::TileInfo> tiles, const DungeonState* state) {
1278 self->DrawSingle2x2(obj, bg, tiles, state);
1279 });
1280
1281 // Routine 111 - Waterfall47 (object 0x47)
1282 // ASM: RoomDraw_Waterfall47 - draws 1x5 columns, count = (size+1)*2
1283 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1285 std::span<const gfx::TileInfo> tiles,
1286 [[maybe_unused]] const DungeonState* state) {
1287 int size = obj.size_ & 0x0F;
1288 int count = (size + 1) * 2; // ASM: ASL $B2
1289
1290 if (tiles.size() < 5)
1291 return;
1292
1293 // Draw first 1x5 column
1294 for (int row = 0; row < 5; row++) {
1295 self->WriteTile8(bg, obj.x_, obj.y_ + row, tiles[row]);
1296 }
1297
1298 // Draw middle columns
1299 for (int s = 0; s < count; s++) {
1300 int col_x = obj.x_ + 1 + s;
1301 for (int row = 0; row < 5; row++) {
1302 // Use tiles at offset 10 for middle columns
1303 size_t tile_idx = std::min(size_t(5 + row), tiles.size() - 1);
1304 self->WriteTile8(bg, col_x, obj.y_ + row, tiles[tile_idx]);
1305 }
1306 }
1307
1308 // Draw last 1x5 column
1309 int last_x = obj.x_ + 1 + count;
1310 for (int row = 0; row < 5; row++) {
1311 size_t tile_idx = std::min(size_t(10 + row), tiles.size() - 1);
1312 self->WriteTile8(bg, last_x, obj.y_ + row, tiles[tile_idx]);
1313 }
1314 });
1315
1316 // Routine 112 - Waterfall48 (object 0x48)
1317 // ASM: RoomDraw_Waterfall48 - draws 1x3 columns, count = (size+1)*2
1318 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1320 std::span<const gfx::TileInfo> tiles,
1321 [[maybe_unused]] const DungeonState* state) {
1322 int size = obj.size_ & 0x0F;
1323 int count = (size + 1) * 2; // ASM: ASL $B2
1324
1325 if (tiles.size() < 3)
1326 return;
1327
1328 // Draw first 1x3 column
1329 for (int row = 0; row < 3; row++) {
1330 self->WriteTile8(bg, obj.x_, obj.y_ + row, tiles[row]);
1331 }
1332
1333 // Draw middle columns
1334 for (int s = 0; s < count; s++) {
1335 int col_x = obj.x_ + 1 + s;
1336 for (int row = 0; row < 3; row++) {
1337 self->WriteTile8(bg, col_x, obj.y_ + row, tiles[row]);
1338 }
1339 }
1340
1341 // Draw last 1x3 column using different tiles
1342 int last_x = obj.x_ + 1 + count;
1343 for (int row = 0; row < 3; row++) {
1344 size_t tile_idx = std::min(size_t(3 + row), tiles.size() - 1);
1345 self->WriteTile8(bg, last_x, obj.y_ + row, tiles[tile_idx]);
1346 }
1347 });
1348
1349 // Routine 113 - Single 4x4 (NO repetition)
1350 // ASM: RoomDraw_4x4 - draws a single 4x4 pattern (16 tiles)
1351 // Used for: 0xFEB (large decor), and other single 4x4 objects
1352 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1354 std::span<const gfx::TileInfo> tiles,
1355 [[maybe_unused]] const DungeonState* state) {
1356 self->DrawSingle4x4(obj, bg, tiles, state);
1357 });
1358
1359 // Routine 114 - Single 4x3 (NO repetition)
1360 // ASM: RoomDraw_TableRock4x3 - draws a single 4x3 pattern (12 tiles)
1361 // Used for: 0xFED (water grate), 0xFB1 (big chest), etc.
1362 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1364 std::span<const gfx::TileInfo> tiles,
1365 [[maybe_unused]] const DungeonState* state) {
1366 self->DrawSingle4x3(obj, bg, tiles, state);
1367 });
1368
1369 // Routine 115 - RupeeFloor (special pattern for 0xF92)
1370 // ASM: RoomDraw_RupeeFloor - draws 3 columns of 2-tile pairs at 3 Y positions
1371 // Pattern: 6 tiles wide, 8 rows tall with gaps (rows 2 and 5 are empty)
1372 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1374 std::span<const gfx::TileInfo> tiles,
1375 [[maybe_unused]] const DungeonState* state) {
1376 self->DrawRupeeFloor(obj, bg, tiles, state);
1377 });
1378
1379 // Routine 116 - Actual 4x4 tile8 pattern (32x32 pixels, NO repetition)
1380 // ASM: RoomDraw_4x4 - draws exactly 4 columns x 4 rows = 16 tiles
1381 // Used for: 0xFE6 (pit)
1382 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1384 std::span<const gfx::TileInfo> tiles,
1385 [[maybe_unused]] const DungeonState* state) {
1386 self->DrawActual4x4(obj, bg, tiles, state);
1387 });
1388
1389 auto ensure_index = [this](size_t index) {
1390 while (draw_routines_.size() <= index) {
1391 draw_routines_.push_back([](ObjectDrawer* self, const RoomObject& obj,
1393 std::span<const gfx::TileInfo> tiles,
1394 [[maybe_unused]] const DungeonState* state) {
1395 self->DrawNothing(obj, bg, tiles, state);
1396 });
1397 }
1398 };
1399
1400 // Routine 117 - Vertical rails with CORNER+MIDDLE+END pattern (0x8A-0x8C)
1401 // ASM: RoomDraw_DownwardsHasEdge1x1_1to16_plus23 - matches horizontal 0x22
1402 ensure_index(117);
1403 draw_routines_[117] = [](ObjectDrawer* self, const RoomObject& obj,
1405 std::span<const gfx::TileInfo> tiles,
1406 [[maybe_unused]] const DungeonState* state) {
1407 self->DrawDownwardsHasEdge1x1_1to16_plus23(obj, bg, tiles, state);
1408 };
1409
1410 // Routine 118 - Horizontal long rails with CORNER+MIDDLE+END pattern (0x5F)
1411 ensure_index(118);
1412 draw_routines_[118] = [](ObjectDrawer* self, const RoomObject& obj,
1414 std::span<const gfx::TileInfo> tiles,
1415 [[maybe_unused]] const DungeonState* state) {
1416 self->DrawRightwardsHasEdge1x1_1to16_plus23(obj, bg, tiles, state);
1417 };
1418
1419 // Routine 130 - Custom Object (Oracle of Secrets 0x31, 0x32)
1420 // Uses external binary files instead of ROM tile data.
1421 // Requires CustomObjectManager initialization and enable_custom_objects flag.
1422 ensure_index(130);
1423 draw_routines_[130] = [](ObjectDrawer* self, const RoomObject& obj,
1425 std::span<const gfx::TileInfo> tiles,
1426 [[maybe_unused]] const DungeonState* state) {
1427 self->DrawCustomObject(obj, bg, tiles, state);
1428 };
1429
1430 routines_initialized_ = true;
1431}
1432
1433int ObjectDrawer::GetDrawRoutineId(int16_t object_id) const {
1434 // Delegate to the unified registry for the canonical mapping
1436}
1437
1438// ============================================================================
1439// Draw Routine Implementations (Based on ZScream patterns)
1440// ============================================================================
1441
1442void ObjectDrawer::DrawDoor(const DoorDef& door, int door_index,
1445 [[maybe_unused]] const DungeonState* state) {
1446 // Door rendering based on ZELDA3_DUNGEON_SPEC.md Section 5 and disassembly
1447 // Uses DoorType and DoorDirection enums for type safety
1448 // Position calculations via DoorPositionManager
1449
1450 LOG_DEBUG("ObjectDrawer", "DrawDoor: idx=%d type=%d dir=%d pos=%d",
1451 door_index, static_cast<int>(door.type),
1452 static_cast<int>(door.direction), door.position);
1453
1454 if (!rom_ || !rom_->is_loaded() || !room_gfx_buffer_) {
1455 LOG_DEBUG("ObjectDrawer", "DrawDoor: SKIPPED - rom=%p loaded=%d gfx=%p",
1456 (void*)rom_, rom_ ? rom_->is_loaded() : 0,
1457 (void*)room_gfx_buffer_);
1458 return;
1459 }
1460
1461 auto& bitmap = bg1.bitmap();
1462 if (!bitmap.is_active() || bitmap.width() == 0) {
1463 LOG_DEBUG("ObjectDrawer",
1464 "DrawDoor: SKIPPED - bitmap not active or zero width");
1465 return;
1466 }
1467
1468 // Get door position from DoorPositionManager
1469 auto [tile_x, tile_y] = door.GetTileCoords();
1470 auto dims = door.GetDimensions();
1471 int door_width = dims.width_tiles;
1472 int door_height = dims.height_tiles;
1473
1474 LOG_DEBUG("ObjectDrawer", "DrawDoor: tile_pos=(%d,%d) dims=%dx%d", tile_x,
1475 tile_y, door_width, door_height);
1476
1477 // Door graphics use an indirect addressing scheme:
1478 // 1. kDoorGfxUp/Down/Left/Right point to offset tables (DoorGFXDataOffset_*)
1479 // 2. Each table entry is a 16-bit offset into RoomDrawObjectData
1480 // 3. RoomDrawObjectData base is at PC 0x1B52 (SNES $00:9B52)
1481 // 4. Actual tile data = 0x1B52 + offset_from_table
1482
1483 // Select offset table based on direction
1484 int offset_table_addr = 0;
1485 switch (door.direction) {
1487 offset_table_addr = kDoorGfxUp;
1488 break; // 0x4D9E
1490 offset_table_addr = kDoorGfxDown;
1491 break; // 0x4E06
1493 offset_table_addr = kDoorGfxLeft;
1494 break; // 0x4E66
1496 offset_table_addr = kDoorGfxRight;
1497 break; // 0x4EC6
1498 }
1499
1500 // Calculate door type index (door types step by 2: 0x00, 0x02, 0x04, ...)
1501 int type_value = static_cast<int>(door.type);
1502 int type_index = type_value / 2;
1503
1504 // Read offset from table (each entry is 2 bytes)
1505 int table_entry_addr = offset_table_addr + (type_index * 2);
1506 if (table_entry_addr + 1 >= static_cast<int>(rom_->size())) {
1507 DrawDoorIndicator(bg1, tile_x, tile_y, door_width, door_height,
1508 door.type, door.direction);
1509 return;
1510 }
1511
1512 const auto& rom_data = rom_->data();
1513 uint16_t tile_offset =
1514 rom_data[table_entry_addr] | (rom_data[table_entry_addr + 1] << 8);
1515
1516 // RoomDrawObjectData base address (PC offset)
1517 constexpr int kRoomDrawObjectDataBase = 0x1B52;
1518 int tile_data_addr = kRoomDrawObjectDataBase + tile_offset;
1519
1520 LOG_DEBUG(
1521 "ObjectDrawer",
1522 "DrawDoor: offset_table=0x%X type_idx=%d tile_offset=0x%X tile_addr=0x%X",
1523 offset_table_addr, type_index, tile_offset, tile_data_addr);
1524
1525 // Validate address range (12 tiles * 2 bytes = 24 bytes)
1526 int tiles_per_door = door_width * door_height; // 12 tiles (4x3 or 3x4)
1527 int data_size = tiles_per_door * 2;
1528 if (tile_data_addr < 0 ||
1529 tile_data_addr + data_size > static_cast<int>(rom_->size())) {
1530 LOG_DEBUG("ObjectDrawer",
1531 "DrawDoor: INVALID ADDRESS - falling back to indicator");
1532 DrawDoorIndicator(bg1, tile_x, tile_y, door_width, door_height,
1533 door.type, door.direction);
1534 return;
1535 }
1536
1537 // Read and render door tiles
1538 // All directions use column-major tile order (matching ASM draw routines)
1539 // The ROM stores tiles in column-major order for all door directions
1540 LOG_DEBUG("ObjectDrawer", "DrawDoor: Reading %d tiles from 0x%X",
1541 tiles_per_door, tile_data_addr);
1542 int tile_idx = 0;
1543 auto& priority_buffer = bg1.mutable_priority_data();
1544 auto& coverage_buffer = bg1.mutable_coverage_data();
1545 int bitmap_width = bitmap.width();
1546
1547 for (int dx = 0; dx < door_width; dx++) {
1548 for (int dy = 0; dy < door_height; dy++) {
1549 int addr = tile_data_addr + (tile_idx * 2);
1550 uint16_t tile_word = rom_data[addr] | (rom_data[addr + 1] << 8);
1551
1552 auto tile_info = gfx::WordToTileInfo(tile_word);
1553 int pixel_x = (tile_x + dx) * 8;
1554 int pixel_y = (tile_y + dy) * 8;
1555
1556 if (tile_idx < 4) {
1557 LOG_DEBUG("ObjectDrawer",
1558 "DrawDoor: tile[%d] word=0x%04X id=%d pal=%d pixel=(%d,%d)",
1559 tile_idx, tile_word, tile_info.id_, tile_info.palette_,
1560 pixel_x, pixel_y);
1561 }
1562
1563 DrawTileToBitmap(bitmap, tile_info, pixel_x, pixel_y, room_gfx_buffer_);
1564
1565 // Update priority buffer for this tile
1566 uint8_t priority = tile_info.over_ ? 1 : 0;
1567 const auto& bitmap_data = bitmap.vector();
1568 for (int py = 0; py < 8; py++) {
1569 int dest_y = pixel_y + py;
1570 if (dest_y < 0 || dest_y >= bitmap.height())
1571 continue;
1572 for (int px = 0; px < 8; px++) {
1573 int dest_x = pixel_x + px;
1574 if (dest_x < 0 || dest_x >= bitmap_width)
1575 continue;
1576 int dest_index = dest_y * bitmap_width + dest_x;
1577 if (dest_index >= 0 &&
1578 dest_index < static_cast<int>(coverage_buffer.size())) {
1579 coverage_buffer[dest_index] = 1;
1580 }
1581 if (dest_index < static_cast<int>(bitmap_data.size()) &&
1582 bitmap_data[dest_index] != 255) {
1583 priority_buffer[dest_index] = priority;
1584 }
1585 }
1586 }
1587
1588 tile_idx++;
1589 }
1590 }
1591
1592 LOG_DEBUG("ObjectDrawer",
1593 "DrawDoor: type=%s dir=%s pos=%d at tile(%d,%d) size=%dx%d "
1594 "offset_table=0x%X tile_offset=0x%X tile_addr=0x%X",
1595 std::string(GetDoorTypeName(door.type)).c_str(),
1596 std::string(GetDoorDirectionName(door.direction)).c_str(),
1597 door.position, tile_x, tile_y, door_width, door_height,
1598 offset_table_addr, tile_offset, tile_data_addr);
1599}
1600
1602 int tile_y, int width, int height,
1603 DoorType type, DoorDirection direction) {
1604 // Draw a simple colored rectangle as door indicator when graphics unavailable
1605 // Different colors for different door types using DoorType enum
1606
1607 auto& bitmap = bg.bitmap();
1608 auto& coverage_buffer = bg.mutable_coverage_data();
1609
1610 uint8_t color_idx;
1611 switch (type) {
1614 color_idx = 45; // Standard door color (brown)
1615 break;
1616
1622 color_idx = 60; // Key door - yellowish
1623 break;
1624
1627 color_idx = 58; // Big key - golden
1628 break;
1629
1633 case DoorType::DashWall:
1634 color_idx = 15; // Bombable/destructible - brownish/cracked
1635 break;
1636
1643 color_idx = 30; // Shutter - greenish
1644 break;
1645
1647 color_idx = 42; // Eye watch - lighter brown
1648 break;
1649
1651 color_idx = 35; // Curtain - special
1652 break;
1653
1654 case DoorType::CaveExit:
1659 color_idx = 25; // Cave/dungeon exit - dark
1660 break;
1661
1665 color_idx = 5; // Markers - very faint
1666 break;
1667
1668 default:
1669 color_idx = 50; // Default door color
1670 break;
1671 }
1672
1673 int pixel_x = tile_x * 8;
1674 int pixel_y = tile_y * 8;
1675 int pixel_width = width * 8;
1676 int pixel_height = height * 8;
1677
1678 int bitmap_width = bitmap.width();
1679 int bitmap_height = bitmap.height();
1680
1681 // Draw filled rectangle with border
1682 for (int py = 0; py < pixel_height; py++) {
1683 for (int px = 0; px < pixel_width; px++) {
1684 int dest_x = pixel_x + px;
1685 int dest_y = pixel_y + py;
1686
1687 if (dest_x >= 0 && dest_x < bitmap_width && dest_y >= 0 &&
1688 dest_y < bitmap_height) {
1689 // Draw border (2 pixel thick) or fill
1690 bool is_border = (px < 2 || px >= pixel_width - 2 || py < 2 ||
1691 py >= pixel_height - 2);
1692 uint8_t final_color = is_border ? (color_idx + 5) : color_idx;
1693
1694 int offset = (dest_y * bitmap_width) + dest_x;
1695 bitmap.WriteToPixel(offset, final_color);
1696
1697 if (offset >= 0 &&
1698 offset < static_cast<int>(coverage_buffer.size())) {
1699 coverage_buffer[offset] = 1;
1700 }
1701 }
1702 }
1703 }
1704}
1705
1707 std::span<const gfx::TileInfo> tiles,
1708 [[maybe_unused]] const DungeonState* state) {
1709 // ASM: RoomDraw_Chest - draws a SINGLE chest (2x2) without size-based repetition
1710 // 0xF99 = closed chest, 0xF9A = open chest
1711 // The size byte is NOT used for repetition
1712
1713 // Determine if chest is open
1714 bool is_open = false;
1715 if (state) {
1716 is_open = state->IsChestOpen(room_id_, current_chest_index_);
1717 }
1718
1719 // Increment index for next chest
1721
1722 // Draw SINGLE chest - no repetition based on size
1723 // Standard chests are 2x2 (4 tiles)
1724 // If we have extra tiles loaded, the second 4 are for open state
1725
1726 if (is_open && tiles.size() >= 8) {
1727 // Small chest open tiles (indices 4-7) - SINGLE 2x2 draw
1728 if (tiles.size() >= 8) {
1729 WriteTile8(bg, obj.x_, obj.y_, tiles[4]); // top-left
1730 WriteTile8(bg, obj.x_, obj.y_ + 1, tiles[5]); // bottom-left
1731 WriteTile8(bg, obj.x_ + 1, obj.y_, tiles[6]); // top-right
1732 WriteTile8(bg, obj.x_ + 1, obj.y_ + 1, tiles[7]); // bottom-right
1733 }
1734 return;
1735 }
1736
1737 // Draw closed chest - SINGLE 2x2 pattern (column-major order)
1738 if (tiles.size() >= 4) {
1739 WriteTile8(bg, obj.x_, obj.y_, tiles[0]); // top-left
1740 WriteTile8(bg, obj.x_, obj.y_ + 1, tiles[1]); // bottom-left
1741 WriteTile8(bg, obj.x_ + 1, obj.y_, tiles[2]); // top-right
1742 WriteTile8(bg, obj.x_ + 1, obj.y_ + 1, tiles[3]); // bottom-right
1743 }
1744}
1745
1747 std::span<const gfx::TileInfo> tiles,
1748 [[maybe_unused]] const DungeonState* state) {
1749 // Intentionally empty - represents invisible logic objects or placeholders
1750 // ASM: RoomDraw_Nothing_A ($0190F2), RoomDraw_Nothing_B ($01932E), etc.
1751 // These routines typically just RTS.
1752 LOG_DEBUG("ObjectDrawer", "DrawNothing for object 0x%02X (logic/invisible)",
1753 obj.id_);
1754}
1755
1757 const RoomObject& obj, gfx::BackgroundBuffer& bg,
1758 std::span<const gfx::TileInfo> tiles,
1759 [[maybe_unused]] const DungeonState* state) {
1760 // Pattern: Draws 2x2 tiles rightward (object 0x00 = ceiling, 0xB8-0xB9)
1761 // Size byte determines how many times to repeat (1-15 or 32)
1762 // ROM tile order is COLUMN-MAJOR: [col0_row0, col0_row1, col1_row0, col1_row1]
1763 int size = obj.size_;
1764 if (size == 0)
1765 size = 32; // Special case for object 0x00
1766
1767 // Debug: Log ceiling objects (0x00, 0xB8, 0xB9)
1768 bool is_ceiling = (obj.id_ == 0x00 || obj.id_ == 0xB8 || obj.id_ == 0xB9);
1769 if (is_ceiling && tiles.size() >= 4) {
1770 LOG_DEBUG("ObjectDrawer",
1771 "Ceiling Draw: obj=0x%02X pos=(%d,%d) size=%d tiles=%zu", obj.id_,
1772 obj.x_, obj.y_, size, tiles.size());
1773 LOG_DEBUG("ObjectDrawer", " Tile IDs: [%d, %d, %d, %d]", tiles[0].id_,
1774 tiles[1].id_, tiles[2].id_, tiles[3].id_);
1775 LOG_DEBUG("ObjectDrawer", " Palettes: [%d, %d, %d, %d]", tiles[0].palette_,
1776 tiles[1].palette_, tiles[2].palette_, tiles[3].palette_);
1777 }
1778
1779 LOG_DEBUG("ObjectDrawer",
1780 "DrawRightwards2x2: obj=%04X pos=(%d,%d) size=%d tiles=%zu",
1781 obj.id_, obj.x_, obj.y_, size, tiles.size());
1782
1783 for (int s = 0; s < size; s++) {
1784 if (tiles.size() >= 4) {
1785 // Draw 2x2 pattern in COLUMN-MAJOR order (matching assembly)
1786 // tiles[0] → $BF → (col 0, row 0) = top-left
1787 // tiles[1] → $CB → (col 0, row 1) = bottom-left
1788 // tiles[2] → $C2 → (col 1, row 0) = top-right
1789 // tiles[3] → $CE → (col 1, row 1) = bottom-right
1790 WriteTile8(bg, obj.x_ + (s * 2), obj.y_, tiles[0]); // col 0, row 0
1791 WriteTile8(bg, obj.x_ + (s * 2), obj.y_ + 1, tiles[1]); // col 0, row 1
1792 WriteTile8(bg, obj.x_ + (s * 2) + 1, obj.y_, tiles[2]); // col 1, row 0
1793 WriteTile8(bg, obj.x_ + (s * 2) + 1, obj.y_ + 1,
1794 tiles[3]); // col 1, row 1
1795 } else {
1796 LOG_DEBUG("ObjectDrawer",
1797 "DrawRightwards2x2: SKIPPING - tiles.size()=%zu < 4",
1798 tiles.size());
1799 }
1800 }
1801}
1802
1804 const RoomObject& obj, gfx::BackgroundBuffer& bg,
1805 std::span<const gfx::TileInfo> tiles,
1806 [[maybe_unused]] const DungeonState* state) {
1807 // Pattern: Draws 2x4 tiles rightward (objects 0x01-0x02)
1808 // Uses RoomDraw_Nx4 with N=2, tiles are COLUMN-MAJOR:
1809 // [col0_row0, col0_row1, col0_row2, col0_row3, col1_row0, col1_row1, col1_row2, col1_row3]
1810 int size = obj.size_;
1811 if (size == 0)
1812 size = 26; // Special case
1813
1814 LOG_DEBUG("ObjectDrawer",
1815 "Wall Draw 2x4: obj=0x%03X pos=(%d,%d) size=%d tiles=%zu", obj.id_,
1816 obj.x_, obj.y_, size, tiles.size());
1817 LOG_DEBUG("ObjectDrawer",
1818 "DrawRightwards2x4: obj=%04X pos=(%d,%d) size=%d tiles=%zu",
1819 obj.id_, obj.x_, obj.y_, size, tiles.size());
1820
1821 for (int s = 0; s < size; s++) {
1822 if (tiles.size() >= 8) {
1823 // Draw 2x4 pattern in COLUMN-MAJOR order (matching RoomDraw_Nx4)
1824 // Column 0 (tiles 0-3)
1825 WriteTile8(bg, obj.x_ + (s * 2), obj.y_, tiles[0]); // col 0, row 0
1826 WriteTile8(bg, obj.x_ + (s * 2), obj.y_ + 1, tiles[1]); // col 0, row 1
1827 WriteTile8(bg, obj.x_ + (s * 2), obj.y_ + 2, tiles[2]); // col 0, row 2
1828 WriteTile8(bg, obj.x_ + (s * 2), obj.y_ + 3, tiles[3]); // col 0, row 3
1829 // Column 1 (tiles 4-7)
1830 WriteTile8(bg, obj.x_ + (s * 2) + 1, obj.y_, tiles[4]); // col 1, row 0
1831 WriteTile8(bg, obj.x_ + (s * 2) + 1, obj.y_ + 1,
1832 tiles[5]); // col 1, row 1
1833 WriteTile8(bg, obj.x_ + (s * 2) + 1, obj.y_ + 2,
1834 tiles[6]); // col 1, row 2
1835 WriteTile8(bg, obj.x_ + (s * 2) + 1, obj.y_ + 3,
1836 tiles[7]); // col 1, row 3
1837 } else if (tiles.size() >= 4) {
1838 // Fallback: with 4 tiles we can only draw 1 column (1x4 pattern)
1839 WriteTile8(bg, obj.x_ + (s * 2), obj.y_, tiles[0]);
1840 WriteTile8(bg, obj.x_ + (s * 2), obj.y_ + 1, tiles[1]);
1841 WriteTile8(bg, obj.x_ + (s * 2), obj.y_ + 2, tiles[2]);
1842 WriteTile8(bg, obj.x_ + (s * 2), obj.y_ + 3, tiles[3]);
1843 }
1844 }
1845}
1846
1848 const RoomObject& obj, gfx::BackgroundBuffer& bg,
1849 std::span<const gfx::TileInfo> tiles,
1850 [[maybe_unused]] const DungeonState* state) {
1851 // Pattern: Draws 2x4 tiles rightward with adjacent spacing (objects 0x03-0x04)
1852 // Uses RoomDraw_Nx4 with N=2, tiles are COLUMN-MAJOR
1853 // ASM: GetSize_1to16 means count = size + 1
1854 int size = obj.size_ & 0x0F;
1855 int count = size + 1;
1856
1857 LOG_DEBUG("ObjectDrawer",
1858 "DrawRightwards2x4_1to16: obj=%04X pos=(%d,%d) size=%d count=%d "
1859 "tiles=%zu",
1860 obj.id_, obj.x_, obj.y_, size, count, tiles.size());
1861
1862 for (int s = 0; s < count; s++) {
1863 if (tiles.size() >= 8) {
1864 // Draw 2x4 pattern in COLUMN-MAJOR order with adjacent spacing (s * 2)
1865 // Column 0 (tiles 0-3)
1866 WriteTile8(bg, obj.x_ + (s * 2), obj.y_, tiles[0]); // col 0, row 0
1867 WriteTile8(bg, obj.x_ + (s * 2), obj.y_ + 1, tiles[1]); // col 0, row 1
1868 WriteTile8(bg, obj.x_ + (s * 2), obj.y_ + 2, tiles[2]); // col 0, row 2
1869 WriteTile8(bg, obj.x_ + (s * 2), obj.y_ + 3, tiles[3]); // col 0, row 3
1870 // Column 1 (tiles 4-7)
1871 WriteTile8(bg, obj.x_ + (s * 2) + 1, obj.y_, tiles[4]); // col 1, row 0
1872 WriteTile8(bg, obj.x_ + (s * 2) + 1, obj.y_ + 1,
1873 tiles[5]); // col 1, row 1
1874 WriteTile8(bg, obj.x_ + (s * 2) + 1, obj.y_ + 2,
1875 tiles[6]); // col 1, row 2
1876 WriteTile8(bg, obj.x_ + (s * 2) + 1, obj.y_ + 3,
1877 tiles[7]); // col 1, row 3
1878 } else if (tiles.size() >= 4) {
1879 // Fallback: with 4 tiles we can only draw 1 column (1x4 pattern)
1880 WriteTile8(bg, obj.x_ + (s * 2), obj.y_, tiles[0]);
1881 WriteTile8(bg, obj.x_ + (s * 2), obj.y_ + 1, tiles[1]);
1882 WriteTile8(bg, obj.x_ + (s * 2), obj.y_ + 2, tiles[2]);
1883 WriteTile8(bg, obj.x_ + (s * 2), obj.y_ + 3, tiles[3]);
1884 }
1885 }
1886}
1887
1889 const RoomObject& obj, gfx::BackgroundBuffer& bg,
1890 std::span<const gfx::TileInfo> tiles,
1891 [[maybe_unused]] const DungeonState* state) {
1892 // Pattern: Same as DrawRightwards2x4_1to16 but draws to both BG1 and BG2 (objects 0x05-0x06)
1893 DrawRightwards2x4_1to16(obj, bg, tiles);
1894 // Note: BothBG would require access to both buffers - simplified for now
1895}
1896
1898 const RoomObject& obj, gfx::BackgroundBuffer& bg,
1899 std::span<const gfx::TileInfo> tiles,
1900 [[maybe_unused]] const DungeonState* state) {
1901 // Pattern: Draws 2x2 tiles rightward (objects 0x07-0x08)
1902 // ROM tile order is COLUMN-MAJOR: [col0_row0, col0_row1, col1_row0, col1_row1]
1903 int size = obj.size_ & 0x0F;
1904
1905 // Assembly: JSR RoomDraw_GetSize_1to16
1906 // GetSize_1to16: count = size + 1
1907 int count = size + 1;
1908
1909 for (int s = 0; s < count; s++) {
1910 if (tiles.size() >= 4) {
1911 // Draw 2x2 pattern in COLUMN-MAJOR order (matching assembly)
1912 WriteTile8(bg, obj.x_ + (s * 2), obj.y_, tiles[0]); // col 0, row 0
1913 WriteTile8(bg, obj.x_ + (s * 2), obj.y_ + 1, tiles[1]); // col 0, row 1
1914 WriteTile8(bg, obj.x_ + (s * 2) + 1, obj.y_, tiles[2]); // col 1, row 0
1915 WriteTile8(bg, obj.x_ + (s * 2) + 1, obj.y_ + 1,
1916 tiles[3]); // col 1, row 1
1917 }
1918 }
1919}
1920
1922 const RoomObject& obj, gfx::BackgroundBuffer& bg,
1923 std::span<const gfx::TileInfo> tiles,
1924 [[maybe_unused]] const DungeonState* state) {
1925 // Pattern: Diagonal acute (/) - draws 5 tiles vertically, moves up-right
1926 // Based on bank_01.asm RoomDraw_DiagonalAcute_1to16 at $018C58
1927 // Uses RoomDraw_2x2and1 to draw 5 tiles at rows 0-4, then moves Y -= $7E
1928 int size = obj.size_ & 0x0F;
1929
1930 // Assembly: LDA #$0007; JSR RoomDraw_GetSize_1to16_timesA
1931 // GetSize_1to16_timesA formula: ((B2 << 2) | B4) + A
1932 // Since size_ already contains ((B2 << 2) | B4), count = size + 7
1933 int count = size + 7;
1934
1935 if (tiles.size() < 5)
1936 return;
1937
1938 for (int s = 0; s < count; s++) {
1939 // Draw 5 tiles in a vertical column (RoomDraw_2x2and1 pattern)
1940 // Assembly stores to [$BF],Y, [$CB],Y, [$D7],Y, [$DA],Y, [$DD],Y
1941 // These are rows 0, 1, 2, 3, 4 at the same X position
1942 int tile_x = obj.x_ + s; // Move right each iteration
1943 int tile_y = obj.y_ - s; // Move up each iteration (acute = /)
1944
1945 WriteTile8(bg, tile_x, tile_y + 0, tiles[0]);
1946 WriteTile8(bg, tile_x, tile_y + 1, tiles[1]);
1947 WriteTile8(bg, tile_x, tile_y + 2, tiles[2]);
1948 WriteTile8(bg, tile_x, tile_y + 3, tiles[3]);
1949 WriteTile8(bg, tile_x, tile_y + 4, tiles[4]);
1950 }
1951}
1952
1954 const RoomObject& obj, gfx::BackgroundBuffer& bg,
1955 std::span<const gfx::TileInfo> tiles,
1956 [[maybe_unused]] const DungeonState* state) {
1957 // Pattern: Diagonal grave (\‍) - draws 5 tiles vertically, moves down-right
1958 // Based on bank_01.asm RoomDraw_DiagonalGrave_1to16 at $018C61
1959 // Uses RoomDraw_2x2and1 to draw 5 tiles at rows 0-4, then moves Y += $82
1960 int size = obj.size_ & 0x0F;
1961
1962 // Assembly: LDA #$0007; JSR RoomDraw_GetSize_1to16_timesA
1963 // GetSize_1to16_timesA formula: ((B2 << 2) | B4) + A
1964 // Since size_ already contains ((B2 << 2) | B4), count = size + 7
1965 int count = size + 7;
1966
1967 if (tiles.size() < 5)
1968 return;
1969
1970 for (int s = 0; s < count; s++) {
1971 // Draw 5 tiles in a vertical column (RoomDraw_2x2and1 pattern)
1972 // Assembly stores to [$BF],Y, [$CB],Y, [$D7],Y, [$DA],Y, [$DD],Y
1973 // These are rows 0, 1, 2, 3, 4 at the same X position
1974 int tile_x = obj.x_ + s; // Move right each iteration
1975 int tile_y = obj.y_ + s; // Move down each iteration (grave = \‍)
1976
1977 WriteTile8(bg, tile_x, tile_y + 0, tiles[0]);
1978 WriteTile8(bg, tile_x, tile_y + 1, tiles[1]);
1979 WriteTile8(bg, tile_x, tile_y + 2, tiles[2]);
1980 WriteTile8(bg, tile_x, tile_y + 3, tiles[3]);
1981 WriteTile8(bg, tile_x, tile_y + 4, tiles[4]);
1982 }
1983}
1984
1986 const RoomObject& obj, gfx::BackgroundBuffer& bg,
1987 std::span<const gfx::TileInfo> tiles,
1988 [[maybe_unused]] const DungeonState* state) {
1989 // Pattern: Diagonal acute (/) for both BG layers (objects 0x15, 0x18-0x1D, 0x20)
1990 // Based on bank_01.asm RoomDraw_DiagonalAcute_1to16_BothBG at $018C6A
1991 // Assembly: LDA #$0006; JSR RoomDraw_GetSize_1to16_timesA
1992 // GetSize_1to16_timesA formula: ((B2 << 2) | B4) + A
1993 // Since size_ already contains ((B2 << 2) | B4), count = size + 6
1994 int size = obj.size_ & 0x0F;
1995 int count = size + 6;
1996
1997 if (tiles.size() < 5)
1998 return;
1999
2000 for (int s = 0; s < count; s++) {
2001 int tile_x = obj.x_ + s;
2002 int tile_y = obj.y_ - s;
2003
2004 WriteTile8(bg, tile_x, tile_y + 0, tiles[0]);
2005 WriteTile8(bg, tile_x, tile_y + 1, tiles[1]);
2006 WriteTile8(bg, tile_x, tile_y + 2, tiles[2]);
2007 WriteTile8(bg, tile_x, tile_y + 3, tiles[3]);
2008 WriteTile8(bg, tile_x, tile_y + 4, tiles[4]);
2009 }
2010 // Note: BothBG should write to both BG1 and BG2 - handled by DrawObject caller
2011}
2012
2014 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2015 std::span<const gfx::TileInfo> tiles,
2016 [[maybe_unused]] const DungeonState* state) {
2017 // Pattern: Diagonal grave (\‍) for both BG layers (objects 0x16-0x17, 0x1A-0x1B, 0x1E-0x1F)
2018 // Based on bank_01.asm RoomDraw_DiagonalGrave_1to16_BothBG at $018CB9
2019 // Assembly: LDA #$0006; JSR RoomDraw_GetSize_1to16_timesA
2020 // GetSize_1to16_timesA formula: ((B2 << 2) | B4) + A
2021 // Since size_ already contains ((B2 << 2) | B4), count = size + 6
2022 int size = obj.size_ & 0x0F;
2023 int count = size + 6;
2024
2025 if (tiles.size() < 5)
2026 return;
2027
2028 for (int s = 0; s < count; s++) {
2029 int tile_x = obj.x_ + s;
2030 int tile_y = obj.y_ + s;
2031
2032 WriteTile8(bg, tile_x, tile_y + 0, tiles[0]);
2033 WriteTile8(bg, tile_x, tile_y + 1, tiles[1]);
2034 WriteTile8(bg, tile_x, tile_y + 2, tiles[2]);
2035 WriteTile8(bg, tile_x, tile_y + 3, tiles[3]);
2036 WriteTile8(bg, tile_x, tile_y + 4, tiles[4]);
2037 }
2038 // Note: BothBG should write to both BG1 and BG2 - handled by DrawObject caller
2039}
2040
2043 std::span<const gfx::TileInfo> tiles,
2044 [[maybe_unused]] const DungeonState* state) {
2045 // Pattern: 4x4 grid corner for Type 2 objects
2046 // LAYOUT CORNERS: 0x100-0x103 (upper-left, lower-left, upper-right, lower-right)
2047 // - These are the concave corners used in room layouts
2048 // - GetSubtype2TileCount returns 16 tiles for these (32 bytes of tile data)
2049 // OTHER CORNERS: 0x108-0x10F (BothBG variants handled by routine 35)
2050 // Type 2 objects may have 8 or 16 tiles depending on the specific object
2051
2052 if (tiles.size() >= 16) {
2053 // Full 4x4 pattern - Column-major ordering per ZScream
2054 int tid = 0;
2055 for (int xx = 0; xx < 4; xx++) {
2056 for (int yy = 0; yy < 4; yy++) {
2057 WriteTile8(bg, obj.x_ + xx, obj.y_ + yy, tiles[tid++]);
2058 }
2059 }
2060 } else if (tiles.size() >= 8) {
2061 // Type 2 objects: 8 tiles arranged in 2x4 column-major pattern
2062 // This is the standard Type 2 tile layout
2063 int tid = 0;
2064 for (int xx = 0; xx < 2; xx++) {
2065 for (int yy = 0; yy < 4; yy++) {
2066 WriteTile8(bg, obj.x_ + xx, obj.y_ + yy, tiles[tid++]);
2067 }
2068 }
2069 } else if (tiles.size() >= 4) {
2070 // Fallback: 2x2 pattern
2071 int tid = 0;
2072 for (int xx = 0; xx < 2; xx++) {
2073 for (int yy = 0; yy < 2; yy++) {
2074 WriteTile8(bg, obj.x_ + xx, obj.y_ + yy, tiles[tid++]);
2075 }
2076 }
2077 }
2078}
2079
2081 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2082 std::span<const gfx::TileInfo> tiles,
2083 [[maybe_unused]] const DungeonState* state) {
2084 // Pattern: 1x3 tiles rightward with caps (object 0x21)
2085 // ZScream: left cap (tiles 0-2), middle columns (tiles 3-5), right cap (tiles 6-8)
2086 int size = obj.size_ & 0x0F;
2087
2088 if (tiles.size() >= 9) {
2089 auto draw_column = [&](int x, int base) {
2090 WriteTile8(bg, x, obj.y_, tiles[base + 0]);
2091 WriteTile8(bg, x, obj.y_ + 1, tiles[base + 1]);
2092 WriteTile8(bg, x, obj.y_ + 2, tiles[base + 2]);
2093 };
2094
2095 // Left cap at origin
2096 draw_column(obj.x_, 0);
2097
2098 // Middle columns: two per size step (size + 1 iterations)
2099 int mid_cols = (size + 1) * 2;
2100 for (int s = 0; s < mid_cols; s++) {
2101 draw_column(obj.x_ + 1 + s, 3);
2102 }
2103
2104 // Right cap
2105 draw_column(obj.x_ + 1 + mid_cols, 6);
2106 return;
2107 }
2108
2109 // Fallback: simple 1x2 pattern
2110 int count = (size * 2) + 1;
2111 for (int s = 0; s < count; s++) {
2112 if (tiles.size() >= 2) {
2113 WriteTile8(bg, obj.x_ + s + 2, obj.y_, tiles[0]);
2114 WriteTile8(bg, obj.x_ + s + 2, obj.y_ + 1, tiles[1]);
2115 }
2116 }
2117}
2118
2120 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2121 std::span<const gfx::TileInfo> tiles,
2122 [[maybe_unused]] const DungeonState* state) {
2123 // Pattern: Rail with corner check (object 0x22 small rails)
2124 // ASM: RoomDraw_RightwardsHasEdge1x1_1to16_plus3
2125 // ZScream: count = size + 2
2126 // Structure: [CORNER] -> [MIDDLE * count] -> [END]
2127 int size = obj.size_ & 0x0F;
2128 int count = size + 2;
2129
2130 if (tiles.size() < 3)
2131 return;
2132
2133 int x = obj.x_;
2134
2135 // Draw corner tile (tile 0) - in editor we always draw it
2136 WriteTile8(bg, x, obj.y_, tiles[0]);
2137 x++;
2138
2139 // Draw middle tiles (tile 1) repeated
2140 for (int s = 0; s < count; s++) {
2141 WriteTile8(bg, x, obj.y_, tiles[1]);
2142 x++;
2143 }
2144
2145 // Draw end tile (tile 2)
2146 WriteTile8(bg, x, obj.y_, tiles[2]);
2147}
2148
2150 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2151 std::span<const gfx::TileInfo> tiles,
2152 [[maybe_unused]] const DungeonState* state) {
2153 // Pattern: Long rail with corner/middle/end (object 0x5F)
2154 // ZScream: count = size + 21
2155 int size = obj.size_ & 0x0F;
2156 int count = size + 21;
2157
2158 if (tiles.size() < 3)
2159 return;
2160
2161 int x = obj.x_;
2162 WriteTile8(bg, x, obj.y_, tiles[0]);
2163 x++;
2164
2165 for (int s = 0; s < count; s++) {
2166 WriteTile8(bg, x, obj.y_, tiles[1]);
2167 x++;
2168 }
2169
2170 WriteTile8(bg, x, obj.y_, tiles[2]);
2171}
2172
2174 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2175 std::span<const gfx::TileInfo> tiles,
2176 [[maybe_unused]] const DungeonState* state) {
2177 // Pattern: Rail with corner check (objects 0x23-0x2E carpet trim, 0x3F-0x46)
2178 // ASM: RoomDraw_RightwardsHasEdge1x1_1to16_plus2
2179 // Uses GetSize_1to16, count = size + 1
2180 // Structure: [CORNER if needed] -> [MIDDLE * count] -> [END]
2181 int size = obj.size_ & 0x0F;
2182 int count = size + 1;
2183
2184 if (tiles.size() < 3)
2185 return;
2186
2187 int x = obj.x_;
2188
2189 // Draw corner tile (tile 0) - in editor we always draw it
2190 WriteTile8(bg, x, obj.y_, tiles[0]);
2191 x++;
2192
2193 // Draw middle tiles (tile 1) repeated
2194 for (int s = 0; s < count; s++) {
2195 WriteTile8(bg, x, obj.y_, tiles[1]);
2196 x++;
2197 }
2198
2199 // Draw end tile (tile 2)
2200 WriteTile8(bg, x, obj.y_, tiles[2]);
2201}
2202
2204 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2205 std::span<const gfx::TileInfo> tiles,
2206 [[maybe_unused]] const DungeonState* state) {
2207 // Pattern: Top corner 1x2 tiles with +13 offset (object 0x2F)
2208 int size = obj.size_ & 0x0F;
2209
2210 // Assembly: GetSize_1to16_timesA(0x0A), so count = size + 10
2211 int count = size + 10;
2212
2213 for (int s = 0; s < count; s++) {
2214 if (tiles.size() >= 2) {
2215 // Use first tile span for 1x2 pattern
2216 WriteTile8(bg, obj.x_ + s + 13, obj.y_, tiles[0]);
2217 WriteTile8(bg, obj.x_ + s + 13, obj.y_ + 1, tiles[1]);
2218 }
2219 }
2220}
2221
2223 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2224 const std::span<const gfx::TileInfo> tiles,
2225 [[maybe_unused]] const DungeonState* state) {
2226 // Pattern: Bottom corner 1x2 tiles with +13 offset (object 0x30)
2227 int size = obj.size_ & 0x0F;
2228
2229 // Assembly: GetSize_1to16_timesA(0x0A), so count = size + 10
2230 int count = size + 10;
2231
2232 for (int s = 0; s < count; s++) {
2233 if (tiles.size() >= 2) {
2234 // Use first tile span for 1x2 pattern
2235 WriteTile8(bg, obj.x_ + s + 13, obj.y_ + 1, tiles[0]);
2236 WriteTile8(bg, obj.x_ + s + 13, obj.y_ + 2, tiles[1]);
2237 }
2238 }
2239}
2240
2242 std::span<const gfx::TileInfo> tiles,
2243 [[maybe_unused]] const DungeonState* state) {
2244 // Pattern: Custom draw routine (objects 0x31-0x32)
2245 // For now, fall back to simple 1x1
2246 if (tiles.size() >= 1) {
2247 // Use first 8x8 tile from span
2248 WriteTile8(bg, obj.x_, obj.y_, tiles[0]);
2249 }
2250}
2251
2253 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2254 std::span<const gfx::TileInfo> tiles,
2255 [[maybe_unused]] const DungeonState* state) {
2256 // Pattern: 4x4 block rightward (objects 0x33, 0xBA = large ceiling, etc.)
2257 int size = obj.size_ & 0x0F;
2258
2259 // Assembly: GetSize_1to16, so count = size + 1
2260 int count = size + 1;
2261
2262 // Debug: Log large ceiling objects (0xBA)
2263 if (obj.id_ == 0xBA && tiles.size() >= 16) {
2264 LOG_DEBUG("ObjectDrawer",
2265 "Large Ceiling Draw: obj=0x%02X pos=(%d,%d) size=%d tiles=%zu",
2266 obj.id_, obj.x_, obj.y_, size, tiles.size());
2267 LOG_DEBUG("ObjectDrawer", " First 4 Tile IDs: [%d, %d, %d, %d]",
2268 tiles[0].id_, tiles[1].id_, tiles[2].id_, tiles[3].id_);
2269 LOG_DEBUG("ObjectDrawer", " First 4 Palettes: [%d, %d, %d, %d]",
2270 tiles[0].palette_, tiles[1].palette_, tiles[2].palette_,
2271 tiles[3].palette_);
2272 }
2273
2274 for (int s = 0; s < count; s++) {
2275 if (tiles.size() >= 16) {
2276 // Draw 4x4 pattern in COLUMN-MAJOR order (matching assembly)
2277 // Iterate columns (x) first, then rows (y) within each column
2278 for (int x = 0; x < 4; ++x) {
2279 for (int y = 0; y < 4; ++y) {
2280 WriteTile8(bg, obj.x_ + (s * 4) + x, obj.y_ + y, tiles[x * 4 + y]);
2281 }
2282 }
2283 }
2284 }
2285}
2286
2288 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2289 std::span<const gfx::TileInfo> tiles,
2290 [[maybe_unused]] const DungeonState* state) {
2291 // Pattern: 1x1 solid tiles +3 offset (object 0x34)
2292 int size = obj.size_ & 0x0F;
2293
2294 // Assembly: GetSize_1to16_timesA(4), so count = size + 4
2295 int count = size + 4;
2296
2297 for (int s = 0; s < count; s++) {
2298 if (tiles.size() >= 1) {
2299 // Use first 8x8 tile from span
2300 WriteTile8(bg, obj.x_ + s + 3, obj.y_, tiles[0]);
2301 }
2302 }
2303}
2304
2306 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2307 std::span<const gfx::TileInfo> tiles,
2308 [[maybe_unused]] const DungeonState* state) {
2309 // Pattern: Door switcher (object 0x35)
2310 // Special door logic
2311 // Check state to decide graphic
2312 int tile_index = 0;
2313 if (state && state->IsDoorSwitchActive(room_id_)) {
2314 // Use active tile if available (assuming 2nd tile is active state)
2315 if (tiles.size() >= 2) {
2316 tile_index = 1;
2317 }
2318 }
2319
2320 if (tiles.size() > tile_index) {
2321 WriteTile8(bg, obj.x_, obj.y_, tiles[tile_index]);
2322 }
2323}
2324
2326 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2327 std::span<const gfx::TileInfo> tiles,
2328 [[maybe_unused]] const DungeonState* state) {
2329 // Pattern: 4x4 decoration with spacing (objects 0x36-0x37)
2330 int size = obj.size_ & 0x0F;
2331
2332 // Assembly: GetSize_1to16, so count = size + 1
2333 int count = size + 1;
2334
2335 for (int s = 0; s < count; s++) {
2336 if (tiles.size() >= 16) {
2337 // Draw 4x4 pattern with spacing in COLUMN-MAJOR order (matching assembly)
2338 for (int x = 0; x < 4; ++x) {
2339 for (int y = 0; y < 4; ++y) {
2340 WriteTile8(bg, obj.x_ + (s * 6) + x, obj.y_ + y, tiles[x * 4 + y]);
2341 }
2342 }
2343 }
2344 }
2345}
2346
2348 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2349 std::span<const gfx::TileInfo> tiles,
2350 [[maybe_unused]] const DungeonState* state) {
2351 // Pattern: 2x3 statue with spacing (object 0x38)
2352 // 2 columns × 3 rows = 6 tiles in COLUMN-MAJOR order
2353 int size = obj.size_ & 0x0F;
2354
2355 // Assembly: GetSize_1to16, so count = size + 1
2356 int count = size + 1;
2357
2358 for (int s = 0; s < count; s++) {
2359 if (tiles.size() >= 6) {
2360 // Draw 2x3 pattern in COLUMN-MAJOR order (matching assembly)
2361 for (int x = 0; x < 2; ++x) {
2362 for (int y = 0; y < 3; ++y) {
2363 WriteTile8(bg, obj.x_ + (s * 4) + x, obj.y_ + y, tiles[x * 3 + y]);
2364 }
2365 }
2366 }
2367 }
2368}
2369
2371 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2372 std::span<const gfx::TileInfo> tiles,
2373 [[maybe_unused]] const DungeonState* state) {
2374 // Pattern: 2x4 pillar with spacing (objects 0x39, 0x3D)
2375 // 2 columns × 4 rows = 8 tiles in COLUMN-MAJOR order
2376 // ASM: calls RoomDraw_Nx4(A=2) which advances X by 2 tiles, then adds
2377 // ADC #$0008 (4 tiles). Total start-to-start stride is 6 tiles, i.e. 4 tiles
2378 // of empty space between 2-tile-wide pillars.
2379 int size = obj.size_ & 0x0F;
2380
2381 // Assembly: GetSize_1to16, so count = size + 1
2382 int count = size + 1;
2383
2384 for (int s = 0; s < count; s++) {
2385 if (tiles.size() >= 8) {
2386 // Draw 2x4 pattern in COLUMN-MAJOR order (matching assembly)
2387 for (int x = 0; x < 2; ++x) {
2388 for (int y = 0; y < 4; ++y) {
2389 WriteTile8(bg, obj.x_ + (s * 6) + x, obj.y_ + y, tiles[x * 4 + y]);
2390 }
2391 }
2392 }
2393 }
2394}
2395
2397 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2398 std::span<const gfx::TileInfo> tiles,
2399 [[maybe_unused]] const DungeonState* state) {
2400 // Pattern: 4x3 decoration with spacing (objects 0x3A-0x3B)
2401 // 4 columns × 3 rows = 12 tiles in COLUMN-MAJOR order
2402 // ASM: ADC #$0008 to Y = 8-byte advance = 4 tiles per iteration
2403 // Total spacing: 4 (object width) + 4 (gap) = 8 tiles between starts
2404 int size = obj.size_ & 0x0F;
2405
2406 // Assembly: GetSize_1to16, so count = size + 1
2407 int count = size + 1;
2408
2409 for (int s = 0; s < count; s++) {
2410 if (tiles.size() >= 12) {
2411 // Draw 4x3 pattern in COLUMN-MAJOR order (matching assembly)
2412 // Spacing: 8 tiles (4 object + 4 gap) per ASM ADC #$0008
2413 for (int x = 0; x < 4; ++x) {
2414 for (int y = 0; y < 3; ++y) {
2415 WriteTile8(bg, obj.x_ + (s * 8) + x, obj.y_ + y, tiles[x * 3 + y]);
2416 }
2417 }
2418 }
2419 }
2420}
2421
2423 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2424 std::span<const gfx::TileInfo> tiles,
2425 [[maybe_unused]] const DungeonState* state) {
2426 // Pattern: 2x2 block repeated with a vertical gap (object 0x3C)
2427 int size = obj.size_ & 0x0F;
2428 int count = size + 1;
2429
2430 if (tiles.size() < 4)
2431 return;
2432
2433 for (int s = 0; s < count; s++) {
2434 int base_x = obj.x_ + (s * 4);
2435
2436 // Top 2x2 block
2437 WriteTile8(bg, base_x, obj.y_, tiles[0]);
2438 WriteTile8(bg, base_x + 1, obj.y_, tiles[2]);
2439 WriteTile8(bg, base_x, obj.y_ + 1, tiles[1]);
2440 WriteTile8(bg, base_x + 1, obj.y_ + 1, tiles[3]);
2441
2442 // Bottom 2x2 block (offset by 6 tiles)
2443 WriteTile8(bg, base_x, obj.y_ + 6, tiles[0]);
2444 WriteTile8(bg, base_x + 1, obj.y_ + 6, tiles[2]);
2445 WriteTile8(bg, base_x, obj.y_ + 7, tiles[1]);
2446 WriteTile8(bg, base_x + 1, obj.y_ + 7, tiles[3]);
2447 }
2448}
2449
2451 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2452 std::span<const gfx::TileInfo> tiles,
2453 [[maybe_unused]] const DungeonState* state) {
2454 // Pattern: 2x2 decoration with large spacing (object 0x3E)
2455 int size = obj.size_ & 0x0F;
2456
2457 // Assembly: GetSize_1to16, so count = size + 1
2458 int count = size + 1;
2459
2460 for (int s = 0; s < count; s++) {
2461 if (tiles.size() >= 4) {
2462 // Draw 2x2 pattern in COLUMN-MAJOR order (matching assembly)
2463 // tiles[0] → col 0, row 0 = top-left
2464 // tiles[1] → col 0, row 1 = bottom-left
2465 // tiles[2] → col 1, row 0 = top-right
2466 // tiles[3] → col 1, row 1 = bottom-right
2467 WriteTile8(bg, obj.x_ + (s * 14), obj.y_, tiles[0]); // col 0, row 0
2468 WriteTile8(bg, obj.x_ + (s * 14), obj.y_ + 1, tiles[1]); // col 0, row 1
2469 WriteTile8(bg, obj.x_ + (s * 14) + 1, obj.y_, tiles[2]); // col 1, row 0
2470 WriteTile8(bg, obj.x_ + (s * 14) + 1, obj.y_ + 1,
2471 tiles[3]); // col 1, row 1
2472 }
2473 }
2474}
2475
2477 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2478 std::span<const gfx::TileInfo> tiles,
2479 [[maybe_unused]] const DungeonState* state) {
2480 // Pattern: Draws 4x2 tiles rightward (objects 0x49-0x4A: Floor Tile 4x2)
2481 // Assembly: RoomDraw_RightwardsFloorTile4x2_1to16 -> RoomDraw_Downwards4x2VariableSpacing
2482 // This is 4 columns × 2 rows = 8 tiles in ROW-MAJOR order with horizontal spacing
2483 // Spacing of $0008 = 4 tile columns = 32 pixels
2484 int size = obj.size_ & 0x0F;
2485 int count = size + 1; // GetSize_1to16
2486
2487 for (int s = 0; s < count; s++) {
2488 if (tiles.size() >= 8) {
2489 // Draw 4x2 pattern in ROW-MAJOR order (matching assembly RoomDraw_Downwards4x2VariableSpacing)
2490 // Row 0: tiles 0, 1, 2, 3 at x+0, x+1, x+2, x+3
2491 WriteTile8(bg, obj.x_ + (s * 4), obj.y_, tiles[0]);
2492 WriteTile8(bg, obj.x_ + (s * 4) + 1, obj.y_, tiles[1]);
2493 WriteTile8(bg, obj.x_ + (s * 4) + 2, obj.y_, tiles[2]);
2494 WriteTile8(bg, obj.x_ + (s * 4) + 3, obj.y_, tiles[3]);
2495 // Row 1: tiles 4, 5, 6, 7 at x+0, x+1, x+2, x+3
2496 WriteTile8(bg, obj.x_ + (s * 4), obj.y_ + 1, tiles[4]);
2497 WriteTile8(bg, obj.x_ + (s * 4) + 1, obj.y_ + 1, tiles[5]);
2498 WriteTile8(bg, obj.x_ + (s * 4) + 2, obj.y_ + 1, tiles[6]);
2499 WriteTile8(bg, obj.x_ + (s * 4) + 3, obj.y_ + 1, tiles[7]);
2500 }
2501 }
2502}
2503
2505 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2506 std::span<const gfx::TileInfo> tiles,
2507 [[maybe_unused]] const DungeonState* state) {
2508 // Pattern: Draws 1x8 column tiles rightward with spacing (objects 0x55-0x56 wall torches)
2509 // Assembly: RoomDraw_RightwardsDecor4x2spaced8_1to16 -> RoomDraw_Downwards4x2VariableSpacing
2510 // ASM writes to 8 consecutive row buffers ([$BF] through [$D4]) = 1 column × 8 rows
2511 // Spacing = 0x18 = 24 bytes = 12 tile columns between repetitions
2512 int size = obj.size_ & 0x0F;
2513 int count = size + 1; // GetSize_1to16
2514
2515 if (tiles.size() < 8)
2516 return;
2517
2518 for (int s = 0; s < count; s++) {
2519 // Draw 1x8 column pattern with 12-tile horizontal spacing
2520 int base_x = obj.x_ + (s * 12); // spacing of 12 columns
2521 for (int row = 0; row < 8; row++) {
2522 WriteTile8(bg, base_x, obj.y_ + row, tiles[row]);
2523 }
2524 }
2525}
2526
2528 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2529 std::span<const gfx::TileInfo> tiles,
2530 [[maybe_unused]] const DungeonState* state) {
2531 // Pattern: Draws 4x3 tiles (objects 0x51-0x52, 0x5B-0x5C: Cannon Hole)
2532 // Assembly: RoomDraw_RightwardsCannonHole4x3_1to16
2533 //
2534 // USDASM ground truth:
2535 // - RoomDraw_RightwardsCannonHole4x3_1to16 calls RoomDraw_1x3N_rightwards
2536 // with A=2 multiple times, repeating the LEFT 2-column segment "count"
2537 // times, then draws the RIGHT 2-column edge once (after X += 0x000C).
2538 // - RoomDraw_1x3N_rightwards draws in COLUMN-MAJOR order (per-column 3 words).
2539 //
2540 // So the tile span is treated as 4 columns x 3 rows in COLUMN-MAJOR order:
2541 // col0: tiles[0..2], col1: tiles[3..5], col2: tiles[6..8], col3: tiles[9..11]
2542 int size = obj.size_ & 0x0F;
2543 int count = size + 1; // GetSize_1to16
2544
2545 if (tiles.size() < 12) {
2546 return;
2547 }
2548
2549 auto draw_column = [&](int x, int y, const gfx::TileInfo& t0,
2550 const gfx::TileInfo& t1, const gfx::TileInfo& t2) {
2551 // Match RoomDraw_1x3N_rightwards order: row0, row1, row2 per column.
2552 WriteTile8(bg, x, y, t0);
2553 WriteTile8(bg, x, y + 1, t1);
2554 WriteTile8(bg, x, y + 2, t2);
2555 };
2556
2557 // Repeat the left 2-column segment count times (each repetition advances by 2
2558 // tiles to the right).
2559 for (int s = 0; s < count; ++s) {
2560 int base_x = obj.x_ + (s * 2);
2561 draw_column(base_x + 0, obj.y_, tiles[0], tiles[1], tiles[2]);
2562 draw_column(base_x + 1, obj.y_, tiles[3], tiles[4], tiles[5]);
2563 }
2564
2565 // Draw the right edge once after the repeated middle.
2566 int right_base_x = obj.x_ + (count * 2);
2567 draw_column(right_base_x + 0, obj.y_, tiles[6], tiles[7], tiles[8]);
2568 draw_column(right_base_x + 1, obj.y_, tiles[9], tiles[10], tiles[11]);
2569}
2570
2571// Additional Rightwards draw routines (0x47-0x5E range)
2572
2574 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2575 std::span<const gfx::TileInfo> tiles,
2576 [[maybe_unused]] const DungeonState* state) {
2577 // Pattern: 1x1 line rightward with +1 modifier (object 0x50)
2578 // Assembly: RoomDraw_RightwardsLine1x1_1to16plus1
2579 int size = obj.size_ & 0x0F;
2580 int count = size + 2; // +1 gives +2 iterations total
2581
2582 for (int s = 0; s < count; s++) {
2583 if (tiles.size() >= 1) {
2584 WriteTile8(bg, obj.x_ + s, obj.y_, tiles[0]);
2585 }
2586 }
2587}
2588
2590 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2591 std::span<const gfx::TileInfo> tiles,
2592 [[maybe_unused]] const DungeonState* state) {
2593 // Pattern: 4x3 bar with left/right caps and repeating middle columns
2594 int size = obj.size_ & 0x0F;
2595 int count = size + 1;
2596
2597 if (tiles.size() < 9)
2598 return;
2599
2600 // Middle columns
2601 for (int s = 0; s < count; s++) {
2602 int base_x = obj.x_ + (s * 2);
2603 WriteTile8(bg, base_x + 1, obj.y_, tiles[3]);
2604 WriteTile8(bg, base_x + 2, obj.y_, tiles[3]);
2605 WriteTile8(bg, base_x + 1, obj.y_ + 1, tiles[4]);
2606 WriteTile8(bg, base_x + 2, obj.y_ + 1, tiles[4]);
2607 WriteTile8(bg, base_x + 1, obj.y_ + 2, tiles[5]);
2608 WriteTile8(bg, base_x + 2, obj.y_ + 2, tiles[5]);
2609 }
2610
2611 // Left cap
2612 WriteTile8(bg, obj.x_, obj.y_, tiles[0]);
2613 WriteTile8(bg, obj.x_, obj.y_ + 1, tiles[1]);
2614 WriteTile8(bg, obj.x_, obj.y_ + 2, tiles[2]);
2615
2616 // Right cap
2617 int right_x = obj.x_ + (size * 2) + 3;
2618 WriteTile8(bg, right_x, obj.y_, tiles[6]);
2619 WriteTile8(bg, right_x, obj.y_ + 1, tiles[7]);
2620 WriteTile8(bg, right_x, obj.y_ + 2, tiles[8]);
2621}
2622
2624 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2625 std::span<const gfx::TileInfo> tiles,
2626 [[maybe_unused]] const DungeonState* state) {
2627 // Pattern: 4x4 shelf rightward with complex column pattern (objects 0x4D-0x4F)
2628 // Assembly: RoomDraw_RightwardsShelf4x4_1to16
2629 // This draws 4x4 patterns with 6-tile X spacing
2630 int size = obj.size_ & 0x0F;
2631 int count = size + 1;
2632
2633 for (int s = 0; s < count; s++) {
2634 if (tiles.size() >= 16) {
2635 int base_x = obj.x_ + (s * 6); // 6-tile X spacing
2636 // Draw 4x4 pattern in COLUMN-MAJOR order
2637 for (int col = 0; col < 4; col++) {
2638 for (int row = 0; row < 4; row++) {
2639 int tile_idx = col * 4 + row;
2640 WriteTile8(bg, base_x + col, obj.y_ + row, tiles[tile_idx]);
2641 }
2642 }
2643 }
2644 }
2645}
2646
2648 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2649 std::span<const gfx::TileInfo> tiles,
2650 [[maybe_unused]] const DungeonState* state) {
2651 // Pattern: Big rail with LEFT CAP (2x3) + MIDDLE (1x3 repeated) + RIGHT CAP (2x3)
2652 // Assembly: RoomDraw_RightwardsBigRail1x3_1to16plus5
2653 // Tile layout: [6 tiles for left cap] [3 tiles for middle] [6 tiles for right cap]
2654 // Left cap: 2 columns x 3 rows in COLUMN-MAJOR order
2655 // Middle: 1 column x 3 rows, repeated (size + 2) times
2656 // Right cap: 2 columns x 3 rows in COLUMN-MAJOR order
2657
2658 int size = obj.size_ & 0x0F;
2659 int middle_count =
2660 size + 2; // ASM: INC B2 after GetSize_1to16 = (size+1)+1 = size+2
2661
2662 if (tiles.size() < 15)
2663 return; // Need 6+3+6=15 tiles minimum
2664
2665 int x = obj.x_;
2666
2667 // Draw LEFT CAP: 2 columns x 3 rows (tiles 0-5 in column-major order)
2668 // Column 0
2669 WriteTile8(bg, x, obj.y_, tiles[0]);
2670 WriteTile8(bg, x, obj.y_ + 1, tiles[1]);
2671 WriteTile8(bg, x, obj.y_ + 2, tiles[2]);
2672 // Column 1
2673 WriteTile8(bg, x + 1, obj.y_, tiles[3]);
2674 WriteTile8(bg, x + 1, obj.y_ + 1, tiles[4]);
2675 WriteTile8(bg, x + 1, obj.y_ + 2, tiles[5]);
2676 x += 2;
2677
2678 // Draw MIDDLE: 1 column x 3 rows (tiles 6-8), repeated middle_count times
2679 for (int s = 0; s < middle_count; s++) {
2680 WriteTile8(bg, x + s, obj.y_, tiles[6]);
2681 WriteTile8(bg, x + s, obj.y_ + 1, tiles[7]);
2682 WriteTile8(bg, x + s, obj.y_ + 2, tiles[8]);
2683 }
2684 x += middle_count;
2685
2686 // Draw RIGHT CAP: 2 columns x 3 rows (tiles 9-14 in column-major order)
2687 // Column 0
2688 WriteTile8(bg, x, obj.y_, tiles[9]);
2689 WriteTile8(bg, x, obj.y_ + 1, tiles[10]);
2690 WriteTile8(bg, x, obj.y_ + 2, tiles[11]);
2691 // Column 1
2692 WriteTile8(bg, x + 1, obj.y_, tiles[12]);
2693 WriteTile8(bg, x + 1, obj.y_ + 1, tiles[13]);
2694 WriteTile8(bg, x + 1, obj.y_ + 2, tiles[14]);
2695}
2696
2698 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2699 std::span<const gfx::TileInfo> tiles,
2700 [[maybe_unused]] const DungeonState* state) {
2701 // Pattern: 2x2 block rightward with 4-tile X spacing (object 0x5E)
2702 // Assembly: RoomDraw_RightwardsBlock2x2spaced2_1to16 - uses RoomDraw_Rightwards2x2
2703 // Tiles are in COLUMN-MAJOR order: [top-left, bottom-left, top-right, bottom-right]
2704 int size = obj.size_ & 0x0F;
2705 int count = size + 1;
2706
2707 for (int s = 0; s < count; s++) {
2708 if (tiles.size() >= 4) {
2709 int base_x = obj.x_ + (s * 4); // 4-tile X spacing (INY x4 = 2 tiles)
2710 // Draw 2x2 pattern in COLUMN-MAJOR order (matching ASM)
2711 WriteTile8(bg, base_x, obj.y_, tiles[0]); // top-left
2712 WriteTile8(bg, base_x, obj.y_ + 1, tiles[1]); // bottom-left
2713 WriteTile8(bg, base_x + 1, obj.y_, tiles[2]); // top-right
2714 WriteTile8(bg, base_x + 1, obj.y_ + 1, tiles[3]); // bottom-right
2715 }
2716 }
2717}
2718
2719// ============================================================================
2720// Downwards Draw Routines
2721// ============================================================================
2722
2724 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2725 std::span<const gfx::TileInfo> tiles,
2726 [[maybe_unused]] const DungeonState* state) {
2727 // Pattern: Draws 2x2 tiles downward (object 0x60)
2728 // Size byte determines how many times to repeat (1-15 or 32)
2729 int size = obj.size_;
2730 if (size == 0)
2731 size = 32; // Special case for object 0x60
2732
2733 for (int s = 0; s < size; s++) {
2734 if (tiles.size() >= 4) {
2735 // Draw 2x2 pattern in COLUMN-MAJOR order (matching assembly)
2736 // Assembly uses indirect pointers: $BF, $CB, $C2, $CE
2737 // tiles[0] → $BF → (col 0, row 0) = top-left
2738 // tiles[1] → $CB → (col 0, row 1) = bottom-left
2739 // tiles[2] → $C2 → (col 1, row 0) = top-right
2740 // tiles[3] → $CE → (col 1, row 1) = bottom-right
2741 WriteTile8(bg, obj.x_, obj.y_ + (s * 2), tiles[0]); // col 0, row 0
2742 WriteTile8(bg, obj.x_, obj.y_ + (s * 2) + 1, tiles[1]); // col 0, row 1
2743 WriteTile8(bg, obj.x_ + 1, obj.y_ + (s * 2), tiles[2]); // col 1, row 0
2744 WriteTile8(bg, obj.x_ + 1, obj.y_ + (s * 2) + 1,
2745 tiles[3]); // col 1, row 1
2746 }
2747 }
2748}
2749
2751 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2752 std::span<const gfx::TileInfo> tiles,
2753 [[maybe_unused]] const DungeonState* state) {
2754 // Pattern: Draws 4x2 tiles downward (objects 0x61-0x62)
2755 // This is 4 columns × 2 rows = 8 tiles in ROW-MAJOR order (per ZScream)
2756 int size = obj.size_;
2757 if (size == 0)
2758 size = 26; // Special case
2759
2760 LOG_DEBUG("ObjectDrawer",
2761 "Wall Draw 4x2 Vertical: obj=0x%03X pos=(%d,%d) size=%d tiles=%zu",
2762 obj.id_, obj.x_, obj.y_, size, tiles.size());
2763
2764 for (int s = 0; s < size; s++) {
2765 if (tiles.size() >= 8) {
2766 // Draw 4x2 pattern in ROW-MAJOR order (matching ZScream)
2767 // Row 0: tiles 0, 1, 2, 3 at x+0, x+1, x+2, x+3
2768 WriteTile8(bg, obj.x_, obj.y_ + (s * 2), tiles[0]);
2769 WriteTile8(bg, obj.x_ + 1, obj.y_ + (s * 2), tiles[1]);
2770 WriteTile8(bg, obj.x_ + 2, obj.y_ + (s * 2), tiles[2]);
2771 WriteTile8(bg, obj.x_ + 3, obj.y_ + (s * 2), tiles[3]);
2772 // Row 1: tiles 4, 5, 6, 7 at x+0, x+1, x+2, x+3
2773 WriteTile8(bg, obj.x_, obj.y_ + (s * 2) + 1, tiles[4]);
2774 WriteTile8(bg, obj.x_ + 1, obj.y_ + (s * 2) + 1, tiles[5]);
2775 WriteTile8(bg, obj.x_ + 2, obj.y_ + (s * 2) + 1, tiles[6]);
2776 WriteTile8(bg, obj.x_ + 3, obj.y_ + (s * 2) + 1, tiles[7]);
2777 } else if (tiles.size() >= 4) {
2778 // Fallback: with 4 tiles draw 4x1 row pattern
2779 WriteTile8(bg, obj.x_, obj.y_ + (s * 2), tiles[0]);
2780 WriteTile8(bg, obj.x_ + 1, obj.y_ + (s * 2), tiles[1]);
2781 WriteTile8(bg, obj.x_ + 2, obj.y_ + (s * 2), tiles[2]);
2782 WriteTile8(bg, obj.x_ + 3, obj.y_ + (s * 2), tiles[3]);
2783 }
2784 }
2785}
2786
2788 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2789 std::span<const gfx::TileInfo> tiles,
2790 [[maybe_unused]] const DungeonState* state) {
2791 // Pattern: Same as above but draws to both BG1 and BG2 (objects 0x63-0x64)
2792 DrawDownwards4x2_1to15or26(obj, bg, tiles);
2793 // Note: BothBG would require access to both buffers - simplified for now
2794}
2795
2797 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2798 std::span<const gfx::TileInfo> tiles,
2799 [[maybe_unused]] const DungeonState* state) {
2800 // Pattern: Draws 4x2 decoration downward with spacing (objects 0x65-0x66)
2801 // This is 4 columns × 2 rows = 8 tiles in ROW-MAJOR order with 6-tile Y
2802 // spacing.
2803 int size = obj.size_ & 0x0F;
2804
2805 // Assembly: GetSize_1to16, so count = size + 1
2806 int count = size + 1;
2807
2808 for (int s = 0; s < count; s++) {
2809 if (tiles.size() >= 8) {
2810 // Draw 4x2 pattern in ROW-MAJOR order:
2811 // Row 0: tiles[0..3], Row 1: tiles[4..7].
2812 const int base_y = obj.y_ + (s * 6);
2813 WriteTile8(bg, obj.x_, base_y, tiles[0]);
2814 WriteTile8(bg, obj.x_ + 1, base_y, tiles[1]);
2815 WriteTile8(bg, obj.x_ + 2, base_y, tiles[2]);
2816 WriteTile8(bg, obj.x_ + 3, base_y, tiles[3]);
2817 WriteTile8(bg, obj.x_, base_y + 1, tiles[4]);
2818 WriteTile8(bg, obj.x_ + 1, base_y + 1, tiles[5]);
2819 WriteTile8(bg, obj.x_ + 2, base_y + 1, tiles[6]);
2820 WriteTile8(bg, obj.x_ + 3, base_y + 1, tiles[7]);
2821 }
2822 }
2823}
2824
2826 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2827 std::span<const gfx::TileInfo> tiles,
2828 [[maybe_unused]] const DungeonState* state) {
2829 // Pattern: Draws 2x2 tiles downward (objects 0x67-0x68)
2830 int size = obj.size_ & 0x0F;
2831
2832 // Assembly: GetSize_1to16, so count = size + 1
2833 int count = size + 1;
2834
2835 for (int s = 0; s < count; s++) {
2836 if (tiles.size() >= 4) {
2837 // Draw 2x2 pattern in COLUMN-MAJOR order (matching assembly)
2838 // tiles[0] → col 0, row 0 = top-left
2839 // tiles[1] → col 0, row 1 = bottom-left
2840 // tiles[2] → col 1, row 0 = top-right
2841 // tiles[3] → col 1, row 1 = bottom-right
2842 WriteTile8(bg, obj.x_, obj.y_ + (s * 2), tiles[0]); // col 0, row 0
2843 WriteTile8(bg, obj.x_, obj.y_ + (s * 2) + 1, tiles[1]); // col 0, row 1
2844 WriteTile8(bg, obj.x_ + 1, obj.y_ + (s * 2), tiles[2]); // col 1, row 0
2845 WriteTile8(bg, obj.x_ + 1, obj.y_ + (s * 2) + 1,
2846 tiles[3]); // col 1, row 1
2847 }
2848 }
2849}
2850
2852 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2853 std::span<const gfx::TileInfo> tiles,
2854 [[maybe_unused]] const DungeonState* state) {
2855 // Pattern: Vertical rail with corner/middle/end (object 0x69)
2856 int size = obj.size_ & 0x0F;
2857 int count = size + 1;
2858
2859 if (tiles.size() < 3)
2860 return;
2861
2862 int y = obj.y_;
2863 WriteTile8(bg, obj.x_, y, tiles[0]);
2864 y++;
2865
2866 for (int s = 0; s < count; s++) {
2867 WriteTile8(bg, obj.x_, y, tiles[1]);
2868 y++;
2869 }
2870
2871 WriteTile8(bg, obj.x_, y, tiles[2]);
2872}
2873
2875 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2876 std::span<const gfx::TileInfo> tiles,
2877 [[maybe_unused]] const DungeonState* state) {
2878 // Pattern: Vertical rail with CORNER+MIDDLE+END pattern (objects 0x8A-0x8C)
2879 // ASM: RoomDraw_DownwardsHasEdge1x1_1to16_plus23 ($019314)
2880 // ZScream: count = size + 21
2881 // Structure: [CORNER] -> [MIDDLE * count] -> [END]
2882 int size = obj.size_ & 0x0F;
2883 int count = size + 21;
2884
2885 if (tiles.size() < 3)
2886 return;
2887
2888 int tile_y = obj.y_;
2889
2890 // Draw corner tile (tile 0) - always draw in editor
2891 WriteTile8(bg, obj.x_, tile_y, tiles[0]);
2892 tile_y++;
2893
2894 // Draw middle tiles (tile 1) repeated
2895 for (int s = 0; s < count; s++) {
2896 WriteTile8(bg, obj.x_, tile_y, tiles[1]);
2897 tile_y++;
2898 }
2899
2900 // Draw end tile (tile 2)
2901 WriteTile8(bg, obj.x_, tile_y, tiles[2]);
2902}
2903
2905 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2906 std::span<const gfx::TileInfo> tiles,
2907 [[maybe_unused]] const DungeonState* state) {
2908 // Pattern: 1x1 edge tiles downward (objects 0x6A-0x6B)
2909 int size = obj.size_ & 0x0F;
2910
2911 // Assembly: GetSize_1to16, so count = size + 1
2912 int count = size + 1;
2913
2914 for (int s = 0; s < count; s++) {
2915 if (tiles.size() >= 1) {
2916 // Use first 8x8 tile from span
2917 WriteTile8(bg, obj.x_, obj.y_ + s, tiles[0]);
2918 }
2919 }
2920}
2921
2923 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2924 std::span<const gfx::TileInfo> tiles,
2925 [[maybe_unused]] const DungeonState* state) {
2926 // Pattern: Left corner 2x1 tiles with +12 offset downward (object 0x6C)
2927 int size = obj.size_ & 0x0F;
2928
2929 // Assembly: GetSize_1to16_timesA(0x0A), so count = size + 10
2930 int count = size + 10;
2931
2932 for (int s = 0; s < count; s++) {
2933 if (tiles.size() >= 2) {
2934 // Use first tile span for 2x1 pattern
2935 WriteTile8(bg, obj.x_ + 12, obj.y_ + s, tiles[0]);
2936 WriteTile8(bg, obj.x_ + 12 + 1, obj.y_ + s, tiles[1]);
2937 }
2938 }
2939}
2940
2942 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2943 std::span<const gfx::TileInfo> tiles,
2944 [[maybe_unused]] const DungeonState* state) {
2945 // Pattern: Right corner 2x1 tiles with +12 offset downward (object 0x6D)
2946 int size = obj.size_ & 0x0F;
2947
2948 // Assembly: GetSize_1to16_timesA(0x0A), so count = size + 10
2949 int count = size + 10;
2950
2951 for (int s = 0; s < count; s++) {
2952 if (tiles.size() >= 2) {
2953 // Use first tile span for 2x1 pattern
2954 WriteTile8(bg, obj.x_ + 12, obj.y_ + s, tiles[0]);
2955 WriteTile8(bg, obj.x_ + 12 + 1, obj.y_ + s, tiles[1]);
2956 }
2957 }
2958}
2959
2960// Additional Downwards draw routines (0x70-0x7F range)
2961
2963 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2964 std::span<const gfx::TileInfo> tiles,
2965 [[maybe_unused]] const DungeonState* state) {
2966 // Pattern: Draws 4x4 floor tiles downward (object 0x70)
2967 // Assembly: RoomDraw_DownwardsFloor4x4_1to16 - adds $0200 to Y per iteration
2968 // $0200 = 512 = 4 tile rows (tilemap is 64 tiles wide = 128 bytes/row)
2969 int size = obj.size_ & 0x0F;
2970 int count = size + 1;
2971
2972 for (int s = 0; s < count; s++) {
2973 if (tiles.size() >= 16) {
2974 // Draw 4x4 pattern in COLUMN-MAJOR order
2975 for (int col = 0; col < 4; col++) {
2976 for (int row = 0; row < 4; row++) {
2977 int tile_idx = col * 4 + row;
2978 WriteTile8(bg, obj.x_ + col, obj.y_ + (s * 4) + row, tiles[tile_idx]);
2979 }
2980 }
2981 }
2982 }
2983}
2984
2986 const RoomObject& obj, gfx::BackgroundBuffer& bg,
2987 std::span<const gfx::TileInfo> tiles,
2988 [[maybe_unused]] const DungeonState* state) {
2989 // Pattern: 1x1 solid tiles downward with +3 modifier (object 0x71)
2990 // Assembly: RoomDraw_Downwards1x1Solid_1to16_plus3 - adds $0080 to Y per iteration
2991 int size = obj.size_ & 0x0F;
2992 int count = size + 4; // +3 gives +4 iterations total (1 + size + 3)
2993
2994 for (int s = 0; s < count; s++) {
2995 if (tiles.size() >= 1) {
2996 WriteTile8(bg, obj.x_, obj.y_ + s, tiles[0]);
2997 }
2998 }
2999}
3000
3002 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3003 std::span<const gfx::TileInfo> tiles,
3004 [[maybe_unused]] const DungeonState* state) {
3005 // Pattern: 4x4 decoration with 2-tile spacing downward (objects 0x73-0x74)
3006 // Assembly: RoomDraw_DownwardsDecor4x4spaced2_1to16 - adds $0300 to Y per iteration
3007 // $0300 = 6 tile rows spacing
3008 int size = obj.size_ & 0x0F;
3009 int count = size + 1;
3010
3011 for (int s = 0; s < count; s++) {
3012 if (tiles.size() >= 16) {
3013 // Draw 4x4 pattern in COLUMN-MAJOR order with 6-tile Y spacing
3014 for (int col = 0; col < 4; col++) {
3015 for (int row = 0; row < 4; row++) {
3016 int tile_idx = col * 4 + row;
3017 WriteTile8(bg, obj.x_ + col, obj.y_ + (s * 6) + row, tiles[tile_idx]);
3018 }
3019 }
3020 }
3021 }
3022}
3023
3025 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3026 std::span<const gfx::TileInfo> tiles,
3027 [[maybe_unused]] const DungeonState* state) {
3028 // Pattern: 2x4 pillar with 2-tile spacing downward (object 0x75, 0x87)
3029 // Assembly: RoomDraw_DownwardsPillar2x4spaced2_1to16 - adds $0300 to Y per iteration
3030 // $0300 = 6 tile rows spacing
3031 int size = obj.size_ & 0x0F;
3032 int count = size + 1;
3033
3034 for (int s = 0; s < count; s++) {
3035 if (tiles.size() >= 8) {
3036 // Draw 2x4 pattern in COLUMN-MAJOR order with 6-tile Y spacing
3037 for (int col = 0; col < 2; col++) {
3038 for (int row = 0; row < 4; row++) {
3039 int tile_idx = col * 4 + row;
3040 WriteTile8(bg, obj.x_ + col, obj.y_ + (s * 6) + row, tiles[tile_idx]);
3041 }
3042 }
3043 }
3044 }
3045}
3046
3048 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3049 std::span<const gfx::TileInfo> tiles,
3050 [[maybe_unused]] const DungeonState* state) {
3051 // Pattern: 3x4 decoration with 4-tile spacing downward (objects 0x76-0x77)
3052 // Assembly: RoomDraw_DownwardsDecor3x4spaced4_1to16 - adds $0400 to Y per iteration
3053 // $0400 = 8 tile rows spacing
3054 int size = obj.size_ & 0x0F;
3055 int count = size + 1;
3056
3057 for (int s = 0; s < count; s++) {
3058 if (tiles.size() >= 12) {
3059 // Draw 3x4 pattern in COLUMN-MAJOR order with 8-tile Y spacing
3060 for (int col = 0; col < 3; col++) {
3061 for (int row = 0; row < 4; row++) {
3062 int tile_idx = col * 4 + row;
3063 WriteTile8(bg, obj.x_ + col, obj.y_ + (s * 8) + row, tiles[tile_idx]);
3064 }
3065 }
3066 }
3067 }
3068}
3069
3071 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3072 std::span<const gfx::TileInfo> tiles,
3073 [[maybe_unused]] const DungeonState* state) {
3074 // Pattern: 2x2 decoration with 12-tile spacing downward (objects 0x78, 0x7B)
3075 // Assembly: RoomDraw_DownwardsDecor2x2spaced12_1to16 - adds $0600 to Y per iteration
3076 // $0600 = 14 tile rows spacing
3077 int size = obj.size_ & 0x0F;
3078 int count = size + 1;
3079
3080 for (int s = 0; s < count; s++) {
3081 if (tiles.size() >= 4) {
3082 // Draw 2x2 pattern in COLUMN-MAJOR order with 14-tile Y spacing
3083 for (int col = 0; col < 2; col++) {
3084 for (int row = 0; row < 2; row++) {
3085 int tile_idx = col * 2 + row;
3086 WriteTile8(bg, obj.x_ + col, obj.y_ + (s * 14) + row,
3087 tiles[tile_idx]);
3088 }
3089 }
3090 }
3091 }
3092}
3093
3095 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3096 std::span<const gfx::TileInfo> tiles,
3097 [[maybe_unused]] const DungeonState* state) {
3098 // Pattern: 1x1 line downward with +1 modifier (object 0x7C)
3099 // Assembly: RoomDraw_DownwardsLine1x1_1to16plus1
3100 int size = obj.size_ & 0x0F;
3101 int count = size + 2; // +1 gives +2 iterations total
3102
3103 for (int s = 0; s < count; s++) {
3104 if (tiles.size() >= 1) {
3105 WriteTile8(bg, obj.x_, obj.y_ + s, tiles[0]);
3106 }
3107 }
3108}
3109
3111 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3112 std::span<const gfx::TileInfo> tiles,
3113 [[maybe_unused]] const DungeonState* state) {
3114 // Pattern: 2x4 decoration with 8-tile spacing downward (objects 0x7F, 0x80)
3115 // Assembly: RoomDraw_DownwardsDecor2x4spaced8_1to16 - adds $0500 to Y per iteration
3116 // $0500 = 10 tile rows spacing
3117 int size = obj.size_ & 0x0F;
3118 int count = size + 1;
3119
3120 // Debug: Log vertical ceiling objects (0x80)
3121 if (obj.id_ == 0x80 && tiles.size() >= 8) {
3122 LOG_DEBUG("ObjectDrawer",
3123 "Vertical Ceiling Draw: obj=0x%02X pos=(%d,%d) size=%d tiles=%zu",
3124 obj.id_, obj.x_, obj.y_, size, tiles.size());
3125 LOG_DEBUG("ObjectDrawer", " Tile IDs: [%d, %d, %d, %d, %d, %d, %d, %d]",
3126 tiles[0].id_, tiles[1].id_, tiles[2].id_, tiles[3].id_,
3127 tiles[4].id_, tiles[5].id_, tiles[6].id_, tiles[7].id_);
3128 LOG_DEBUG("ObjectDrawer", " Palettes: [%d, %d, %d, %d, %d, %d, %d, %d]",
3129 tiles[0].palette_, tiles[1].palette_, tiles[2].palette_,
3130 tiles[3].palette_, tiles[4].palette_, tiles[5].palette_,
3131 tiles[6].palette_, tiles[7].palette_);
3132 }
3133
3134 for (int s = 0; s < count; s++) {
3135 if (tiles.size() >= 8) {
3136 // Draw 2x4 pattern in COLUMN-MAJOR order with 10-tile Y spacing
3137 for (int col = 0; col < 2; col++) {
3138 for (int row = 0; row < 4; row++) {
3139 int tile_idx = col * 4 + row;
3140 WriteTile8(bg, obj.x_ + col, obj.y_ + (s * 10) + row,
3141 tiles[tile_idx]);
3142 }
3143 }
3144 }
3145 }
3146}
3147
3148// ============================================================================
3149// Phase 4 Step 2: Simple Variant Routines (0x80-0x96, 0xB0-0xBD range)
3150// ============================================================================
3151
3153 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3154 std::span<const gfx::TileInfo> tiles,
3155 [[maybe_unused]] const DungeonState* state) {
3156 // Pattern: 3x4 decoration with 2-tile spacing downward (objects 0x81-0x84)
3157 // 3 cols × 4 rows = 12 tiles, with 6-tile Y spacing (4 tile height + 2 spacing)
3158 int size = obj.size_ & 0x0F;
3159 int count = size + 1;
3160
3161 for (int s = 0; s < count; s++) {
3162 if (tiles.size() >= 12) {
3163 // Draw 3x4 pattern in COLUMN-MAJOR order with 6-tile Y spacing
3164 for (int col = 0; col < 3; col++) {
3165 for (int row = 0; row < 4; row++) {
3166 int tile_idx = col * 4 + row;
3167 WriteTile8(bg, obj.x_ + col, obj.y_ + (s * 6) + row, tiles[tile_idx]);
3168 }
3169 }
3170 }
3171 }
3172}
3173
3175 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3176 std::span<const gfx::TileInfo> tiles,
3177 [[maybe_unused]] const DungeonState* state) {
3178 // Pattern: Big rail with TOP CAP (2x2) + MIDDLE (2x1 repeated) + BOTTOM CAP (2x3)
3179 // Assembly: RoomDraw_DownwardsBigRail3x1_1to16plus5
3180 // Tile layout: [4 tiles for top cap 2x2] [2 tiles for middle] [6 tiles for bottom cap 2x3]
3181 // Top cap: 2x2 in COLUMN-MAJOR order
3182 // Middle: 2 tiles wide (1 row), repeated (size + 1) times
3183 // Bottom cap: 2 columns x 3 rows in COLUMN-MAJOR order
3184
3185 int size = obj.size_ & 0x0F;
3186 int middle_count = size + 1; // GetSize_1to16 = size + 1
3187
3188 if (tiles.size() < 12)
3189 return; // Need 4+2+6=12 tiles minimum
3190
3191 int y = obj.y_;
3192
3193 // Draw TOP CAP: 2x2 block (tiles 0-3 in column-major order)
3194 // Column 0
3195 WriteTile8(bg, obj.x_, y, tiles[0]);
3196 WriteTile8(bg, obj.x_, y + 1, tiles[1]);
3197 // Column 1
3198 WriteTile8(bg, obj.x_ + 1, y, tiles[2]);
3199 WriteTile8(bg, obj.x_ + 1, y + 1, tiles[3]);
3200 y += 2;
3201
3202 // Draw MIDDLE: 2 tiles wide x 1 row (tiles 4-5), repeated middle_count times
3203 for (int s = 0; s < middle_count; s++) {
3204 WriteTile8(bg, obj.x_, y + s, tiles[4]);
3205 WriteTile8(bg, obj.x_ + 1, y + s, tiles[5]);
3206 }
3207 y += middle_count;
3208
3209 // Draw BOTTOM CAP: 2 columns x 3 rows (tiles 6-11 in column-major order)
3210 // Column 0
3211 WriteTile8(bg, obj.x_, y, tiles[6]);
3212 WriteTile8(bg, obj.x_, y + 1, tiles[7]);
3213 WriteTile8(bg, obj.x_, y + 2, tiles[8]);
3214 // Column 1
3215 WriteTile8(bg, obj.x_ + 1, y, tiles[9]);
3216 WriteTile8(bg, obj.x_ + 1, y + 1, tiles[10]);
3217 WriteTile8(bg, obj.x_ + 1, y + 2, tiles[11]);
3218}
3219
3221 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3222 std::span<const gfx::TileInfo> tiles,
3223 [[maybe_unused]] const DungeonState* state) {
3224 // Pattern: 2x2 block downward with 4-tile Y spacing (object 0x89)
3225 // Vertical version of RightwardsBlock2x2spaced2_1to16
3226 int size = obj.size_ & 0x0F;
3227 int count = size + 1;
3228
3229 for (int s = 0; s < count; s++) {
3230 if (tiles.size() >= 4) {
3231 int base_y = obj.y_ + (s * 4); // 4-tile Y spacing
3232 // Draw 2x2 pattern in COLUMN-MAJOR order
3233 WriteTile8(bg, obj.x_, base_y, tiles[0]);
3234 WriteTile8(bg, obj.x_, base_y + 1, tiles[1]);
3235 WriteTile8(bg, obj.x_ + 1, base_y, tiles[2]);
3236 WriteTile8(bg, obj.x_ + 1, base_y + 1, tiles[3]);
3237 }
3238 }
3239}
3240
3242 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3243 std::span<const gfx::TileInfo> tiles,
3244 [[maybe_unused]] const DungeonState* state) {
3245 // Pattern: 3x6 cannon hole downward (objects 0x85-0x86)
3246 // 3 cols × 6 rows = 18 tiles
3247 // Vertical version of RightwardsCannonHole4x3_1to16
3248 int size = obj.size_ & 0x0F;
3249 int count = size + 1;
3250
3251 for (int s = 0; s < count; s++) {
3252 if (tiles.size() >= 18) {
3253 // Draw 3x6 pattern in COLUMN-MAJOR order
3254 for (int col = 0; col < 3; col++) {
3255 for (int row = 0; row < 6; row++) {
3256 int tile_idx = col * 6 + row;
3257 WriteTile8(bg, obj.x_ + col, obj.y_ + (s * 6) + row, tiles[tile_idx]);
3258 }
3259 }
3260 }
3261 }
3262}
3263
3265 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3266 std::span<const gfx::TileInfo> tiles,
3267 [[maybe_unused]] const DungeonState* state) {
3268 // Pattern: 2x3 bar downward (object 0x8F)
3269 // 2 cols × 3 rows = 6 tiles
3270 int size = obj.size_ & 0x0F;
3271 int count = size + 1;
3272
3273 for (int s = 0; s < count; s++) {
3274 if (tiles.size() >= 6) {
3275 // Draw 2x3 pattern in COLUMN-MAJOR order
3276 for (int col = 0; col < 2; col++) {
3277 for (int row = 0; row < 3; row++) {
3278 int tile_idx = col * 3 + row;
3279 WriteTile8(bg, obj.x_ + col, obj.y_ + (s * 3) + row, tiles[tile_idx]);
3280 }
3281 }
3282 }
3283 }
3284}
3285
3287 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3288 std::span<const gfx::TileInfo> tiles,
3289 [[maybe_unused]] const DungeonState* state) {
3290 // Pattern: 2x2 pots downward (object 0x95)
3291 // Interactive object - draws 2x2 pattern vertically repeating
3292 int size = obj.size_ & 0x0F;
3293 int count = size + 1;
3294
3295 for (int s = 0; s < count; s++) {
3296 if (tiles.size() >= 4) {
3297 int base_y = obj.y_ + (s * 2);
3298 // Draw 2x2 pattern in COLUMN-MAJOR order
3299 WriteTile8(bg, obj.x_, base_y, tiles[0]);
3300 WriteTile8(bg, obj.x_, base_y + 1, tiles[1]);
3301 WriteTile8(bg, obj.x_ + 1, base_y, tiles[2]);
3302 WriteTile8(bg, obj.x_ + 1, base_y + 1, tiles[3]);
3303 }
3304 }
3305}
3306
3308 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3309 std::span<const gfx::TileInfo> tiles,
3310 [[maybe_unused]] const DungeonState* state) {
3311 // Pattern: 2x2 hammer pegs downward (object 0x96)
3312 // Interactive object - draws 2x2 pattern vertically repeating
3313 int size = obj.size_ & 0x0F;
3314 int count = size + 1;
3315
3316 for (int s = 0; s < count; s++) {
3317 if (tiles.size() >= 4) {
3318 int base_y = obj.y_ + (s * 2);
3319 // Draw 2x2 pattern in COLUMN-MAJOR order
3320 WriteTile8(bg, obj.x_, base_y, tiles[0]);
3321 WriteTile8(bg, obj.x_, base_y + 1, tiles[1]);
3322 WriteTile8(bg, obj.x_ + 1, base_y, tiles[2]);
3323 WriteTile8(bg, obj.x_ + 1, base_y + 1, tiles[3]);
3324 }
3325 }
3326}
3327
3329 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3330 std::span<const gfx::TileInfo> tiles,
3331 [[maybe_unused]] const DungeonState* state) {
3332 // Pattern: 1x1 edge tiles rightward with +7 offset (objects 0xB0-0xB1)
3333 // Assembly: RoomDraw_RightwardsEdge1x1_1to16plus7
3334 int size = obj.size_ & 0x0F;
3335 int count = size + 8; // +7 gives +8 iterations total
3336
3337 for (int s = 0; s < count; s++) {
3338 if (tiles.size() >= 1) {
3339 WriteTile8(bg, obj.x_ + s, obj.y_, tiles[0]);
3340 }
3341 }
3342}
3343
3345 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3346 std::span<const gfx::TileInfo> tiles,
3347 [[maybe_unused]] const DungeonState* state) {
3348 // Pattern: 2x2 pots rightward (object 0xBC)
3349 // Interactive object - draws 2x2 pattern horizontally repeating
3350 int size = obj.size_ & 0x0F;
3351 int count = size + 1;
3352
3353 for (int s = 0; s < count; s++) {
3354 if (tiles.size() >= 4) {
3355 int base_x = obj.x_ + (s * 2);
3356 // Draw 2x2 pattern in COLUMN-MAJOR order
3357 WriteTile8(bg, base_x, obj.y_, tiles[0]);
3358 WriteTile8(bg, base_x, obj.y_ + 1, tiles[1]);
3359 WriteTile8(bg, base_x + 1, obj.y_, tiles[2]);
3360 WriteTile8(bg, base_x + 1, obj.y_ + 1, tiles[3]);
3361 }
3362 }
3363}
3364
3366 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3367 std::span<const gfx::TileInfo> tiles,
3368 [[maybe_unused]] const DungeonState* state) {
3369 // Pattern: 2x2 hammer pegs rightward (object 0xBD)
3370 // Interactive object - draws 2x2 pattern horizontally repeating
3371 int size = obj.size_ & 0x0F;
3372 int count = size + 1;
3373
3374 for (int s = 0; s < count; s++) {
3375 if (tiles.size() >= 4) {
3376 int base_x = obj.x_ + (s * 2);
3377 // Draw 2x2 pattern in COLUMN-MAJOR order
3378 WriteTile8(bg, base_x, obj.y_, tiles[0]);
3379 WriteTile8(bg, base_x, obj.y_ + 1, tiles[1]);
3380 WriteTile8(bg, base_x + 1, obj.y_, tiles[2]);
3381 WriteTile8(bg, base_x + 1, obj.y_ + 1, tiles[3]);
3382 }
3383 }
3384}
3385
3386// ============================================================================
3387// Phase 4 Step 3: Diagonal Ceiling Routines
3388// ============================================================================
3389
3391 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3392 std::span<const gfx::TileInfo> tiles,
3393 [[maybe_unused]] const DungeonState* state) {
3394 // Pattern: Diagonal ceiling top-left - TRIANGLE shape (objects 0xA0, 0xA5, 0xA9)
3395 // Assembly: RoomDraw_DiagonalCeilingTopLeftA/B with GetSize_1to16_timesA(4)
3396 // count = (size & 0x0F) + 4 per ASM
3397 int count = (obj.size_ & 0x0F) + 4;
3398
3399 if (tiles.empty()) {
3400 LOG_DEBUG("ObjectDrawer", "DiagonalCeilingTopLeft: No tiles for obj 0x%02X",
3401 obj.id_);
3402 return;
3403 }
3404
3405 LOG_DEBUG("ObjectDrawer",
3406 "DiagonalCeilingTopLeft: obj=0x%02X pos=(%d,%d) size=%d count=%d",
3407 obj.id_, obj.x_, obj.y_, obj.size_, count);
3408
3409 // Each row: starts with 'count' tiles, then count-1, count-2, etc.
3410 int tiles_in_row = count;
3411 for (int row = 0; row < count && tiles_in_row > 0; row++) {
3412 for (int col = 0; col < tiles_in_row; col++) {
3413 WriteTile8(bg, obj.x_ + col, obj.y_ + row, tiles[0]);
3414 }
3415 tiles_in_row--; // One fewer tile each row (DEC $B2)
3416 }
3417}
3418
3420 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3421 std::span<const gfx::TileInfo> tiles,
3422 [[maybe_unused]] const DungeonState* state) {
3423 // Pattern: Diagonal ceiling bottom-left - TRIANGLE shape (objects 0xA1, 0xA6, 0xAA)
3424 // Assembly: RoomDraw_DiagonalCeilingBottomLeftA/B with GetSize_1to16_timesA(4)
3425 // count = (size & 0x0F) + 4 per ASM
3426 int count = (obj.size_ & 0x0F) + 4;
3427
3428 if (tiles.empty())
3429 return;
3430
3431 // Row 0: 1 tile, Row 1: 2 tiles, ..., Row count-1: count tiles
3432 for (int row = 0; row < count; row++) {
3433 int tiles_in_row = row + 1;
3434 for (int col = 0; col < tiles_in_row; col++) {
3435 WriteTile8(bg, obj.x_ + col, obj.y_ + row, tiles[0]);
3436 }
3437 }
3438}
3439
3441 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3442 std::span<const gfx::TileInfo> tiles,
3443 [[maybe_unused]] const DungeonState* state) {
3444 // Pattern: Diagonal ceiling top-right - TRIANGLE shape (objects 0xA2, 0xA7, 0xAB)
3445 // Assembly: RoomDraw_DiagonalCeilingTopRightA/B with GetSize_1to16_timesA(4)
3446 // count = (size & 0x0F) + 4 per ASM
3447 int count = (obj.size_ & 0x0F) + 4;
3448
3449 if (tiles.empty())
3450 return;
3451
3452 // Row 0 at (x,y): count tiles, Row 1 at (x+1,y+1): count-1 tiles, etc.
3453 int tiles_in_row = count;
3454 for (int row = 0; row < count && tiles_in_row > 0; row++) {
3455 for (int col = 0; col < tiles_in_row; col++) {
3456 WriteTile8(bg, obj.x_ + row + col, obj.y_ + row, tiles[0]);
3457 }
3458 tiles_in_row--;
3459 }
3460}
3461
3463 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3464 std::span<const gfx::TileInfo> tiles,
3465 [[maybe_unused]] const DungeonState* state) {
3466 // Pattern: Diagonal ceiling bottom-right - TRIANGLE shape (objects 0xA3, 0xA8, 0xAC)
3467 // Assembly: RoomDraw_DiagonalCeilingBottomRightA/B with GetSize_1to16_timesA(4)
3468 // count = (size & 0x0F) + 4 per ASM
3469 int count = (obj.size_ & 0x0F) + 4;
3470
3471 if (tiles.empty())
3472 return;
3473
3474 // Row 0 at (x, y): count tiles, Row 1 at (x+1, y-1): count-1 tiles, etc.
3475 int tiles_in_row = count;
3476 for (int row = 0; row < count && tiles_in_row > 0; row++) {
3477 for (int col = 0; col < tiles_in_row; col++) {
3478 WriteTile8(bg, obj.x_ + row + col, obj.y_ - row, tiles[0]);
3479 }
3480 tiles_in_row--;
3481 }
3482}
3483
3484// ============================================================================
3485// Phase 4 Step 5: Special Routines (0xC1, 0xCD, 0xCE, 0xDC)
3486// ============================================================================
3487
3489 const RoomObject& obj, gfx::BackgroundBuffer& bg,
3490 std::span<const gfx::TileInfo> tiles) {
3491 // ASM: RoomDraw_ClosedChestPlatform ($018CC7)
3492 //
3493 // Structure:
3494 // LDA.b $B2 ; Width size
3495 // CLC
3496 // ADC.w #$0004 ; width = B2 + 4
3497 // STA.b $B2
3498 // STA.b $0A
3499 // INC.b $B4 ; height = B4 + 1
3500 // JSR RoomDraw_ChestPlatformHorizontalWallWithCorners
3501 // ... vertical walls on sides ...
3502 //
3503 // The platform has 68 tiles forming a frame + carpet pattern.
3504 // Layer handling uses same $BF check as OpenChestPlatform.
3505
3506 if (tiles.size() < 16)
3507 return;
3508
3509 // width = B2 + 4, height = B4 + 1
3510 int width = (obj.size_ & 0x0F) + 4;
3511 int height = ((obj.size_ >> 4) & 0x0F) + 1;
3512
3513 LOG_DEBUG("ObjectDrawer",
3514 "DrawClosedChestPlatform: obj=0x%03X pos=(%d,%d) size=0x%02X "
3515 "width=%d height=%d",
3516 obj.id_, obj.x_, obj.y_, obj.size_, width, height);
3517
3518 // Draw outer frame + interior carpet pattern
3519 // The routine builds a rectangular platform with walls and floor
3520 size_t tile_idx = 0;
3521 for (int row = 0; row < height * 3; ++row) {
3522 for (int col = 0; col < width * 2; ++col) {
3523 WriteTile8(bg, obj.x_ + col, obj.y_ + row,
3524 tiles[tile_idx % tiles.size()]);
3525 tile_idx++;
3526 }
3527 }
3528}
3529
3532 std::span<const gfx::TileInfo> tiles) {
3533 // ASM: RoomDraw_MovingWallWest ($019190)
3534 // Draw vertical wall structure (28 tiles) - always draw in editor
3535 if (tiles.size() < 6)
3536 return;
3537
3538 int count = ((obj.size_ >> 4) & 0x0F) + 4;
3539
3540 // Draw wall columns
3541 for (int row = 0; row < count; ++row) {
3542 for (int col = 0; col < 3; ++col) {
3543 size_t tile_idx = (row * 3 + col) % tiles.size();
3544 WriteTile8(bg, obj.x_ + col, obj.y_ + row, tiles[tile_idx]);
3545 }
3546 }
3547}
3548
3551 std::span<const gfx::TileInfo> tiles) {
3552 // ASM: RoomDraw_MovingWallEast ($01921C)
3553 // Mirror of West - draws from right-to-left
3554 if (tiles.size() < 6)
3555 return;
3556
3557 int count = ((obj.size_ >> 4) & 0x0F) + 4;
3558
3559 for (int row = 0; row < count; ++row) {
3560 for (int col = 0; col < 3; ++col) {
3561 size_t tile_idx = (row * 3 + col) % tiles.size();
3562 WriteTile8(bg, obj.x_ - col, obj.y_ + row, tiles[tile_idx]);
3563 }
3564 }
3565}
3566
3569 std::span<const gfx::TileInfo> tiles) {
3570 // ASM: RoomDraw_OpenChestPlatform ($019733)
3571 //
3572 // Layer handling from ASM:
3573 // LDA.b $BF ; Load current layer pointer
3574 // CMP.w #$4000 ; Compare with lower_layer ($4000)
3575 // BNE .upper_layer ; Skip if upper layer
3576 // TYA
3577 // ORA.w #$2000 ; Set BG2 priority bit for lower layer
3578 // TAY
3579 //
3580 // In the editor, layer handling is done via the object's layer_ field
3581 // and the DrawObject() method routes to the appropriate buffer.
3582 // The BG2 priority bit is not directly applicable here since we use
3583 // separate bitmaps for compositing.
3584
3585 if (tiles.size() < 8)
3586 return;
3587
3588 // width = B2 + 1 (ASM: INC.b $B2)
3589 int width = (obj.size_ & 0x0F) + 1;
3590
3591 // segments = (B4 * 2) + 5 (ASM: LDA.b $B4; ASL A; CLC; ADC.w #$0005)
3592 int segments = ((obj.size_ >> 4) & 0x0F) * 2 + 5;
3593
3594 LOG_DEBUG("ObjectDrawer",
3595 "DrawOpenChestPlatform: obj=0x%03X pos=(%d,%d) size=0x%02X "
3596 "width=%d segments=%d",
3597 obj.id_, obj.x_, obj.y_, obj.size_, width, segments);
3598
3599 // ASM: obj0AB4 is the tile data source (21 tiles total)
3600 // The routine draws segments, each segment is a row of 'width' tiles
3601 // Then draws two additional segments at the end (INY INY; JSR .draw_segment; INY INY)
3602
3603 // Draw the main segments
3604 int tile_offset = 0;
3605 for (int seg = 0; seg < segments; ++seg) {
3606 for (int col = 0; col < width; ++col) {
3607 size_t tile_idx = tile_offset % tiles.size();
3608 WriteTile8(bg, obj.x_ + col, obj.y_ + seg, tiles[tile_idx]);
3609 tile_offset++;
3610 }
3611 }
3612
3613 // Draw final two segments (ASM: INY INY; JSR .draw_segment; INY INY; JSR .draw_segment)
3614 // These use tiles at offset +2 and +4 from the current position
3615 tile_offset += 2;
3616 for (int col = 0; col < width; ++col) {
3617 size_t tile_idx = tile_offset % tiles.size();
3618 WriteTile8(bg, obj.x_ + col, obj.y_ + segments, tiles[tile_idx]);
3619 tile_offset++;
3620 }
3621 tile_offset += 2;
3622 for (int col = 0; col < width; ++col) {
3623 size_t tile_idx = tile_offset % tiles.size();
3624 WriteTile8(bg, obj.x_ + col, obj.y_ + segments + 1, tiles[tile_idx]);
3625 tile_offset++;
3626 }
3627}
3628
3629// ============================================================================
3630// Utility Methods
3631// ============================================================================
3632
3634 int tile_y, int pixel_width,
3635 int pixel_height) {
3636 auto& bitmap = bg1.bitmap();
3637 if (!bitmap.is_active() || bitmap.width() == 0) {
3638 LOG_DEBUG("ObjectDrawer", "MarkBG1Transparent: Bitmap not ready, skipping");
3639 return; // Bitmap not ready
3640 }
3641
3642 int start_px = tile_x * 8;
3643 int start_py = tile_y * 8;
3644 int canvas_width = bitmap.width();
3645 int canvas_height = bitmap.height();
3646 auto& data = bitmap.mutable_data();
3647
3648 int pixels_marked = 0;
3649
3650 // Mark pixels as transparent (255) where BG2 overlay objects are drawn
3651 // This creates "holes" in BG1 that allow BG2 content to show through
3652 for (int py = start_py; py < start_py + pixel_height && py < canvas_height;
3653 py++) {
3654 if (py < 0)
3655 continue;
3656 for (int px = start_px; px < start_px + pixel_width && px < canvas_width;
3657 px++) {
3658 if (px < 0)
3659 continue;
3660 int idx = py * canvas_width + px;
3661 if (idx >= 0 && idx < static_cast<int>(data.size())) {
3662 data[idx] = 255; // 255 = transparent in our compositing system
3663 pixels_marked++;
3664 }
3665 }
3666 }
3667 bitmap.set_modified(true);
3668
3669 LOG_DEBUG("ObjectDrawer",
3670 "MarkBG1Transparent: Marked %d pixels at tile(%d,%d) pixel(%d,%d) "
3671 "size(%d,%d)",
3672 pixels_marked, tile_x, tile_y, start_px, start_py, pixel_width,
3673 pixel_height);
3674}
3675
3676void ObjectDrawer::WriteTile8(gfx::BackgroundBuffer& bg, int tile_x, int tile_y,
3677 const gfx::TileInfo& tile_info) {
3678 if (!IsValidTilePosition(tile_x, tile_y)) {
3679 return;
3680 }
3681 PushTrace(tile_x, tile_y, tile_info);
3682 if (trace_only_) {
3683 return;
3684 }
3685 // Draw directly to bitmap instead of tile buffer to avoid being overwritten
3686 auto& bitmap = bg.bitmap();
3687 if (!bitmap.is_active() || bitmap.width() == 0) {
3688 return; // Bitmap not ready
3689 }
3690
3691 // The room-specific graphics buffer (current_gfx16_) contains the assembled
3692 // tile graphics for the current room. Object tile IDs are relative to this
3693 // buffer.
3694 const uint8_t* gfx_data = room_gfx_buffer_;
3695
3696 if (!gfx_data) {
3697 LOG_DEBUG("ObjectDrawer", "ERROR: No graphics data available");
3698 return;
3699 }
3700
3701 // Draw single 8x8 tile directly to bitmap
3702 DrawTileToBitmap(bitmap, tile_info, tile_x * 8, tile_y * 8, gfx_data);
3703
3704 // Mark coverage for the full 8x8 tile region (even if pixels are transparent).
3705 //
3706 // This distinguishes "tilemap entry written but transparent" from "no write",
3707 // which is required to emulate SNES behavior where a transparent tile still
3708 // overwrites the previous tilemap entry (clearing BG1 and revealing BG2/backdrop).
3709 auto& coverage_buffer = bg.mutable_coverage_data();
3710
3711 // Also update priority buffer with tile's priority bit.
3712 // Priority (over_) affects Z-ordering in SNES Mode 1 compositing.
3713 uint8_t priority = tile_info.over_ ? 1 : 0;
3714 int pixel_x = tile_x * 8;
3715 int pixel_y = tile_y * 8;
3716 auto& priority_buffer = bg.mutable_priority_data();
3717 int width = bitmap.width();
3718
3719 // Update priority for each pixel in the 8x8 tile
3720 const auto& bitmap_data = bitmap.vector();
3721 for (int py = 0; py < 8; py++) {
3722 int dest_y = pixel_y + py;
3723 if (dest_y < 0 || dest_y >= bitmap.height())
3724 continue;
3725
3726 for (int px = 0; px < 8; px++) {
3727 int dest_x = pixel_x + px;
3728 if (dest_x < 0 || dest_x >= width)
3729 continue;
3730
3731 int dest_index = dest_y * width + dest_x;
3732
3733 // Coverage is set for all pixels in the tile footprint.
3734 if (dest_index >= 0 &&
3735 dest_index < static_cast<int>(coverage_buffer.size())) {
3736 coverage_buffer[dest_index] = 1;
3737 }
3738
3739 // Store priority only for opaque pixels; transparent writes clear stale
3740 // priority at this location.
3741 if (dest_index < static_cast<int>(bitmap_data.size()) &&
3742 bitmap_data[dest_index] != 255) {
3743 priority_buffer[dest_index] = priority;
3744 } else {
3745 priority_buffer[dest_index] = 0xFF;
3746 }
3747 }
3748 }
3749}
3750
3751bool ObjectDrawer::IsValidTilePosition(int tile_x, int tile_y) const {
3752 return tile_x >= 0 && tile_x < kMaxTilesX && tile_y >= 0 &&
3753 tile_y < kMaxTilesY;
3754}
3755
3757 const gfx::TileInfo& tile_info, int pixel_x,
3758 int pixel_y, const uint8_t* tiledata) {
3759 // Draw an 8x8 tile directly to bitmap at pixel coordinates
3760 // Graphics data is in 8BPP linear format (1 pixel per byte)
3761 if (!tiledata)
3762 return;
3763
3764 // DEBUG: Check if bitmap is valid
3765 if (!bitmap.is_active() || bitmap.width() == 0 || bitmap.height() == 0) {
3766 LOG_DEBUG("ObjectDrawer", "ERROR: Invalid bitmap - active=%d, size=%dx%d",
3767 bitmap.is_active(), bitmap.width(), bitmap.height());
3768 return;
3769 }
3770
3771 // Calculate tile position in 8BPP graphics buffer
3772 // Layout: 16 tiles per row, each tile is 8 pixels wide (8 bytes)
3773 // Row stride: 128 bytes (16 tiles * 8 bytes)
3774 // Buffer size: 0x10000 (65536 bytes) = 64 tile rows max
3775 constexpr int kGfxBufferSize = 0x10000;
3776 constexpr int kMaxTileRow = 63; // 64 rows (0-63), each 1024 bytes
3777
3778 int tile_col = tile_info.id_ % 16;
3779 int tile_row = tile_info.id_ / 16;
3780
3781 // CRITICAL: Validate tile_row to prevent index out of bounds
3782 if (tile_row > kMaxTileRow) {
3783 LOG_DEBUG("ObjectDrawer", "Tile ID 0x%03X out of bounds (row %d > %d)",
3784 tile_info.id_, tile_row, kMaxTileRow);
3785 return;
3786 }
3787
3788 int tile_base_x = tile_col * 8; // 8 bytes per tile horizontally
3789 int tile_base_y =
3790 tile_row * 1024; // 1024 bytes per tile row (8 rows * 128 bytes)
3791
3792 // DEBUG: Log first few tiles being drawn with their graphics data
3793 static int draw_debug_count = 0;
3794 if (draw_debug_count < 5) {
3795 int sample_index = tile_base_y + tile_base_x;
3796 LOG_DEBUG("ObjectDrawer",
3797 "DrawTile: id=%d (col=%d,row=%d) gfx_offset=%d (0x%04X)",
3798 tile_info.id_, tile_col, tile_row, sample_index, sample_index);
3799 draw_debug_count++;
3800 }
3801
3802 // Palette offset calculation using 16-color bank chunking (matches SNES CGRAM)
3803 //
3804 // SNES CGRAM layout:
3805 // - Each CGRAM row has 16 colors, with index 0 being transparent
3806 // - Dungeon tiles use palette bits 2-7, mapping to CGRAM rows 2-7
3807 // - We map palette bits 2-7 to SDL banks 0-5
3808 //
3809 // Drawing formula: final_color = pixel + (bank * 16)
3810 // Where pixel 0 = transparent (not written), pixel 1-15 = colors within bank
3811 uint8_t pal = tile_info.palette_ & 0x07;
3812 uint8_t palette_offset;
3813 if (pal >= 2 && pal <= 7) {
3814 // Map palette bits 2-7 to SDL banks 0-5 using 16-color stride
3815 palette_offset = (pal - 2) * 16;
3816 } else {
3817 // Palette 0-1 are for HUD/other - fallback to first bank
3818 palette_offset = 0;
3819 }
3820
3821 // Draw 8x8 pixels with overwrite semantics.
3822 //
3823 // Important SNES behavior: writing a tilemap entry replaces the previous
3824 // contents for the full 8x8 footprint. Source pixel 0 is transparent, but it
3825 // still clears what was there before. We model that by writing 255
3826 // (transparent key) for zero pixels.
3827 bool any_pixels_changed = false;
3828
3829 for (int py = 0; py < 8; py++) {
3830 // Source row with vertical mirroring
3831 int src_row = tile_info.vertical_mirror_ ? (7 - py) : py;
3832
3833 for (int px = 0; px < 8; px++) {
3834 // Source column with horizontal mirroring
3835 int src_col = tile_info.horizontal_mirror_ ? (7 - px) : px;
3836
3837 // Calculate source index in 8BPP buffer
3838 // Stride is 128 bytes (sheet width)
3839 int src_index = (src_row * 128) + src_col + tile_base_x + tile_base_y;
3840 uint8_t pixel = tiledata[src_index];
3841 uint8_t out_pixel = 255; // transparent/clear
3842 if (pixel != 0) {
3843 // Pixels 1-15 map into a 16-color bank chunk.
3844 out_pixel = static_cast<uint8_t>(pixel + palette_offset);
3845 }
3846
3847 int dest_x = pixel_x + px;
3848 int dest_y = pixel_y + py;
3849 if (dest_x < 0 || dest_x >= bitmap.width() || dest_y < 0 ||
3850 dest_y >= bitmap.height()) {
3851 continue;
3852 }
3853
3854 int dest_index = dest_y * bitmap.width() + dest_x;
3855 if (dest_index < 0 ||
3856 dest_index >= static_cast<int>(bitmap.mutable_data().size())) {
3857 continue;
3858 }
3859
3860 auto& dst = bitmap.mutable_data()[dest_index];
3861 if (dst != out_pixel) {
3862 dst = out_pixel;
3863 any_pixels_changed = true;
3864 }
3865 }
3866 }
3867
3868 if (any_pixels_changed) {
3869 bitmap.set_modified(true);
3870 }
3871}
3872
3874 uint16_t object_id, int tile_x, int tile_y, RoomObject::LayerType layer,
3875 uint16_t room_draw_object_data_offset, gfx::BackgroundBuffer& bg1,
3876 gfx::BackgroundBuffer& bg2) {
3877 if (!rom_ || !rom_->is_loaded()) {
3878 return absl::FailedPreconditionError("ROM not loaded");
3879 }
3880
3881 const auto& rom_data = rom_->vector();
3882 const int base =
3883 kRoomObjectTileAddress + static_cast<int>(room_draw_object_data_offset);
3884 if (base < 0 || base + 7 >= static_cast<int>(rom_data.size())) {
3885 return absl::OutOfRangeError(
3886 absl::StrFormat("RoomDrawObjectData 2x2 out of range: base=0x%X",
3887 base));
3888 }
3889
3890 auto read_word = [&](int off) -> uint16_t {
3891 return static_cast<uint16_t>(rom_data[off]) |
3892 (static_cast<uint16_t>(rom_data[off + 1]) << 8);
3893 };
3894
3895 const uint16_t w0 = read_word(base + 0);
3896 const uint16_t w1 = read_word(base + 2);
3897 const uint16_t w2 = read_word(base + 4);
3898 const uint16_t w3 = read_word(base + 6);
3899
3900 const gfx::TileInfo t0 = gfx::WordToTileInfo(w0);
3901 const gfx::TileInfo t1 = gfx::WordToTileInfo(w1);
3902 const gfx::TileInfo t2 = gfx::WordToTileInfo(w2);
3903 const gfx::TileInfo t3 = gfx::WordToTileInfo(w3);
3904
3905 // Set trace context once; WriteTile8 will emit per-tile traces.
3906 RoomObject trace_obj(static_cast<int16_t>(object_id),
3907 static_cast<uint8_t>(tile_x),
3908 static_cast<uint8_t>(tile_y), 0,
3909 static_cast<uint8_t>(layer));
3910 SetTraceContext(trace_obj, layer);
3911
3912 gfx::BackgroundBuffer& target_bg =
3913 (layer == RoomObject::LayerType::BG2) ? bg2 : bg1;
3914
3915 // Column-major order (matches USDASM $BF/$CB/$C2/$CE writes).
3916 WriteTile8(target_bg, tile_x + 0, tile_y + 0, t0); // top-left
3917 WriteTile8(target_bg, tile_x + 0, tile_y + 1, t1); // bottom-left
3918 WriteTile8(target_bg, tile_x + 1, tile_y + 0, t2); // top-right
3919 WriteTile8(target_bg, tile_x + 1, tile_y + 1, t3); // bottom-right
3920
3921 return absl::OkStatus();
3922}
3923
3924// ============================================================================
3925// Type 3 / Special Routine Implementations
3926// ============================================================================
3927
3930 std::span<const gfx::TileInfo> tiles,
3931 [[maybe_unused]] const DungeonState* state) {
3932 // Pattern: Somaria Line (objects 0x203-0x20F, 0x214)
3933 // Draws a line of tiles based on direction encoded in object ID
3934 // Direction mapping based on ZScream reference:
3935 // 0x203: Horizontal right
3936 // 0x204: Vertical down
3937 // 0x205: Diagonal down-right
3938 // 0x206: Diagonal down-left
3939 // 0x207-0x209: Variations
3940 // 0x20A-0x20C: More variations
3941 // 0x20E-0x20F: Additional patterns
3942 // 0x214: Another line type
3943
3944 if (tiles.empty())
3945 return;
3946
3947 int length = (obj.size_ & 0x0F) + 1;
3948 int obj_subid = obj.id_ & 0x0F; // Low nibble determines direction
3949
3950 // Determine direction based on object sub-ID
3951 int dx = 1, dy = 0; // Default: horizontal right
3952 switch (obj_subid) {
3953 case 0x03:
3954 dx = 1;
3955 dy = 0;
3956 break; // Horizontal right
3957 case 0x04:
3958 dx = 0;
3959 dy = 1;
3960 break; // Vertical down
3961 case 0x05:
3962 dx = 1;
3963 dy = 1;
3964 break; // Diagonal down-right
3965 case 0x06:
3966 dx = -1;
3967 dy = 1;
3968 break; // Diagonal down-left
3969 case 0x07:
3970 dx = 1;
3971 dy = 0;
3972 break; // Horizontal (variant)
3973 case 0x08:
3974 dx = 0;
3975 dy = 1;
3976 break; // Vertical (variant)
3977 case 0x09:
3978 dx = 1;
3979 dy = 1;
3980 break; // Diagonal (variant)
3981 case 0x0A:
3982 dx = 1;
3983 dy = 0;
3984 break; // Horizontal
3985 case 0x0B:
3986 dx = 0;
3987 dy = 1;
3988 break; // Vertical
3989 case 0x0C:
3990 dx = 1;
3991 dy = 1;
3992 break; // Diagonal
3993 case 0x0E:
3994 dx = 1;
3995 dy = 0;
3996 break; // Horizontal
3997 case 0x0F:
3998 dx = 0;
3999 dy = 1;
4000 break; // Vertical
4001 default:
4002 dx = 1;
4003 dy = 0;
4004 break; // Default horizontal
4005 }
4006
4007 // Draw tiles along the path using first tile (Somaria uses single tile)
4008 for (int i = 0; i < length; ++i) {
4009 int tile_idx = i % tiles.size(); // Cycle through tiles if multiple
4010 WriteTile8(bg, obj.x_ + (i * dx), obj.y_ + (i * dy), tiles[tile_idx]);
4011 }
4012}
4013
4016 std::span<const gfx::TileInfo> tiles,
4017 [[maybe_unused]] const DungeonState* state) {
4018 // Generic 2x2 face helper used by corner-routine fallbacks.
4019 //
4020 // The true Type-3 water-face variants (0xF80-0xF82, routines 94-96) are
4021 // handled in draw_routines::special_routines with state-aware 4xN patterns.
4022 if (tiles.size() >= 4) {
4023 WriteTile8(bg, obj.x_, obj.y_, tiles[0]); // col 0, row 0
4024 WriteTile8(bg, obj.x_, obj.y_ + 1, tiles[1]); // col 0, row 1
4025 WriteTile8(bg, obj.x_ + 1, obj.y_, tiles[2]); // col 1, row 0
4026 WriteTile8(bg, obj.x_ + 1, obj.y_ + 1, tiles[3]); // col 1, row 1
4027 }
4028}
4029
4031 const RoomObject& obj, gfx::BackgroundBuffer& bg,
4032 std::span<const gfx::TileInfo> tiles,
4033 [[maybe_unused]] const DungeonState* state) {
4034 // Pattern: 4x4 Corner for Both BG (objects 0x108-0x10F for Type 2)
4035 // Type 3 objects (0xF9B-0xF9D) only have 8 tiles, draw 2x4 pattern
4036 if (tiles.size() >= 16) {
4037 DrawCorner4x4(obj, bg, tiles);
4038 } else if (tiles.size() >= 8) {
4039 // Draw 2x4 corner pattern (column-major)
4040 int tid = 0;
4041 for (int xx = 0; xx < 2; xx++) {
4042 for (int yy = 0; yy < 4; yy++) {
4043 WriteTile8(bg, obj.x_ + xx, obj.y_ + yy, tiles[tid++]);
4044 }
4045 }
4046 } else if (tiles.size() >= 4) {
4047 // Fallback: 2x2 pattern
4048 DrawWaterFace(obj, bg, tiles);
4049 }
4050}
4051
4053 const RoomObject& obj, gfx::BackgroundBuffer& bg,
4054 std::span<const gfx::TileInfo> tiles,
4055 [[maybe_unused]] const DungeonState* state) {
4056 // Pattern: Weird Corner Bottom (objects 0x110-0x113 for Type 2)
4057 // ASM: RoomDraw_WeirdCornerBottom_BothBG sets count=3, uses 4x4Corner pattern
4058 // Pattern: 3 columns × 4 rows = 12 tiles, column-major order
4059 if (tiles.size() >= 16) {
4060 DrawCorner4x4(obj, bg, tiles);
4061 } else if (tiles.size() >= 12) {
4062 // Draw 3x4 corner pattern (column-major, 3 columns × 4 rows)
4063 int tid = 0;
4064 for (int xx = 0; xx < 3; xx++) {
4065 for (int yy = 0; yy < 4; yy++) {
4066 WriteTile8(bg, obj.x_ + xx, obj.y_ + yy, tiles[tid++]);
4067 }
4068 }
4069 } else if (tiles.size() >= 8) {
4070 // Fallback: 2x4 column-major pattern
4071 int tid = 0;
4072 for (int xx = 0; xx < 2; xx++) {
4073 for (int yy = 0; yy < 4; yy++) {
4074 WriteTile8(bg, obj.x_ + xx, obj.y_ + yy, tiles[tid++]);
4075 }
4076 }
4077 } else if (tiles.size() >= 4) {
4078 DrawWaterFace(obj, bg, tiles);
4079 }
4080}
4081
4083 const RoomObject& obj, gfx::BackgroundBuffer& bg,
4084 std::span<const gfx::TileInfo> tiles,
4085 [[maybe_unused]] const DungeonState* state) {
4086 // Pattern: Weird Corner Top (objects 0x114-0x117 for Type 2)
4087 // ASM: RoomDraw_WeirdCornerTop_BothBG draws 4 columns of 3 tiles each
4088 // Pattern: 4 columns × 3 rows = 12 tiles, column-major order
4089 if (tiles.size() >= 16) {
4090 DrawCorner4x4(obj, bg, tiles);
4091 } else if (tiles.size() >= 12) {
4092 // Draw 4x3 corner pattern (column-major, 4 columns × 3 rows)
4093 int tid = 0;
4094 for (int xx = 0; xx < 4; xx++) {
4095 for (int yy = 0; yy < 3; yy++) {
4096 WriteTile8(bg, obj.x_ + xx, obj.y_ + yy, tiles[tid++]);
4097 }
4098 }
4099 } else if (tiles.size() >= 8) {
4100 // Fallback: 2x4 column-major pattern
4101 int tid = 0;
4102 for (int xx = 0; xx < 2; xx++) {
4103 for (int yy = 0; yy < 4; yy++) {
4104 WriteTile8(bg, obj.x_ + xx, obj.y_ + yy, tiles[tid++]);
4105 }
4106 }
4107 } else if (tiles.size() >= 4) {
4108 DrawWaterFace(obj, bg, tiles);
4109 }
4110}
4111
4114 std::span<const gfx::TileInfo> tiles,
4115 int width, int height) {
4116 // Generic large object drawer
4117 if (tiles.size() >= static_cast<size_t>(width * height)) {
4118 for (int y = 0; y < height; ++y) {
4119 for (int x = 0; x < width; ++x) {
4120 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[y * width + x]);
4121 }
4122 }
4123 }
4124}
4125
4126// ============================================================================
4127// Type 2 Special Object Routines
4128// ============================================================================
4129
4131 std::span<const gfx::TileInfo> tiles,
4132 [[maybe_unused]] const DungeonState* state) {
4133 // Pattern: 4 wide x 5 tall (20 tiles) in column-major order
4134 // Objects: 0x122, 0x128
4135 constexpr int kWidth = 4;
4136 constexpr int kHeight = 5;
4137
4138 if (tiles.size() >= kWidth * kHeight) {
4139 // Column-major layout (tiles go down first, then across)
4140 int tid = 0;
4141 for (int x = 0; x < kWidth && tid < static_cast<int>(tiles.size()); ++x) {
4142 for (int y = 0; y < kHeight && tid < static_cast<int>(tiles.size());
4143 ++y) {
4144 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[tid++]);
4145 }
4146 }
4147 } else {
4148 // Fallback: use 4x4 pattern
4149 DrawRightwards4x4_1to16(obj, bg, tiles);
4150 }
4151}
4152
4154 const RoomObject& obj, gfx::BackgroundBuffer& bg,
4155 std::span<const gfx::TileInfo> tiles,
4156 [[maybe_unused]] const DungeonState* state) {
4157 // Pattern: 3 wide x 6 tall (18 tiles) in column-major order
4158 // Objects: 0x12C, 0x236, 0x237
4159 constexpr int kWidth = 3;
4160 constexpr int kHeight = 6;
4161
4162 if (tiles.size() >= kWidth * kHeight) {
4163 int tid = 0;
4164 for (int x = 0; x < kWidth && tid < static_cast<int>(tiles.size()); ++x) {
4165 for (int y = 0; y < kHeight && tid < static_cast<int>(tiles.size());
4166 ++y) {
4167 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[tid++]);
4168 }
4169 }
4170 } else {
4171 // Fallback: use available tiles in 4x4 pattern
4172 DrawRightwards4x4_1to16(obj, bg, tiles);
4173 }
4174}
4175
4178 std::span<const gfx::TileInfo> tiles,
4179 [[maybe_unused]] const DungeonState* state) {
4180 // Pattern: 6 wide x 3 tall (18 tiles) in row-major order
4181 // Objects: 0x13E, 0x24D, 0x25D
4182 constexpr int kWidth = 6;
4183 constexpr int kHeight = 3;
4184
4185 if (tiles.size() >= kWidth * kHeight) {
4186 int tid = 0;
4187 for (int y = 0; y < kHeight && tid < static_cast<int>(tiles.size()); ++y) {
4188 for (int x = 0; x < kWidth && tid < static_cast<int>(tiles.size()); ++x) {
4189 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[tid++]);
4190 }
4191 }
4192 } else {
4193 // Fallback: use 4x3 pattern
4195 }
4196}
4197
4200 std::span<const gfx::TileInfo> tiles,
4201 [[maybe_unused]] const DungeonState* state) {
4202 // Pattern: 3 wide x 5 tall (15 tiles) in column-major order
4203 // Objects: 0x255, 0x25B
4204 constexpr int kWidth = 3;
4205 constexpr int kHeight = 5;
4206
4207 if (tiles.size() >= kWidth * kHeight) {
4208 int tid = 0;
4209 for (int x = 0; x < kWidth && tid < static_cast<int>(tiles.size()); ++x) {
4210 for (int y = 0; y < kHeight && tid < static_cast<int>(tiles.size());
4211 ++y) {
4212 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[tid++]);
4213 }
4214 }
4215 } else {
4216 // Fallback: use 4x4 pattern
4217 DrawRightwards4x4_1to16(obj, bg, tiles);
4218 }
4219}
4220
4221// ============================================================================
4222// Type 3 Special Object Routines
4223// ============================================================================
4224
4226 const RoomObject& obj, gfx::BackgroundBuffer& bg,
4227 std::span<const gfx::TileInfo> tiles,
4228 [[maybe_unused]] const DungeonState* state) {
4229 // Pattern: Vertical pipe - 2 wide x 6 tall (12 tiles) in column-major order
4230 // Objects: 0xFBA, 0xFBB (ASM 23A, 23B)
4231 constexpr int kWidth = 2;
4232 constexpr int kHeight = 6;
4233
4234 if (tiles.empty())
4235 return;
4236
4237 int tid = 0;
4238 int num_tiles = static_cast<int>(tiles.size());
4239 for (int x = 0; x < kWidth; ++x) {
4240 for (int y = 0; y < kHeight; ++y) {
4241 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[tid % num_tiles]);
4242 tid++;
4243 }
4244 }
4245}
4246
4248 const RoomObject& obj, gfx::BackgroundBuffer& bg,
4249 std::span<const gfx::TileInfo> tiles,
4250 [[maybe_unused]] const DungeonState* state) {
4251 // Pattern: Horizontal pipe - 6 wide x 2 tall (12 tiles) in row-major order
4252 // Objects: 0xFBC, 0xFBD, 0xFDC (ASM 23C, 23D, 25C)
4253 constexpr int kWidth = 6;
4254 constexpr int kHeight = 2;
4255
4256 if (tiles.empty())
4257 return;
4258
4259 int tid = 0;
4260 int num_tiles = static_cast<int>(tiles.size());
4261 for (int y = 0; y < kHeight; ++y) {
4262 for (int x = 0; x < kWidth; ++x) {
4263 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[tid % num_tiles]);
4264 tid++;
4265 }
4266 }
4267}
4268
4271 std::span<const gfx::TileInfo> tiles,
4272 [[maybe_unused]] const DungeonState* state) {
4273 // Pattern: Light beam on floor - 4x4 grid
4274 // Objects: 0xFF0 (ASM 270)
4275 // Standard 4x4 pattern, but tiles may have special transparency
4276 constexpr int kWidth = 4;
4277 constexpr int kHeight = 4;
4278
4279 if (tiles.size() >= kWidth * kHeight) {
4280 int tid = 0;
4281 for (int y = 0; y < kHeight && tid < static_cast<int>(tiles.size()); ++y) {
4282 for (int x = 0; x < kWidth && tid < static_cast<int>(tiles.size()); ++x) {
4283 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[tid++]);
4284 }
4285 }
4286 } else {
4287 // Fallback
4288 DrawRightwards4x4_1to16(obj, bg, tiles);
4289 }
4290}
4291
4293 const RoomObject& obj, gfx::BackgroundBuffer& bg,
4294 std::span<const gfx::TileInfo> tiles,
4295 [[maybe_unused]] const DungeonState* state) {
4296 // Pattern: Big light beam - 6x6 grid
4297 // Objects: 0xFF1 (ASM 271)
4298 constexpr int kWidth = 6;
4299 constexpr int kHeight = 6;
4300
4301 if (tiles.size() >= kWidth * kHeight) {
4302 int tid = 0;
4303 for (int y = 0; y < kHeight && tid < static_cast<int>(tiles.size()); ++y) {
4304 for (int x = 0; x < kWidth && tid < static_cast<int>(tiles.size()); ++x) {
4305 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[tid++]);
4306 }
4307 }
4308 } else {
4309 // Fallback: use 4x4 pattern
4310 DrawRightwards4x4_1to16(obj, bg, tiles);
4311 }
4312}
4313
4315 const RoomObject& obj, gfx::BackgroundBuffer& bg,
4316 std::span<const gfx::TileInfo> tiles,
4317 [[maybe_unused]] const DungeonState* state) {
4318 // Pattern: Boss shell (Trinexx, Vitreous, Kholdstare) - 4x4 grid
4319 // Objects: 0xFF2 (Trinexx 272), 0xF95 (Kholdstare 215), 0xFFB (Vitreous 27B)
4320 // Uses standard 4x4 pattern but may have state-dependent tile selection
4321 DrawRightwards4x4_1to16(obj, bg, tiles);
4322}
4323
4325 const RoomObject& obj, gfx::BackgroundBuffer& bg,
4326 std::span<const gfx::TileInfo> tiles,
4327 [[maybe_unused]] const DungeonState* state) {
4328 // Pattern: Solid wall decoration - 3 wide x 4 tall (12 tiles) in column-major order
4329 // Objects: 0xFE9-0xFEA, 0xFEE-0xFEF (ASM 269-26A, 26E-26F)
4330 constexpr int kWidth = 3;
4331 constexpr int kHeight = 4;
4332
4333 if (tiles.size() >= kWidth * kHeight) {
4334 int tid = 0;
4335 for (int x = 0; x < kWidth && tid < static_cast<int>(tiles.size()); ++x) {
4336 for (int y = 0; y < kHeight && tid < static_cast<int>(tiles.size());
4337 ++y) {
4338 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[tid++]);
4339 }
4340 }
4341 } else {
4342 // Fallback: use 4x4 pattern
4343 DrawRightwards4x4_1to16(obj, bg, tiles);
4344 }
4345}
4346
4348 const RoomObject& obj, gfx::BackgroundBuffer& bg,
4349 std::span<const gfx::TileInfo> tiles,
4350 [[maybe_unused]] const DungeonState* state) {
4351 // Pattern: Archery Game Target Door - two 3x3 sections stacked vertically
4352 // ASM: RoomDraw_ArcheryGameTargetDoor at $01A7A3:
4353 // 1. LDA #$0003, JSR RoomDraw_1x3N_rightwards -> 3 columns of 3 tiles (9 tiles)
4354 // 2. Y += 0x180 (0x180 / 0x80 = 3 rows down)
4355 // 3. LDA #$0003, JMP RoomDraw_1x3N_rightwards -> another 3 columns of 3 tiles
4356 //
4357 // RoomDraw_1x3N_rightwards draws COLUMN-MAJOR: each column is 3 tiles tall
4358 // Tile order: col0[row0,row1,row2], col1[row0,row1,row2], col2[row0,row1,row2]
4359 // Objects: 0xFE0-0xFE1 (ASM 260-261)
4360 // Total: 3 wide x 6 tall (18 tiles) in column-major order per section
4361 constexpr int kWidth = 3;
4362 constexpr int kSectionHeight = 3;
4363
4364 if (tiles.size() >= 18) {
4365 int tid = 0;
4366 // Section 1: Top 3x3 in COLUMN-MAJOR order
4367 for (int x = 0; x < kWidth; ++x) {
4368 for (int y = 0; y < kSectionHeight; ++y) {
4369 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[tid++]);
4370 }
4371 }
4372
4373 // Section 2: Bottom 3x3 in COLUMN-MAJOR order (continues tile data)
4374 for (int x = 0; x < kWidth; ++x) {
4375 for (int y = 0; y < kSectionHeight; ++y) {
4376 WriteTile8(bg, obj.x_ + x, obj.y_ + kSectionHeight + y, tiles[tid++]);
4377 }
4378 }
4379 } else if (tiles.size() >= 9) {
4380 // Draw just the first section if not enough tiles
4381 int tid = 0;
4382 for (int x = 0; x < kWidth; ++x) {
4383 for (int y = 0; y < kSectionHeight; ++y) {
4384 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[tid++]);
4385 }
4386 }
4387 } else {
4388 // Fallback: use 4x4 pattern
4389 DrawRightwards4x4_1to16(obj, bg, tiles);
4390 }
4391}
4392
4395 std::span<const gfx::TileInfo> tiles,
4396 [[maybe_unused]] const DungeonState* state) {
4397 // Pattern: Single 2x2 block (NO repetition based on size)
4398 // Used for pots, statues, and other single-instance 2x2 objects
4399 // ASM: RoomDraw_Rightwards2x2 at $019895 draws 4 tiles once
4400 // Tile order is COLUMN-MAJOR: [col0_row0, col0_row1, col1_row0, col1_row1]
4401 if (tiles.size() >= 4) {
4402 WriteTile8(bg, obj.x_, obj.y_, tiles[0]); // col 0, row 0
4403 WriteTile8(bg, obj.x_, obj.y_ + 1, tiles[1]); // col 0, row 1
4404 WriteTile8(bg, obj.x_ + 1, obj.y_, tiles[2]); // col 1, row 0
4405 WriteTile8(bg, obj.x_ + 1, obj.y_ + 1, tiles[3]); // col 1, row 1
4406 }
4407}
4408
4411 std::span<const gfx::TileInfo> tiles,
4412 [[maybe_unused]] const DungeonState* state) {
4413 // Pattern: Single 4x4 block (NO repetition based on size)
4414 // Used for: 0xFEB (large decor) and other single 4x4 objects
4415 // Tile order is COLUMN-MAJOR: tiles advance down each column, then right
4416 if (tiles.size() < 16)
4417 return;
4418
4419 int tid = 0;
4420 for (int x = 0; x < 4; ++x) {
4421 for (int y = 0; y < 4; ++y) {
4422 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[tid++]);
4423 }
4424 }
4425}
4426
4429 std::span<const gfx::TileInfo> tiles,
4430 [[maybe_unused]] const DungeonState* state) {
4431 // Pattern: Single 4x3 block (NO repetition based on size)
4432 // ASM: RoomDraw_TableRock4x3 at $0199E6 - draws 4 columns of 3 rows (12 tiles)
4433 // Calls RoomDraw_1x3N_rightwards with A=4, which draws 4 columns of 3 rows
4434 // Tile order is COLUMN-MAJOR: tiles advance down each column, then right
4435 if (tiles.size() >= 12) {
4436 int tid = 0;
4437 for (int x = 0; x < 4; ++x) {
4438 for (int y = 0; y < 3; ++y) {
4439 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[tid++]);
4440 }
4441 }
4442 }
4443}
4444
4447 std::span<const gfx::TileInfo> tiles,
4448 [[maybe_unused]] const DungeonState* state) {
4449 // ASM: RoomDraw_RupeeFloor at $019AA9
4450 // Draws 3 columns of tile pairs, each pair drawn at 3 Y positions:
4451 // Row 0-1: First pair
4452 // Row 3-4: Second pair (row 2 is skipped)
4453 // Row 6-7: Third pair (row 5 is skipped)
4454 // Uses 2 tiles per column (tile 0 and tile 1), drawn 3 times each
4455 // Total visual: 6 tiles wide x 8 tiles tall (with gaps at rows 2 and 5)
4456
4457 // Note: Original ASM checks room flags to hide if rupees collected
4458 // For editor preview, we always draw the pattern
4459
4460 if (tiles.size() < 2)
4461 return;
4462
4463 // Draw 3 columns (INX x4 = 2 tiles per iteration, 3 iterations)
4464 for (int col = 0; col < 3; ++col) {
4465 int x = obj.x_ + (col * 2); // 2 tiles per column (left and right of pair)
4466
4467 // Draw the tile pair at 3 Y positions
4468 // Rows 0-1
4469 WriteTile8(bg, x, obj.y_, tiles[0]);
4470 WriteTile8(bg, x + 1, obj.y_, tiles[0]);
4471 WriteTile8(bg, x, obj.y_ + 1, tiles[1]);
4472 WriteTile8(bg, x + 1, obj.y_ + 1, tiles[1]);
4473
4474 // Rows 3-4 (skip row 2)
4475 WriteTile8(bg, x, obj.y_ + 3, tiles[0]);
4476 WriteTile8(bg, x + 1, obj.y_ + 3, tiles[0]);
4477 WriteTile8(bg, x, obj.y_ + 4, tiles[1]);
4478 WriteTile8(bg, x + 1, obj.y_ + 4, tiles[1]);
4479
4480 // Rows 6-7 (skip row 5)
4481 WriteTile8(bg, x, obj.y_ + 6, tiles[0]);
4482 WriteTile8(bg, x + 1, obj.y_ + 6, tiles[0]);
4483 WriteTile8(bg, x, obj.y_ + 7, tiles[1]);
4484 WriteTile8(bg, x + 1, obj.y_ + 7, tiles[1]);
4485 }
4486}
4487
4490 std::span<const gfx::TileInfo> tiles,
4491 [[maybe_unused]] const DungeonState* state) {
4492 // ASM: RoomDraw_4x4 at $0197ED - draws exactly 4 columns x 4 rows = 16 tiles
4493 // This is an actual 4x4 tile8 pattern (32x32 pixels), NO repetition
4494 // Used for: 0xFE6 (pit) and other single 4x4 objects
4495 // Tile order is COLUMN-MAJOR: tiles advance down each column, then right
4496
4497 if (tiles.size() < 16)
4498 return;
4499
4500 int tid = 0;
4501 for (int x = 0; x < 4; ++x) {
4502 for (int y = 0; y < 4; ++y) {
4503 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[tid++]);
4504 }
4505 }
4506}
4507
4509 const RoomObject& obj, gfx::BackgroundBuffer& bg,
4510 std::span<const gfx::TileInfo> tiles,
4511 [[maybe_unused]] const DungeonState* state) {
4512 // Pattern: Ganon's Triforce Floor Decoration - THREE 4x4 blocks forming Triforce
4513 // ASM: RoomDraw_GanonTriforceFloorDecor at $01A7F0:
4514 // 1. JSR RoomDraw_4x4 -> Block 1 at Y, X advances to tiles 16
4515 // 2. PHX -> Save X (now at tiles 16)
4516 // 3. Y += 0x1FC, JSR RoomDraw_4x4 -> Block 2 with tiles 16-31
4517 // 4. PLX -> Restore X back to tiles 16
4518 // 5. Y += 0x204, JMP RoomDraw_4x4 -> Block 3 with tiles 16-31 (SAME as block 2!)
4519 //
4520 // VRAM layout: 0x80 bytes per row (64 tiles @ 2 bytes each = 128 bytes)
4521 // 0x1FC = 508 -> ~4 rows down, -2 cols (124 mod 128 = wraps left)
4522 // 0x204 = 516 -> ~4 rows down, +2 cols
4523 //
4524 // Visual pattern (Triforce):
4525 // [Block 1] <- Top center (tiles 0-15)
4526 // [Block 2][Block 3] <- Bottom row (BOTH use tiles 16-31!)
4527 constexpr int kBlockSize = 4;
4528
4529 if (tiles.size() >= 32) {
4530 // Block 1: Top center (tiles 0-15)
4531 int tid = 0;
4532 for (int y = 0; y < kBlockSize; ++y) {
4533 for (int x = 0; x < kBlockSize; ++x) {
4534 WriteTile8(bg, obj.x_ + x + 2, obj.y_ + y, tiles[tid++]);
4535 }
4536 }
4537
4538 // Block 2: Bottom-left (tiles 16-31)
4539 tid = 16;
4540 for (int y = 0; y < kBlockSize; ++y) {
4541 for (int x = 0; x < kBlockSize; ++x) {
4542 WriteTile8(bg, obj.x_ + x, obj.y_ + kBlockSize + y, tiles[tid++]);
4543 }
4544 }
4545
4546 // Block 3: Bottom-right (SAME tiles 16-31, per PLX restoring X)
4547 tid = 16;
4548 for (int y = 0; y < kBlockSize; ++y) {
4549 for (int x = 0; x < kBlockSize; ++x) {
4550 WriteTile8(bg, obj.x_ + x + 4, obj.y_ + kBlockSize + y, tiles[tid++]);
4551 }
4552 }
4553 } else if (tiles.size() >= 16) {
4554 // Draw just one 4x4 block if not enough tiles
4555 int tid = 0;
4556 for (int y = 0; y < kBlockSize; ++y) {
4557 for (int x = 0; x < kBlockSize; ++x) {
4558 WriteTile8(bg, obj.x_ + x, obj.y_ + y, tiles[tid++]);
4559 }
4560 }
4561 } else {
4562 // Fallback
4563 DrawRightwards4x4_1to16(obj, bg, tiles);
4564 }
4565}
4566
4567} // namespace zelda3
4568} // namespace yaze
4569
4571 const RoomObject& object) {
4572 if (!routines_initialized_) {
4574 }
4575
4576 // Default size 16x16 (2x2 tiles)
4577 int width = 16;
4578 int height = 16;
4579
4580 int routine_id = GetDrawRoutineId(object.id_);
4581 int size = object.size_;
4582
4583 // Based on routine ID, calculate dimensions
4584 // This logic must match the draw routines
4585 switch (routine_id) {
4586 case 0: // DrawRightwards2x2_1to15or32
4587 case 4: // DrawRightwards2x2_1to16
4588 case 7: // DrawDownwards2x2_1to15or32
4589 case 11: // DrawDownwards2x2_1to16
4590 // 2x2 tiles repeated
4591 if (routine_id == 0 || routine_id == 7) {
4592 if (size == 0)
4593 size = 32;
4594 } else {
4595 size = size & 0x0F;
4596 if (size == 0)
4597 size = 16; // 0 usually means 16 for 1to16 routines
4598 }
4599
4600 if (routine_id == 0 || routine_id == 4) {
4601 // Rightwards: size * 2 tiles width, 2 tiles height
4602 width = size * 16;
4603 height = 16;
4604 } else {
4605 // Downwards: 2 tiles width, size * 2 tiles height
4606 width = 16;
4607 height = size * 16;
4608 }
4609 break;
4610
4611 case 1: // RoomDraw_Rightwards2x4_1to15or26 (layout walls 0x01-0x02)
4612 {
4613 // ASM: GetSize_1to15or26 - defaults to 26 when size is 0
4614 int effective_size = (size == 0) ? 26 : (size & 0x0F);
4615 // Draws 2x4 tiles repeated 'effective_size' times horizontally
4616 width = effective_size * 16; // 2 tiles wide per repetition
4617 height = 32; // 4 tiles tall
4618 break;
4619 }
4620 case 2: // RoomDraw_Rightwards2x4spaced4_1to16 (objects 0x03-0x04)
4621 case 3: // RoomDraw_Rightwards2x4spaced4_1to16_BothBG (objects 0x05-0x06)
4622 {
4623 // ASM: GetSize_1to16, draws 2x4 tiles with 4-tile adjacent spacing
4624 size = size & 0x0F;
4625 int count = size + 1;
4626 width = count * 16; // 2 tiles wide per repetition (adjacent)
4627 height = 32; // 4 tiles tall
4628 break;
4629 }
4630
4631 case 5: // DrawDiagonalAcute_1to16
4632 case 6: // DrawDiagonalGrave_1to16
4633 {
4634 // ASM: RoomDraw_DiagonalAcute/Grave_1to16
4635 // Uses LDA #$0007; JSR RoomDraw_GetSize_1to16_timesA
4636 // count = size + 7
4637 // Each iteration draws 5 tiles vertically (RoomDraw_2x2and1 pattern)
4638 // Width = count tiles, Height = 5 tiles base + (count-1) diagonal offset
4639 size = size & 0x0F;
4640 int count = size + 7;
4641 width = count * 8;
4642 height = (count + 4) * 8; // 5 tiles + (count-1) = count + 4
4643 break;
4644 }
4645 case 17: // DrawDiagonalAcute_1to16_BothBG
4646 case 18: // DrawDiagonalGrave_1to16_BothBG
4647 {
4648 // ASM: RoomDraw_DiagonalAcute/Grave_1to16_BothBG
4649 // Uses LDA #$0006; JSR RoomDraw_GetSize_1to16_timesA
4650 // count = size + 6 (one less than non-BothBG)
4651 size = size & 0x0F;
4652 int count = size + 6;
4653 width = count * 8;
4654 height = (count + 4) * 8; // 5 tiles + (count-1) = count + 4
4655 break;
4656 }
4657
4658 case 8: // RoomDraw_Downwards4x2_1to15or26 (layout walls 0x61-0x62)
4659 {
4660 // ASM: GetSize_1to15or26 - defaults to 26 when size is 0
4661 int effective_size = (size == 0) ? 26 : (size & 0x0F);
4662 // Draws 4x2 tiles repeated 'effective_size' times vertically
4663 width = 32; // 4 tiles wide
4664 height = effective_size * 16; // 2 tiles tall per repetition
4665 break;
4666 }
4667 case 9: // RoomDraw_Downwards4x2_1to16_BothBG (objects 0x63-0x64)
4668 case 10: // RoomDraw_DownwardsDecor4x2spaced4_1to16 (objects 0x65-0x66)
4669 {
4670 // ASM: GetSize_1to16, draws 4x2 tiles with spacing
4671 size = size & 0x0F;
4672 int count = size + 1;
4673 width = 32; // 4 tiles wide
4674 height = count * 16; // 2 tiles tall per repetition (adjacent)
4675 break;
4676 }
4677
4678 case 12: // RoomDraw_DownwardsHasEdge1x1_1to16_plus3
4679 size = size & 0x0F;
4680 width = 8;
4681 height = (size + 3) * 8;
4682 break;
4683 case 13: // RoomDraw_DownwardsEdge1x1_1to16
4684 size = size & 0x0F;
4685 width = 8;
4686 height = (size + 1) * 8;
4687 break;
4688 case 14: // RoomDraw_DownwardsLeftCorners2x1_1to16_plus12
4689 case 15: // RoomDraw_DownwardsRightCorners2x1_1to16_plus12
4690 size = size & 0x0F;
4691 width = 16;
4692 height = (size + 10) * 8;
4693 break;
4694
4695 case 16: // DrawRightwards4x4_1to16 (Routine 16)
4696 {
4697 // 4x4 block repeated horizontally based on size
4698 // ASM: GetSize_1to16, count = (size & 0x0F) + 1
4699 int count = (size & 0x0F) + 1;
4700 width = 32 * count; // 4 tiles * 8 pixels * count
4701 height = 32; // 4 tiles * 8 pixels
4702 break;
4703 }
4704 case 19: // DrawCorner4x4 (Type 2 corners 0x100-0x103)
4705 case 34: // Water Face (4x4)
4706 case 35: // 4x4 Corner BothBG
4707 case 36: // Weird Corner Bottom
4708 case 37: // Weird Corner Top
4709 // 4x4 tiles (32x32 pixels) - fixed size, no repetition
4710 width = 32;
4711 height = 32;
4712 break;
4713 case 39: { // Chest routine (small or big)
4714 // Infer size from tile span: big chests provide >=16 tiles
4715 int tile_count = object.tiles().size();
4716 if (tile_count >= 16) {
4717 width = height = 32; // Big chest 4x4
4718 } else {
4719 width = height = 16; // Small chest 2x2
4720 }
4721 break;
4722 }
4723
4724 case 20: // Edge 1x2 (RoomDraw_Rightwards1x2_1to16_plus2)
4725 {
4726 // ZScream: width = size * 2 + 4, height = 3 tiles
4727 size = size & 0x0F;
4728 width = (size * 2 + 4) * 8;
4729 height = 24;
4730 break;
4731 }
4732
4733 case 21: // RoomDraw_RightwardsHasEdge1x1_1to16_plus3 (small rails 0x22)
4734 {
4735 // ZScream: count = size + 2 (corner + middle*count + end)
4736 size = size & 0x0F;
4737 width = (size + 4) * 8;
4738 height = 8;
4739 break;
4740 }
4741 case 22: // RoomDraw_RightwardsHasEdge1x1_1to16_plus2 (carpet trim 0x23-0x2E)
4742 {
4743 // ASM: GetSize_1to16, count = size + 1
4744 // Plus corner (1) + end (1) = count + 2 total width
4745 size = size & 0x0F;
4746 int count = size + 1;
4747 width = (count + 2) * 8; // corner + middle*count + end
4748 height = 8;
4749 break;
4750 }
4751 case 118: // RoomDraw_RightwardsHasEdge1x1_1to16_plus23 (long rails 0x5F)
4752 {
4753 size = size & 0x0F;
4754 width = (size + 23) * 8;
4755 height = 8;
4756 break;
4757 }
4758 case 25: // RoomDraw_Rightwards1x1Solid_1to16_plus3
4759 {
4760 // ASM: GetSize_1to16_timesA(4), so count = size + 4
4761 size = size & 0x0F;
4762 width = (size + 4) * 8;
4763 height = 8;
4764 break;
4765 }
4766
4767 case 23: // RightwardsTopCorners1x2_1to16_plus13
4768 case 24: // RightwardsBottomCorners1x2_1to16_plus13
4769 size = size & 0x0F;
4770 width = 8 + size * 8;
4771 height = 16;
4772 break;
4773
4774 case 26: // Door Switcher
4775 width = 32;
4776 height = 32;
4777 break;
4778
4779 case 27: // RoomDraw_RightwardsDecor4x4spaced2_1to16
4780 {
4781 // 4x4 tiles with 6-tile X spacing per repetition
4782 // ASM: s * 6 spacing, count = size + 1
4783 size = size & 0x0F;
4784 int count = size + 1;
4785 // Total width = (count - 1) * 6 (spacing) + 4 (last block)
4786 width = ((count - 1) * 6 + 4) * 8;
4787 height = 32; // 4 tiles
4788 break;
4789 }
4790
4791 case 28: // RoomDraw_RightwardsStatue2x3spaced2_1to16
4792 {
4793 // 2x3 tiles with 4-tile X spacing per repetition
4794 // ASM: s * 4 spacing, count = size + 1
4795 size = size & 0x0F;
4796 int count = size + 1;
4797 // Total width = (count - 1) * 4 (spacing) + 2 (last block)
4798 width = ((count - 1) * 4 + 2) * 8;
4799 height = 24; // 3 tiles
4800 break;
4801 }
4802
4803 case 29: // RoomDraw_RightwardsPillar2x4spaced4_1to16
4804 {
4805 // 2x4 tiles with 4-tile X spacing per repetition
4806 // ASM: ADC #$0008 = 4 tiles between starts
4807 size = size & 0x0F;
4808 int count = size + 1;
4809 // Total width = (count - 1) * 4 (spacing) + 2 (last block)
4810 width = ((count - 1) * 4 + 2) * 8;
4811 height = 32; // 4 tiles
4812 break;
4813 }
4814
4815 case 30: // RoomDraw_RightwardsDecor4x3spaced4_1to16
4816 {
4817 // 4x3 tiles with 8-tile X spacing per repetition
4818 // ASM: ADC #$0008 = 8-byte advance = 4 tiles gap between 4-tile objects
4819 size = size & 0x0F;
4820 int count = size + 1;
4821 // Total width = (count - 1) * 8 (spacing) + 4 (last block)
4822 width = ((count - 1) * 8 + 4) * 8;
4823 height = 24; // 3 tiles
4824 break;
4825 }
4826
4827 case 31: // RoomDraw_RightwardsDoubled2x2spaced2_1to16
4828 {
4829 // 4x2 tiles (doubled 2x2) with 6-tile X spacing
4830 // ASM: s * 6 spacing, count = size + 1
4831 size = size & 0x0F;
4832 int count = size + 1;
4833 // Total width = (count - 1) * 6 (spacing) + 4 (last block)
4834 width = ((count - 1) * 6 + 4) * 8;
4835 height = 16; // 2 tiles
4836 break;
4837 }
4838 case 32: // RoomDraw_RightwardsDecor2x2spaced12_1to16
4839 {
4840 // 2x2 tiles with 14-tile X spacing per repetition
4841 // ASM: s * 14 spacing, count = size + 1
4842 size = size & 0x0F;
4843 int count = size + 1;
4844 // Total width = (count - 1) * 14 (spacing) + 2 (last block)
4845 width = ((count - 1) * 14 + 2) * 8;
4846 height = 16; // 2 tiles
4847 break;
4848 }
4849
4850 case 33: // Somaria Line
4851 // Variable length, estimate from size
4852 size = size & 0x0F;
4853 width = 8 + size * 8;
4854 height = 8;
4855 break;
4856
4857 case 38: // Nothing (RoomDraw_Nothing)
4858 width = 8;
4859 height = 8;
4860 break;
4861
4862 case 40: // Rightwards 4x2 (FloorTile)
4863 {
4864 // 4 cols x 2 rows, GetSize_1to16
4865 size = size & 0x0F;
4866 int count = size + 1;
4867 width = count * 4 * 8; // 4 tiles per repetition
4868 height = 16; // 2 tiles
4869 break;
4870 }
4871
4872 case 41: // Rightwards Decor 1x8 spaced 12 (wall torches 0x55-0x56)
4873 {
4874 // ASM: 1 column x 8 rows with 12-tile horizontal spacing
4875 size = size & 0x0F;
4876 int count = size + 1;
4877 width = ((count - 1) * 12 + 1) * 8; // 1 tile wide per block
4878 height = 64; // 8 tiles tall
4879 break;
4880 }
4881
4882 case 42: // Rightwards Cannon Hole 4x3
4883 {
4884 // 4x3 tiles, GetSize_1to16
4885 size = size & 0x0F;
4886 int count = size + 1;
4887 width = count * 4 * 8;
4888 height = 24;
4889 break;
4890 }
4891
4892 case 43: // Downwards Floor 4x4
4893 {
4894 // 4x4 tiles, GetSize_1to16
4895 size = size & 0x0F;
4896 int count = size + 1;
4897 width = 32;
4898 height = count * 4 * 8;
4899 break;
4900 }
4901
4902 case 44: // Downwards 1x1 Solid +3
4903 {
4904 size = size & 0x0F;
4905 width = 8;
4906 height = (size + 4) * 8;
4907 break;
4908 }
4909
4910 case 45: // Downwards Decor 4x4 spaced 2
4911 {
4912 size = size & 0x0F;
4913 int count = size + 1;
4914 width = 32;
4915 height = ((count - 1) * 6 + 4) * 8;
4916 break;
4917 }
4918
4919 case 46: // Downwards Pillar 2x4 spaced 2
4920 {
4921 size = size & 0x0F;
4922 int count = size + 1;
4923 width = 16;
4924 height = ((count - 1) * 6 + 4) * 8;
4925 break;
4926 }
4927
4928 case 47: // Downwards Decor 3x4 spaced 4
4929 {
4930 size = size & 0x0F;
4931 int count = size + 1;
4932 width = 24;
4933 height = ((count - 1) * 6 + 4) * 8;
4934 break;
4935 }
4936
4937 case 48: // Downwards Decor 2x2 spaced 12
4938 {
4939 size = size & 0x0F;
4940 int count = size + 1;
4941 width = 16;
4942 height = ((count - 1) * 14 + 2) * 8;
4943 break;
4944 }
4945
4946 case 49: // Downwards Line 1x1 +1
4947 {
4948 size = size & 0x0F;
4949 width = 8;
4950 height = (size + 2) * 8;
4951 break;
4952 }
4953
4954 case 50: // Downwards Decor 2x4 spaced 8
4955 {
4956 size = size & 0x0F;
4957 int count = size + 1;
4958 width = 16;
4959 height = ((count - 1) * 12 + 4) * 8;
4960 break;
4961 }
4962
4963 case 51: // Rightwards Line 1x1 +1
4964 {
4965 size = size & 0x0F;
4966 width = (size + 2) * 8;
4967 height = 8;
4968 break;
4969 }
4970
4971 case 52: // Rightwards Bar 4x3
4972 {
4973 size = size & 0x0F;
4974 int count = size + 1;
4975 width = ((count - 1) * 6 + 4) * 8;
4976 height = 24;
4977 break;
4978 }
4979
4980 case 53: // Rightwards Shelf 4x4
4981 {
4982 size = size & 0x0F;
4983 int count = size + 1;
4984 width = ((count - 1) * 6 + 4) * 8;
4985 height = 32;
4986 break;
4987 }
4988
4989 case 54: // Rightwards Big Rail 1x3 +5
4990 {
4991 size = size & 0x0F;
4992 width = (size + 6) * 8;
4993 height = 24;
4994 break;
4995 }
4996
4997 case 55: // Rightwards Block 2x2 spaced 2
4998 {
4999 size = size & 0x0F;
5000 int count = size + 1;
5001 width = ((count - 1) * 4 + 2) * 8;
5002 height = 16;
5003 break;
5004 }
5005
5006 // Routines 56-64: SuperSquare patterns
5007 // ASM: Type1/Type3 objects pack 2-bit X/Y sizes into a 4-bit size:
5008 // size = (x_size << 2) | y_size, where x_size/y_size are 0..3 (meaning 1..4).
5009 // Each super square unit is 4 tiles (32 pixels) in each dimension.
5010 case 56: // 4x4BlocksIn4x4SuperSquare
5011 case 57: // 3x3FloorIn4x4SuperSquare
5012 case 58: // 4x4FloorIn4x4SuperSquare
5013 case 59: // 4x4FloorOneIn4x4SuperSquare
5014 case 60: // 4x4FloorTwoIn4x4SuperSquare
5015 case 62: // Spike2x2In4x4SuperSquare
5016 {
5017 int size_x = ((size >> 2) & 0x03) + 1;
5018 int size_y = (size & 0x03) + 1;
5019 width = size_x * 32; // 4 tiles per super square
5020 height = size_y * 32; // 4 tiles per super square
5021 break;
5022 }
5023 case 61: // BigHole4x4
5024 case 63: // TableRock4x4
5025 case 64: // WaterOverlay8x8
5026 width = 32;
5027 height = 32;
5028 break;
5029
5030 // Routines 65-74: Various downwards/rightwards patterns
5031 case 65: // DownwardsDecor3x4spaced2
5032 {
5033 size = size & 0x0F;
5034 int count = size + 1;
5035 width = 24;
5036 height = ((count - 1) * 5 + 4) * 8;
5037 break;
5038 }
5039
5040 case 66: // DownwardsBigRail3x1 +5
5041 {
5042 // Top cap (2x2) + Middle (2x1 x count) + Bottom cap (2x3)
5043 // Total: 2 tiles wide, 2 + (size+1) + 3 = size + 6 tiles tall
5044 size = size & 0x0F;
5045 width = 16; // 2 tiles wide
5046 height = (size + 6) * 8;
5047 break;
5048 }
5049
5050 case 67: // DownwardsBlock2x2spaced2
5051 {
5052 size = size & 0x0F;
5053 int count = size + 1;
5054 width = 16;
5055 height = ((count - 1) * 4 + 2) * 8;
5056 break;
5057 }
5058
5059 case 68: // DownwardsCannonHole3x6
5060 {
5061 size = size & 0x0F;
5062 int count = size + 1;
5063 width = 24;
5064 height = count * 6 * 8;
5065 break;
5066 }
5067
5068 case 69: // DownwardsBar2x3
5069 {
5070 size = size & 0x0F;
5071 int count = size + 1;
5072 width = 16;
5073 height = ((count - 1) * 3 + 3) * 8;
5074 break;
5075 }
5076
5077 case 70: // DownwardsPots2x2
5078 case 71: // DownwardsHammerPegs2x2
5079 {
5080 size = size & 0x0F;
5081 int count = size + 1;
5082 width = 16;
5083 height = count * 2 * 8;
5084 break;
5085 }
5086
5087 case 72: // RightwardsEdge1x1 +7
5088 {
5089 size = size & 0x0F;
5090 width = (size + 8) * 8;
5091 height = 8;
5092 break;
5093 }
5094
5095 case 73: // RightwardsPots2x2
5096 case 74: // RightwardsHammerPegs2x2
5097 {
5098 size = size & 0x0F;
5099 int count = size + 1;
5100 width = count * 2 * 8;
5101 height = 16;
5102 break;
5103 }
5104
5105 // Diagonal ceilings (75-78) - TRIANGLE shapes
5106 // Draw uses count = (size & 0x0F) + 4
5107 // Outline uses smaller size since triangle only fills half the square area
5108 case 75: // DiagonalCeilingTopLeft - triangle at origin
5109 case 76: // DiagonalCeilingBottomLeft - triangle at origin
5110 {
5111 // Smaller outline for triangle - use half the drawn area
5112 int count = (size & 0x0F) + 2;
5113 width = count * 8;
5114 height = count * 8;
5115 break;
5116 }
5117 case 77: // DiagonalCeilingTopRight - triangle shifts diagonally
5118 case 78: // DiagonalCeilingBottomRight - triangle shifts diagonally
5119 {
5120 // Smaller outline for diagonal triangles
5121 int count = (size & 0x0F) + 2;
5122 width = count * 8;
5123 height = count * 8;
5124 break;
5125 }
5126
5127 // Special platform routines (79-82)
5128 case 79: // ClosedChestPlatform
5129 case 80: // MovingWallWest
5130 case 81: // MovingWallEast
5131 case 82: // OpenChestPlatform
5132 width = 64; // 8 tiles wide
5133 height = 64; // 8 tiles tall
5134 break;
5135
5136 // Stair routines - different sizes for different types
5137
5138 // 4x4 stair patterns (32x32 pixels)
5139 case 83: // InterRoomFatStairsUp (0x12D)
5140 case 84: // InterRoomFatStairsDownA (0x12E)
5141 case 85: // InterRoomFatStairsDownB (0x12F)
5142 case 86: // AutoStairs (0x130-0x133)
5143 case 87: // StraightInterroomStairs (0xF9E-0xFA9)
5144 width = 32; // 4 tiles
5145 height = 32; // 4 tiles (4x4 pattern)
5146 break;
5147
5148 // 4x3 stair patterns (32x24 pixels)
5149 case 88: // SpiralStairsGoingUpUpper (0x138)
5150 case 89: // SpiralStairsGoingDownUpper (0x139)
5151 case 90: // SpiralStairsGoingUpLower (0x13A)
5152 case 91: // SpiralStairsGoingDownLower (0x13B)
5153 // ASM: RoomDraw_1x3N_rightwards with A=4 -> 4 columns x 3 rows
5154 width = 32; // 4 tiles
5155 height = 24; // 3 tiles
5156 break;
5157
5158 case 92: // BigKeyLock
5159 width = 16;
5160 height = 24;
5161 break;
5162
5163 case 93: // BombableFloor
5164 width = 32;
5165 height = 32;
5166 break;
5167
5168 case 94: // EmptyWaterFace
5169 width = 32;
5170 // Base empty-water variant is 4x3; stateful runtime branch can extend to
5171 // 4x5 when water is active.
5172 height = 24;
5173 break;
5174
5175 case 95: // SpittingWaterFace
5176 width = 32;
5177 height = 40;
5178 break;
5179
5180 case 96: // DrenchingWaterFace
5181 width = 32;
5182 height = 56;
5183 break;
5184
5185 case 97: // PrisonCell
5186 width = 32;
5187 height = 48;
5188 break;
5189
5190 case 98: // Bed4x5
5191 width = 32;
5192 height = 40;
5193 break;
5194
5195 case 99: // Rightwards3x6
5196 width = 24;
5197 height = 48;
5198 break;
5199
5200 case 100: // Utility6x3
5201 width = 48;
5202 height = 24;
5203 break;
5204
5205 case 101: // Utility3x5
5206 width = 24;
5207 height = 40;
5208 break;
5209
5210 case 102: // VerticalTurtleRockPipe
5211 width = 16;
5212 height = 48;
5213 break;
5214
5215 case 103: // HorizontalTurtleRockPipe
5216 width = 48;
5217 height = 16;
5218 break;
5219
5220 case 104: // LightBeam
5221 width = 16;
5222 height = 32;
5223 break;
5224
5225 case 105: // BigLightBeam
5226 width = 32;
5227 height = 64;
5228 break;
5229
5230 case 106: // BossShell4x4
5231 width = 32;
5232 height = 32;
5233 break;
5234
5235 case 107: // SolidWallDecor3x4
5236 width = 24;
5237 height = 32;
5238 break;
5239
5240 case 108: // ArcheryGameTargetDoor
5241 width = 24;
5242 height = 48;
5243 break;
5244
5245 case 109: // GanonTriforceFloorDecor
5246 width = 32;
5247 height = 64;
5248 break;
5249
5250 case 110: // Single2x2
5251 width = 16;
5252 height = 16;
5253 break;
5254
5255 case 111: // Waterfall47 (object 0x47)
5256 {
5257 // ASM: count = (size+1)*2, draws 1x5 columns
5258 // Width = first column + middle columns + last column = 2 + count tiles
5259 size = size & 0x0F;
5260 int count = (size + 1) * 2;
5261 width = (2 + count) * 8;
5262 height = 40; // 5 tiles
5263 break;
5264 }
5265 case 112: // Waterfall48 (object 0x48)
5266 {
5267 // ASM: count = (size+1)*2, draws 1x3 columns
5268 // Width = first column + middle columns + last column = 2 + count tiles
5269 size = size & 0x0F;
5270 int count = (size + 1) * 2;
5271 width = (2 + count) * 8;
5272 height = 24; // 3 tiles
5273 break;
5274 }
5275
5276 case 113: // Single4x4 (no repetition) - 4x4 TILE16 = 8x8 TILE8
5277 // 4 tile16 wide x 4 tile16 tall = 8 tile8 x 8 tile8 = 64x64 pixels
5278 width = 64;
5279 height = 64;
5280 break;
5281
5282 case 114: // Single4x3 (no repetition)
5283 // 4 tiles wide x 3 tiles tall = 32x24 pixels
5284 width = 32;
5285 height = 24;
5286 break;
5287
5288 case 115: // RupeeFloor (special pattern)
5289 // 6 tiles wide (3 columns x 2 tiles) x 8 tiles tall = 48x64 pixels
5290 width = 48;
5291 height = 64;
5292 break;
5293
5294 case 116: // Actual4x4 (true 4x4 tile8 pattern, no repetition)
5295 // 4 tile8s x 4 tile8s = 32x32 pixels
5296 width = 32;
5297 height = 32;
5298 break;
5299
5300 default:
5301 // Fallback to naive calculation if not handled
5302 // Matches DungeonCanvasViewer::DrawRoomObjects logic
5303 {
5304 int size_h = (object.size_ & 0x0F);
5305 int size_v = (object.size_ >> 4) & 0x0F;
5306 width = (size_h + 1) * 8;
5307 height = (size_v + 1) * 8;
5308 }
5309 break;
5310 }
5311
5312 return {width, height};
5313}
5314
5316 const RoomObject& obj, gfx::BackgroundBuffer& bg,
5317 [[maybe_unused]] std::span<const gfx::TileInfo> tiles,
5318 [[maybe_unused]] const DungeonState* state) {
5319 // CustomObjectManager should be initialized by DungeonEditorV2 with the
5320 // project's custom_objects_folder path before any objects are drawn
5321 auto& manager = CustomObjectManager::Get();
5322
5323 int subtype = obj.size_ & 0x1F;
5324 auto result = manager.GetObjectInternal(obj.id_, subtype);
5325 if (!result.ok()) {
5326 LOG_DEBUG("ObjectDrawer", "Custom object 0x%03X subtype %d not found: %s",
5327 obj.id_, subtype, result.status().message().data());
5328 return;
5329 }
5330
5331 auto custom_obj = result.value();
5332 if (!custom_obj || custom_obj->IsEmpty())
5333 return;
5334
5335 int tile_x = obj.x_;
5336 int tile_y = obj.y_;
5337
5338 for (const auto& entry : custom_obj->tiles) {
5339 // entry.tile_data is vhopppcc cccccccc (SNES tilemap word format)
5340 // Convert to TileInfo and render using WriteTile8 (not SetTileAt which
5341 // only stores to buffer without rendering)
5342 gfx::TileInfo tile_info = gfx::WordToTileInfo(entry.tile_data);
5343 WriteTile8(bg, tile_x + entry.rel_x, tile_y + entry.rel_y, tile_info);
5344 }
5345}
5346
5347void yaze::zelda3::ObjectDrawer::DrawPotItem(uint8_t item_id, int x, int y,
5349 // Draw a small colored indicator for pot items
5350 // Item types from ZELDA3_DUNGEON_SPEC.md Section 7.2
5351 // Uses palette indices that map to recognizable colors
5352
5353 if (item_id == 0)
5354 return; // Nothing - skip
5355
5356 auto& bitmap = bg.bitmap();
5357 auto& coverage_buffer = bg.mutable_coverage_data();
5358 if (!bitmap.is_active() || bitmap.width() == 0)
5359 return;
5360
5361 // Convert tile coordinates to pixel coordinates
5362 // Items are drawn offset from pot position (centered on pot)
5363 int pixel_x = (x * 8) + 2; // Offset 2 pixels into the pot tile
5364 int pixel_y = (y * 8) + 2;
5365
5366 // Choose color based on item category
5367 // Using palette indices that should be visible in dungeon palettes
5368 uint8_t color_idx;
5369 switch (item_id) {
5370 // Rupees (green/blue/red tones)
5371 case 1: // Green rupee
5372 case 7: // Blue rupee
5373 case 12: // Blue rupee variant
5374 color_idx = 30; // Greenish (palette 2, index 0)
5375 break;
5376
5377 // Hearts (red tones)
5378 case 6: // Heart
5379 case 11: // Heart
5380 case 13: // Heart variant
5381 // NOTE: Avoid palette indices 0/16/32/.. which are transparent in SNES
5382 // CGRAM rows. Using 5 gives a consistently visible indicator.
5383 color_idx = 5;
5384 break;
5385
5386 // Keys (yellow/gold)
5387 case 8: // Key*8
5388 case 19: // Key
5389 color_idx = 45; // Yellowish (palette 3)
5390 break;
5391
5392 // Bombs (dark/black)
5393 case 5: // Bomb
5394 case 10: // 1 bomb
5395 case 16: // Bomb refill
5396 color_idx = 60; // Darker color (palette 4)
5397 break;
5398
5399 // Arrows (brown/wood)
5400 case 9: // Arrow
5401 case 17: // Arrow refill
5402 color_idx = 15; // Brownish (palette 1)
5403 break;
5404
5405 // Magic (blue/purple)
5406 case 14: // Small magic
5407 case 15: // Big magic
5408 color_idx = 75; // Bluish (palette 5)
5409 break;
5410
5411 // Fairy (pink/light)
5412 case 18: // Fairy
5413 case 20: // Fairy*8
5414 color_idx = 5; // Pinkish
5415 break;
5416
5417 // Special/Traps (distinct colors)
5418 case 2: // Rock crab
5419 case 3: // Bee
5420 color_idx = 20; // Enemy indicator
5421 break;
5422
5423 case 23: // Hole
5424 case 24: // Warp
5425 case 25: // Staircase
5426 color_idx = 10; // Transport indicator
5427 break;
5428
5429 case 26: // Bombable
5430 case 27: // Switch
5431 color_idx = 35; // Interactive indicator
5432 break;
5433
5434 case 4: // Random
5435 default:
5436 color_idx = 50; // Default/random indicator
5437 break;
5438 }
5439
5440 // Safety: never use CGRAM transparent slots (0,16,32,...) for the indicator.
5441 // In the editor these would appear invisible or "missing" depending on
5442 // compositing.
5443 if (color_idx != 255 && (color_idx % 16) == 0) {
5444 color_idx++;
5445 }
5446
5447 // Draw a 4x4 colored square as item indicator
5448 int bitmap_width = bitmap.width();
5449 int bitmap_height = bitmap.height();
5450
5451 for (int py = 0; py < 4; py++) {
5452 for (int px = 0; px < 4; px++) {
5453 int dest_x = pixel_x + px;
5454 int dest_y = pixel_y + py;
5455
5456 // Bounds check
5457 if (dest_x >= 0 && dest_x < bitmap_width && dest_y >= 0 &&
5458 dest_y < bitmap_height) {
5459 int offset = (dest_y * bitmap_width) + dest_x;
5460 bitmap.WriteToPixel(offset, color_idx);
5461 if (offset >= 0 &&
5462 offset < static_cast<int>(coverage_buffer.size())) {
5463 coverage_buffer[offset] = 1;
5464 }
5465 }
5466 }
5467 }
5468}
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.
Draws dungeon objects to background buffers using game patterns.
void DrawDiagonalAcute_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
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 DrawDiagonalGrave_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawBed4x5(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawClosedChestPlatform(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles)
void DrawRightwards2x4_1to15or26(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwards2x4_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
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 DrawRightwardsPillar2x4spaced4_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwardsBottomCorners1x2_1to16_plus13(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwards4x4_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawUtility6x3(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
std::vector< TileTrace > * trace_collector_
void DrawRightwardsEdge1x1_1to16plus7(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
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 DrawRightwardsDecor4x2spaced8_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDownwardsHammerPegs2x2_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDownwardsPillar2x4spaced2_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawHorizontalTurtleRockPipe(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDownwards2x2_1to15or32(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDiagonalCeilingBottomRight(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwardsBlock2x2spaced2_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawSingle2x2(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
std::pair< int, int > CalculateObjectDimensions(const RoomObject &object)
Calculate the dimensions (width, height) of an object in pixels.
void SetTraceContext(const RoomObject &object, RoomObject::LayerType layer)
void DrawDoorSwitcherer(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwards2x2_1to15or32(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
const uint8_t * room_gfx_buffer_
void DrawUtility3x5(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawSingle4x3(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRupeeFloor(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDownwardsEdge1x1_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDownwardsBar2x3_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
static bool RoutineDrawsToBothBGs(int routine_id)
void DrawDownwardsDecor2x4spaced8_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawNothing(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)
Draw all objects in a room.
void DrawDownwardsDecor4x2spaced4_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwardsLine1x1_1to16plus1(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 DrawDiagonalCeilingTopLeft(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwardsTopCorners1x2_1to16_plus13(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDownwardsHasEdge1x1_1to16_plus3(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDiagonalAcute_1to16_BothBG(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwards2x2_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
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 Draw4x4Corner_BothBG(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void PushTrace(int tile_x, int tile_y, const gfx::TileInfo &tile_info)
void DrawBigLightBeam(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
static void TraceHookThunk(int tile_x, int tile_y, const gfx::TileInfo &tile_info, void *user_data)
void DrawDownwardsCannonHole3x6_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDownwards2x2_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDownwardsLine1x1_1to16plus1(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawVerticalTurtleRockPipe(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwards4x2_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawCorner4x4(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwardsHasEdge1x1_1to16_plus3(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwardsHammerPegs2x2_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwards2x4_1to16_BothBG(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawCustomObject(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwardsStatue2x3spaced2_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDiagonalCeilingTopRight(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawWaterFace(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwardsDecor4x3spaced4_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwardsShelf4x4_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDiagonalGrave_1to16_BothBG(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawMovingWallEast(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles)
void DrawDownwardsPots2x2_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDownwardsFloor4x4_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDownwardsDecor3x4spaced2_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwardsPots2x2_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawActual4x4(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawOpenChestPlatform(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles)
void DrawRightwards1x1Solid_1to16_plus3(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
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 DrawDownwardsDecor4x4spaced2_1to16(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 DrawSolidWallDecor3x4(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDiagonalCeilingBottomLeft(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwardsCannonHole4x3_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawWeirdCornerBottom_BothBG(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwards3x6(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwardsHasEdge1x1_1to16_plus2(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDownwards4x2_1to15or26(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
ObjectDrawer(Rom *rom, int room_id, const uint8_t *room_gfx_buffer=nullptr)
void DrawRightwardsDecor4x4spaced2_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawSingle4x4(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawLightBeam(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=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.
void DrawWeirdCornerTop_BothBG(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDownwards4x2_1to16_BothBG(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDownwardsDecor3x4spaced4_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDownwardsRightCorners2x1_1to16_plus12(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
static constexpr int kMaxTilesY
void DrawRightwardsDoubled2x2spaced2_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawBossShell4x4(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
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 DrawRightwardsHasEdge1x1_1to16_plus23(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwards1x2_1to16_plus2(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void SetTraceCollector(std::vector< TileTrace > *collector, bool trace_only=false)
void DrawGanonTriforceFloorDecor(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwardsBar4x3_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
bool IsValidTilePosition(int tile_x, int tile_y) const
void DrawRightwardsDecor2x2spaced12_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDownwardsBlock2x2spaced2_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDownwardsHasEdge1x1_1to16_plus23(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDownwards1x1Solid_1to16_plus3(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawSomariaLine(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawArcheryGameTargetDoor(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawRightwardsBigRail1x3_1to16plus5(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawMovingWallWest(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles)
void DrawDownwardsLeftCorners2x1_1to16_plus12(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDownwardsDecor2x2spaced12_1to16(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
void DrawDownwardsBigRail3x1_1to16plus5(const RoomObject &obj, gfx::BackgroundBuffer &bg, std::span< const gfx::TileInfo > tiles, const DungeonState *state=nullptr)
const std::vector< gfx::TileInfo > & tiles() const
Definition room_object.h:91
void SetRom(Rom *rom)
Definition room_object.h:70
#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)
DoorType
Door types from ALTTP.
Definition door_types.h:33
@ 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.
@ 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