yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
room.cc
Go to the documentation of this file.
1#include "room.h"
2
3#include <yaze.h>
4
5#include <algorithm>
6#include <cstdint>
7#include <unordered_set>
8#include <vector>
9
10#include "absl/strings/str_cat.h"
11#include "absl/strings/str_format.h"
15#include "rom/rom.h"
16#include "rom/snes.h"
17#include "rom/write_fence.h"
18#include "util/log.h"
26
27namespace yaze {
28namespace zelda3 {
29
30// Define room effect names in a single translation unit to avoid SIOF
31const std::string RoomEffect[8] = {"Nothing",
32 "Nothing",
33 "Moving Floor",
34 "Moving Water",
35 "Trinexx Shell",
36 "Red Flashes",
37 "Light Torch to See Floor",
38 "Ganon's Darkness"};
39
40// Define room tag names in a single translation unit to avoid SIOF
41const std::string RoomTag[65] = {"Nothing",
42 "NW Kill Enemy to Open",
43 "NE Kill Enemy to Open",
44 "SW Kill Enemy to Open",
45 "SE Kill Enemy to Open",
46 "W Kill Enemy to Open",
47 "E Kill Enemy to Open",
48 "N Kill Enemy to Open",
49 "S Kill Enemy to Open",
50 "Clear Quadrant to Open",
51 "Clear Full Tile to Open",
52 "NW Push Block to Open",
53 "NE Push Block to Open",
54 "SW Push Block to Open",
55 "SE Push Block to Open",
56 "W Push Block to Open",
57 "E Push Block to Open",
58 "N Push Block to Open",
59 "S Push Block to Open",
60 "Push Block to Open",
61 "Pull Lever to Open",
62 "Collect Prize to Open",
63 "Hold Switch Open Door",
64 "Toggle Switch to Open Door",
65 "Turn off Water",
66 "Turn on Water",
67 "Water Gate",
68 "Water Twin",
69 "Moving Wall Right",
70 "Moving Wall Left",
71 "Crash",
72 "Crash",
73 "Push Switch Exploding Wall",
74 "Holes 0",
75 "Open Chest (Holes 0)",
76 "Holes 1",
77 "Holes 2",
78 "Defeat Boss for Dungeon Prize",
79 "SE Kill Enemy to Push Block",
80 "Trigger Switch Chest",
81 "Pull Lever Exploding Wall",
82 "NW Kill Enemy for Chest",
83 "NE Kill Enemy for Chest",
84 "SW Kill Enemy for Chest",
85 "SE Kill Enemy for Chest",
86 "W Kill Enemy for Chest",
87 "E Kill Enemy for Chest",
88 "N Kill Enemy for Chest",
89 "S Kill Enemy for Chest",
90 "Clear Quadrant for Chest",
91 "Clear Full Tile for Chest",
92 "Light Torches to Open",
93 "Holes 3",
94 "Holes 4",
95 "Holes 5",
96 "Holes 6",
97 "Agahnim Room",
98 "Holes 7",
99 "Holes 8",
100 "Open Chest for Holes 8",
101 "Push Block for Chest",
102 "Clear Room for Triforce Door",
103 "Light Torches for Chest",
104 "Kill Boss Again"};
105
106namespace {
107
108absl::Status GetSpritePointerTablePc(const std::vector<uint8_t>& rom_data,
109 int* table_pc) {
110 if (table_pc == nullptr) {
111 return absl::InvalidArgumentError("table_pc pointer is null");
112 }
113 if (kRoomsSpritePointer + 1 >= static_cast<int>(rom_data.size())) {
114 return absl::OutOfRangeError("Sprite pointer table address is out of range");
115 }
116
117 int table_snes = (0x09 << 16) | (rom_data[kRoomsSpritePointer + 1] << 8) |
118 rom_data[kRoomsSpritePointer];
119 int pc = SnesToPc(table_snes);
120 if (pc < 0 || pc + (kNumberOfRooms * 2) > static_cast<int>(rom_data.size())) {
121 return absl::OutOfRangeError("Sprite pointer table is out of range");
122 }
123
124 *table_pc = pc;
125 return absl::OkStatus();
126}
127
128int ReadRoomSpriteAddressPc(const std::vector<uint8_t>& rom_data, int table_pc,
129 int room_id) {
130 if (room_id < 0 || room_id >= kNumberOfRooms) {
131 return -1;
132 }
133 const int ptr_off = table_pc + (room_id * 2);
134 if (ptr_off < 0 || ptr_off + 1 >= static_cast<int>(rom_data.size())) {
135 return -1;
136 }
137
138 int sprite_address_snes = (0x09 << 16) | (rom_data[ptr_off + 1] << 8) |
139 rom_data[ptr_off];
140 return SnesToPc(sprite_address_snes);
141}
142
143int MeasureSpriteStreamSize(const std::vector<uint8_t>& rom_data,
144 int sprite_address, int hard_end) {
145 if (sprite_address < 0 || sprite_address >= hard_end ||
146 sprite_address >= static_cast<int>(rom_data.size())) {
147 return 0;
148 }
149
150 int cursor = sprite_address + 1; // Skip SortSprites mode byte.
151 while (cursor < hard_end) {
152 if (rom_data[cursor] == 0xFF) {
153 ++cursor; // Include terminator.
154 break;
155 }
156 if (cursor + 2 >= hard_end) {
157 cursor = hard_end;
158 break;
159 }
160 cursor += 3;
161 }
162
163 return std::max(0, cursor - sprite_address);
164}
165
166bool IsSpritePointerShared(const std::vector<uint8_t>& rom_data, int table_pc,
167 int room_id, int sprite_address) {
168 for (int r = 0; r < kNumberOfRooms; ++r) {
169 if (r == room_id) {
170 continue;
171 }
172 if (ReadRoomSpriteAddressPc(rom_data, table_pc, r) == sprite_address) {
173 return true;
174 }
175 }
176 return false;
177}
178
179} // namespace
180
181RoomSize CalculateRoomSize(Rom* rom, int room_id) {
182 // Calculate the size of the room based on how many objects are used per room
183 // Some notes from hacker Zarby89
184 // vanilla rooms are using in average ~0x80 bytes
185 // a "normal" person who wants more details than vanilla will use around 0x100
186 // bytes per rooms you could fit 128 rooms like that in 1 bank
187 // F8000 I don't remember if that's PC or snes tho
188 // Check last rooms
189 // F8000+(roomid*3)
190 // So we want to search the rom() object at this addressed based on the room
191 // ID since it's the roomid * 3 we will by pulling 3 bytes at a time We can do
192 // this with the rom()->ReadByteVector(addr, size)
193 // Existing room size address calculation...
194 RoomSize room_size;
195 room_size.room_size_pointer = 0;
196 room_size.room_size = 0;
197
198 if (!rom || !rom->is_loaded() || rom->size() == 0) {
199 return room_size;
200 }
201
202 auto room_size_address = 0xF8000 + (room_id * 3);
203
204 // Bounds check
205 if (room_size_address < 0 ||
206 room_size_address + 2 >= static_cast<int>(rom->size())) {
207 return room_size;
208 }
209
210 // Reading bytes for long address construction
211 uint8_t low = rom->data()[room_size_address];
212 uint8_t high = rom->data()[room_size_address + 1];
213 uint8_t bank = rom->data()[room_size_address + 2];
214
215 // Constructing the long address
216 int long_address = (bank << 16) | (high << 8) | low;
217 room_size.room_size_pointer = long_address;
218
219 if (long_address == 0x0A8000) {
220 // Blank room disregard in size calculation
221 room_size.room_size = 0;
222 } else {
223 // use the long address to calculate the size of the room
224 // we will use the room_id_ to calculate the next room's address
225 // and subtract the two to get the size of the room
226
227 int next_room_address = 0xF8000 + ((room_id + 1) * 3);
228
229 // Bounds check for next room address
230 if (next_room_address < 0 ||
231 next_room_address + 2 >= static_cast<int>(rom->size())) {
232 return room_size;
233 }
234
235 // Reading bytes for long address construction
236 uint8_t next_low = rom->data()[next_room_address];
237 uint8_t next_high = rom->data()[next_room_address + 1];
238 uint8_t next_bank = rom->data()[next_room_address + 2];
239
240 // Constructing the long address
241 int next_long_address = (next_bank << 16) | (next_high << 8) | next_low;
242
243 // Calculate the size of the room
244 int actual_room_size = next_long_address - long_address;
245 room_size.room_size = actual_room_size;
246 }
247
248 return room_size;
249}
250
251// Loads a room from the ROM.
252// ASM: Bank 01, Underworld_LoadRoom ($01873A)
253Room LoadRoomFromRom(Rom* rom, int room_id) {
254 // Use the header loader to get the base room with properties
255 // ASM: JSR Underworld_LoadHeader ($01873A)
256 Room room = LoadRoomHeaderFromRom(rom, room_id);
257
258 // Load additional room features
259 //
260 // USDASM ground truth: LoadAndBuildRoom ($01:873A) draws the variable-length
261 // room object stream first (RoomDraw_DrawAllObjects), then draws pushable
262 // blocks ($7EF940) and torches ($7EFB40). These "special" objects are not
263 // part of the room object stream and must not be saved into it.
264 room.LoadObjects();
265 room.LoadPotItems();
266 room.LoadTorches();
267 room.LoadBlocks();
268 room.LoadPits();
269
270 room.SetLoaded(true);
271 return room;
272}
273
274Room LoadRoomHeaderFromRom(Rom* rom, int room_id) {
275 Room room(room_id, rom);
276
277 if (!rom || !rom->is_loaded() || rom->size() == 0) {
278 return room;
279 }
280
281 // Validate kRoomHeaderPointer access
282 if (kRoomHeaderPointer < 0 ||
283 kRoomHeaderPointer + 2 >= static_cast<int>(rom->size())) {
284 return room;
285 }
286
287 // ASM: RoomHeader_RoomToPointer table lookup
288 int header_pointer = (rom->data()[kRoomHeaderPointer + 2] << 16) +
289 (rom->data()[kRoomHeaderPointer + 1] << 8) +
290 (rom->data()[kRoomHeaderPointer]);
291 header_pointer = SnesToPc(header_pointer);
292
293 // Validate kRoomHeaderPointerBank access
294 if (kRoomHeaderPointerBank < 0 ||
295 kRoomHeaderPointerBank >= static_cast<int>(rom->size())) {
296 return room;
297 }
298
299 // Validate header_pointer table access
300 int table_offset = (header_pointer) + (room_id * 2);
301 if (table_offset < 0 || table_offset + 1 >= static_cast<int>(rom->size())) {
302 return room;
303 }
304
305 int address = (rom->data()[kRoomHeaderPointerBank] << 16) +
306 (rom->data()[table_offset + 1] << 8) +
307 rom->data()[table_offset];
308
309 auto header_location = SnesToPc(address);
310
311 // Validate header_location access (we read up to +13 bytes)
312 if (header_location < 0 ||
313 header_location + 13 >= static_cast<int>(rom->size())) {
314 return room;
315 }
316
317 room.SetBg2((background2)((rom->data()[header_location] >> 5) & 0x07));
318 room.SetCollision((CollisionKey)((rom->data()[header_location] >> 2) & 0x07));
319 room.SetIsLight(((rom->data()[header_location]) & 0x01) == 1);
320
321 if (room.IsLight()) {
322 room.SetBg2(background2::DarkRoom);
323 }
324
325 // USDASM grounding (bank_01.asm LoadRoomHeader, e.g. $01:B61B):
326 // The room header stores an 8-bit "palette set ID" (0-71 in vanilla), which
327 // is later multiplied by 4 to index UnderworldPaletteSets. Do NOT truncate to
328 // 6 bits: IDs 0x40-0x47 are valid and were previously corrupted by & 0x3F.
329 room.SetPalette(rom->data()[header_location + 1]);
330 room.SetBlockset((rom->data()[header_location + 2]));
331 room.SetSpriteset((rom->data()[header_location + 3]));
332 room.SetEffect((EffectKey)((rom->data()[header_location + 4])));
333 room.SetTag1((TagKey)((rom->data()[header_location + 5])));
334 room.SetTag2((TagKey)((rom->data()[header_location + 6])));
335
336 room.SetStaircasePlane(0, ((rom->data()[header_location + 7] >> 2) & 0x03));
337 room.SetStaircasePlane(1, ((rom->data()[header_location + 7] >> 4) & 0x03));
338 room.SetStaircasePlane(2, ((rom->data()[header_location + 7] >> 6) & 0x03));
339 room.SetStaircasePlane(3, ((rom->data()[header_location + 8]) & 0x03));
340
341 room.SetHolewarp((rom->data()[header_location + 9]));
342 room.SetStaircaseRoom(0, (rom->data()[header_location + 10]));
343 room.SetStaircaseRoom(1, (rom->data()[header_location + 11]));
344 room.SetStaircaseRoom(2, (rom->data()[header_location + 12]));
345 room.SetStaircaseRoom(3, (rom->data()[header_location + 13]));
346
347 // =====
348
349 // Validate kRoomHeaderPointer access (again, just in case)
350 if (kRoomHeaderPointer < 0 ||
351 kRoomHeaderPointer + 2 >= static_cast<int>(rom->size())) {
352 return room;
353 }
354
355 int header_pointer_2 = (rom->data()[kRoomHeaderPointer + 2] << 16) +
356 (rom->data()[kRoomHeaderPointer + 1] << 8) +
357 (rom->data()[kRoomHeaderPointer]);
358 header_pointer_2 = SnesToPc(header_pointer_2);
359
360 // Validate kRoomHeaderPointerBank access
361 if (kRoomHeaderPointerBank < 0 ||
362 kRoomHeaderPointerBank >= static_cast<int>(rom->size())) {
363 return room;
364 }
365
366 // Validate header_pointer_2 table access
367 int table_offset_2 = (header_pointer_2) + (room_id * 2);
368 if (table_offset_2 < 0 ||
369 table_offset_2 + 1 >= static_cast<int>(rom->size())) {
370 return room;
371 }
372
373 int address_2 = (rom->data()[kRoomHeaderPointerBank] << 16) +
374 (rom->data()[table_offset_2 + 1] << 8) +
375 rom->data()[table_offset_2];
376
377 int msg_addr = kMessagesIdDungeon + (room_id * 2);
378 if (msg_addr >= 0 && msg_addr + 1 < static_cast<int>(rom->size())) {
379 uint16_t msg_val = (rom->data()[msg_addr + 1] << 8) | rom->data()[msg_addr];
380 room.SetMessageId(msg_val);
381 }
382
383 auto hpos = SnesToPc(address_2);
384
385 // Validate hpos access (we read sequentially)
386 // We read about 14 bytes (hpos++ calls)
387 if (hpos < 0 || hpos + 14 >= static_cast<int>(rom->size())) {
388 return room;
389 }
390
391 uint8_t b = rom->data()[hpos];
392
393 room.SetLayer2Mode((b >> 5));
394 room.SetLayerMerging(kLayerMergeTypeList[(b & 0x0C) >> 2]);
395
396 room.SetIsDark((b & 0x01) == 0x01);
397 hpos++;
398 // Skip palette byte here - already set by SetPalette() from the primary
399 // header table above (line ~329). The old SetPaletteDirect wrote to a
400 // separate dead-code member; now palette_ is unified.
401 hpos++;
402
403 room.SetBackgroundTileset(rom->data()[hpos]);
404 hpos++;
405
406 room.SetSpriteTileset(rom->data()[hpos]);
407 hpos++;
408
409 room.SetLayer2Behavior(rom->data()[hpos]);
410 hpos++;
411
412 room.SetTag1Direct((TagKey)rom->data()[hpos]);
413 hpos++;
414
415 room.SetTag2Direct((TagKey)rom->data()[hpos]);
416 hpos++;
417
418 b = rom->data()[hpos];
419
420 room.SetPitsTargetLayer((uint8_t)(b & 0x03));
421 room.SetStair1TargetLayer((uint8_t)((b >> 2) & 0x03));
422 room.SetStair2TargetLayer((uint8_t)((b >> 4) & 0x03));
423 room.SetStair3TargetLayer((uint8_t)((b >> 6) & 0x03));
424 hpos++;
425 room.SetStair4TargetLayer((uint8_t)(rom->data()[hpos] & 0x03));
426 hpos++;
427
428 room.SetPitsTarget(rom->data()[hpos]);
429 hpos++;
430 room.SetStair1Target(rom->data()[hpos]);
431 hpos++;
432 room.SetStair2Target(rom->data()[hpos]);
433 hpos++;
434 room.SetStair3Target(rom->data()[hpos]);
435 hpos++;
436 room.SetStair4Target(rom->data()[hpos]);
437
438 // Note: We do NOT set is_loaded_ to true here, as this is just the header
439 return room;
440}
441
442Room::Room(int room_id, Rom* rom, GameData* game_data)
443 : room_id_(room_id),
444 rom_(rom),
445 game_data_(game_data),
446 dungeon_state_(std::make_unique<EditorDungeonState>(rom, game_data)) {}
447
448Room::Room() = default;
449Room::~Room() = default;
450Room::Room(Room&&) = default;
451Room& Room::operator=(Room&&) = default;
452
453void Room::LoadRoomGraphics(uint8_t entrance_blockset) {
454 if (!game_data_) {
455 LOG_DEBUG("Room", "GameData not set for room %d", room_id_);
456 return;
457 }
458
459 const auto& room_gfx = game_data_->room_blockset_ids;
460 const auto& sprite_gfx = game_data_->spriteset_ids;
461
462 LOG_DEBUG("Room", "Room %d: blockset=%d, spriteset=%d, palette=%d", room_id_,
464
465 for (int i = 0; i < 8; i++) {
467 // Block 6 can be overridden by entrance-specific room graphics (index 3)
468 // Note: The "3-6" comment was misleading - only block 6 uses room_gfx
469 if (i == 6) {
470 if (entrance_blockset != 0xFF && room_gfx[entrance_blockset][3] != 0) {
471 blocks_[i] = room_gfx[entrance_blockset][3];
472 }
473 }
474 }
475
476 blocks_[8] = 115 + 0; // Static Sprites Blocksets (fairy,pot,ect...)
477 blocks_[9] = 115 + 10;
478 blocks_[10] = 115 + 6;
479 blocks_[11] = 115 + 7;
480 for (int i = 0; i < 4; i++) {
481 blocks_[12 + i] = (uint8_t)(sprite_gfx[spriteset_ + 64][i] + 115);
482 } // 12-16 sprites
483
484 LOG_DEBUG("Room", "Sheet IDs BG[0-7]: %d %d %d %d %d %d %d %d", blocks_[0],
485 blocks_[1], blocks_[2], blocks_[3], blocks_[4], blocks_[5],
486 blocks_[6], blocks_[7]);
487}
488
489constexpr int kGfxBufferOffset = 92 * 2048;
490constexpr int kGfxBufferStride = 1024;
491constexpr int kGfxBufferAnimatedFrameOffset = 7 * 4096;
492constexpr int kGfxBufferAnimatedFrameStride = 1024;
493constexpr int kGfxBufferRoomOffset = 4096;
494constexpr int kGfxBufferRoomSpriteOffset = 1024;
495constexpr int kGfxBufferRoomSpriteStride = 4096;
497
499 if (!rom_ || !rom_->is_loaded()) {
500 LOG_DEBUG("Room", "CopyRoomGraphicsToBuffer: ROM not loaded");
501 return;
502 }
503
504 if (!game_data_) {
505 LOG_DEBUG("Room", "CopyRoomGraphicsToBuffer: GameData not set");
506 return;
507 }
508 auto* gfx_buffer_data = &game_data_->graphics_buffer;
509 if (gfx_buffer_data->empty()) {
510 LOG_DEBUG("Room", "CopyRoomGraphicsToBuffer: Graphics buffer is empty");
511 return;
512 }
513
514 LOG_DEBUG("Room", "Room %d: Copying 8BPP graphics (buffer size: %zu)",
515 room_id_, gfx_buffer_data->size());
516
517 // Clear destination buffer
518 std::fill(current_gfx16_.begin(), current_gfx16_.end(), 0);
519
520 // USDASM grounding (bank_00.asm LoadBackgroundGraphics):
521 // The engine expands 3BPP graphics to 4BPP in two modes:
522 // - Left palette: plane3 = 0 (pixel values 0-7).
523 // - Right palette: plane3 = OR(planes0..2), so non-zero pixels get bit3=1
524 // (pixel values 1-7 become 9-15; 0 remains 0/transparent).
525 //
526 // For background graphics sets, the game selects Left/Right based on the
527 // "graphics group" ($0AA1, our Room::blockset) and the slot index ($0F).
528 // For UW groups (< $20), slots 4-7 use Right; for OW groups (>= $20), the
529 // Right slots are {2,3,4,7}. We mirror this by shifting non-zero pixels by
530 // +8 when copying those background blocks into current_gfx16_.
531 auto is_right_palette_background_slot = [&](int slot) -> bool {
532 if (slot < 0 || slot >= 8) {
533 return false;
534 }
535 if (blockset_ < 0x20) {
536 return slot >= 4;
537 }
538 return (slot == 2 || slot == 3 || slot == 4 || slot == 7);
539 };
540
541 // Process each of the 16 graphics blocks
542 for (int block = 0; block < 16; block++) {
543 int sheet_id = blocks_[block];
544
545 // Validate block index
546 if (sheet_id >= 223) { // kNumGfxSheets
547 LOG_WARN("Room", "Invalid sheet index %d for block %d", sheet_id, block);
548 continue;
549 }
550
551 // Source offset in ROM graphics buffer (now 8BPP format)
552 // Each 8BPP sheet is 4096 bytes (128x32 pixels)
553 int src_sheet_offset = sheet_id * 4096;
554
555 // Validate source bounds
556 if (src_sheet_offset + 4096 > gfx_buffer_data->size()) {
557 LOG_ERROR("Room", "Graphics offset out of bounds: %d (size: %zu)",
558 src_sheet_offset, gfx_buffer_data->size());
559 continue;
560 }
561
562 // Copy 4096 bytes for the 8BPP sheet
563 int dest_index_base = block * 4096;
564 if (dest_index_base + 4096 <= current_gfx16_.size()) {
565 const uint8_t* src = gfx_buffer_data->data() + src_sheet_offset;
566 uint8_t* dst = current_gfx16_.data() + dest_index_base;
567
568 // Only background blocks (0-7) participate in Left/Right palette
569 // expansion. Sprite sheets are handled separately by the game.
570 const bool right_pal = is_right_palette_background_slot(block);
571 if (!right_pal) {
572 memcpy(dst, src, 4096);
573 } else {
574 // Right palette expansion: set bit3 for non-zero pixels (1-7 -> 9-15).
575 for (int i = 0; i < 4096; ++i) {
576 uint8_t p = src[i];
577 if (p != 0 && p < 8) {
578 p |= 0x08;
579 }
580 dst[i] = p;
581 }
582 }
583 }
584 }
585
586 LOG_DEBUG("Room", "Room %d: Graphics blocks copied successfully", room_id_);
588}
589
597
599 // PERFORMANCE OPTIMIZATION: Check if room properties have changed
600 bool properties_changed = false;
601
602 // Check if graphics properties changed
613 dirty_state_.graphics = true;
614 properties_changed = true;
615 }
616
617 // Check if effect/tags changed
618 if (cached_effect_ != static_cast<uint8_t>(effect_) ||
620 cached_effect_ = static_cast<uint8_t>(effect_);
623 dirty_state_.objects = true;
624 properties_changed = true;
625 }
626
627 // If nothing changed and textures exist, skip rendering
628 if (!properties_changed && !dirty_state_.graphics && !dirty_state_.objects &&
630 auto& bg1_bmp = bg1_buffer_.bitmap();
631 auto& bg2_bmp = bg2_buffer_.bitmap();
632 if (bg1_bmp.texture() && bg2_bmp.texture()) {
633 LOG_DEBUG("[RenderRoomGraphics]",
634 "Room %d: No changes detected, skipping render", room_id_);
635 return;
636 }
637 }
638
639 LOG_DEBUG("[RenderRoomGraphics]",
640 "Room %d: Rendering graphics (dirty_flags: g=%d o=%d l=%d t=%d)",
643
644 // Capture dirty state BEFORE clearing flags (needed for floor/bg draw logic)
645 bool was_graphics_dirty = dirty_state_.graphics;
646 bool was_layout_dirty = dirty_state_.layout;
647
648 // STEP 0: Load graphics if needed
650 // Ensure blocks_[] array is properly initialized before copying graphics
651 // LoadRoomGraphics sets up which sheets go into which blocks
654 dirty_state_.graphics = false;
655 }
656
657 // Debug: Log floor graphics values
658 LOG_DEBUG("[RenderRoomGraphics]",
659 "Room %d: floor1=%d, floor2=%d, blocks_size=%zu", room_id_,
661
662 // STEP 1: Draw floor tiles to bitmaps (base layer) - if graphics changed OR
663 // bitmaps not created yet
664 bool need_floor_draw = was_graphics_dirty;
665 auto& bg1_bmp = bg1_buffer_.bitmap();
666 auto& bg2_bmp = bg2_buffer_.bitmap();
667
668 // Always draw floor if bitmaps don't exist yet (first time rendering)
669 if (!bg1_bmp.is_active() || bg1_bmp.width() == 0 || !bg2_bmp.is_active() ||
670 bg2_bmp.width() == 0) {
671 need_floor_draw = true;
672 LOG_DEBUG("[RenderRoomGraphics]",
673 "Room %d: Bitmaps not created yet, forcing floor draw", room_id_);
674 }
675
676 if (need_floor_draw) {
681 }
682
683 // STEP 2: Draw background tiles (floor pattern) to bitmap
684 // This converts the floor tile buffer to pixels
685 bool need_bg_draw = was_graphics_dirty || need_floor_draw;
686 if (need_bg_draw) {
687 bg1_buffer_.DrawBackground(std::span<uint8_t>(current_gfx16_));
688 bg2_buffer_.DrawBackground(std::span<uint8_t>(current_gfx16_));
689 }
690
691 // STEP 3: Draw layout objects ON TOP of floor
692 // Layout objects (walls, corners) are drawn after floor so they appear over it
693 // NOTE: SNES uses a four-pass pipeline (layout, main, BG2 overlay, BG1
694 // overlay) per bank_01.asm. We currently emit one layout pass + one object
695 // list. RoomLayerManager handles BG2 translucency and room effects, and
696 // DrawRoutineRegistry routes BothBG objects correctly, but splitting into
697 // four distinct streams would fix edge cases with overlay ordering.
698 // See docs/internal/agents/dungeon-object-rendering-spec.md.
699 if (was_layout_dirty || need_floor_draw) {
701 dirty_state_.layout = false;
702 }
703
704 // Get and apply palette BEFORE rendering objects (so objects use correct colors)
705 if (!game_data_)
706 return;
707 auto& dungeon_pal_group = game_data_->palette_groups.dungeon_main;
708 int num_palettes = dungeon_pal_group.size();
709 if (num_palettes == 0)
710 return;
711
712 // Look up dungeon palette ID using the two-level paletteset_ids table.
713 // paletteset_ids[palette][0] contains a BYTE OFFSET into the palette pointer
714 // table at kDungeonPalettePointerTable. The word at that offset, divided by
715 // 180 (bytes per palette), gives the actual palette index (0-19).
716 int palette_id = palette_;
717 if (palette_ < game_data_->paletteset_ids.size() &&
719 auto dungeon_palette_ptr = game_data_->paletteset_ids[palette_][0];
720 auto palette_word =
721 rom()->ReadWord(kDungeonPalettePointerTable + dungeon_palette_ptr);
722 if (palette_word.ok()) {
723 palette_id = palette_word.value() / 180;
724 }
725 }
726 if (palette_id < 0 || palette_id >= num_palettes) {
727 palette_id = 0;
728 }
729
730 auto bg1_palette = dungeon_pal_group[palette_id];
731
732 // DEBUG: Log palette loading
733 PaletteDebugger::Get().LogPaletteLoad("Room::RenderRoomGraphics", palette_id,
734 bg1_palette);
735
736 LOG_DEBUG("Room", "RenderRoomGraphics: Palette ID=%d, Size=%zu", palette_id,
737 bg1_palette.size());
738 if (!bg1_palette.empty()) {
739 LOG_DEBUG("Room", "RenderRoomGraphics: First color: R=%d G=%d B=%d",
740 bg1_palette[0].rom_color().red, bg1_palette[0].rom_color().green,
741 bg1_palette[0].rom_color().blue);
742 }
743
744 // Store current palette and bitmap for pixel inspector debugging
747
748 if (bg1_palette.size() > 0) {
749 // Apply dungeon palette using 16-color bank chunking (matches SNES CGRAM)
750 //
751 // SNES CGRAM layout for dungeons:
752 // - CGRAM has 16-color banks, each bank's index 0 is transparent
753 // - Dungeon tiles use palette bits 2-7, mapping to CGRAM rows 2-7
754 // - ROM stores 15 colors per bank (excluding transparent index 0)
755 // - 6 banks × 15 colors = 90 colors in ROM
756 //
757 // SDL palette mapping (16-color chunks):
758 // - Bank N (N=0-5): SDL indices [N*16 .. N*16+15]
759 // - Index N*16 = transparent for that bank
760 // - ROM colors [N*15 .. N*15+14] → SDL indices [N*16+1 .. N*16+15]
761 //
762 // Drawing formula: final_color = pixel + (bank * 16)
763 // Where pixel 0 = transparent (not written), pixel 1-15 = colors 1-15 in bank
764 auto set_dungeon_palette = [](gfx::Bitmap& bmp,
765 const gfx::SnesPalette& pal) {
766 std::vector<SDL_Color> colors(
767 256, {0, 0, 0, 0}); // Initialize all transparent
768
769 // Map ROM palette to 16-color banks
770 // ROM: 90 colors (6 banks × 15 colors each)
771 // SDL: 96 indices (6 banks × 16 indices each)
772 constexpr int kColorsPerRomBank = 15;
773 constexpr int kIndicesPerSdlBank = 16;
774 constexpr int kNumBanks = 6;
775
776 for (int bank = 0; bank < kNumBanks; bank++) {
777 // Index 0 of each bank is transparent (already initialized to {0,0,0,0})
778 // ROM colors map to SDL indices 1-15 within each bank
779 for (int color = 0; color < kColorsPerRomBank; color++) {
780 size_t rom_index = bank * kColorsPerRomBank + color;
781 if (rom_index >= pal.size())
782 break;
783
784 int sdl_index =
785 bank * kIndicesPerSdlBank + color + 1; // +1 to skip transparent
786 ImVec4 rgb = pal[rom_index].rgb();
787 colors[sdl_index] = {
788 static_cast<Uint8>(rgb.x), static_cast<Uint8>(rgb.y),
789 static_cast<Uint8>(rgb.z),
790 255 // Opaque
791 };
792 }
793 }
794
795 // Index 255 is also transparent (fill color for undrawn areas)
796 colors[255] = {0, 0, 0, 0};
797
798 bmp.SetPalette(colors);
799 if (bmp.surface()) {
800 // Set color key to 255 for proper alpha blending (undrawn areas)
801 SDL_SetColorKey(bmp.surface(), SDL_TRUE, 255);
802 SDL_SetSurfaceBlendMode(bmp.surface(), SDL_BLENDMODE_BLEND);
803 }
804 };
805
806 set_dungeon_palette(bg1_bmp, bg1_palette);
807 set_dungeon_palette(bg2_bmp, bg1_palette);
808 set_dungeon_palette(object_bg1_buffer_.bitmap(), bg1_palette);
809 set_dungeon_palette(object_bg2_buffer_.bitmap(), bg1_palette);
810
811 // DEBUG: Verify palette was applied to SDL surface
812 auto* surface = bg1_bmp.surface();
813 if (surface) {
814 SDL_Palette* palette = platform::GetSurfacePalette(surface);
815 if (palette) {
817 "Room::RenderRoomGraphics (BG1)", palette_id, true);
818
819 // Log surface state for detailed debugging
821 "Room::RenderRoomGraphics (after SetPalette)", surface);
822 } else {
824 "Room::RenderRoomGraphics", palette_id, false,
825 "SDL surface has no palette!");
826 }
827 }
828
829 // Apply Layer Merge effects (Transparency/Blending) to BG2
830 // NOTE: These SDL blend settings are for direct SDL rendering paths.
831 // RoomLayerManager::CompositeToOutput uses manual pixel compositing and
832 // handles blend modes separately via its layer_blend_mode_ array.
833 // NOTE: RoomLayerManager::CompositeToOutput() now handles translucent
834 // blending with proper SNES color math. These SDL alpha settings are a
835 // legacy fallback for direct SDL rendering paths. Consolidation would
836 // remove this in favor of RoomLayerManager exclusively.
838 // Set alpha mod for translucency (50%)
839 if (bg2_bmp.surface()) {
840 SDL_SetSurfaceAlphaMod(bg2_bmp.surface(), 128);
841 }
842 if (object_bg2_buffer_.bitmap().surface()) {
843 SDL_SetSurfaceAlphaMod(object_bg2_buffer_.bitmap().surface(), 128);
844 }
845
846 // Check for Addition mode (ID 0x05)
847 if (layer_merging_.ID == 0x05) {
848 if (bg2_bmp.surface()) {
849 SDL_SetSurfaceBlendMode(bg2_bmp.surface(), SDL_BLENDMODE_ADD);
850 }
851 if (object_bg2_buffer_.bitmap().surface()) {
852 SDL_SetSurfaceBlendMode(object_bg2_buffer_.bitmap().surface(),
853 SDL_BLENDMODE_ADD);
854 }
855 }
856 }
857 }
858
859 // Render objects ON TOP of background tiles (AFTER palette is set)
860 // ObjectDrawer will write indexed pixel data that uses the palette we just
861 // set
863
864 // PERFORMANCE OPTIMIZATION: Queue texture commands but DON'T process
865 // immediately. This allows multiple rooms to batch their texture updates
866 // together. Processing happens in DrawDungeonCanvas() once per frame.
867 //
868 // IMPORTANT: Check each buffer INDIVIDUALLY for existing texture.
869 // Layout and object buffers may have different states (e.g., layout rendered
870 // but objects added later need CREATE, not UPDATE).
871 auto queue_texture = [](gfx::Bitmap* bitmap, const char* name) {
872 if (bitmap->texture()) {
873 LOG_DEBUG("[RenderRoomGraphics]", "Queueing UPDATE for %s", name);
876 } else {
877 LOG_DEBUG("[RenderRoomGraphics]", "Queueing CREATE for %s", name);
880 }
881 };
882
883 queue_texture(&bg1_bmp, "bg1_buffer");
884 queue_texture(&bg2_bmp, "bg2_buffer");
885 queue_texture(&object_bg1_buffer_.bitmap(), "object_bg1_buffer");
886 queue_texture(&object_bg2_buffer_.bitmap(), "object_bg2_buffer");
887
888 // Mark textures as clean after successful queuing
889 dirty_state_.textures = false;
890
891 // IMPORTANT: Mark composite as dirty after any render work
892 // This ensures GetCompositeBitmap() regenerates the merged output
893 dirty_state_.composite = true;
894
895 // REMOVED: Don't process texture queue here - let it be batched!
896 // Processing happens once per frame in DrawDungeonCanvas()
897 // This dramatically improves performance when multiple rooms are open
898 // gfx::Arena::Get().ProcessTextureQueue(nullptr); // OLD: Caused slowdown!
899 LOG_DEBUG("[RenderRoomGraphics]",
900 "Texture commands queued for batch processing");
901}
902
904 LOG_DEBUG("Room", "LoadLayoutTilesToBuffer for room %d, layout=%d", room_id_,
905 layout_id_);
906
907 if (!rom_ || !rom_->is_loaded()) {
908 LOG_DEBUG("Room", "ROM not loaded, aborting");
909 return;
910 }
911
912 // Load layout tiles from ROM if not already loaded
914 auto layout_status = layout_.LoadLayout(layout_id_);
915 if (!layout_status.ok()) {
916 LOG_DEBUG("Room", "Failed to load layout %d: %s", layout_id_,
917 layout_status.message().data());
918 return;
919 }
920
921 const auto& layout_objects = layout_.GetObjects();
922 LOG_DEBUG("Room", "Layout %d has %zu objects", layout_id_, layout_objects.size());
923 if (layout_objects.empty()) {
924 return;
925 }
926
927 // Use ObjectDrawer to render layout objects properly
928 // Layout objects are the same format as room objects and need draw routines
929 // to render correctly (walls, corners, etc.)
930 if (!game_data_) {
931 LOG_DEBUG("RenderRoomGraphics", "GameData not set, cannot render layout");
932 return;
933 }
934
935 // Get palette for layout rendering
936 // Get palette for layout rendering using two-level lookup
937 auto& dungeon_pal_group = game_data_->palette_groups.dungeon_main;
938 int num_palettes = dungeon_pal_group.size();
939 if (num_palettes == 0)
940 return;
941 int palette_id = palette_;
942 if (palette_ < game_data_->paletteset_ids.size() &&
944 auto dungeon_palette_ptr = game_data_->paletteset_ids[palette_][0];
945 auto palette_word =
946 rom()->ReadWord(kDungeonPalettePointerTable + dungeon_palette_ptr);
947 if (palette_word.ok()) {
948 palette_id = palette_word.value() / 180;
949 }
950 }
951 if (palette_id < 0 || palette_id >= num_palettes) {
952 palette_id = 0;
953 }
954
955 auto room_palette = dungeon_pal_group[palette_id];
956 gfx::PaletteGroup palette_group;
957 palette_group.AddPalette(room_palette);
958 // Palette chunking follows 16-color banks: subpalettes 2-7 map to SDL
959 // indices [(pal-2)*16..(pal-2)*16+15] with index 0 transparent. Palette
960 // fix plan completed (see docs/internal/archive/completed_features/
961 // dungeon-palette-fix-plan-2025-12.md).
962
963 // Draw layout objects using proper draw routines via RoomLayout
964 auto status = layout_.Draw(room_id_, current_gfx16_.data(), bg1_buffer_,
965 bg2_buffer_, palette_group, dungeon_state_.get());
966
967 if (!status.ok()) {
968 LOG_DEBUG(
969 "RenderRoomGraphics", "Layout Draw failed: %s",
970 std::string(status.message().data(), status.message().size()).c_str());
971 } else {
972 LOG_DEBUG("RenderRoomGraphics", "Layout rendered with %zu objects",
973 layout_objects.size());
974 }
975}
976
978 LOG_DEBUG("[RenderObjectsToBackground]",
979 "Starting object rendering for room %d", room_id_);
980
981 if (!rom_ || !rom_->is_loaded()) {
982 LOG_DEBUG("[RenderObjectsToBackground]", "ROM not loaded, aborting");
983 return;
984 }
985
986 // PERFORMANCE OPTIMIZATION: Only render objects if they have changed or if
987 // graphics changed Also render if bitmaps were just created (need_floor_draw
988 // was true in RenderRoomGraphics)
989 auto& bg1_bmp = bg1_buffer_.bitmap();
990 auto& bg2_bmp = bg2_buffer_.bitmap();
991 bool bitmaps_exist = bg1_bmp.is_active() && bg1_bmp.width() > 0 &&
992 bg2_bmp.is_active() && bg2_bmp.width() > 0;
993
994 if (!dirty_state_.objects && !dirty_state_.graphics && bitmaps_exist) {
995 LOG_DEBUG("[RenderObjectsToBackground]",
996 "Room %d: Objects not dirty, skipping render", room_id_);
997 return;
998 }
999
1000 // Handle rendering based on mode (currently using emulator-based rendering)
1001 // Emulator or Hybrid mode (use ObjectDrawer)
1002 LOG_DEBUG("[RenderObjectsToBackground]",
1003 "Room %d: Emulator rendering objects", room_id_);
1004 // Get palette group for object rendering (use SAME lookup as
1005 // RenderRoomGraphics)
1006 if (!game_data_)
1007 return;
1008 auto& dungeon_pal_group = game_data_->palette_groups.dungeon_main;
1009 int num_palettes = dungeon_pal_group.size();
1010
1011 // Look up dungeon palette ID using the two-level paletteset_ids table.
1012 // (same lookup as RenderRoomGraphics and LoadLayoutTilesToBuffer)
1013 int palette_id = palette_;
1014 if (palette_ < game_data_->paletteset_ids.size() &&
1015 !game_data_->paletteset_ids[palette_].empty()) {
1016 auto dungeon_palette_ptr = game_data_->paletteset_ids[palette_][0];
1017 auto palette_word =
1018 rom()->ReadWord(kDungeonPalettePointerTable + dungeon_palette_ptr);
1019 if (palette_word.ok()) {
1020 palette_id = palette_word.value() / 180;
1021 }
1022 }
1023 if (palette_id < 0 || palette_id >= num_palettes) {
1024 palette_id = 0;
1025 }
1026
1027 auto room_palette = dungeon_pal_group[palette_id];
1028 // Dungeon palettes are 90-color palettes for 3BPP graphics (8-color strides)
1029 // Pass the full palette to ObjectDrawer so it can handle all palette indices
1030 gfx::PaletteGroup palette_group;
1031 palette_group.AddPalette(room_palette);
1032
1033 // Use ObjectDrawer for pattern-based object rendering
1034 // This provides proper wall/object drawing patterns
1035 // Pass the room-specific graphics buffer (current_gfx16_) so objects use
1036 // correct tiles
1038 // NOTE: BothBG routines (ceiling corners, merged stairs, prison cells) are
1039 // handled by DrawRoutineRegistry's draws_to_both_bgs flag. Full four-pass
1040 // stream splitting (layout → main → BG2 overlay → BG1 overlay) remains
1041 // future work. See docs/internal/agents/dungeon-object-rendering-spec.md.
1042
1043 // Clear object buffers before rendering
1044 // IMPORTANT: Fill with 255 (transparent color key) so objects overlay correctly
1045 // on the floor. We use index 255 as transparent since palette has 90 colors (0-89).
1048 object_bg1_buffer_.bitmap().Fill(255);
1049 object_bg2_buffer_.bitmap().Fill(255);
1050
1051 // IMPORTANT: Clear priority buffers when clearing object buffers
1052 // Otherwise, old priority values persist and cause incorrect Z-ordering
1055
1056 // IMPORTANT: Clear coverage buffers when clearing object buffers.
1057 // Coverage distinguishes "no draw" vs "drew transparent", so stale values
1058 // can cause objects to incorrectly clear the layout.
1061
1062 // Log layer distribution for this room
1063 int layer0_count = 0, layer1_count = 0, layer2_count = 0;
1064 for (const auto& obj : tile_objects_) {
1065 switch (obj.GetLayerValue()) {
1066 case 0:
1067 layer0_count++;
1068 break;
1069 case 1:
1070 layer1_count++;
1071 break;
1072 case 2:
1073 layer2_count++;
1074 break;
1075 }
1076 }
1077 LOG_DEBUG(
1078 "Room",
1079 "Room %03X Object Layer Summary: L0(BG1)=%d, L1(BG2)=%d, L2(BG3)=%d",
1080 room_id_, layer0_count, layer1_count, layer2_count);
1081
1082 // Render objects to appropriate buffers
1083 // BG1 = Floor/Main (Layer 0, 2)
1084 // BG2 = Overlay (Layer 1)
1085 // Pass bg1_buffer_ for BG2 object masking - this creates "holes" in the floor
1086 // so BG2 overlay content (platforms, statues) shows through BG1 floor tiles
1087 std::vector<RoomObject> objects_to_draw;
1088 objects_to_draw.reserve(tile_objects_.size());
1089 for (const auto& obj : tile_objects_) {
1090 // Torches and pushable blocks are NOT part of the room object stream.
1091 // They come from the global tables and are drawn after the stream in
1092 // USDASM (LoadAndBuildRoom $01:873A). Draw them in a dedicated pass.
1093 if ((obj.options() & ObjectOption::Torch) != ObjectOption::Nothing) {
1094 continue;
1095 }
1096 if ((obj.options() & ObjectOption::Block) != ObjectOption::Nothing) {
1097 continue;
1098 }
1099 objects_to_draw.push_back(obj);
1100 }
1101
1102 auto status = drawer.DrawObjectList(objects_to_draw, object_bg1_buffer_,
1103 object_bg2_buffer_, palette_group,
1104 dungeon_state_.get(), &bg1_buffer_);
1105
1106 // Render doors using DoorDef struct with enum types
1107 // Doors are drawn to the OBJECT buffer for layer visibility control
1108 // This allows doors to remain visible when toggling BG1_Layout off
1109 for (int i = 0; i < static_cast<int>(doors_.size()); ++i) {
1110 const auto& door = doors_[i];
1111 ObjectDrawer::DoorDef door_def;
1112 door_def.type = door.type;
1113 door_def.direction = door.direction;
1114 door_def.position = door.position;
1115 // Draw doors to object buffers (not layout buffers) so they remain visible
1116 // when BG1_Layout is hidden. Doors are objects, not layout tiles.
1117 drawer.DrawDoor(door_def, i, object_bg1_buffer_, object_bg2_buffer_,
1118 dungeon_state_.get());
1119 }
1120 // Mark object buffer as modified so texture gets updated
1121 if (!doors_.empty()) {
1122 object_bg1_buffer_.bitmap().set_modified(true);
1123 }
1124
1125 // Render pot items
1126 // Pot items now have their own position from ROM data
1127 // No need to match to objects - each item has exact coordinates
1128 for (const auto& pot_item : pot_items_) {
1129 if (pot_item.item != 0) { // Skip "Nothing" items
1130 // PotItem provides pixel coordinates, convert to tile coords
1131 int tile_x = pot_item.GetTileX();
1132 int tile_y = pot_item.GetTileY();
1133 drawer.DrawPotItem(pot_item.item, tile_x, tile_y, object_bg1_buffer_);
1134 }
1135 }
1136
1137 // Render sprites (for key drops)
1138 // We don't have full sprite rendering yet, but we can visualize key drops
1139 for (const auto& sprite : sprites_) {
1140 if (sprite.key_drop() > 0) {
1141 // Draw key drop visualization
1142 // Use a special item ID or just draw a key icon
1143 // We can reuse DrawPotItem with a special ID for key
1144 // Or add DrawKeyDrop to ObjectDrawer
1145 // For now, let's use DrawPotItem with ID 0xFD (Small Key) or 0xFE (Big Key)
1146 uint8_t key_item = (sprite.key_drop() == 1) ? 0xFD : 0xFE;
1147 drawer.DrawPotItem(key_item, sprite.x(), sprite.y(), object_bg1_buffer_);
1148 }
1149 }
1150
1151 // Special tables pass (USDASM-aligned):
1152 // - Pushable blocks: bank_01.asm RoomDraw_PushableBlock uses RoomDrawObjectData
1153 // offset $0E52 (bank_00.asm #obj0E52).
1154 // - Lightable torches: bank_01.asm RoomDraw_LightableTorch chooses between
1155 // offsets $0EC2 (unlit) and $0ECA (lit) (bank_00.asm #obj0EC2/#obj0ECA).
1156 constexpr uint16_t kRoomDrawObj_PushableBlock = 0x0E52;
1157 constexpr uint16_t kRoomDrawObj_TorchUnlit = 0x0EC2;
1158 constexpr uint16_t kRoomDrawObj_TorchLit = 0x0ECA;
1159 for (const auto& obj : tile_objects_) {
1160 if ((obj.options() & ObjectOption::Block) != ObjectOption::Nothing) {
1161 (void)drawer.DrawRoomDrawObjectData2x2(
1162 static_cast<uint16_t>(obj.id_), obj.x_, obj.y_, obj.layer_,
1163 kRoomDrawObj_PushableBlock, object_bg1_buffer_, object_bg2_buffer_);
1164 continue;
1165 }
1166 if ((obj.options() & ObjectOption::Torch) != ObjectOption::Nothing) {
1167 const uint16_t off = obj.lit_ ? kRoomDrawObj_TorchLit
1168 : kRoomDrawObj_TorchUnlit;
1169 (void)drawer.DrawRoomDrawObjectData2x2(
1170 static_cast<uint16_t>(obj.id_), obj.x_, obj.y_, obj.layer_, off,
1172 continue;
1173 }
1174 }
1175
1176 // Log only failures, not successes
1177 if (!status.ok()) {
1178 LOG_DEBUG(
1179 "[RenderObjectsToBackground]",
1180 "ObjectDrawer failed: %s - FALLING BACK TO MANUAL",
1181 std::string(status.message().data(), status.message().size()).c_str());
1182
1183 LOG_DEBUG("[RenderObjectsToBackground]",
1184 "Room %d: Manual rendering objects (fallback)", room_id_);
1185 auto& bg1_bmp = bg1_buffer_.bitmap();
1186 // Simple manual rendering: draw a colored rectangle for each object
1187 for (const auto& obj : tile_objects_) {
1188 int x = obj.x() * 8;
1189 int y = obj.y() * 8;
1190 int width = 16; // Default size for manual draw
1191 int height = 16;
1192
1193 // Basic layer-based coloring for manual mode
1194 uint8_t color_idx = 0; // Default transparent
1195 if (obj.GetLayerValue() == 0) {
1196 color_idx = 5; // Example: Reddish for Layer 0
1197 } else if (obj.GetLayerValue() == 1) {
1198 color_idx = 10; // Example: Greenish for Layer 1
1199 } else {
1200 color_idx = 15; // Example: Bluish for Layer 2
1201 }
1202 // Draw simple rectangle using WriteToBGRSurface
1203 for (int py = y; py < y + height && py < bg1_bmp.height(); ++py) {
1204 for (int px = x; px < x + width && px < bg1_bmp.width(); ++px) {
1205 int pixel_offset = (py * bg1_bmp.width()) + px;
1206 bg1_bmp.WriteToPixel(pixel_offset, color_idx);
1207 }
1208 }
1209 }
1210 dirty_state_.objects = false; // Mark as clean after manual draw
1211 } else {
1212 // Mark objects as clean after successful render
1213 dirty_state_.objects = false;
1214 LOG_DEBUG("[RenderObjectsToBackground]",
1215 "Room %d: Objects rendered successfully", room_id_);
1216 }
1217}
1218
1219// LoadGraphicsSheetsIntoArena() removed - using per-room graphics instead
1220// Room rendering no longer depends on Arena graphics sheets
1221
1223 if (!rom_ || !rom_->is_loaded()) {
1224 return;
1225 }
1226
1227 if (!game_data_) {
1228 return;
1229 }
1230 auto* gfx_buffer_data = &game_data_->graphics_buffer;
1231 if (gfx_buffer_data->empty()) {
1232 return;
1233 }
1234
1235 auto rom_data = rom()->vector();
1236 if (rom_data.empty()) {
1237 return;
1238 }
1239
1240 // Validate animated_frame_ bounds
1241 if (animated_frame_ < 0 || animated_frame_ > 10) {
1242 return;
1243 }
1244
1245 // Validate background_tileset_ bounds
1246 if (background_tileset_ < 0 || background_tileset_ > 255) {
1247 return;
1248 }
1249
1250 int gfx_ptr = SnesToPc(version_constants().kGfxAnimatedPointer);
1251 if (gfx_ptr < 0 || gfx_ptr >= static_cast<int>(rom_data.size())) {
1252 return;
1253 }
1254
1255 int data = 0;
1256 while (data < 1024) {
1257 // Validate buffer access for first operation
1258 // 92 * 4096 = 376832. 1024 * 10 = 10240. Total ~387KB.
1259 int first_offset = data + (92 * 4096) + (1024 * animated_frame_);
1260 if (first_offset >= 0 &&
1261 first_offset < static_cast<int>(gfx_buffer_data->size())) {
1262 uint8_t map_byte = (*gfx_buffer_data)[first_offset];
1263
1264 // Validate current_gfx16_ access
1265 int gfx_offset = data + (7 * 4096);
1266 if (gfx_offset >= 0 &&
1267 gfx_offset < static_cast<int>(current_gfx16_.size())) {
1268 current_gfx16_[gfx_offset] = map_byte;
1269 }
1270 }
1271
1272 // Validate buffer access for second operation
1273 int tileset_index = rom_data[gfx_ptr + background_tileset_];
1274 int second_offset =
1275 data + (tileset_index * 4096) + (1024 * animated_frame_);
1276 if (second_offset >= 0 &&
1277 second_offset < static_cast<int>(gfx_buffer_data->size())) {
1278 uint8_t map_byte = (*gfx_buffer_data)[second_offset];
1279
1280 // Validate current_gfx16_ access
1281 int gfx_offset = data + (7 * 4096) - 1024;
1282 if (gfx_offset >= 0 &&
1283 gfx_offset < static_cast<int>(current_gfx16_.size())) {
1284 current_gfx16_[gfx_offset] = map_byte;
1285 }
1286 }
1287
1288 data++;
1289 }
1290}
1291
1293 LOG_DEBUG("[LoadObjects]", "Starting LoadObjects for room %d", room_id_);
1294 auto rom_data = rom()->vector();
1295
1296 // Enhanced object loading with comprehensive validation
1297 int object_pointer = (rom_data[kRoomObjectPointer + 2] << 16) +
1298 (rom_data[kRoomObjectPointer + 1] << 8) +
1299 (rom_data[kRoomObjectPointer]);
1300 object_pointer = SnesToPc(object_pointer);
1301
1302 // Enhanced bounds checking for object pointer
1303 if (object_pointer < 0 || object_pointer >= (int)rom_->size()) {
1304 return;
1305 }
1306
1307 int room_address = object_pointer + (room_id_ * 3);
1308
1309 // Enhanced bounds checking for room address
1310 if (room_address < 0 || room_address + 2 >= (int)rom_->size()) {
1311 return;
1312 }
1313
1314 int tile_address = (rom_data[room_address + 2] << 16) +
1315 (rom_data[room_address + 1] << 8) + rom_data[room_address];
1316
1317 int objects_location = SnesToPc(tile_address);
1318
1319 // Enhanced bounds checking for objects location
1320 if (objects_location < 0 || objects_location >= (int)rom_->size()) {
1321 return;
1322 }
1323
1324 // Parse floor graphics and layout with validation
1325 if (objects_location + 1 < (int)rom_->size()) {
1326 if (is_floor_) {
1328 static_cast<uint8_t>(rom_data[objects_location] & 0x0F);
1330 static_cast<uint8_t>((rom_data[objects_location] >> 4) & 0x0F);
1331 LOG_DEBUG("[LoadObjects]",
1332 "Room %d: Set floor1_graphics_=%d, floor2_graphics_=%d",
1334 }
1335
1336 layout_id_ = static_cast<uint8_t>((rom_data[objects_location + 1] >> 2) & 0x07);
1337 }
1338
1339 LoadChests();
1340
1341 // Parse objects with enhanced error handling
1342 ParseObjectsFromLocation(objects_location + 2);
1343
1344 // Load custom collision map if present
1345 if (auto res = LoadCustomCollisionMap(rom_, room_id_); res.ok()) {
1346 custom_collision_ = std::move(res.value());
1347 }
1348
1349 // Freshly loaded from ROM; not dirty until the editor mutates it.
1351}
1352
1353void Room::ParseObjectsFromLocation(int objects_location) {
1354 auto rom_data = rom()->vector();
1355
1356 // Clear existing objects before parsing to prevent accumulation on reload
1357 tile_objects_.clear();
1358 doors_.clear();
1359 z3_staircases_.clear();
1360 int nbr_of_staircase = 0;
1361
1362 int pos = objects_location;
1363 uint8_t b1 = 0;
1364 uint8_t b2 = 0;
1365 uint8_t b3 = 0;
1366 int layer = 0;
1367 bool door = false;
1368 bool end_read = false;
1369
1370 // Enhanced parsing loop with bounds checking
1371 // ASM: Main object loop logic (implicit in structure)
1372 while (!end_read && pos < (int)rom_->size()) {
1373 // Check if we have enough bytes to read
1374 if (pos + 1 >= (int)rom_->size()) {
1375 break;
1376 }
1377
1378 b1 = rom_data[pos];
1379 b2 = rom_data[pos + 1];
1380
1381 // ASM Marker: 0xFF 0xFF - End of Layer
1382 // Signals transition between object layers:
1383 // Layer 0 -> BG1 buffer (main floor/walls)
1384 // Layer 1 -> BG2 buffer (overlay layer)
1385 // Layer 2 -> BG1 buffer (priority objects, still on BG1)
1386 if (b1 == 0xFF && b2 == 0xFF) {
1387 pos += 2; // Jump to next layer
1388 layer++;
1389 LOG_DEBUG("Room", "Room %03X: Layer transition to layer %d (%s)",
1390 room_id_, layer,
1391 layer == 1 ? "BG2" : (layer == 2 ? "BG3" : "END"));
1392 door = false;
1393 if (layer == 3) {
1394 break;
1395 }
1396 continue;
1397 }
1398
1399 // ASM Marker: 0xF0 0xFF - Start of Door List
1400 // See RoomDraw_DoorObject ($018916) logic
1401 if (b1 == 0xF0 && b2 == 0xFF) {
1402 pos += 2; // Jump to door section
1403 door = true;
1404 continue;
1405 }
1406
1407 // Check if we have enough bytes for object data
1408 if (pos + 2 >= (int)rom_->size()) {
1409 break;
1410 }
1411
1412 b3 = rom_data[pos + 2];
1413 if (door) {
1414 pos += 2;
1415 } else {
1416 pos += 3;
1417 }
1418
1419 if (!door) {
1420 // ASM: RoomDraw_RoomObject ($01893C)
1421 // Handles Subtype 1, 2, 3 parsing based on byte values
1423 b1, b2, b3, static_cast<uint8_t>(layer));
1424
1425 LOG_DEBUG("Room", "Room %03X: Object 0x%03X at (%d,%d) layer=%d (%s)",
1426 room_id_, r.id_, r.x_, r.y_, layer,
1427 layer == 0 ? "BG1" : (layer == 1 ? "BG2" : "BG3"));
1428
1429 // Validate object ID before adding to the room
1430 // Object IDs can be up to 12-bit (0xFFF) to support Type 3 objects
1431 if (r.id_ >= 0 && r.id_ <= 0xFFF) {
1432 r.SetRom(rom_);
1433 tile_objects_.push_back(r);
1434
1435 // Handle special object types (staircases, chests, etc.)
1436 HandleSpecialObjects(r.id_, r.x(), r.y(), nbr_of_staircase);
1437 }
1438 } else {
1439 // Handle door objects
1440 // ASM format (from RoomDraw_DoorObject):
1441 // b1: bits 4-7 = position index, bits 0-1 = direction
1442 // b2: door type (full byte)
1443 auto door = Door::FromRomBytes(b1, b2);
1444 LOG_DEBUG("Room",
1445 "ParseDoor: room=%d b1=0x%02X b2=0x%02X pos=%d dir=%d type=%d",
1446 room_id_, b1, b2, door.position,
1447 static_cast<int>(door.direction), static_cast<int>(door.type));
1448 doors_.push_back(door);
1449 }
1450 }
1451}
1452
1453// ============================================================================
1454// Object Saving Implementation (Phase 1, Task 1.3)
1455// ============================================================================
1456
1457std::vector<uint8_t> Room::EncodeObjects() const {
1458 std::vector<uint8_t> bytes;
1459
1460 // Organize objects by layer
1461 std::vector<RoomObject> layer0_objects;
1462 std::vector<RoomObject> layer1_objects;
1463 std::vector<RoomObject> layer2_objects;
1464
1465 // IMPORTANT: Torches and pushable blocks are stored in global per-dungeon
1466 // tables (see USDASM: LoadAndBuildRoom $01:873A). They are drawn after the
1467 // room object stream passes, so they must never be encoded into the room
1468 // object stream.
1469 for (const auto& obj : tile_objects_) {
1470 if ((obj.options() & ObjectOption::Torch) != ObjectOption::Nothing) {
1471 continue;
1472 }
1473 if ((obj.options() & ObjectOption::Block) != ObjectOption::Nothing) {
1474 continue;
1475 }
1476 switch (obj.GetLayerValue()) {
1477 case 0:
1478 layer0_objects.push_back(obj);
1479 break;
1480 case 1:
1481 layer1_objects.push_back(obj);
1482 break;
1483 case 2:
1484 layer2_objects.push_back(obj);
1485 break;
1486 }
1487 }
1488
1489 // Object stream format (USDASM bank_01.asm LoadAndBuildRoom / RoomDraw_DrawAllObjects):
1490 // - Layer 0 object list (terminated by $FFFF)
1491 // - Layer 1 object list (terminated by $FFFF)
1492 // - Layer 2 object list ends with door marker $FFF0 (bytes F0 FF), then
1493 // 2-byte door entries, and finally $FFFF which terminates both the door
1494 // list and the Layer 2 list.
1495 //
1496 // NOTE: We always emit the door marker and a terminator, even if there are
1497 // zero doors, because vanilla room data does so as well.
1498
1499 // Encode Layer 0
1500 for (const auto& obj : layer0_objects) {
1501 auto encoded = obj.EncodeObjectToBytes();
1502 bytes.push_back(encoded.b1);
1503 bytes.push_back(encoded.b2);
1504 bytes.push_back(encoded.b3);
1505 }
1506 bytes.push_back(0xFF);
1507 bytes.push_back(0xFF);
1508
1509 // Encode Layer 1
1510 for (const auto& obj : layer1_objects) {
1511 auto encoded = obj.EncodeObjectToBytes();
1512 bytes.push_back(encoded.b1);
1513 bytes.push_back(encoded.b2);
1514 bytes.push_back(encoded.b3);
1515 }
1516 bytes.push_back(0xFF);
1517 bytes.push_back(0xFF);
1518
1519 // Encode Layer 2 objects.
1520 for (const auto& obj : layer2_objects) {
1521 auto encoded = obj.EncodeObjectToBytes();
1522 bytes.push_back(encoded.b1);
1523 bytes.push_back(encoded.b2);
1524 bytes.push_back(encoded.b3);
1525 }
1526
1527 // ASM marker 0xF0 0xFF - start of door list (RoomDraw_DrawAllObjects checks
1528 // for word $FFF0).
1529 bytes.push_back(0xF0);
1530 bytes.push_back(0xFF);
1531 for (const auto& door : doors_) {
1532 auto [b1, b2] = door.EncodeBytes();
1533 bytes.push_back(b1);
1534 bytes.push_back(b2);
1535 }
1536
1537 // Door list terminator (word $FFFF). This is also the Layer 2 terminator.
1538 bytes.push_back(0xFF);
1539 bytes.push_back(0xFF);
1540
1541 return bytes;
1542}
1543
1544std::vector<uint8_t> Room::EncodeSprites() const {
1545 std::vector<uint8_t> bytes;
1546
1547 for (const auto& sprite : sprites_) {
1548 uint8_t b1, b2, b3;
1549
1550 // b3 is simply the ID
1551 b3 = sprite.id();
1552
1553 // b2 = (X & 0x1F) | ((Flags & 0x07) << 5)
1554 // Flags 0-2 come from b2 5-7
1555 b2 = (sprite.x() & 0x1F) | ((sprite.subtype() & 0x07) << 5);
1556
1557 // b1 = (Y & 0x1F) | ((Flags & 0x18) << 2) | ((Layer & 1) << 7)
1558 // Flags 3-4 come from b1 5-6. (0x18 is 00011000)
1559 // Layer bit 0 comes from b1 7
1560 b1 = (sprite.y() & 0x1F) | ((sprite.subtype() & 0x18) << 2) |
1561 ((sprite.layer() & 0x01) << 7);
1562
1563 bytes.push_back(b1);
1564 bytes.push_back(b2);
1565 bytes.push_back(b3);
1566 }
1567
1568 // Terminator
1569 bytes.push_back(0xFF);
1570
1571 return bytes;
1572}
1573
1575 if (!rom || !rom->is_loaded()) {
1576 return kSpritesEndData;
1577 }
1578
1579 const auto& rom_data = rom->vector();
1580 int sprite_pointer = 0;
1581 if (!GetSpritePointerTablePc(rom_data, &sprite_pointer).ok()) {
1582 return kSpritesEndData;
1583 }
1584
1585 const int hard_end = std::min(static_cast<int>(rom_data.size()), kSpritesEndData);
1586 if (hard_end <= 0) {
1587 return kSpritesEndData;
1588 }
1589
1590 int max_used = std::min(hard_end, kSpritesData);
1591 std::unordered_set<int> visited_addresses;
1592 for (int room_id = 0; room_id < kNumberOfRooms; ++room_id) {
1593 int sprite_address = ReadRoomSpriteAddressPc(rom_data, sprite_pointer, room_id);
1594 if (sprite_address < kSpritesData || sprite_address >= hard_end) {
1595 continue;
1596 }
1597 if (!visited_addresses.insert(sprite_address).second) {
1598 continue;
1599 }
1600
1601 int stream_size = MeasureSpriteStreamSize(rom_data, sprite_address, hard_end);
1602 int stream_end = sprite_address + stream_size;
1603 if (stream_end > max_used) {
1604 max_used = stream_end;
1605 }
1606 }
1607
1608 return max_used;
1609}
1610
1611absl::Status RelocateSpriteData(Rom* rom, int room_id,
1612 const std::vector<uint8_t>& encoded_bytes) {
1613 if (!rom || !rom->is_loaded()) {
1614 return absl::InvalidArgumentError("ROM not loaded");
1615 }
1616 if (room_id < 0 || room_id >= kNumberOfRooms) {
1617 return absl::OutOfRangeError("Room ID out of range");
1618 }
1619 if (encoded_bytes.empty() || encoded_bytes.back() != 0xFF ||
1620 (encoded_bytes.size() % 3) != 1) {
1621 return absl::InvalidArgumentError(
1622 "Encoded sprite payload must be N*3 bytes plus 0xFF terminator");
1623 }
1624
1625 const auto& rom_data = rom->vector();
1626 int sprite_pointer = 0;
1627 RETURN_IF_ERROR(GetSpritePointerTablePc(rom_data, &sprite_pointer));
1628
1629 int old_sprite_address =
1630 ReadRoomSpriteAddressPc(rom_data, sprite_pointer, room_id);
1631 if (old_sprite_address < 0 ||
1632 old_sprite_address >= static_cast<int>(rom_data.size())) {
1633 return absl::OutOfRangeError("Sprite address out of range");
1634 }
1635
1636 const int hard_end = std::min(static_cast<int>(rom_data.size()), kSpritesEndData);
1637 const int old_stream_size =
1638 MeasureSpriteStreamSize(rom_data, old_sprite_address, hard_end);
1639 const uint8_t sort_mode = rom_data[old_sprite_address];
1640 const bool old_pointer_shared =
1641 IsSpritePointerShared(rom_data, sprite_pointer, room_id, old_sprite_address);
1642
1643 const int write_pos = FindMaxUsedSpriteAddress(rom);
1644 const size_t required_size = 1u + encoded_bytes.size();
1645 if (write_pos < kSpritesData ||
1646 static_cast<size_t>(write_pos) + required_size >
1647 static_cast<size_t>(kSpritesEndData)) {
1648 return absl::ResourceExhaustedError(absl::StrFormat(
1649 "Not enough sprite data space. Need %d bytes at 0x%06X, "
1650 "region ends at 0x%06X",
1651 static_cast<int>(required_size), write_pos, kSpritesEndData));
1652 }
1653 if (static_cast<size_t>(write_pos) + required_size > rom_data.size()) {
1654 const int required_end = write_pos + static_cast<int>(required_size);
1655 return absl::OutOfRangeError(absl::StrFormat(
1656 "ROM too small for sprite relocation write (need end=0x%06X, size=0x%06X)",
1657 required_end, static_cast<int>(rom_data.size())));
1658 }
1659
1660 std::vector<uint8_t> relocated;
1661 relocated.reserve(required_size);
1662 relocated.push_back(sort_mode);
1663 relocated.insert(relocated.end(), encoded_bytes.begin(), encoded_bytes.end());
1664 RETURN_IF_ERROR(rom->WriteVector(write_pos, std::move(relocated)));
1665
1666 const uint32_t snes_addr = PcToSnes(write_pos);
1667 const int ptr_off = sprite_pointer + (room_id * 2);
1668 RETURN_IF_ERROR(rom->WriteByte(ptr_off, snes_addr & 0xFF));
1669 RETURN_IF_ERROR(rom->WriteByte(ptr_off + 1, (snes_addr >> 8) & 0xFF));
1670
1671 if (!old_pointer_shared && old_stream_size > 0 &&
1672 old_sprite_address + old_stream_size <= static_cast<int>(rom_data.size())) {
1674 rom->WriteVector(old_sprite_address,
1675 std::vector<uint8_t>(old_stream_size, 0x00)));
1676 }
1677
1678 return absl::OkStatus();
1679}
1680
1681absl::Status Room::SaveObjects() {
1682 if (rom_ == nullptr) {
1683 return absl::InvalidArgumentError("ROM pointer is null");
1684 }
1685
1686 auto rom_data = rom()->vector();
1687
1688 // Get object pointer
1689 int object_pointer = (rom_data[kRoomObjectPointer + 2] << 16) +
1690 (rom_data[kRoomObjectPointer + 1] << 8) +
1691 (rom_data[kRoomObjectPointer]);
1692 object_pointer = SnesToPc(object_pointer);
1693
1694 if (object_pointer < 0 || object_pointer >= (int)rom_->size()) {
1695 return absl::OutOfRangeError("Object pointer out of range");
1696 }
1697
1698 int room_address = object_pointer + (room_id_ * 3);
1699
1700 if (room_address < 0 || room_address + 2 >= (int)rom_->size()) {
1701 return absl::OutOfRangeError("Room address out of range");
1702 }
1703
1704 int tile_address = (rom_data[room_address + 2] << 16) +
1705 (rom_data[room_address + 1] << 8) + rom_data[room_address];
1706
1707 int objects_location = SnesToPc(tile_address);
1708
1709 if (objects_location < 0 || objects_location >= (int)rom_->size()) {
1710 return absl::OutOfRangeError("Objects location out of range");
1711 }
1712
1713 // Calculate available space
1714 RoomSize room_size_info = CalculateRoomSize(rom_, room_id_);
1715 int available_size = room_size_info.room_size;
1716
1717 // Skip graphics/layout header (2 bytes)
1718 int write_pos = objects_location + 2;
1719
1720 // Encode all objects
1721 auto encoded_bytes = EncodeObjects();
1722
1723 // VALIDATION: Check if new data fits in available space
1724 // We subtract 2 bytes for the header which is not part of encoded_bytes
1725 if (encoded_bytes.size() > available_size - 2) {
1726 return absl::OutOfRangeError(absl::StrFormat(
1727 "Room %d object data too large! Size: %d, Available: %d", room_id_,
1728 encoded_bytes.size(), available_size - 2));
1729 }
1730
1731 // Write encoded bytes to ROM (includes 0xF0 0xFF + door list)
1732 RETURN_IF_ERROR(rom_->WriteVector(write_pos, encoded_bytes));
1733
1734 // Write door pointer: first byte after 0xF0 0xFF (per ZScreamDungeon Save.cs)
1735 const int door_list_offset = static_cast<int>(encoded_bytes.size()) -
1736 static_cast<int>(doors_.size()) * 2 - 2;
1737 const int door_pointer_pc = write_pos + door_list_offset;
1740 static_cast<uint32_t>(PcToSnes(door_pointer_pc))));
1741
1742 return absl::OkStatus();
1743}
1744
1745absl::Status Room::SaveSprites() {
1746 if (rom_ == nullptr) {
1747 return absl::InvalidArgumentError("ROM pointer is null");
1748 }
1749
1750 const auto& rom_data = rom()->vector();
1751 if (room_id_ < 0 || room_id_ >= kNumberOfRooms) {
1752 return absl::OutOfRangeError("Room ID out of range");
1753 }
1754
1755 int sprite_pointer = 0;
1756 RETURN_IF_ERROR(GetSpritePointerTablePc(rom_data, &sprite_pointer));
1757 int sprite_address =
1758 ReadRoomSpriteAddressPc(rom_data, sprite_pointer, room_id_);
1759 if (sprite_address < 0 || sprite_address >= static_cast<int>(rom_->size())) {
1760 return absl::OutOfRangeError("Sprite address out of range");
1761 }
1762
1763 int available_payload_size = 0;
1764 if (room_id_ + 1 < kNumberOfRooms) {
1765 int next_sprite_address =
1766 ReadRoomSpriteAddressPc(rom_data, sprite_pointer, room_id_ + 1);
1767 if (next_sprite_address >= 0) {
1768 int available_size = next_sprite_address - sprite_address;
1769 if (available_size > 0 && available_size <= 0x1000) {
1770 available_payload_size = std::max(0, available_size - 1);
1771 }
1772 }
1773 }
1774
1775 if (available_payload_size == 0) {
1776 const int hard_end =
1777 std::min(static_cast<int>(rom_data.size()), kSpritesEndData);
1778 const int current_stream_size =
1779 MeasureSpriteStreamSize(rom_data, sprite_address, hard_end);
1780 available_payload_size = std::max(0, current_stream_size - 1);
1781 }
1782
1783 const int payload_address = sprite_address + 1;
1784 if (payload_address < 0 || payload_address >= static_cast<int>(rom_->size())) {
1785 return absl::OutOfRangeError(absl::StrFormat(
1786 "Room %d has invalid sprite payload address", room_id_));
1787 }
1788
1789 auto encoded_bytes = EncodeSprites();
1790 if (static_cast<int>(encoded_bytes.size()) > available_payload_size) {
1791 return RelocateSpriteData(rom_, room_id_, encoded_bytes);
1792 }
1793
1794 return rom_->WriteVector(payload_address, encoded_bytes);
1795}
1796
1797absl::Status Room::SaveRoomHeader() {
1798 if (rom_ == nullptr) {
1799 return absl::InvalidArgumentError("ROM pointer is null");
1800 }
1801
1802 const auto& rom_data = rom()->vector();
1803 if (kRoomHeaderPointer < 0 ||
1804 kRoomHeaderPointer + 2 >= static_cast<int>(rom_data.size())) {
1805 return absl::OutOfRangeError("Room header pointer out of range");
1806 }
1807 if (kRoomHeaderPointerBank < 0 ||
1808 kRoomHeaderPointerBank >= static_cast<int>(rom_data.size())) {
1809 return absl::OutOfRangeError("Room header pointer bank out of range");
1810 }
1811
1812 int header_pointer = (rom_data[kRoomHeaderPointer + 2] << 16) +
1813 (rom_data[kRoomHeaderPointer + 1] << 8) +
1814 rom_data[kRoomHeaderPointer];
1815 header_pointer = SnesToPc(header_pointer);
1816
1817 int table_offset = header_pointer + (room_id_ * 2);
1818 if (table_offset < 0 ||
1819 table_offset + 1 >= static_cast<int>(rom_data.size())) {
1820 return absl::OutOfRangeError("Room header table offset out of range");
1821 }
1822
1823 int address = (rom_data[kRoomHeaderPointerBank] << 16) +
1824 (rom_data[table_offset + 1] << 8) + rom_data[table_offset];
1825 int header_location = SnesToPc(address);
1826
1827 if (header_location < 0 ||
1828 header_location + 13 >= static_cast<int>(rom_data.size())) {
1829 return absl::OutOfRangeError("Room header location out of range");
1830 }
1831
1832 // Build 14-byte header to match LoadRoomHeaderFromRom layout
1833 uint8_t byte0 = (static_cast<uint8_t>(bg2()) << 5) |
1834 (static_cast<uint8_t>(collision()) << 2) |
1835 (IsLight() ? 1 : 0);
1836 // Preserve the full palette set ID byte (USDASM LoadRoomHeader uses 8-bit).
1837 uint8_t byte1 = palette_;
1838 uint8_t byte7 = (staircase_plane(0) << 2) | (staircase_plane(1) << 4) |
1839 (staircase_plane(2) << 6);
1840
1841 RETURN_IF_ERROR(rom_->WriteByte(header_location + 0, byte0));
1842 RETURN_IF_ERROR(rom_->WriteByte(header_location + 1, byte1));
1843 RETURN_IF_ERROR(rom_->WriteByte(header_location + 2, blockset_));
1844 RETURN_IF_ERROR(rom_->WriteByte(header_location + 3, spriteset_));
1846 rom_->WriteByte(header_location + 4, static_cast<uint8_t>(effect())));
1848 rom_->WriteByte(header_location + 5, static_cast<uint8_t>(tag1())));
1850 rom_->WriteByte(header_location + 6, static_cast<uint8_t>(tag2())));
1851 RETURN_IF_ERROR(rom_->WriteByte(header_location + 7, byte7));
1852 RETURN_IF_ERROR(rom_->WriteByte(header_location + 8, staircase_plane(3)));
1853 RETURN_IF_ERROR(rom_->WriteByte(header_location + 9, holewarp_));
1854 RETURN_IF_ERROR(rom_->WriteByte(header_location + 10, staircase_room(0)));
1855 RETURN_IF_ERROR(rom_->WriteByte(header_location + 11, staircase_room(1)));
1856 RETURN_IF_ERROR(rom_->WriteByte(header_location + 12, staircase_room(2)));
1857 RETURN_IF_ERROR(rom_->WriteByte(header_location + 13, staircase_room(3)));
1858
1859 int msg_addr = kMessagesIdDungeon + (room_id_ * 2);
1860 if (msg_addr < 0 || msg_addr + 1 >= static_cast<int>(rom_data.size())) {
1861 return absl::OutOfRangeError("Message ID address out of range");
1862 }
1864
1865 return absl::OkStatus();
1866}
1867
1868// ============================================================================
1869// Object Manipulation Methods (Phase 3)
1870// ============================================================================
1871
1872absl::Status Room::AddObject(const RoomObject& object) {
1873 // Validate object
1874 if (!ValidateObject(object)) {
1875 return absl::InvalidArgumentError("Invalid object parameters");
1876 }
1877
1878 // Add to internal list
1879 tile_objects_.push_back(object);
1881
1882 return absl::OkStatus();
1883}
1884
1885absl::Status Room::RemoveObject(size_t index) {
1886 if (index >= tile_objects_.size()) {
1887 return absl::OutOfRangeError("Object index out of range");
1888 }
1889
1890 tile_objects_.erase(tile_objects_.begin() + index);
1892
1893 return absl::OkStatus();
1894}
1895
1896absl::Status Room::UpdateObject(size_t index, const RoomObject& object) {
1897 if (index >= tile_objects_.size()) {
1898 return absl::OutOfRangeError("Object index out of range");
1899 }
1900
1901 if (!ValidateObject(object)) {
1902 return absl::InvalidArgumentError("Invalid object parameters");
1903 }
1904
1905 tile_objects_[index] = object;
1907
1908 return absl::OkStatus();
1909}
1910
1911absl::StatusOr<size_t> Room::FindObjectAt(int x, int y, int layer) const {
1912 for (size_t i = 0; i < tile_objects_.size(); i++) {
1913 const auto& obj = tile_objects_[i];
1914 if (obj.x() == x && obj.y() == y && obj.GetLayerValue() == layer) {
1915 return i;
1916 }
1917 }
1918 return absl::NotFoundError("No object found at position");
1919}
1920
1921bool Room::ValidateObject(const RoomObject& object) const {
1922 // Validate position (0-63 for both X and Y)
1923 if (object.x() < 0 || object.x() > 63)
1924 return false;
1925 if (object.y() < 0 || object.y() > 63)
1926 return false;
1927
1928 // Validate layer (0-2)
1929 if (object.GetLayerValue() < 0 || object.GetLayerValue() > 2)
1930 return false;
1931
1932 // Validate object ID range
1933 if (object.id_ < 0 || object.id_ > 0xFFF)
1934 return false;
1935
1936 // Validate size for Type 1 objects
1937 if (object.id_ < 0x100 && object.size() > 15)
1938 return false;
1939
1940 return true;
1941}
1942
1943void Room::HandleSpecialObjects(short oid, uint8_t posX, uint8_t posY,
1944 int& nbr_of_staircase) {
1945 // Handle staircase objects
1946 for (short stair : kStairsObjects) {
1947 if (stair == oid) {
1948 if (nbr_of_staircase < 4) {
1949 tile_objects_.back().set_options(ObjectOption::Stairs |
1950 tile_objects_.back().options());
1951 z3_staircases_.push_back(
1952 {posX, posY,
1953 absl::StrCat("To ", staircase_rooms_[nbr_of_staircase]).data()});
1954 nbr_of_staircase++;
1955 } else {
1956 tile_objects_.back().set_options(ObjectOption::Stairs |
1957 tile_objects_.back().options());
1958 z3_staircases_.push_back({posX, posY, "To ???"});
1959 }
1960 break;
1961 }
1962 }
1963
1964 // Handle chest objects
1965 if (oid == 0xF99) {
1966 if (chests_in_room_.size() > 0) {
1967 tile_objects_.back().set_options(ObjectOption::Chest |
1968 tile_objects_.back().options());
1969 chests_in_room_.erase(chests_in_room_.begin());
1970 }
1971 } else if (oid == 0xFB1) {
1972 if (chests_in_room_.size() > 0) {
1973 tile_objects_.back().set_options(ObjectOption::Chest |
1974 tile_objects_.back().options());
1975 chests_in_room_.erase(chests_in_room_.begin());
1976 }
1977 }
1978}
1979
1981 const auto& rom_data = rom()->vector();
1982 // Avoid duplicate entries if callers reload sprite data on the same room.
1983 sprites_.clear();
1984 if (room_id_ < 0 || room_id_ >= kNumberOfRooms) {
1985 return;
1986 }
1987
1988 int sprite_pointer = 0;
1989 if (!GetSpritePointerTablePc(rom_data, &sprite_pointer).ok()) {
1990 return;
1991 }
1992
1993 int sprite_address =
1994 ReadRoomSpriteAddressPc(rom_data, sprite_pointer, room_id_);
1995 if (sprite_address < 0 || sprite_address + 1 >= static_cast<int>(rom_data.size())) {
1996 return;
1997 }
1998
1999 // First byte is the SortSprites mode (0 or 1), not sprite data.
2000 sprite_address += 1;
2001
2002 while (sprite_address + 2 < static_cast<int>(rom_data.size())) {
2003 uint8_t b1 = rom_data[sprite_address];
2004 uint8_t b2 = rom_data[sprite_address + 1];
2005 uint8_t b3 = rom_data[sprite_address + 2];
2006
2007 if (b1 == 0xFF) {
2008 break;
2009 }
2010
2011 sprites_.emplace_back(b3, (b2 & 0x1F), (b1 & 0x1F),
2012 ((b2 & 0xE0) >> 5) + ((b1 & 0x60) >> 2),
2013 (b1 & 0x80) >> 7);
2014
2015 if (sprites_.size() > 1) {
2016 Sprite& spr = sprites_.back();
2017 Sprite& prevSprite = sprites_[sprites_.size() - 2];
2018
2019 if (spr.id() == 0xE4 && spr.x() == 0x00 && spr.y() == 0x1E &&
2020 spr.layer() == 1 && spr.subtype() == 0x18) {
2021 prevSprite.set_key_drop(1);
2022 sprites_.pop_back();
2023 }
2024
2025 if (spr.id() == 0xE4 && spr.x() == 0x00 && spr.y() == 0x1D &&
2026 spr.layer() == 1 && spr.subtype() == 0x18) {
2027 prevSprite.set_key_drop(2);
2028 sprites_.pop_back();
2029 }
2030 }
2031
2032 sprite_address += 3;
2033 }
2034}
2035
2037 auto rom_data = rom()->vector();
2038 uint32_t cpos = SnesToPc((rom_data[kChestsDataPointer1 + 2] << 16) +
2039 (rom_data[kChestsDataPointer1 + 1] << 8) +
2040 (rom_data[kChestsDataPointer1]));
2041 size_t clength = (rom_data[kChestsLengthPointer + 1] << 8) +
2042 (rom_data[kChestsLengthPointer]);
2043
2044 for (size_t i = 0; i < clength; i++) {
2045 if ((((rom_data[cpos + (i * 3) + 1] << 8) + (rom_data[cpos + (i * 3)])) &
2046 0x7FFF) == room_id_) {
2047 // There's a chest in that room !
2048 bool big = false;
2049 if ((((rom_data[cpos + (i * 3) + 1] << 8) + (rom_data[cpos + (i * 3)])) &
2050 0x8000) == 0x8000) {
2051 big = true;
2052 }
2053
2054 chests_in_room_.emplace_back(
2055 chest_data{rom_data[cpos + (i * 3) + 2], big});
2056 }
2057 }
2058}
2059
2061 auto rom_data = rom()->vector();
2062
2063 // Doors are loaded as part of the object stream in LoadObjects()
2064 // When the parser encounters 0xF0 0xFF, it enters door mode
2065 // Door objects have format: b1 (position/direction), b2 (type)
2066 // Door encoding: b1 = (door_pos << 4) | (door_dir & 0x03)
2067 // position in bits 4-7, direction in bits 0-1
2068 // b2 = door_type (full byte, values 0x00, 0x02, 0x04, etc.)
2069 // This is already handled in ParseObjectsFromLocation()
2070
2071 LOG_DEBUG("Room",
2072 "LoadDoors for room %d - doors are loaded via object stream",
2073 room_id_);
2074}
2075
2077 auto rom_data = rom()->vector();
2078
2079 // Read torch data length
2080 int bytes_count = (rom_data[kTorchesLengthPointer + 1] << 8) |
2081 rom_data[kTorchesLengthPointer];
2082
2083 LOG_DEBUG("Room", "LoadTorches: room_id=%d, bytes_count=%d", room_id_,
2084 bytes_count);
2085
2086 // Avoid duplication if LoadTorches is called multiple times.
2087 tile_objects_.erase(
2088 std::remove_if(tile_objects_.begin(), tile_objects_.end(),
2089 [](const RoomObject& obj) {
2090 return (obj.options() & ObjectOption::Torch) !=
2091 ObjectOption::Nothing;
2092 }),
2093 tile_objects_.end());
2094
2095 // Iterate through torch data to find torches for this room
2096 for (int i = 0; i < bytes_count; i += 2) {
2097 if (i + 1 >= bytes_count)
2098 break;
2099
2100 uint8_t b1 = rom_data[kTorchData + i];
2101 uint8_t b2 = rom_data[kTorchData + i + 1];
2102
2103 // Skip 0xFFFF markers
2104 if (b1 == 0xFF && b2 == 0xFF) {
2105 continue;
2106 }
2107
2108 // Check if this entry is for our room
2109 uint16_t torch_room_id = (b2 << 8) | b1;
2110 if (torch_room_id == room_id_) {
2111 // Found torches for this room, read them
2112 i += 2;
2113 while (i < bytes_count) {
2114 if (i + 1 >= bytes_count)
2115 break;
2116
2117 b1 = rom_data[kTorchData + i];
2118 b2 = rom_data[kTorchData + i + 1];
2119
2120 // End of torch list for this room
2121 if (b1 == 0xFF && b2 == 0xFF) {
2122 break;
2123 }
2124
2125 // Decode torch position and properties
2126 int address = ((b2 & 0x1F) << 8 | b1) >> 1;
2127 uint8_t px = address % 64;
2128 uint8_t py = address >> 6;
2129 uint8_t layer = (b2 & 0x20) >> 5;
2130 bool lit = (b2 & 0x80) == 0x80;
2131
2132 // Create torch object (ID 0x150)
2133 RoomObject torch_obj(0x150, px, py, 0, layer);
2134 torch_obj.SetRom(rom_);
2136 torch_obj.lit_ = lit;
2137
2138 tile_objects_.push_back(torch_obj);
2139
2140 LOG_DEBUG("Room", "Loaded torch at (%d,%d) layer=%d lit=%d", px, py,
2141 layer, lit);
2142
2143 i += 2;
2144 }
2145 break; // Found and processed our room's torches
2146 } else {
2147 // Skip to next room's torches
2148 i += 2;
2149 while (i < bytes_count) {
2150 if (i + 1 >= bytes_count)
2151 break;
2152 b1 = rom_data[kTorchData + i];
2153 b2 = rom_data[kTorchData + i + 1];
2154 if (b1 == 0xFF && b2 == 0xFF) {
2155 break;
2156 }
2157 i += 2;
2158 }
2159 }
2160 }
2161}
2162
2163namespace {
2164
2165constexpr int kTorchesMaxSize = 0x120; // ZScream Constants.TorchesMaxSize
2166
2167// Parse current ROM torch blob into per-room segments for preserve-merge.
2168std::vector<std::vector<uint8_t>> ParseRomTorchSegments(
2169 const std::vector<uint8_t>& rom_data, int bytes_count) {
2170 std::vector<std::vector<uint8_t>> segments(kNumberOfRooms);
2171 int i = 0;
2172 while (i + 1 < bytes_count && i < kTorchesMaxSize) {
2173 uint8_t b1 = rom_data[kTorchData + i];
2174 uint8_t b2 = rom_data[kTorchData + i + 1];
2175 if (b1 == 0xFF && b2 == 0xFF) {
2176 i += 2;
2177 continue;
2178 }
2179 uint16_t room_id = (b2 << 8) | b1;
2180 if (room_id >= kNumberOfRooms) {
2181 i += 2;
2182 continue;
2183 }
2184 std::vector<uint8_t> seg;
2185 seg.push_back(b1);
2186 seg.push_back(b2);
2187 i += 2;
2188 while (i + 1 < bytes_count && i < kTorchesMaxSize) {
2189 b1 = rom_data[kTorchData + i];
2190 b2 = rom_data[kTorchData + i + 1];
2191 if (b1 == 0xFF && b2 == 0xFF) {
2192 seg.push_back(0xFF);
2193 seg.push_back(0xFF);
2194 i += 2;
2195 break;
2196 }
2197 seg.push_back(b1);
2198 seg.push_back(b2);
2199 i += 2;
2200 }
2201 if (room_id < segments.size()) {
2202 segments[room_id] = std::move(seg);
2203 }
2204 }
2205 return segments;
2206}
2207
2208} // namespace
2209
2210absl::Status SaveAllTorches(Rom* rom, absl::Span<const Room> rooms) {
2211 if (!rom || !rom->is_loaded()) {
2212 return absl::InvalidArgumentError("ROM not loaded");
2213 }
2214
2215 bool any_torch_objects = false;
2216 for (const auto& room : rooms) {
2217 for (const auto& obj : room.GetTileObjects()) {
2218 if ((obj.options() & ObjectOption::Torch) != ObjectOption::Nothing) {
2219 any_torch_objects = true;
2220 break;
2221 }
2222 }
2223 if (any_torch_objects) {
2224 break;
2225 }
2226 }
2227 if (!any_torch_objects) {
2228 return absl::OkStatus();
2229 }
2230
2231 const auto& rom_data = rom->vector();
2232 int existing_count = (rom_data[kTorchesLengthPointer + 1] << 8) |
2233 rom_data[kTorchesLengthPointer];
2234 if (existing_count > kTorchesMaxSize) {
2235 existing_count = kTorchesMaxSize;
2236 }
2237 auto rom_segments = ParseRomTorchSegments(rom_data, existing_count);
2238
2239 std::vector<uint8_t> bytes;
2240 for (size_t room_id = 0;
2241 room_id < rooms.size() && room_id < rom_segments.size(); ++room_id) {
2242 const auto& room = rooms[room_id];
2243 bool has_torch_objects = false;
2244 for (const auto& obj : room.GetTileObjects()) {
2245 if ((obj.options() & ObjectOption::Torch) != ObjectOption::Nothing) {
2246 has_torch_objects = true;
2247 break;
2248 }
2249 }
2250 if (has_torch_objects) {
2251 bytes.push_back(room_id & 0xFF);
2252 bytes.push_back((room_id >> 8) & 0xFF);
2253 for (const auto& obj : room.GetTileObjects()) {
2254 if ((obj.options() & ObjectOption::Torch) == ObjectOption::Nothing) {
2255 continue;
2256 }
2257 int address = obj.x() + (obj.y() * 64);
2258 int word = address << 1;
2259 uint8_t b1 = word & 0xFF;
2260 uint8_t b2 = ((word >> 8) & 0x1F) | ((obj.GetLayerValue() & 1) << 5);
2261 if (obj.lit_) {
2262 b2 |= 0x80;
2263 }
2264 bytes.push_back(b1);
2265 bytes.push_back(b2);
2266 }
2267 bytes.push_back(0xFF);
2268 bytes.push_back(0xFF);
2269 } else if (!rom_segments[room_id].empty()) {
2270 for (uint8_t b : rom_segments[room_id]) {
2271 bytes.push_back(b);
2272 }
2273 }
2274 }
2275
2276 if (bytes.size() > kTorchesMaxSize) {
2277 return absl::ResourceExhaustedError(
2278 absl::StrFormat("Torch data too large: %d bytes (max %d)", bytes.size(),
2279 kTorchesMaxSize));
2280 }
2281
2282 // Avoid unnecessary writes: if the generated torch blob matches the ROM
2283 // exactly, keep the ROM clean. This prevents "save with no changes" from
2284 // dirtying the ROM and makes diffs stable.
2285 const uint16_t current_len =
2286 static_cast<uint16_t>(rom_data[kTorchesLengthPointer]) |
2287 (static_cast<uint16_t>(rom_data[kTorchesLengthPointer + 1]) << 8);
2288 if (current_len == bytes.size() &&
2289 kTorchData + static_cast<int>(bytes.size()) <=
2290 static_cast<int>(rom_data.size()) &&
2291 std::equal(bytes.begin(), bytes.end(), rom_data.begin() + kTorchData)) {
2292 return absl::OkStatus();
2293 }
2294
2296 static_cast<uint16_t>(bytes.size())));
2297 return rom->WriteVector(kTorchData, bytes);
2298}
2299
2300absl::Status SaveAllPits(Rom* rom) {
2301 if (!rom || !rom->is_loaded()) {
2302 return absl::InvalidArgumentError("ROM not loaded");
2303 }
2304 const auto& rom_data = rom->vector();
2305 if (kPitCount < 0 || kPitCount >= static_cast<int>(rom_data.size()) ||
2306 kPitPointer + 2 >= static_cast<int>(rom_data.size())) {
2307 return absl::OutOfRangeError("Pit count/pointer out of range");
2308 }
2309 int pit_count_byte = rom_data[kPitCount];
2310 int pit_entries = pit_count_byte / 2;
2311 if (pit_entries <= 0) {
2312 return absl::OkStatus();
2313 }
2314 int pit_ptr_snes = (rom_data[kPitPointer + 2] << 16) |
2315 (rom_data[kPitPointer + 1] << 8) | rom_data[kPitPointer];
2316 int pit_data_pc = SnesToPc(pit_ptr_snes);
2317 int data_len = pit_entries * 2;
2318 if (pit_data_pc < 0 ||
2319 pit_data_pc + data_len > static_cast<int>(rom_data.size())) {
2320 return absl::OutOfRangeError("Pit data region out of range");
2321 }
2322 std::vector<uint8_t> data(rom_data.begin() + pit_data_pc,
2323 rom_data.begin() + pit_data_pc + data_len);
2324 RETURN_IF_ERROR(rom->WriteByte(kPitCount, pit_count_byte));
2325 RETURN_IF_ERROR(rom->WriteByte(kPitPointer, pit_ptr_snes & 0xFF));
2326 RETURN_IF_ERROR(rom->WriteByte(kPitPointer + 1, (pit_ptr_snes >> 8) & 0xFF));
2327 RETURN_IF_ERROR(rom->WriteByte(kPitPointer + 2, (pit_ptr_snes >> 16) & 0xFF));
2328 return rom->WriteVector(pit_data_pc, data);
2329}
2330
2331absl::Status SaveAllBlocks(Rom* rom) {
2332 if (!rom || !rom->is_loaded()) {
2333 return absl::InvalidArgumentError("ROM not loaded");
2334 }
2335 const auto& rom_data = rom->vector();
2336 if (kBlocksLength + 1 >= static_cast<int>(rom_data.size())) {
2337 return absl::OutOfRangeError("Blocks length out of range");
2338 }
2339 int blocks_count =
2340 (rom_data[kBlocksLength + 1] << 8) | rom_data[kBlocksLength];
2341 if (blocks_count <= 0) {
2342 return absl::OkStatus();
2343 }
2344 const int kRegionSize = 0x80;
2347 for (int r = 0; r < 4; ++r) {
2348 if (ptrs[r] + 2 >= static_cast<int>(rom_data.size())) {
2349 return absl::OutOfRangeError("Blocks pointer out of range");
2350 }
2351 int snes = (rom_data[ptrs[r] + 2] << 16) | (rom_data[ptrs[r] + 1] << 8) |
2352 rom_data[ptrs[r]];
2353 int pc = SnesToPc(snes);
2354 int off = r * kRegionSize;
2355 int len = std::min(kRegionSize, blocks_count - off);
2356 if (len <= 0)
2357 break;
2358 if (pc < 0 || pc + len > static_cast<int>(rom_data.size())) {
2359 return absl::OutOfRangeError("Blocks data region out of range");
2360 }
2361 std::vector<uint8_t> chunk(rom_data.begin() + pc,
2362 rom_data.begin() + pc + len);
2363 RETURN_IF_ERROR(rom->WriteVector(pc, chunk));
2364 }
2366 rom->WriteWord(kBlocksLength, static_cast<uint16_t>(blocks_count)));
2367 return absl::OkStatus();
2368}
2369
2370absl::Status SaveAllCollision(Rom* rom, absl::Span<Room> rooms) {
2371 if (!rom || !rom->is_loaded()) {
2372 return absl::InvalidArgumentError("ROM not loaded");
2373 }
2374
2375 // If the custom collision region doesn't exist (vanilla ROM), treat as a noop
2376 // only when there are no pending custom collision edits. This avoids silently
2377 // dropping user-authored collision changes on ROMs that don't support the
2378 // expanded collision bank.
2379 const auto& rom_data = rom->vector();
2380 const int ptrs_size = kNumberOfRooms * 3;
2381 const bool has_ptr_table = HasCustomCollisionPointerTable(rom_data.size());
2382 const bool has_data_region = HasCustomCollisionDataRegion(rom_data.size());
2383
2384 if (!has_ptr_table) {
2385 for (auto& room : rooms) {
2386 if (room.custom_collision_dirty()) {
2387 return absl::FailedPreconditionError(
2388 "Custom collision region not present in this ROM");
2389 }
2390 }
2391 return absl::OkStatus();
2392 }
2393
2394 if (!has_data_region) {
2395 for (auto& room : rooms) {
2396 if (room.custom_collision_dirty()) {
2397 return absl::FailedPreconditionError(
2398 "Custom collision data region not present in this ROM");
2399 }
2400 }
2401 return absl::OkStatus();
2402 }
2403
2404 // Save-time guardrails: custom collision writes must never clobber the
2405 // reserved WaterFill tail region (Oracle of Secrets).
2408 fence.Allow(static_cast<uint32_t>(kCustomCollisionRoomPointers),
2409 static_cast<uint32_t>(kCustomCollisionRoomPointers + ptrs_size),
2410 "CustomCollisionPointers"));
2411 RETURN_IF_ERROR(fence.Allow(static_cast<uint32_t>(kCustomCollisionDataPosition),
2412 static_cast<uint32_t>(kCustomCollisionDataSoftEnd),
2413 "CustomCollisionData"));
2414 yaze::rom::ScopedWriteFence scope(rom, &fence);
2415
2416 for (auto& room : rooms) {
2417 if (!room.custom_collision_dirty()) {
2418 continue;
2419 }
2420
2421 const int room_id = room.id();
2422 const int ptr_offset = kCustomCollisionRoomPointers + (room_id * 3);
2423 if (ptr_offset + 2 >= static_cast<int>(rom_data.size())) {
2424 return absl::OutOfRangeError("Custom collision pointer out of range");
2425 }
2426
2427 if (!room.has_custom_collision()) {
2428 // Disable: clear the pointer entry.
2429 RETURN_IF_ERROR(rom->WriteByte(ptr_offset, 0));
2430 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 1, 0));
2431 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 2, 0));
2432 room.ClearCustomCollisionDirty();
2433 continue;
2434 }
2435
2436 // Treat an all-zero map as disabled to avoid wasting space.
2437 bool any = false;
2438 for (uint8_t v : room.custom_collision().tiles) {
2439 if (v != 0) {
2440 any = true;
2441 break;
2442 }
2443 }
2444 if (!any) {
2445 RETURN_IF_ERROR(rom->WriteByte(ptr_offset, 0));
2446 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 1, 0));
2447 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 2, 0));
2448 room.ClearCustomCollisionDirty();
2449 continue;
2450 }
2451
2452 RETURN_IF_ERROR(WriteTrackCollision(rom, room_id, room.custom_collision()));
2453 room.ClearCustomCollisionDirty();
2454 }
2455
2456 return absl::OkStatus();
2457}
2458
2459namespace {
2460
2461// Parse current ROM chest data into per-room lists (room_id, chest_data).
2462std::vector<std::vector<std::pair<uint8_t, bool>>> ParseRomChests(
2463 const std::vector<uint8_t>& rom_data, int cpos, int clength) {
2464 std::vector<std::vector<std::pair<uint8_t, bool>>> per_room(kNumberOfRooms);
2465 for (int i = 0;
2466 i < clength && cpos + i * 3 + 2 < static_cast<int>(rom_data.size());
2467 ++i) {
2468 int off = cpos + i * 3;
2469 uint16_t word = (rom_data[off + 1] << 8) | rom_data[off];
2470 uint16_t room_id = word & 0x7FFF;
2471 bool big = (word & 0x8000) != 0;
2472 uint8_t id = rom_data[off + 2];
2473 if (room_id < kNumberOfRooms) {
2474 per_room[room_id].emplace_back(id, big);
2475 }
2476 }
2477 return per_room;
2478}
2479
2480std::vector<std::vector<uint8_t>> ParseRomPotItems(
2481 const std::vector<uint8_t>& rom_data) {
2482 std::vector<std::vector<uint8_t>> per_room(kNumberOfRooms);
2483 int table_addr = kRoomItemsPointers;
2484 if (table_addr + (kNumberOfRooms * 2) > static_cast<int>(rom_data.size())) {
2485 return per_room;
2486 }
2487 for (int room_id = 0; room_id < kNumberOfRooms; ++room_id) {
2488 int ptr_off = table_addr + (room_id * 2);
2489 uint16_t item_ptr = (rom_data[ptr_off + 1] << 8) | rom_data[ptr_off];
2490 int item_addr = SnesToPc(0x010000 | item_ptr);
2491 if (item_addr < 0 || item_addr >= static_cast<int>(rom_data.size())) {
2492 continue;
2493 }
2494 int next_ptr_off = table_addr + ((room_id + 1) * 2);
2495 int next_item_addr =
2496 (room_id + 1 < kNumberOfRooms)
2497 ? SnesToPc(0x010000 | ((rom_data[next_ptr_off + 1] << 8) |
2498 rom_data[next_ptr_off]))
2499 : item_addr + 0x100;
2500 int max_len = next_item_addr - item_addr;
2501 if (max_len <= 0)
2502 continue;
2503
2504 std::vector<uint8_t> bytes;
2505 int cursor = item_addr;
2506 const int limit =
2507 std::min(item_addr + max_len, static_cast<int>(rom_data.size()));
2508 while (cursor + 1 < limit) {
2509 uint8_t b1 = rom_data[cursor++];
2510 uint8_t b2 = rom_data[cursor++];
2511 bytes.push_back(b1);
2512 bytes.push_back(b2);
2513 if (b1 == 0xFF && b2 == 0xFF) {
2514 break;
2515 }
2516 if (cursor >= limit) {
2517 break;
2518 }
2519 bytes.push_back(rom_data[cursor++]);
2520 }
2521 if (!bytes.empty()) {
2522 per_room[room_id] = std::move(bytes);
2523 }
2524 }
2525 return per_room;
2526}
2527
2528} // namespace
2529
2530absl::Status SaveAllChests(Rom* rom, absl::Span<const Room> rooms) {
2531 if (!rom || !rom->is_loaded()) {
2532 return absl::InvalidArgumentError("ROM not loaded");
2533 }
2534 const auto& rom_data = rom->vector();
2535 if (kChestsLengthPointer + 1 >= static_cast<int>(rom_data.size()) ||
2536 kChestsDataPointer1 + 2 >= static_cast<int>(rom_data.size())) {
2537 return absl::OutOfRangeError("Chest pointers out of range");
2538 }
2539 int clength = (rom_data[kChestsLengthPointer + 1] << 8) |
2540 rom_data[kChestsLengthPointer];
2541 int cpos = SnesToPc((rom_data[kChestsDataPointer1 + 2] << 16) |
2542 (rom_data[kChestsDataPointer1 + 1] << 8) |
2543 rom_data[kChestsDataPointer1]);
2544 auto rom_chests = ParseRomChests(rom_data, cpos, clength);
2545
2546 std::vector<uint8_t> bytes;
2547 for (size_t room_id = 0;
2548 room_id < rooms.size() && room_id < rom_chests.size(); ++room_id) {
2549 const auto& room = rooms[room_id];
2550 const auto& chests = room.GetChests();
2551 if (chests.empty()) {
2552 for (const auto& [id, big] : rom_chests[room_id]) {
2553 uint16_t word = room_id | (big ? 0x8000 : 0);
2554 bytes.push_back(word & 0xFF);
2555 bytes.push_back((word >> 8) & 0xFF);
2556 bytes.push_back(id);
2557 }
2558 } else {
2559 for (const auto& c : chests) {
2560 uint16_t word = room_id | (c.size ? 0x8000 : 0);
2561 bytes.push_back(word & 0xFF);
2562 bytes.push_back((word >> 8) & 0xFF);
2563 bytes.push_back(c.id);
2564 }
2565 }
2566 }
2567
2568 if (cpos < 0 || cpos + static_cast<int>(bytes.size()) >
2569 static_cast<int>(rom_data.size())) {
2570 return absl::OutOfRangeError("Chest data region out of range");
2571 }
2573 static_cast<uint16_t>(bytes.size() / 3)));
2574 return rom->WriteVector(cpos, bytes);
2575}
2576
2577absl::Status SaveAllPotItems(Rom* rom, absl::Span<const Room> rooms) {
2578 if (!rom || !rom->is_loaded()) {
2579 return absl::InvalidArgumentError("ROM not loaded");
2580 }
2581 const auto& rom_data = rom->vector();
2582 const auto rom_pot_items = ParseRomPotItems(rom_data);
2583 int table_addr = kRoomItemsPointers;
2584 if (table_addr + (kNumberOfRooms * 2) > static_cast<int>(rom_data.size())) {
2585 return absl::OutOfRangeError("Room items pointer table out of range");
2586 }
2587 for (size_t room_id = 0; room_id < rooms.size() && room_id < kNumberOfRooms;
2588 ++room_id) {
2589 const auto& room = rooms[room_id];
2590 const bool room_loaded = room.IsLoaded();
2591 const auto& pot_items = room.GetPotItems();
2592 int ptr_off = table_addr + (room_id * 2);
2593 uint16_t item_ptr = (rom_data[ptr_off + 1] << 8) | rom_data[ptr_off];
2594 int item_addr = SnesToPc(0x010000 | item_ptr);
2595 if (item_addr < 0 || item_addr + 2 >= static_cast<int>(rom_data.size())) {
2596 continue;
2597 }
2598 int next_ptr_off = table_addr + ((room_id + 1) * 2);
2599 int next_item_addr =
2600 (room_id + 1 < kNumberOfRooms)
2601 ? SnesToPc(0x010000 | ((rom_data[next_ptr_off + 1] << 8) |
2602 rom_data[next_ptr_off]))
2603 : item_addr + 0x100;
2604 int max_len = next_item_addr - item_addr;
2605 if (max_len <= 0)
2606 continue;
2607 std::vector<uint8_t> bytes;
2608 if (!room_loaded) {
2609 if (room_id < rom_pot_items.size()) {
2610 bytes = rom_pot_items[room_id];
2611 }
2612 if (bytes.empty()) {
2613 continue; // Preserve ROM data if room not loaded and nothing parsed.
2614 }
2615 } else {
2616 for (const auto& pi : pot_items) {
2617 bytes.push_back(pi.position & 0xFF);
2618 bytes.push_back((pi.position >> 8) & 0xFF);
2619 bytes.push_back(pi.item);
2620 }
2621 bytes.push_back(0xFF);
2622 bytes.push_back(0xFF);
2623 }
2624 if (static_cast<int>(bytes.size()) > max_len) {
2625 continue;
2626 }
2627 RETURN_IF_ERROR(rom->WriteVector(item_addr, bytes));
2628 }
2629 return absl::OkStatus();
2630}
2631
2633 auto rom_data = rom()->vector();
2634
2635 // Read blocks length
2636 int blocks_count =
2637 (rom_data[kBlocksLength + 1] << 8) | rom_data[kBlocksLength];
2638
2639 LOG_DEBUG("Room", "LoadBlocks: room_id=%d, blocks_count=%d", room_id_,
2640 blocks_count);
2641
2642 // Avoid duplication if LoadBlocks is called multiple times.
2643 tile_objects_.erase(
2644 std::remove_if(tile_objects_.begin(), tile_objects_.end(),
2645 [](const RoomObject& obj) {
2646 return (obj.options() & ObjectOption::Block) !=
2647 ObjectOption::Nothing;
2648 }),
2649 tile_objects_.end());
2650
2651 // Load block data from multiple pointers
2652 std::vector<uint8_t> blocks_data(blocks_count);
2653
2654 int pos1 = kBlocksPointer1;
2655 int pos2 = kBlocksPointer2;
2656 int pos3 = kBlocksPointer3;
2657 int pos4 = kBlocksPointer4;
2658
2659 // Read block data from 4 different locations
2660 for (int i = 0; i < 0x80 && i < blocks_count; i++) {
2661 blocks_data[i] = rom_data[pos1 + i];
2662
2663 if (i + 0x80 < blocks_count) {
2664 blocks_data[i + 0x80] = rom_data[pos2 + i];
2665 }
2666 if (i + 0x100 < blocks_count) {
2667 blocks_data[i + 0x100] = rom_data[pos3 + i];
2668 }
2669 if (i + 0x180 < blocks_count) {
2670 blocks_data[i + 0x180] = rom_data[pos4 + i];
2671 }
2672 }
2673
2674 // Parse blocks for this room (4 bytes per block entry)
2675 for (int i = 0; i < blocks_count; i += 4) {
2676 if (i + 3 >= blocks_count)
2677 break;
2678
2679 uint8_t b1 = blocks_data[i];
2680 uint8_t b2 = blocks_data[i + 1];
2681 uint8_t b3 = blocks_data[i + 2];
2682 uint8_t b4 = blocks_data[i + 3];
2683
2684 // Check if this block belongs to our room
2685 uint16_t block_room_id = (b2 << 8) | b1;
2686 if (block_room_id == room_id_) {
2687 // End marker for this room's blocks
2688 if (b3 == 0xFF && b4 == 0xFF) {
2689 break;
2690 }
2691
2692 // Decode block position
2693 int address = ((b4 & 0x1F) << 8 | b3) >> 1;
2694 uint8_t px = address % 64;
2695 uint8_t py = address >> 6;
2696 uint8_t layer = (b4 & 0x20) >> 5;
2697
2698 // Create block object (ID 0x0E00)
2699 RoomObject block_obj(0x0E00, px, py, 0, layer);
2700 block_obj.SetRom(rom_);
2702
2703 tile_objects_.push_back(block_obj);
2704
2705 LOG_DEBUG("Room", "Loaded block at (%d,%d) layer=%d", px, py, layer);
2706 }
2707 }
2708}
2709
2711 if (!rom_ || !rom_->is_loaded())
2712 return;
2713 auto rom_data = rom()->vector();
2714
2715 // Load pot items
2716 // Format per ASM analysis (bank_01.asm):
2717 // - Pointer table at kRoomItemsPointers (0x01DB69)
2718 // - Each room has a pointer to item data
2719 // - Item data format: 3 bytes per item
2720 // - 2 bytes: position word (Y_hi, X_lo encoding)
2721 // - 1 byte: item type
2722 // - Terminated by 0xFFFF position word
2723
2724 int table_addr = kRoomItemsPointers; // 0x01DB69
2725
2726 // Read pointer for this room
2727 int ptr_addr = table_addr + (room_id_ * 2);
2728 if (ptr_addr + 1 >= static_cast<int>(rom_data.size()))
2729 return;
2730
2731 uint16_t item_ptr = (rom_data[ptr_addr + 1] << 8) | rom_data[ptr_addr];
2732
2733 // Convert to PC address (Bank 01 offset)
2734 int item_addr = SnesToPc(0x010000 | item_ptr);
2735
2736 pot_items_.clear();
2737
2738 // Read 3-byte entries until 0xFFFF terminator
2739 while (item_addr + 2 < static_cast<int>(rom_data.size())) {
2740 // Read position word (little endian)
2741 uint16_t position = (rom_data[item_addr + 1] << 8) | rom_data[item_addr];
2742
2743 // Check for terminator
2744 if (position == 0xFFFF)
2745 break;
2746
2747 // Read item type (3rd byte)
2748 uint8_t item_type = rom_data[item_addr + 2];
2749
2750 PotItem pot_item;
2751 pot_item.position = position;
2752 pot_item.item = item_type;
2753 pot_items_.push_back(pot_item);
2754
2755 item_addr += 3; // Move to next entry
2756 }
2757}
2758
2760 auto rom_data = rom()->vector();
2761
2762 // Read pit count
2763 int pit_entries = rom_data[kPitCount] / 2;
2764
2765 // Read pit pointer (long pointer)
2766 int pit_ptr = (rom_data[kPitPointer + 2] << 16) |
2767 (rom_data[kPitPointer + 1] << 8) | rom_data[kPitPointer];
2768 int pit_data_addr = SnesToPc(pit_ptr);
2769
2770 LOG_DEBUG("Room", "LoadPits: room_id=%d, pit_entries=%d, pit_ptr=0x%06X",
2771 room_id_, pit_entries, pit_ptr);
2772
2773 // Pit data is stored as: room_id (2 bytes), target info (2 bytes)
2774 // This data is already loaded in LoadRoomFromRom() into pits_ destination
2775 // struct The pit destination (where you go when you fall) is set via
2776 // SetPitsTarget()
2777
2778 // Pits are typically represented in the layout/collision data, not as objects
2779 // The pits_ member already contains the target room and layer
2780 LOG_DEBUG("Room", "Pit destination - target=%d, target_layer=%d",
2782}
2783
2784// ============================================================================
2785// Object Limit Counting (ZScream Feature Parity)
2786// ============================================================================
2787
2788std::map<DungeonLimit, int> Room::GetLimitedObjectCounts() const {
2789 auto counts = CreateLimitCounter();
2790
2791 // Count sprites
2792 counts[DungeonLimit::kSprites] = static_cast<int>(sprites_.size());
2793
2794 // Count overlords (sprites with ID > 0x40 are overlords in ALTTP)
2795 for (const auto& sprite : sprites_) {
2796 if (sprite.IsOverlord()) {
2797 counts[DungeonLimit::Overlords]++;
2798 }
2799 }
2800
2801 // Count chests
2802 counts[DungeonLimit::kChests] = static_cast<int>(chests_in_room_.size());
2803
2804 // Count doors (total and special)
2805 counts[DungeonLimit::kDoors] = static_cast<int>(doors_.size());
2806 for (const auto& door : doors_) {
2807 // Special doors: shutters and key-locked doors.
2808 const bool is_special = [&]() -> bool {
2809 switch (door.type) {
2824 return true;
2825 default:
2826 return false;
2827 }
2828 }();
2829 if (is_special) {
2831 }
2832 }
2833
2834 // Count stairs
2835 counts[DungeonLimit::StairsTransition] = static_cast<int>(z3_staircases_.size());
2836
2837 // Count objects with specific options
2838 for (const auto& obj : tile_objects_) {
2839 auto options = obj.options();
2840
2841 // Count blocks
2842 if ((options & ObjectOption::Block) != ObjectOption::Nothing) {
2843 counts[DungeonLimit::Blocks]++;
2844 }
2845
2846 // Count torches
2847 if ((options & ObjectOption::Torch) != ObjectOption::Nothing) {
2848 counts[DungeonLimit::Torches]++;
2849 }
2850
2851 // Count star tiles (object IDs 0x11E and 0x11F)
2852 if (obj.id_ == 0x11E || obj.id_ == 0x11F) {
2853 counts[DungeonLimit::StarTiles]++;
2854 }
2855
2856 // Count somaria paths (object IDs in 0xF83-0xF8F range)
2857 if (obj.id_ >= 0xF83 && obj.id_ <= 0xF8F) {
2858 counts[DungeonLimit::SomariaLine]++;
2859 }
2860
2861 // Count staircase objects based on direction
2862 if ((options & ObjectOption::Stairs) != ObjectOption::Nothing) {
2863 // North-facing stairs: IDs 0x130-0x135
2864 if ((obj.id_ >= 0x130 && obj.id_ <= 0x135) ||
2865 obj.id_ == 0x139 || obj.id_ == 0x13A || obj.id_ == 0x13B) {
2866 counts[DungeonLimit::StairsNorth]++;
2867 }
2868 // South-facing stairs: IDs 0x13B-0x13D
2869 else if (obj.id_ >= 0x13C && obj.id_ <= 0x13F) {
2870 counts[DungeonLimit::StairsSouth]++;
2871 }
2872 }
2873
2874 // Count general manipulable objects
2875 if ((options & ObjectOption::Block) != ObjectOption::Nothing ||
2879 }
2880 }
2881
2882 return counts;
2883}
2884
2886 auto counts = GetLimitedObjectCounts();
2887 return yaze::zelda3::HasExceededLimits(counts);
2888}
2889
2890std::vector<DungeonLimitInfo> Room::GetExceededLimitDetails() const {
2891 auto counts = GetLimitedObjectCounts();
2892 return GetExceededLimits(counts);
2893}
2894
2895} // namespace zelda3
2896} // namespace yaze
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
absl::Status WriteByte(int addr, uint8_t value)
Definition rom.cc:476
const auto & vector() const
Definition rom.h:143
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
Definition rom.cc:548
auto data() const
Definition rom.h:139
auto size() const
Definition rom.h:138
bool is_loaded() const
Definition rom.h:132
absl::Status WriteWord(int addr, uint16_t value)
Definition rom.cc:495
absl::Status WriteLong(uint32_t addr, uint32_t value)
Definition rom.cc:522
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:36
static Arena & Get()
Definition arena.cc:21
void DrawBackground(std::span< uint8_t > gfx16_data)
void DrawFloor(const std::vector< uint8_t > &rom_data, int tile_address, int tile_address_floor, uint8_t floor_graphics)
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
TextureHandle texture() const
Definition bitmap.h:380
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:384
SDL_Surface * surface() const
Definition bitmap.h:379
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
absl::Status Allow(uint32_t start, uint32_t end, std::string_view label)
Definition write_fence.h:32
Editor implementation of DungeonState.
Draws dungeon objects to background buffers using game patterns.
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 DrawDoor(const DoorDef &door, int door_index, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2, const DungeonState *state=nullptr)
Draw a door to background buffers.
void DrawPotItem(uint8_t item_id, int x, int y, gfx::BackgroundBuffer &bg)
Draw a pot item visualization.
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 SetCurrentBitmap(gfx::Bitmap *bitmap)
void LogPaletteLoad(const std::string &location, int palette_id, const gfx::SnesPalette &palette)
static PaletteDebugger & Get()
void LogPaletteApplication(const std::string &location, int palette_id, bool success, const std::string &reason="")
void SetCurrentPalette(const gfx::SnesPalette &palette)
void LogSurfaceState(const std::string &location, SDL_Surface *surface)
RoomLayerManager - Manages layer visibility and compositing.
void CompositeToOutput(Room &room, gfx::Bitmap &output) const
Composite all visible layers into a single output bitmap.
void SetRom(Rom *rom)
Definition room_layout.h:21
const std::vector< RoomObject > & GetObjects() const
Definition room_layout.h:31
absl::Status Draw(int room_id, const uint8_t *gfx_data, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2, const gfx::PaletteGroup &palette_group, DungeonState *state) const
absl::Status LoadLayout(int layout_id)
static RoomObject DecodeObjectFromBytes(uint8_t b1, uint8_t b2, uint8_t b3, uint8_t layer)
void SetRom(Rom *rom)
Definition room_object.h:70
void set_options(ObjectOption options)
bool ValidateObject(const RoomObject &object) const
Definition room.cc:1921
destination pits_
Definition room.h:731
std::vector< RoomObject > tile_objects_
Definition room.h:715
uint8_t cached_blockset_
Definition room.h:681
EffectKey effect_
Definition room.h:726
gfx::BackgroundBuffer object_bg1_buffer_
Definition room.h:659
uint8_t palette_
Definition room.h:700
void SetTag2Direct(TagKey tag2)
Definition room.h:536
bool HasExceededLimits() const
Check if any object limits are exceeded.
Definition room.cc:2885
uint8_t cached_layout_
Definition room.h:684
absl::Status UpdateObject(size_t index, const RoomObject &object)
Definition room.cc:1896
TagKey cached_tag2_
Definition room.h:689
void SetStair4Target(uint8_t target)
Definition room.h:546
void SetPitsTarget(uint8_t target)
Definition room.h:542
void SetIsLight(bool is_light)
Definition room.h:474
void LoadChests()
Definition room.cc:2036
void MarkObjectsDirty()
Definition room.h:334
gfx::BackgroundBuffer bg2_buffer_
Definition room.h:658
uint8_t cached_floor2_graphics_
Definition room.h:686
std::vector< zelda3::Sprite > sprites_
Definition room.h:717
CustomCollisionMap custom_collision_
Definition room.h:737
GameData * game_data_
Definition room.h:651
void SetLoaded(bool loaded)
Definition room.h:550
void CopyRoomGraphicsToBuffer()
Definition room.cc:498
uint8_t cached_palette_
Definition room.h:683
zelda3_version_pointers version_constants() const
Definition room.h:621
void LoadRoomGraphics(uint8_t entrance_blockset=0xFF)
Definition room.cc:453
uint8_t cached_effect_
Definition room.h:687
std::vector< Door > doors_
Definition room.h:720
void SetStaircaseRoom(int index, uint8_t room)
Definition room.h:516
absl::Status RemoveObject(size_t index)
Definition room.cc:1885
void SetStair1TargetLayer(uint8_t layer)
Definition room.h:538
void LoadBlocks()
Definition room.cc:2632
void SetLayer2Mode(uint8_t mode)
Definition room.h:529
RoomLayout layout_
Definition room.h:722
void LoadLayoutTilesToBuffer()
Definition room.cc:903
uint8_t staircase_room(int index) const
Definition room.h:562
void SetTag2(TagKey tag2)
Definition room.h:505
std::vector< DungeonLimitInfo > GetExceededLimitDetails() const
Get list of exceeded limits with details.
Definition room.cc:2890
void ParseObjectsFromLocation(int objects_location)
Definition room.cc:1353
uint8_t cached_floor1_graphics_
Definition room.h:685
bool custom_collision_dirty_
Definition room.h:738
void SetTag1Direct(TagKey tag1)
Definition room.h:535
void LoadTorches()
Definition room.cc:2076
void SetHolewarp(uint8_t hw)
Definition room.h:515
TagKey tag2() const
Definition room.h:556
void SetStair2Target(uint8_t target)
Definition room.h:544
int animated_frame_
Definition room.h:692
void SetCollision(CollisionKey collision)
Definition room.h:473
gfx::BackgroundBuffer bg1_buffer_
Definition room.h:657
absl::Status SaveObjects()
Definition room.cc:1681
uint8_t palette() const
Definition room.h:571
void SetStaircasePlane(int index, uint8_t plane)
Definition room.h:511
void SetIsDark(bool is_dark)
Definition room.h:531
auto rom() const
Definition room.h:613
std::map< DungeonLimit, int > GetLimitedObjectCounts() const
Count limited objects in this room.
Definition room.cc:2788
void RenderRoomGraphics()
Definition room.cc:598
absl::Status SaveRoomHeader()
Definition room.cc:1797
gfx::Bitmap composite_bitmap_
Definition room.h:671
Room & operator=(Room &&)
uint16_t message_id_
Definition room.h:703
TagKey tag1() const
Definition room.h:555
CollisionKey collision() const
Definition room.h:557
uint8_t staircase_rooms_[4]
Definition room.h:695
gfx::Bitmap & GetCompositeBitmap(RoomLayerManager &layer_mgr)
Get a composite bitmap of all layers merged.
Definition room.cc:590
std::vector< uint8_t > EncodeObjects() const
Definition room.cc:1457
void SetEffect(EffectKey effect)
Definition room.h:493
gfx::BackgroundBuffer object_bg2_buffer_
Definition room.h:660
uint8_t holewarp_
Definition room.h:702
uint8_t layout_id_
Definition room.h:701
std::array< uint8_t, 16 > blocks_
Definition room.h:712
void SetTag1(TagKey tag1)
Definition room.h:499
void SetBackgroundTileset(uint8_t tileset)
Definition room.h:532
void SetStair3TargetLayer(uint8_t layer)
Definition room.h:540
uint8_t floor2_graphics_
Definition room.h:709
bool IsLight() const
Definition room.h:525
uint8_t floor1_graphics_
Definition room.h:708
absl::Status SaveSprites()
Definition room.cc:1745
void SetLayerMerging(LayerMergeType merging)
Definition room.h:530
void SetPitsTargetLayer(uint8_t layer)
Definition room.h:537
void LoadObjects()
Definition room.cc:1292
void LoadPotItems()
Definition room.cc:2710
uint8_t blockset_
Definition room.h:698
EffectKey effect() const
Definition room.h:554
void SetSpriteTileset(uint8_t tileset)
Definition room.h:533
void SetStair1Target(uint8_t target)
Definition room.h:543
void SetBg2(background2 bg2)
Definition room.h:472
void SetSpriteset(uint8_t ss)
Definition room.h:487
std::unique_ptr< DungeonState > dungeon_state_
Definition room.h:743
void LoadAnimatedGraphics()
Definition room.cc:1222
std::vector< uint8_t > EncodeSprites() const
Definition room.cc:1544
std::vector< chest_data > chests_in_room_
Definition room.h:719
void SetBlockset(uint8_t bs)
Definition room.h:481
uint8_t spriteset_
Definition room.h:699
LayerMergeType layer_merging_
Definition room.h:724
background2 bg2() const
Definition room.h:553
uint8_t staircase_plane(int index) const
Definition room.h:559
uint8_t cached_spriteset_
Definition room.h:682
std::array< uint8_t, 0x10000 > current_gfx16_
Definition room.h:653
std::vector< staircase > z3_staircases_
Definition room.h:718
std::vector< PotItem > pot_items_
Definition room.h:721
void LoadSprites()
Definition room.cc:1980
TagKey cached_tag1_
Definition room.h:688
uint8_t background_tileset_
Definition room.h:705
void SetStair3Target(uint8_t target)
Definition room.h:545
DirtyState dirty_state_
Definition room.h:672
void HandleSpecialObjects(short oid, uint8_t posX, uint8_t posY, int &nbr_of_staircase)
Definition room.cc:1943
void SetStair4TargetLayer(uint8_t layer)
Definition room.h:541
absl::Status AddObject(const RoomObject &object)
Definition room.cc:1872
absl::StatusOr< size_t > FindObjectAt(int x, int y, int layer) const
Definition room.cc:1911
void SetPalette(uint8_t pal)
Definition room.h:475
void SetStair2TargetLayer(uint8_t layer)
Definition room.h:539
void RenderObjectsToBackground()
Definition room.cc:977
void SetLayer2Behavior(uint8_t behavior)
Definition room.h:534
void SetMessageId(uint16_t mid)
Definition room.h:522
A class for managing sprites in the overworld and underworld.
Definition sprite.h:35
auto id() const
Definition sprite.h:95
auto layer() const
Definition sprite.h:106
auto set_key_drop(int key)
Definition sprite.h:114
auto subtype() const
Definition sprite.h:107
auto y() const
Definition sprite.h:98
auto x() const
Definition sprite.h:97
zelda3_bg2_effect
Background layer 2 effects.
Definition zelda.h:369
#define LOG_DEBUG(category, format,...)
Definition log.h:103
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define LOG_WARN(category, format,...)
Definition log.h:107
SDL_Palette * GetSurfacePalette(SDL_Surface *surface)
Get the palette attached to a surface.
Definition sdl_compat.h:375
std::vector< std::vector< uint8_t > > ParseRomPotItems(const std::vector< uint8_t > &rom_data)
Definition room.cc:2480
bool IsSpritePointerShared(const std::vector< uint8_t > &rom_data, int table_pc, int room_id, int sprite_address)
Definition room.cc:166
absl::Status GetSpritePointerTablePc(const std::vector< uint8_t > &rom_data, int *table_pc)
Definition room.cc:108
std::vector< std::vector< uint8_t > > ParseRomTorchSegments(const std::vector< uint8_t > &rom_data, int bytes_count)
Definition room.cc:2168
int ReadRoomSpriteAddressPc(const std::vector< uint8_t > &rom_data, int table_pc, int room_id)
Definition room.cc:128
std::vector< std::vector< std::pair< uint8_t, bool > > > ParseRomChests(const std::vector< uint8_t > &rom_data, int cpos, int clength)
Definition room.cc:2462
int MeasureSpriteStreamSize(const std::vector< uint8_t > &rom_data, int sprite_address, int hard_end)
Definition room.cc:143
constexpr int kBlocksPointer4
absl::Status SaveAllChests(Rom *rom, absl::Span< const Room > rooms)
Definition room.cc:2530
constexpr int kDoorPointers
constexpr int kGfxBufferAnimatedFrameStride
Definition room.cc:492
const std::string RoomTag[65]
Definition room.cc:41
absl::Status WriteTrackCollision(Rom *rom, int room_id, const CustomCollisionMap &map)
constexpr int kGfxBufferAnimatedFrameOffset
Definition room.cc:491
@ NormalDoorOneSidedShutter
Normal door (lower layer; with one-sided shutters)
@ TopShutterLower
Top-sided shutter door (lower layer)
@ SmallKeyDoor
Small key door.
@ BottomShutterLower
Bottom-sided shutter door (lower layer)
@ TopSidedShutter
Top-sided shutter door.
@ DoubleSidedShutterLower
Double-sided shutter (lower layer)
@ UnusableBottomShutter
Unusable bottom-sided shutter door.
@ UnopenableBigKeyDoor
Unopenable, double-sided big key door.
@ BottomSidedShutter
Bottom-sided shutter door.
@ UnusedDoubleSidedShutter
Unused double-sided shutter.
@ CurtainDoor
Curtain door.
@ BigKeyDoor
Big key door.
@ EyeWatchDoor
Eye watch door.
@ DoubleSidedShutter
Double sided shutter door.
constexpr int kSpritesEndData
constexpr int kTorchesLengthPointer
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
Definition room.cc:274
constexpr int kCustomCollisionDataSoftEnd
std::vector< DungeonLimitInfo > GetExceededLimits(const std::map< DungeonLimit, int > &counts)
constexpr int kChestsLengthPointer
int FindMaxUsedSpriteAddress(Rom *rom)
Definition room.cc:1574
constexpr int kMessagesIdDungeon
absl::Status RelocateSpriteData(Rom *rom, int room_id, const std::vector< uint8_t > &encoded_bytes)
Definition room.cc:1611
constexpr int kGfxBufferRoomOffset
Definition room.cc:493
absl::Status SaveAllPotItems(Rom *rom, absl::Span< const Room > rooms)
Definition room.cc:2577
constexpr int kGfxBufferRoomSpriteOffset
Definition room.cc:494
RoomSize CalculateRoomSize(Rom *rom, int room_id)
Definition room.cc:181
constexpr int kPitPointer
constexpr int kSpritesData
absl::Status SaveAllTorches(Rom *rom, absl::Span< const Room > rooms)
Definition room.cc:2210
constexpr int kTileAddress
constexpr int kPitCount
constexpr int kRoomsSpritePointer
constexpr int kTileAddressFloor
absl::Status SaveAllPits(Rom *rom)
Definition room.cc:2300
constexpr int kChestsDataPointer1
constexpr int kBlocksLength
absl::Status SaveAllBlocks(Rom *rom)
Definition room.cc:2331
constexpr int kBlocksPointer1
constexpr int kRoomItemsPointers
constexpr int kGfxBufferRoomSpriteStride
Definition room.cc:495
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
constexpr bool HasCustomCollisionPointerTable(std::size_t rom_size)
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:253
constexpr int kCustomCollisionDataPosition
bool HasExceededLimits(const std::map< DungeonLimit, int > &counts)
constexpr uint32_t kDungeonPalettePointerTable
Definition game_data.h:44
constexpr uint16_t kStairsObjects[]
constexpr int kNumberOfRooms
const std::string RoomEffect[8]
Definition room.cc:31
constexpr int kRoomHeaderPointer
constexpr bool HasCustomCollisionDataRegion(std::size_t rom_size)
constexpr int kBlocksPointer3
constexpr int kRoomHeaderPointerBank
constexpr int kCustomCollisionRoomPointers
constexpr int kGfxBufferStride
Definition room.cc:490
std::map< DungeonLimit, int > CreateLimitCounter()
constexpr int kTorchData
absl::Status SaveAllCollision(Rom *rom, absl::Span< Room > rooms)
Definition room.cc:2370
constexpr int kGfxBufferRoomSpriteLastLineOffset
Definition room.cc:496
constexpr int kBlocksPointer2
constexpr int kRoomObjectPointer
constexpr int kGfxBufferOffset
Definition room.cc:489
uint32_t PcToSnes(uint32_t addr)
Definition snes.h:17
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
SDL2/SDL3 compatibility layer.
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Legacy chest data structure.
Definition zelda.h:438
uint8_t target_layer
Definition zelda.h:451
uint8_t target
Definition zelda.h:450
Represents a group of palettes.
void AddPalette(SnesPalette pal)
std::array< std::array< uint8_t, 4 >, kNumSpritesets > spriteset_ids
Definition game_data.h:93
std::array< std::array< uint8_t, 4 >, kNumRoomBlocksets > room_blockset_ids
Definition game_data.h:92
std::array< std::array< uint8_t, 4 >, kNumPalettesets > paletteset_ids
Definition game_data.h:99
gfx::PaletteGroupMap palette_groups
Definition game_data.h:89
std::array< std::array< uint8_t, 8 >, kNumMainBlocksets > main_blockset_ids
Definition game_data.h:91
std::vector< uint8_t > graphics_buffer
Definition game_data.h:82
uint16_t position
Definition room.h:95
int64_t room_size_pointer
Definition room.h:753
static Door FromRomBytes(uint8_t b1, uint8_t b2)
Definition room.h:278
Public YAZE API umbrella header.