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