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 <array>
7#include <cstdint>
8#include <functional>
9#include <limits>
10#include <optional>
11#include <string>
12#include <unordered_map>
13#include <unordered_set>
14#include <utility>
15#include <vector>
16
17#include "absl/strings/str_cat.h"
18#include "absl/strings/str_format.h"
22#include "rom/rom.h"
23#include "rom/snes.h"
24#include "rom/transaction.h"
25#include "rom/write_fence.h"
26#include "util/log.h"
39
40namespace yaze {
41namespace zelda3 {
42
43namespace {
44
45bool RoomUsesTrackCornerAliases(const std::vector<RoomObject>& objects) {
46 return std::any_of(objects.begin(), objects.end(),
47 [](const RoomObject& obj) { return obj.id_ == 0x31; });
48}
49
50uint8_t Layer2ModeFromHeaderByte(uint8_t byte0) {
51 return static_cast<uint8_t>((byte0 >> 5) & 0x07);
52}
53
54bool IsDarkRoomHeaderByte(uint8_t byte0) {
55 return (byte0 & 0x01) != 0;
56}
57
59 return kLayerMergeTypeList[IsDarkRoomHeaderByte(byte0)
60 ? 8
62}
63
65 if (IsDarkRoomHeaderByte(byte0)) {
66 return background2::DarkRoom;
67 }
68 return static_cast<background2>(Layer2ModeFromHeaderByte(byte0));
69}
70
71template <typename WriteColor>
73 const gfx::SnesPalette* hud_palette,
74 WriteColor write_color) {
75 if (hud_palette != nullptr) {
76 const size_t hud_count = std::min<size_t>(hud_palette->size(), 32);
77 for (size_t i = 0; i < hud_count; ++i) {
78 write_color(static_cast<int>(i), (*hud_palette)[i]);
79 }
80 }
81
82 constexpr int kColorsPerRomBank = 15;
83 constexpr int kIndicesPerSdlBank = 16;
84 constexpr int kNumRomBanks = 6;
85 constexpr int kDungeonBankStart = 2;
86 for (int rom_bank = 0; rom_bank < kNumRomBanks; ++rom_bank) {
87 const int sdl_bank = rom_bank + kDungeonBankStart;
88 for (int color = 0; color < kColorsPerRomBank; ++color) {
89 const size_t rom_index =
90 static_cast<size_t>(rom_bank * kColorsPerRomBank + color);
91 if (rom_index >= dungeon_palette.size()) {
92 return;
93 }
94 const int dst_index = sdl_bank * kIndicesPerSdlBank + color + 1;
95 write_color(dst_index, dungeon_palette[rom_index]);
96 }
97 }
98}
99
100} // namespace
101
102std::vector<SDL_Color> BuildDungeonRenderPalette(
103 const gfx::SnesPalette& dungeon_palette,
104 const gfx::SnesPalette* hud_palette) {
105 std::vector<SDL_Color> colors(256, {0, 0, 0, 0});
106 PopulateDungeonRenderPaletteRows(
107 dungeon_palette, hud_palette,
108 [&](int dst_index, const gfx::SnesColor& color) {
109 if (dst_index < 0 || dst_index >= static_cast<int>(colors.size())) {
110 return;
111 }
112 const ImVec4 rgb = color.rgb();
113 colors[dst_index] = {static_cast<Uint8>(rgb.x),
114 static_cast<Uint8>(rgb.y),
115 static_cast<Uint8>(rgb.z), 255};
116 });
117 colors[255] = {0, 0, 0, 0};
118 return colors;
119}
120
121void LoadDungeonRenderPaletteToCgram(std::span<uint16_t> cgram,
122 const gfx::SnesPalette& dungeon_palette,
123 const gfx::SnesPalette* hud_palette) {
124 PopulateDungeonRenderPaletteRows(
125 dungeon_palette, hud_palette,
126 [&](int dst_index, const gfx::SnesColor& color) {
127 if (dst_index < 0 || dst_index >= static_cast<int>(cgram.size())) {
128 return;
129 }
130 cgram[dst_index] = color.snes();
131 });
132}
133
134// Define room effect names in a single translation unit to avoid SIOF
135const std::string RoomEffect[8] = {"Nothing",
136 "Nothing",
137 "Moving Floor",
138 "Moving Water",
139 "Trinexx Shell",
140 "Red Flashes",
141 "Light Torch to See Floor",
142 "Ganon's Darkness"};
143
144// Define room tag names in a single translation unit to avoid SIOF
145const std::string RoomTag[65] = {"Nothing",
146 "NW Kill Enemy to Open",
147 "NE Kill Enemy to Open",
148 "SW Kill Enemy to Open",
149 "SE Kill Enemy to Open",
150 "W Kill Enemy to Open",
151 "E Kill Enemy to Open",
152 "N Kill Enemy to Open",
153 "S Kill Enemy to Open",
154 "Clear Quadrant to Open",
155 "Clear Full Tile to Open",
156 "NW Push Block to Open",
157 "NE Push Block to Open",
158 "SW Push Block to Open",
159 "SE Push Block to Open",
160 "W Push Block to Open",
161 "E Push Block to Open",
162 "N Push Block to Open",
163 "S Push Block to Open",
164 "Push Block to Open",
165 "Pull Lever to Open",
166 "Collect Prize to Open",
167 "Hold Switch Open Door",
168 "Toggle Switch to Open Door",
169 "Turn off Water",
170 "Turn on Water",
171 "Water Gate",
172 "Water Twin",
173 "Moving Wall Right",
174 "Moving Wall Left",
175 "Crash",
176 "Crash",
177 "Push Switch Exploding Wall",
178 "Holes 0",
179 "Open Chest (Holes 0)",
180 "Holes 1",
181 "Holes 2",
182 "Defeat Boss for Dungeon Prize",
183 "SE Kill Enemy to Push Block",
184 "Trigger Switch Chest",
185 "Pull Lever Exploding Wall",
186 "NW Kill Enemy for Chest",
187 "NE Kill Enemy for Chest",
188 "SW Kill Enemy for Chest",
189 "SE Kill Enemy for Chest",
190 "W Kill Enemy for Chest",
191 "E Kill Enemy for Chest",
192 "N Kill Enemy for Chest",
193 "S Kill Enemy for Chest",
194 "Clear Quadrant for Chest",
195 "Clear Full Tile for Chest",
196 "Light Torches to Open",
197 "Holes 3",
198 "Holes 4",
199 "Holes 5",
200 "Holes 6",
201 "Agahnim Room",
202 "Holes 7",
203 "Holes 8",
204 "Open Chest for Holes 8",
205 "Push Block for Chest",
206 "Clear Room for Triforce Door",
207 "Light Torches for Chest",
208 "Kill Boss Again"};
209
210namespace {
211
213 int address = -1;
214 int physical_end = -1;
215 bool shared = false;
216
217 int capacity() const {
218 return physical_end > address ? physical_end - address : 0;
219 }
220};
221
222PhysicalStreamInfo AnalyzePhysicalStream(const std::vector<int>& room_addresses,
223 int room_id,
224 int known_region_end = -1) {
226 if (room_id < 0 || room_id >= static_cast<int>(room_addresses.size())) {
227 return info;
228 }
229
230 info.address = room_addresses[room_id];
231 if (info.address < 0) {
232 return info;
233 }
234
235 int next_address = std::numeric_limits<int>::max();
236 for (int other_room_id = 0;
237 other_room_id < static_cast<int>(room_addresses.size());
238 ++other_room_id) {
239 if (other_room_id == room_id || room_addresses[other_room_id] < 0) {
240 continue;
241 }
242 const int other_address = room_addresses[other_room_id];
243 if (other_address == info.address) {
244 info.shared = true;
245 } else if (other_address > info.address) {
246 next_address = std::min(next_address, other_address);
247 }
248 }
249
250 if (known_region_end > info.address) {
251 next_address = std::min(next_address, known_region_end);
252 }
253 if (next_address == std::numeric_limits<int>::max()) {
254 return info;
255 }
256
257 // A stream cannot safely grow across a LoROM bank boundary even when the
258 // next pointer happens to live in the following physical bank. The bank end
259 // alone is not a physical data boundary, so fail closed unless an actual
260 // pointer or a supplied region end bounds this bank.
261 constexpr int kLoRomBankSize = 0x8000;
262 const int bank_end = ((info.address / kLoRomBankSize) + 1) * kLoRomBankSize;
263 if (next_address > bank_end) {
264 return info;
265 }
266 info.physical_end = next_address;
267 return info;
268}
269
270absl::Status GetObjectPointerTablePc(const std::vector<uint8_t>& rom_data,
271 int* table_pc) {
272 if (table_pc == nullptr) {
273 return absl::InvalidArgumentError("table_pc pointer is null");
274 }
275 if (kRoomObjectPointer + 2 >= static_cast<int>(rom_data.size())) {
276 return absl::OutOfRangeError(
277 "Object pointer table address is out of range");
278 }
279
280 const uint32_t table_snes =
281 (static_cast<uint32_t>(rom_data[kRoomObjectPointer + 2]) << 16) |
282 (static_cast<uint32_t>(rom_data[kRoomObjectPointer + 1]) << 8) |
283 rom_data[kRoomObjectPointer];
284 const int pc = static_cast<int>(SnesToPc(table_snes));
285 if (pc < 0 || pc + (kNumberOfRooms * 3) > static_cast<int>(rom_data.size())) {
286 return absl::OutOfRangeError("Object pointer table is out of range");
287 }
288
289 *table_pc = pc;
290 return absl::OkStatus();
291}
292
293uint32_t ReadRoomObjectAddressSnes(const std::vector<uint8_t>& rom_data,
294 int table_pc, int room_id) {
295 if (room_id < 0 || room_id >= kNumberOfRooms) {
296 return 0;
297 }
298 const int ptr_off = table_pc + (room_id * 3);
299 if (ptr_off < 0 || ptr_off + 2 >= static_cast<int>(rom_data.size())) {
300 return 0;
301 }
302 return (static_cast<uint32_t>(rom_data[ptr_off + 2]) << 16) |
303 (static_cast<uint32_t>(rom_data[ptr_off + 1]) << 8) |
304 rom_data[ptr_off];
305}
306
307int ReadRoomObjectAddressPc(const std::vector<uint8_t>& rom_data, int table_pc,
308 int room_id) {
309 const uint32_t snes = ReadRoomObjectAddressSnes(rom_data, table_pc, room_id);
310 if ((snes & 0xFFFF) < 0x8000) {
311 return -1;
312 }
313 const int pc = static_cast<int>(SnesToPc(snes));
314 return pc >= 0 && pc < static_cast<int>(rom_data.size()) ? pc : -1;
315}
316
317absl::StatusOr<PhysicalStreamInfo> GetObjectStreamInfo(
318 const std::vector<uint8_t>& rom_data, int room_id) {
319 if (room_id < 0 || room_id >= kNumberOfRooms) {
320 return absl::OutOfRangeError("Room ID out of range");
321 }
322 int table_pc = 0;
323 RETURN_IF_ERROR(GetObjectPointerTablePc(rom_data, &table_pc));
324
325 std::vector<int> addresses(kNumberOfRooms, -1);
326 for (int id = 0; id < kNumberOfRooms; ++id) {
327 addresses[id] = ReadRoomObjectAddressPc(rom_data, table_pc, id);
328 }
329 const int hard_end = GetDungeonObjectDataRegionEnd(addresses[room_id]);
330 PhysicalStreamInfo info = AnalyzePhysicalStream(addresses, room_id, hard_end);
331 if (info.address < 0) {
332 return absl::OutOfRangeError("Object stream pointer is out of range");
333 }
334 return info;
335}
336
337absl::Status GetSpritePointerTablePc(const std::vector<uint8_t>& rom_data,
338 int* table_pc) {
339 if (table_pc == nullptr) {
340 return absl::InvalidArgumentError("table_pc pointer is null");
341 }
342 if (kRoomsSpritePointer + 1 >= static_cast<int>(rom_data.size())) {
343 return absl::OutOfRangeError(
344 "Sprite pointer table address is out of range");
345 }
346
347 int table_snes = (0x09 << 16) | (rom_data[kRoomsSpritePointer + 1] << 8) |
348 rom_data[kRoomsSpritePointer];
349 int pc = SnesToPc(table_snes);
350 if (pc < 0 || pc + (kNumberOfRooms * 2) > static_cast<int>(rom_data.size())) {
351 return absl::OutOfRangeError("Sprite pointer table is out of range");
352 }
353
354 *table_pc = pc;
355 return absl::OkStatus();
356}
357
358int ReadRoomSpriteAddressPc(const std::vector<uint8_t>& rom_data, int table_pc,
359 int room_id) {
360 if (room_id < 0 || room_id >= kNumberOfRooms) {
361 return -1;
362 }
363 const int ptr_off = table_pc + (room_id * 2);
364 if (ptr_off < 0 || ptr_off + 1 >= static_cast<int>(rom_data.size())) {
365 return -1;
366 }
367
368 const uint16_t pointer =
369 (static_cast<uint16_t>(rom_data[ptr_off + 1]) << 8) | rom_data[ptr_off];
370 if (pointer < 0x8000) {
371 return -1;
372 }
373 const int sprite_address = static_cast<int>(SnesToPc((0x09 << 16) | pointer));
374 return sprite_address >= 0 &&
375 sprite_address < static_cast<int>(rom_data.size())
376 ? sprite_address
377 : -1;
378}
379
380absl::StatusOr<PhysicalStreamInfo> GetSpriteStreamInfo(
381 const std::vector<uint8_t>& rom_data, int room_id) {
382 if (room_id < 0 || room_id >= kNumberOfRooms) {
383 return absl::OutOfRangeError("Room ID out of range");
384 }
385 int table_pc = 0;
386 RETURN_IF_ERROR(GetSpritePointerTablePc(rom_data, &table_pc));
387
388 std::vector<int> addresses(kNumberOfRooms, -1);
389 for (int id = 0; id < kNumberOfRooms; ++id) {
390 addresses[id] = ReadRoomSpriteAddressPc(rom_data, table_pc, id);
391 }
392 const int hard_end =
393 std::min(static_cast<int>(rom_data.size()), kSpritesDataEndExclusive);
394 PhysicalStreamInfo info = AnalyzePhysicalStream(addresses, room_id, hard_end);
395 if (info.address < 0 || info.address >= hard_end) {
396 return absl::OutOfRangeError("Sprite stream pointer is out of range");
397 }
398 return info;
399}
400
401int MeasureSpriteStreamSize(const std::vector<uint8_t>& rom_data,
402 int sprite_address, int hard_end) {
403 if (sprite_address < 0 || sprite_address >= hard_end ||
404 sprite_address >= static_cast<int>(rom_data.size())) {
405 return 0;
406 }
407
408 int cursor = sprite_address + 1; // Skip SortSprites mode byte.
409 while (cursor < hard_end) {
410 if (rom_data[cursor] == 0xFF) {
411 ++cursor; // Include terminator.
412 break;
413 }
414 if (cursor + 2 >= hard_end) {
415 cursor = hard_end;
416 break;
417 }
418 cursor += 3;
419 }
420
421 return std::max(0, cursor - sprite_address);
422}
423
424absl::Status RelocateDungeonStream(Rom* rom, int room_id,
425 DungeonStreamKind expected_kind,
426 const DungeonStreamLayout& layout,
427 std::vector<uint8_t> encoded_stream) {
428 if (layout.kind != expected_kind) {
429 return absl::InvalidArgumentError(absl::StrFormat(
430 "Room %d relocation layout has the wrong dungeon stream kind",
431 room_id));
432 }
433
435 InventoryDungeonStreams(*rom, layout));
437 const DungeonStreamWritePlan plan,
438 PlanDungeonStreamWrites(inventory, {{static_cast<uint32_t>(room_id),
439 std::move(encoded_stream)}}));
440 return ApplyDungeonStreamWritePlan(rom, plan);
441}
442
444 const Rom& rom, int room_id, DungeonStreamKind expected_kind,
445 const DungeonStreamLayout& layout, size_t replacement_size) {
446 if (layout.kind != expected_kind) {
447 return absl::InvalidArgumentError(absl::StrFormat(
448 "Room %d save layout has the wrong dungeon stream kind", room_id));
449 }
450 if (room_id < 0 || static_cast<uint32_t>(room_id) >= layout.pointer_count) {
451 return absl::OutOfRangeError(
452 "Room ID is outside the dungeon stream layout");
453 }
454
456 InventoryDungeonStreams(rom, layout));
457 if (!inventory.ok()) {
458 return absl::FailedPreconditionError(absl::StrFormat(
459 "Dungeon stream inventory has %zu issue(s); refusing an in-place "
460 "save",
461 inventory.issues.size()));
462 }
463
464 const auto contains_room = [room_id](const std::vector<uint32_t>& owners) {
465 return std::find(owners.begin(), owners.end(),
466 static_cast<uint32_t>(room_id)) != owners.end();
467 };
468 for (const auto& alias : inventory.aliases) {
469 if (contains_room(alias.room_ids)) {
470 return true;
471 }
472 }
473 for (const auto& overlap : inventory.overlaps) {
474 if (contains_room(overlap.first_room_ids) ||
475 contains_room(overlap.second_room_ids)) {
476 return true;
477 }
478 }
479
480 const auto& record = inventory.streams[room_id];
481 const uint64_t replacement_end =
482 static_cast<uint64_t>(record.data_pc) + replacement_size;
483 const uint32_t bank_end = ((record.data_pc / 0x8000u) + 1u) * 0x8000u;
484 const bool stays_in_declared_data =
485 replacement_end <= bank_end &&
486 std::any_of(inventory.layout.data_ranges.begin(),
487 inventory.layout.data_ranges.end(), [&](const auto& range) {
488 return range.begin <= record.data_pc &&
489 replacement_end <= range.end;
490 });
491 return !stays_in_declared_data;
492}
493
494} // namespace
495
496RoomSize CalculateRoomSize(Rom* rom, int room_id) {
497 RoomSize room_size{};
498 if (!rom || !rom->is_loaded() || rom->size() == 0 || room_id < 0 ||
499 room_id >= kNumberOfRooms) {
500 return room_size;
501 }
502
503 const auto& rom_data = rom->vector();
504 int table_pc = 0;
505 if (!GetObjectPointerTablePc(rom_data, &table_pc).ok()) {
506 return room_size;
507 }
508 room_size.room_size_pointer =
509 ReadRoomObjectAddressSnes(rom_data, table_pc, room_id);
510
511 auto stream_info = GetObjectStreamInfo(rom_data, room_id);
512 if (!stream_info.ok() || stream_info->shared) {
513 return room_size;
514 }
515 room_size.room_size = stream_info->capacity();
516 return room_size;
517}
518
519// Loads a room from the ROM.
520// ASM: Bank 01, Underworld_LoadRoom ($01873A)
521Room LoadRoomFromRom(Rom* rom, int room_id) {
522 // Use the header loader to get the base room with properties
523 // ASM: JSR Underworld_LoadHeader ($01873A)
524 Room room = LoadRoomHeaderFromRom(rom, room_id);
525
526 // Load additional room features
527 //
528 // USDASM ground truth: LoadAndBuildRoom ($01:873A) draws the variable-length
529 // room object stream first (RoomDraw_DrawAllObjects), then draws pushable
530 // blocks ($7EF940) and torches ($7EFB40). These "special" objects are not
531 // part of the room object stream and must not be saved into it.
532 room.LoadObjects();
533 room.LoadChests();
534 room.LoadPotItems();
535 room.LoadTorches();
536 room.LoadBlocks();
537 room.LoadPits();
538
539 room.SetLoaded(true);
540 room.ClearSaveDirtyState();
542 room.ClearWaterFillDirty();
543 return room;
544}
545
546Room LoadRoomHeaderFromRom(Rom* rom, int room_id) {
547 Room room(room_id, rom);
548
549 if (!rom || !rom->is_loaded() || rom->size() == 0) {
550 return room;
551 }
552
553 // Validate kRoomHeaderPointer access
554 if (kRoomHeaderPointer < 0 ||
555 kRoomHeaderPointer + 2 >= static_cast<int>(rom->size())) {
556 return room;
557 }
558
559 // ASM: RoomHeader_RoomToPointer table lookup
560 int header_pointer = (rom->data()[kRoomHeaderPointer + 2] << 16) +
561 (rom->data()[kRoomHeaderPointer + 1] << 8) +
562 (rom->data()[kRoomHeaderPointer]);
563 header_pointer = SnesToPc(header_pointer);
564
565 // Validate kRoomHeaderPointerBank access
566 if (kRoomHeaderPointerBank < 0 ||
567 kRoomHeaderPointerBank >= static_cast<int>(rom->size())) {
568 return room;
569 }
570
571 // Validate header_pointer table access
572 int table_offset = (header_pointer) + (room_id * 2);
573 if (table_offset < 0 || table_offset + 1 >= static_cast<int>(rom->size())) {
574 return room;
575 }
576
577 int address = (rom->data()[kRoomHeaderPointerBank] << 16) +
578 (rom->data()[table_offset + 1] << 8) +
579 rom->data()[table_offset];
580
581 auto header_location = SnesToPc(address);
582
583 // Validate header_location access (we read up to +13 bytes)
584 if (header_location < 0 ||
585 header_location + 13 >= static_cast<int>(rom->size())) {
586 return room;
587 }
588
589 const uint8_t header_byte0 = rom->data()[header_location];
590 room.SetLayer2Mode(Layer2ModeFromHeaderByte(header_byte0));
591 room.SetLayerMerging(LayerMergeFromHeaderByte(header_byte0));
592 room.SetBg2(Background2FromHeaderByte(header_byte0));
593 room.SetCollision((CollisionKey)((header_byte0 >> 2) & 0x07));
594 room.SetIsLight(IsDarkRoomHeaderByte(header_byte0));
595 room.SetIsDark(IsDarkRoomHeaderByte(header_byte0));
596
597 // USDASM grounding (bank_01.asm LoadRoomHeader, e.g. $01:B61B):
598 // The room header stores an 8-bit "palette set ID" (0-71 in vanilla), which
599 // is later multiplied by 4 to index UnderworldPaletteSets. Do NOT truncate to
600 // 6 bits: IDs 0x40-0x47 are valid and were previously corrupted by & 0x3F.
601 room.SetPalette(rom->data()[header_location + 1]);
602 room.SetBlockset((rom->data()[header_location + 2]));
603 room.SetSpriteset((rom->data()[header_location + 3]));
604 room.SetEffect((EffectKey)((rom->data()[header_location + 4])));
605 room.SetTag1((TagKey)((rom->data()[header_location + 5])));
606 room.SetTag2((TagKey)((rom->data()[header_location + 6])));
607
608 room.SetStaircasePlane(0, ((rom->data()[header_location + 7] >> 2) & 0x03));
609 room.SetStaircasePlane(1, ((rom->data()[header_location + 7] >> 4) & 0x03));
610 room.SetStaircasePlane(2, ((rom->data()[header_location + 7] >> 6) & 0x03));
611 room.SetStaircasePlane(3, ((rom->data()[header_location + 8]) & 0x03));
612
613 room.SetHolewarp((rom->data()[header_location + 9]));
614 room.SetStaircaseRoom(0, (rom->data()[header_location + 10]));
615 room.SetStaircaseRoom(1, (rom->data()[header_location + 11]));
616 room.SetStaircaseRoom(2, (rom->data()[header_location + 12]));
617 room.SetStaircaseRoom(3, (rom->data()[header_location + 13]));
618
619 // =====
620
621 // Validate kRoomHeaderPointer access (again, just in case)
622 if (kRoomHeaderPointer < 0 ||
623 kRoomHeaderPointer + 2 >= static_cast<int>(rom->size())) {
624 return room;
625 }
626
627 int header_pointer_2 = (rom->data()[kRoomHeaderPointer + 2] << 16) +
628 (rom->data()[kRoomHeaderPointer + 1] << 8) +
629 (rom->data()[kRoomHeaderPointer]);
630 header_pointer_2 = SnesToPc(header_pointer_2);
631
632 // Validate kRoomHeaderPointerBank access
633 if (kRoomHeaderPointerBank < 0 ||
634 kRoomHeaderPointerBank >= static_cast<int>(rom->size())) {
635 return room;
636 }
637
638 // Validate header_pointer_2 table access
639 int table_offset_2 = (header_pointer_2) + (room_id * 2);
640 if (table_offset_2 < 0 ||
641 table_offset_2 + 1 >= static_cast<int>(rom->size())) {
642 return room;
643 }
644
645 int address_2 = (rom->data()[kRoomHeaderPointerBank] << 16) +
646 (rom->data()[table_offset_2 + 1] << 8) +
647 rom->data()[table_offset_2];
648
649 int msg_addr = kMessagesIdDungeon + (room_id * 2);
650 if (msg_addr >= 0 && msg_addr + 1 < static_cast<int>(rom->size())) {
651 uint16_t msg_val = (rom->data()[msg_addr + 1] << 8) | rom->data()[msg_addr];
652 room.SetMessageId(msg_val);
653 }
654
655 auto hpos = SnesToPc(address_2);
656
657 // Validate hpos access (we read sequentially)
658 // We read about 14 bytes (hpos++ calls)
659 if (hpos < 0 || hpos + 14 >= static_cast<int>(rom->size())) {
660 return room;
661 }
662
663 uint8_t b = rom->data()[hpos];
664
665 room.SetLayer2Mode(Layer2ModeFromHeaderByte(b));
666 room.SetLayerMerging(LayerMergeFromHeaderByte(b));
667 room.SetIsDark(IsDarkRoomHeaderByte(b));
668 hpos++;
669 // Skip palette byte here - already set by SetPalette() from the primary
670 // header table above (line ~329). The old SetPaletteDirect wrote to a
671 // separate dead-code member; now palette_ is unified.
672 hpos++;
673
674 room.SetBackgroundTileset(rom->data()[hpos]);
675 hpos++;
676
677 room.SetSpriteTileset(rom->data()[hpos]);
678 hpos++;
679
680 room.SetLayer2Behavior(rom->data()[hpos]);
681 hpos++;
682
683 room.SetTag1Direct((TagKey)rom->data()[hpos]);
684 hpos++;
685
686 room.SetTag2Direct((TagKey)rom->data()[hpos]);
687 hpos++;
688
689 b = rom->data()[hpos];
690
691 room.SetPitsTargetLayer((uint8_t)(b & 0x03));
692 room.SetStair1TargetLayer((uint8_t)((b >> 2) & 0x03));
693 room.SetStair2TargetLayer((uint8_t)((b >> 4) & 0x03));
694 room.SetStair3TargetLayer((uint8_t)((b >> 6) & 0x03));
695 hpos++;
696 room.SetStair4TargetLayer((uint8_t)(rom->data()[hpos] & 0x03));
697 hpos++;
698
699 room.SetPitsTarget(rom->data()[hpos]);
700 hpos++;
701 room.SetStair1Target(rom->data()[hpos]);
702 hpos++;
703 room.SetStair2Target(rom->data()[hpos]);
704 hpos++;
705 room.SetStair3Target(rom->data()[hpos]);
706 hpos++;
707 room.SetStair4Target(rom->data()[hpos]);
708
709 room.ClearSaveDirtyState();
711 room.ClearWaterFillDirty();
712 // Note: We do NOT set is_loaded_ to true here, as this is just the header
713 return room;
714}
715
716Room::Room(int room_id, Rom* rom, GameData* game_data)
717 : room_id_(room_id),
718 rom_(rom),
719 game_data_(game_data),
720 dungeon_state_(std::make_unique<EditorDungeonState>(rom, game_data)) {}
721
722Room::Room() = default;
723Room::~Room() = default;
724Room::Room(Room&&) = default;
725Room& Room::operator=(Room&&) = default;
726
728 if (!game_data_ || !rom_)
729 return 0;
730 const auto& group = game_data_->palette_groups.dungeon_main;
731 const int num_palettes = static_cast<int>(group.size());
732 if (num_palettes == 0)
733 return 0;
734
735 int id = palette_;
736 if (palette_ < game_data_->paletteset_ids.size() &&
738 const auto offset = game_data_->paletteset_ids[palette_][0];
739 const auto word = rom_->ReadWord(kDungeonPalettePointerTable + offset);
740 if (word.ok()) {
741 id = word.value() / kDungeonPaletteBytes;
742 }
743 }
744 if (id < 0 || id >= num_palettes)
745 id = 0;
746 return id;
747}
748
749void Room::LoadRoomGraphics(std::optional<uint8_t> entrance_blockset) {
750 if (!game_data_) {
751 LOG_DEBUG("Room", "GameData not set for room %d", room_id_);
752 return;
753 }
754
755 const auto& room_gfx = game_data_->room_blockset_ids;
756 const auto& sprite_gfx = game_data_->spriteset_ids;
757 const uint8_t requested_main_blockset =
758 entrance_blockset.value_or(render_entrance_blockset_);
759 uint8_t main_blockset = 0;
760 if (requested_main_blockset != 0xFF &&
761 requested_main_blockset < game_data_->main_blockset_ids.size()) {
762 main_blockset = requested_main_blockset;
763 } else if (blockset_ < game_data_->main_blockset_ids.size()) {
764 main_blockset = blockset_;
765 } else {
766 LOG_WARN("Room",
767 "Room %d: invalid main fallback blockset %d; using main group 0",
769 }
770 if (requested_main_blockset != 0xFF &&
771 requested_main_blockset >= game_data_->main_blockset_ids.size()) {
772 LOG_WARN("Room",
773 "Room %d: entrance main blockset %d out of range; using %d",
774 room_id_, requested_main_blockset, main_blockset);
775 }
776 resolved_main_blockset_ = main_blockset;
777
778 LOG_DEBUG("Room",
779 "Room %d: room_blockset=%d, main_blockset=%d, spriteset=%d, "
780 "palette=%d",
781 room_id_, blockset_, main_blockset, spriteset_, palette_);
782
783 for (int i = 0; i < 8; i++) {
784 blocks_[i] = game_data_->main_blockset_ids[main_blockset][i];
785 if (i >= 3 && i <= 6 && blockset_ < room_gfx.size()) {
786 const uint8_t room_sheet = room_gfx[blockset_][i - 3];
787 if (room_sheet != 0) {
788 blocks_[i] = room_sheet;
789 }
790 }
791 }
792 if (blockset_ >= room_gfx.size()) {
793 LOG_WARN("Room", "Room %d: room blockset %d out of range; skipped $0AA2",
795 }
796
797 blocks_[8] = 115 + 0; // Static Sprites Blocksets (fairy,pot,ect...)
798 blocks_[9] = 115 + 10;
799 blocks_[10] = 115 + 6;
800 blocks_[11] = 115 + 7;
801 const size_t sprite_gfx_index = static_cast<size_t>(spriteset_) + 64;
802 if (sprite_gfx_index < sprite_gfx.size()) {
803 for (int i = 0; i < 4; i++) {
804 blocks_[12 + i] =
805 static_cast<uint8_t>(sprite_gfx[sprite_gfx_index][i] + 115);
806 }
807 } else {
808 LOG_WARN("Room",
809 "Room %d: spriteset %d out of range; clearing sprite sheets",
811 for (int i = 0; i < 4; i++) {
812 blocks_[12 + i] = 0;
813 }
814 } // 12-15 sprites
815
816 LOG_DEBUG("Room", "Sheet IDs BG[0-7]: %d %d %d %d %d %d %d %d", blocks_[0],
817 blocks_[1], blocks_[2], blocks_[3], blocks_[4], blocks_[5],
818 blocks_[6], blocks_[7]);
819}
820
822 if (objects_loaded_) {
823 return;
824 }
825 LoadObjects();
826}
827
829 if (sprites_loaded_) {
830 return;
831 }
832 LoadSprites();
833}
834
836 if (pot_items_loaded_) {
837 return;
838 }
839 LoadPotItems();
840}
841
842void Room::ReloadGraphics(std::optional<uint8_t> entrance_blockset) {
843 if (entrance_blockset.has_value()) {
844 SetRenderEntranceBlockset(*entrance_blockset);
845 }
851}
852
853void Room::PrepareForRender(std::optional<uint8_t> entrance_blockset) {
854 if (entrance_blockset.has_value()) {
855 SetRenderEntranceBlockset(*entrance_blockset);
856 }
858
859 auto& bg1_bmp = bg1_buffer_.bitmap();
860 auto& bg2_bmp = bg2_buffer_.bitmap();
862 dirty_state_.textures || !bg1_bmp.is_active() || bg1_bmp.width() == 0 ||
863 !bg2_bmp.is_active() || bg2_bmp.width() == 0) {
865 }
866}
867
868constexpr int kGfxBufferOffset = 92 * 2048;
869constexpr int kGfxBufferStride = 1024;
870constexpr int kGfxBufferAnimatedFrameOffset = 7 * 4096;
871constexpr int kGfxBufferAnimatedFrameStride = 1024;
872constexpr int kGfxBufferRoomOffset = 4096;
873constexpr int kGfxBufferRoomSpriteOffset = 1024;
874constexpr int kGfxBufferRoomSpriteStride = 4096;
876
878 if (!rom_ || !rom_->is_loaded()) {
879 LOG_DEBUG("Room", "CopyRoomGraphicsToBuffer: ROM not loaded");
880 return;
881 }
882
883 if (!game_data_) {
884 LOG_DEBUG("Room", "CopyRoomGraphicsToBuffer: GameData not set");
885 return;
886 }
887 auto* gfx_buffer_data = &game_data_->graphics_buffer;
888 if (gfx_buffer_data->empty()) {
889 LOG_DEBUG("Room", "CopyRoomGraphicsToBuffer: Graphics buffer is empty");
890 return;
891 }
892
893 LOG_DEBUG("Room", "Room %d: Copying 8BPP graphics (buffer size: %zu)",
894 room_id_, gfx_buffer_data->size());
895
896 // Clear destination buffer
897 std::fill(current_gfx16_.begin(), current_gfx16_.end(), 0);
898
899 // USDASM grounding (bank_00.asm LoadBackgroundGraphics):
900 // The engine expands 3BPP graphics to 4BPP in two modes:
901 // - Left palette: plane3 = 0 (pixel values 0-7).
902 // - Right palette: plane3 = OR(planes0..2), so non-zero pixels get bit3=1
903 // (pixel values 1-7 become 9-15; 0 remains 0/transparent).
904 //
905 // For background graphics sets, the game selects Left/Right based on the
906 // active main graphics group ($0AA1) and the slot index ($0F).
907 // InitializeTilesets starts $0F at 7 for destination block 0 and decrements
908 // it through destination block 7, so the runtime slot is 7 - block. For UW
909 // groups (< $20), runtime slots 4-7 use Right; for OW groups (>= $20), the
910 // Right runtime slots are {2,3,4,7}.
911 const uint8_t active_main_blockset =
915 : blockset_);
916 auto is_right_palette_background_slot = [&](int block) -> bool {
917 if (block < 0 || block >= 8) {
918 return false;
919 }
920 const int runtime_slot = 7 - block;
921 if (active_main_blockset < 0x20) {
922 return runtime_slot >= 4;
923 }
924 return (runtime_slot == 2 || runtime_slot == 3 || runtime_slot == 4 ||
925 runtime_slot == 7);
926 };
927
928 // Process each of the 16 graphics blocks
929 for (int block = 0; block < 16; block++) {
930 int sheet_id = blocks_[block];
931
932 // Validate block index
933 if (sheet_id >= 223) { // kNumGfxSheets
934 LOG_WARN("Room", "Invalid sheet index %d for block %d", sheet_id, block);
935 continue;
936 }
937
938 // Source offset in ROM graphics buffer (now 8BPP format)
939 // Each 8BPP sheet is 4096 bytes (128x32 pixels)
940 int src_sheet_offset = sheet_id * 4096;
941
942 // Validate source bounds
943 if (src_sheet_offset + 4096 > gfx_buffer_data->size()) {
944 LOG_ERROR("Room", "Graphics offset out of bounds: %d (size: %zu)",
945 src_sheet_offset, gfx_buffer_data->size());
946 continue;
947 }
948
949 // Copy 4096 bytes for the 8BPP sheet
950 int dest_index_base = block * 4096;
951 if (dest_index_base + 4096 <= current_gfx16_.size()) {
952 const uint8_t* src = gfx_buffer_data->data() + src_sheet_offset;
953 uint8_t* dst = current_gfx16_.data() + dest_index_base;
954
955 // Only background blocks (0-7) participate in Left/Right palette
956 // expansion. Sprite sheets are handled separately by the game.
957 const bool right_pal = is_right_palette_background_slot(block);
958 if (!right_pal) {
959 memcpy(dst, src, 4096);
960 } else {
961 // Right palette expansion: set bit3 for non-zero pixels (1-7 -> 9-15).
962 for (int i = 0; i < 4096; ++i) {
963 uint8_t p = src[i];
964 if (p != 0 && p < 8) {
965 p |= 0x08;
966 }
967 dst[i] = p;
968 }
969 }
970 }
971 }
972
973 LOG_DEBUG("Room", "Room %d: Graphics blocks copied successfully", room_id_);
975}
976
978 const uint64_t requested_signature = layer_mgr.CompositeStateSignature();
980 composite_signature_ != requested_signature) {
981 layer_mgr.CompositeToOutput(*this, composite_bitmap_);
982 dirty_state_.composite = false;
983 composite_signature_ = requested_signature;
985 }
987 return composite_bitmap_;
988}
989
991 // PERFORMANCE OPTIMIZATION: Check if room properties have changed
992 bool properties_changed = false;
993
994 // Check if graphics properties changed
1005 dirty_state_.graphics = true;
1006 properties_changed = true;
1007 }
1008
1009 // Check if effect/tags changed
1010 if (cached_effect_ != static_cast<uint8_t>(effect_) ||
1012 cached_effect_ = static_cast<uint8_t>(effect_);
1015 dirty_state_.objects = true;
1016 properties_changed = true;
1017 }
1018
1019 // If nothing changed and textures exist, skip rendering
1020 if (!properties_changed && !dirty_state_.graphics && !dirty_state_.objects &&
1022 auto& bg1_bmp = bg1_buffer_.bitmap();
1023 auto& bg2_bmp = bg2_buffer_.bitmap();
1024 if (bg1_bmp.is_active() && bg1_bmp.width() > 0 && bg2_bmp.is_active() &&
1025 bg2_bmp.width() > 0) {
1026 LOG_DEBUG("[RenderRoomGraphics]",
1027 "Room %d: No changes detected, skipping render", room_id_);
1028 return;
1029 }
1030 }
1031
1032 LOG_DEBUG("[RenderRoomGraphics]",
1033 "Room %d: Rendering graphics (dirty_flags: g=%d o=%d l=%d t=%d)",
1036
1037 // Capture dirty state BEFORE clearing flags (needed for floor/bg draw logic)
1038 bool was_graphics_dirty = dirty_state_.graphics;
1039 bool was_layout_dirty = dirty_state_.layout;
1040
1041 // STEP 0: Load graphics if needed
1042 if (dirty_state_.graphics) {
1043 // Ensure blocks_[] array is properly initialized before copying graphics
1044 // LoadRoomGraphics sets up which sheets go into which blocks
1047 dirty_state_.graphics = false;
1048 }
1049
1050 // Debug: Log floor graphics values
1051 LOG_DEBUG("[RenderRoomGraphics]",
1052 "Room %d: floor1=%d, floor2=%d, blocks_size=%zu", room_id_,
1054
1055 // STEP 1: Draw floor tiles to bitmaps (base layer) - if graphics changed OR
1056 // bitmaps not created yet
1057 bool need_floor_draw = was_graphics_dirty;
1058 auto& bg1_bmp = bg1_buffer_.bitmap();
1059 auto& bg2_bmp = bg2_buffer_.bitmap();
1060
1061 // Always draw floor if bitmaps don't exist yet (first time rendering)
1062 if (!bg1_bmp.is_active() || bg1_bmp.width() == 0 || !bg2_bmp.is_active() ||
1063 bg2_bmp.width() == 0) {
1064 need_floor_draw = true;
1065 LOG_DEBUG("[RenderRoomGraphics]",
1066 "Room %d: Bitmaps not created yet, forcing floor draw", room_id_);
1067 }
1068
1069 if (need_floor_draw) {
1074 }
1075
1076 // STEP 2: Draw background tiles (floor pattern) to bitmap
1077 // This converts the floor tile buffer to pixels
1078 bool need_bg_draw = was_graphics_dirty || need_floor_draw;
1079 if (need_bg_draw) {
1080 bg1_buffer_.DrawBackground(std::span<uint8_t>(current_gfx16_));
1081 bg2_buffer_.DrawBackground(std::span<uint8_t>(current_gfx16_));
1082 }
1083
1084 // STEP 3: Draw layout objects ON TOP of floor
1085 // Layout objects (walls, corners) are drawn after floor so they appear over it.
1086 // USDASM order (bank_01.asm LoadAndBuildRoom): floors, layout, primary object
1087 // stream, BG2 overlay stream (post-0xFFFF), BG1 overlay stream, then blocks/
1088 // torches. `RenderObjectsToBackground` runs three object-stream passes; layout
1089 // is emitted here before object buffers. See dungeon-object-rendering-spec.md.
1090 if (was_layout_dirty || need_floor_draw) {
1092 dirty_state_.layout = false;
1093 }
1094
1095 // Get and apply palette BEFORE rendering objects (so objects use correct colors)
1096 if (!game_data_)
1097 return;
1098 auto& dungeon_pal_group = game_data_->palette_groups.dungeon_main;
1099 if (dungeon_pal_group.empty())
1100 return;
1101
1102 const int palette_id = ResolveDungeonPaletteId();
1103 auto bg1_palette = dungeon_pal_group[palette_id];
1104
1107
1108 // DEBUG: Log palette loading
1109 PaletteDebugger::Get().LogPaletteLoad("Room::RenderRoomGraphics", palette_id,
1110 bg1_palette);
1111
1112 LOG_DEBUG("Room", "RenderRoomGraphics: Palette ID=%d, Size=%zu", palette_id,
1113 bg1_palette.size());
1114 if (!bg1_palette.empty()) {
1115 LOG_DEBUG("Room", "RenderRoomGraphics: First color: R=%d G=%d B=%d",
1116 bg1_palette[0].rom_color().red, bg1_palette[0].rom_color().green,
1117 bg1_palette[0].rom_color().blue);
1118 }
1119
1120 if (bg1_palette.size() > 0) {
1121 std::optional<gfx::SnesPalette> hud_palette_storage;
1122 const gfx::SnesPalette* hud_palette = nullptr;
1124 hud_palette_storage = game_data_->palette_groups.hud.palette_ref(0);
1125 hud_palette = &*hud_palette_storage;
1126 }
1127
1128 // Apply dungeon palette in a layout that mirrors SNES CGRAM directly.
1129 //
1130 // SNES CGRAM layout for dungeons:
1131 // Rows 0-1 : HUD palette
1132 // Rows 2-7 : Dungeon main, 6 banks × 15 colors = 90 colors
1133 // (`PaletteLoad_UnderworldSet` copies starting at color $21)
1134 //
1135 // SDL palette (256 indices) mirrors CGRAM rows 1:1:
1136 // SDL indices [bank*16 .. bank*16+15] for bank = CGRAM row 0-7.
1137 // Slot 0 of each bank is still transparent to the tile renderer because
1138 // source pixel value 0 is skipped, but rows 0-1 must still be populated
1139 // with the HUD palette because vanilla floor and ceiling tilewords do use
1140 // palette rows 0 and 1.
1141 //
1142 // Drawing formula (see ObjectDrawer): final_color = pixel + (pal * 16).
1143 // Where pal is the 3-bit tile palette field (0-7) and pixel is 1-15.
1144 const auto render_palette =
1145 BuildDungeonRenderPalette(bg1_palette, hud_palette);
1146
1147 // Store current palette state for pixel inspector / issue report debugging.
1151
1152 auto set_dungeon_palette = [&](gfx::Bitmap& bmp) {
1153 bmp.SetPalette(render_palette);
1154 if (bmp.surface()) {
1155 // Set color key to 255 for proper alpha blending (undrawn areas)
1156 SDL_SetColorKey(bmp.surface(), SDL_TRUE, 255);
1157 SDL_SetSurfaceBlendMode(bmp.surface(), SDL_BLENDMODE_BLEND);
1158 }
1159 };
1160
1161 set_dungeon_palette(bg1_bmp);
1162 set_dungeon_palette(bg2_bmp);
1163 set_dungeon_palette(object_bg1_buffer_.bitmap());
1164 set_dungeon_palette(object_bg2_buffer_.bitmap());
1165
1166 // DEBUG: Verify palette was applied to SDL surface
1167 auto* surface = bg1_bmp.surface();
1168 if (surface) {
1169 SDL_Palette* palette = platform::GetSurfacePalette(surface);
1170 if (palette) {
1172 "Room::RenderRoomGraphics (BG1)", palette_id, true);
1173
1174 // Log surface state for detailed debugging
1176 "Room::RenderRoomGraphics (after SetPalette)", surface);
1177 } else {
1179 "Room::RenderRoomGraphics", palette_id, false,
1180 "SDL surface has no palette!");
1181 }
1182 }
1183
1184 // Apply Layer Merge effects (Transparency/Blending) to BG2
1185 // NOTE: These SDL blend settings are for direct SDL rendering paths.
1186 // RoomLayerManager::CompositeToOutput uses manual pixel compositing and
1187 // handles blend modes separately via its layer_blend_mode_ array.
1188 // NOTE: RoomLayerManager::CompositeToOutput() now handles translucent
1189 // blending with proper SNES color math. These SDL alpha settings are a
1190 // legacy fallback for direct SDL rendering paths. Consolidation would
1191 // remove this in favor of RoomLayerManager exclusively.
1193 // Set alpha mod for translucency (50%)
1194 if (bg2_bmp.surface()) {
1195 SDL_SetSurfaceAlphaMod(bg2_bmp.surface(), 128);
1196 }
1197 if (object_bg2_buffer_.bitmap().surface()) {
1198 SDL_SetSurfaceAlphaMod(object_bg2_buffer_.bitmap().surface(), 128);
1199 }
1200
1201 // Check for Addition mode (ID 0x05)
1202 if (layer_merging_.ID == 0x05) {
1203 if (bg2_bmp.surface()) {
1204 SDL_SetSurfaceBlendMode(bg2_bmp.surface(), SDL_BLENDMODE_ADD);
1205 }
1206 if (object_bg2_buffer_.bitmap().surface()) {
1207 SDL_SetSurfaceBlendMode(object_bg2_buffer_.bitmap().surface(),
1208 SDL_BLENDMODE_ADD);
1209 }
1210 }
1211 }
1212 }
1213
1214 // Render objects ON TOP of background tiles (AFTER palette is set)
1215 // ObjectDrawer will write indexed pixel data that uses the palette we just
1216 // set
1218
1219 auto release_texture = [](gfx::Bitmap* bitmap) {
1220 if (bitmap->texture()) {
1223 }
1224 };
1225
1226 release_texture(&bg1_bmp);
1227 release_texture(&bg2_bmp);
1228 release_texture(&object_bg1_buffer_.bitmap());
1229 release_texture(&object_bg2_buffer_.bitmap());
1230
1231 dirty_state_.textures = false;
1232
1233 // IMPORTANT: Mark composite as dirty after any render work
1234 // This ensures GetCompositeBitmap() regenerates the merged output
1235 dirty_state_.composite = true;
1236
1237 // REMOVED: Don't process texture queue here - let it be batched!
1238 // Processing happens once per frame in DrawDungeonCanvas()
1239 // This dramatically improves performance when multiple rooms are open
1240 // gfx::Arena::Get().ProcessTextureQueue(nullptr); // OLD: Caused slowdown!
1241 LOG_DEBUG("[RenderRoomGraphics]",
1242 "Texture commands queued for batch processing");
1243}
1244
1246 LOG_DEBUG("Room", "LoadLayoutTilesToBuffer for room %d, layout=%d", room_id_,
1247 layout_id_);
1248
1249 if (!rom_ || !rom_->is_loaded()) {
1250 LOG_DEBUG("Room", "ROM not loaded, aborting");
1251 return;
1252 }
1253
1254 // Rebuild only layout-owned reveal requests. Room-object masks share this
1255 // raw BG1 target and remain valid when just the layout is rerendered.
1257
1258 // Load layout tiles from ROM if not already loaded
1260 auto layout_status = layout_.LoadLayout(layout_id_);
1261 if (!layout_status.ok()) {
1262 LOG_DEBUG("Room", "Failed to load layout %d: %s", layout_id_,
1263 layout_status.message().data());
1264 return;
1265 }
1266
1267 const auto& layout_objects = layout_.GetObjects();
1268 LOG_DEBUG("Room", "Layout %d has %zu objects", layout_id_,
1269 layout_objects.size());
1270 if (layout_objects.empty()) {
1271 return;
1272 }
1273
1274 // Use ObjectDrawer to render layout objects properly
1275 // Layout objects are the same format as room objects and need draw routines
1276 // to render correctly (walls, corners, etc.)
1277 if (!game_data_) {
1278 LOG_DEBUG("RenderRoomGraphics", "GameData not set, cannot render layout");
1279 return;
1280 }
1281
1282 // Get palette for layout rendering
1283 auto& dungeon_pal_group = game_data_->palette_groups.dungeon_main;
1284 if (dungeon_pal_group.empty())
1285 return;
1286
1287 const int palette_id = ResolveDungeonPaletteId();
1288 auto room_palette = dungeon_pal_group[palette_id];
1289 gfx::PaletteGroup palette_group;
1290 palette_group.AddPalette(room_palette);
1291 // Palette chunking follows direct CGRAM row mirroring: tile palette bits
1292 // select SDL bank rows 0-7, and dungeon colors live in rows 2-7 with index 0
1293 // transparent within each bank. See the completed palette-fix plan in
1294 // docs/internal/archive/completed_features/dungeon-palette-fix-plan-2025-12.md.
1295
1296 // Draw layout objects using proper draw routines via RoomLayout
1297 auto status = layout_.Draw(room_id_, current_gfx16_.data(), bg1_buffer_,
1298 bg2_buffer_, palette_group, dungeon_state_.get());
1299
1300 if (!status.ok()) {
1301 LOG_DEBUG(
1302 "RenderRoomGraphics", "Layout Draw failed: %s",
1303 std::string(status.message().data(), status.message().size()).c_str());
1304 } else {
1305 LOG_DEBUG("RenderRoomGraphics", "Layout rendered with %zu objects",
1306 layout_objects.size());
1307 }
1308}
1309
1311 LOG_DEBUG("[RenderObjectsToBackground]",
1312 "Starting object rendering for room %d", room_id_);
1313
1314 if (!rom_ || !rom_->is_loaded()) {
1315 LOG_DEBUG("[RenderObjectsToBackground]", "ROM not loaded, aborting");
1316 return;
1317 }
1318
1319 // PERFORMANCE OPTIMIZATION: Only render objects if they have changed or if
1320 // graphics changed Also render if bitmaps were just created (need_floor_draw
1321 // was true in RenderRoomGraphics)
1322 auto& bg1_bmp = bg1_buffer_.bitmap();
1323 auto& bg2_bmp = bg2_buffer_.bitmap();
1324 bool bitmaps_exist = bg1_bmp.is_active() && bg1_bmp.width() > 0 &&
1325 bg2_bmp.is_active() && bg2_bmp.width() > 0;
1326
1327 if (!dirty_state_.objects && !dirty_state_.graphics && bitmaps_exist) {
1328 LOG_DEBUG("[RenderObjectsToBackground]",
1329 "Room %d: Objects not dirty, skipping render", room_id_);
1330 return;
1331 }
1332
1333 // Handle rendering based on mode (currently using emulator-based rendering)
1334 // Emulator or Hybrid mode (use ObjectDrawer)
1335 LOG_DEBUG("[RenderObjectsToBackground]",
1336 "Room %d: Emulator rendering objects", room_id_);
1337 // Get palette group for object rendering (same lookup as other render paths).
1338 if (!game_data_)
1339 return;
1340 auto& dungeon_pal_group = game_data_->palette_groups.dungeon_main;
1341 if (dungeon_pal_group.empty())
1342 return;
1343
1344 const int palette_id = ResolveDungeonPaletteId();
1345 auto room_palette = dungeon_pal_group[palette_id];
1346 // Dungeon palettes are 90-color palettes for 3BPP graphics (8-color strides)
1347 // Pass the full palette to ObjectDrawer so it can handle all palette indices
1348 gfx::PaletteGroup palette_group;
1349 palette_group.AddPalette(room_palette);
1350
1351 // Use ObjectDrawer for pattern-based object rendering
1352 // This provides proper wall/object drawing patterns
1353 // Pass the room-specific graphics buffer (current_gfx16_) so objects use
1354 // correct tiles
1356 drawer.SetAllowTrackCornerAliases(RoomUsesTrackCornerAliases(tile_objects_));
1358 // NOTE: Routines that explicitly write both tilemaps (ceiling corners and
1359 // merged stairs) are handled by DrawRoutineRegistry's draws_to_both_bgs
1360 // flag. The room object stream is split here as primary -> BG2 overlay ->
1361 // BG1 overlay, while the layout pass is rendered separately by
1362 // RoomLayout::Draw.
1363
1364 // Clear object buffers before rendering
1365 // IMPORTANT: Fill with 255 (transparent color key) so objects overlay correctly
1366 // on the floor. We use index 255 as transparent since palette has 90 colors (0-89).
1369 object_bg1_buffer_.bitmap().Fill(255);
1370 object_bg2_buffer_.bitmap().Fill(255);
1371
1372 // IMPORTANT: Clear priority buffers when clearing object buffers
1373 // Otherwise, old priority values persist and cause incorrect Z-ordering
1376
1377 // IMPORTANT: Clear coverage buffers when clearing object buffers.
1378 // Coverage distinguishes "no draw" vs "drew transparent", so stale values
1379 // can cause objects to incorrectly clear the layout.
1382
1383 // Room-object masks target both raw BG1 stacks. Clear only their source bit
1384 // so layout-owned reveals survive an object-only rerender.
1387
1388 // Log stream distribution for this room.
1389 // USDASM order is: main list -> BG2 overlay list -> BG1 overlay list.
1390 int layer0_count = 0, layer1_count = 0, layer2_count = 0;
1391 for (const auto& obj : tile_objects_) {
1392 switch (obj.GetLayerValue()) {
1393 case 0:
1394 layer0_count++;
1395 break;
1396 case 1:
1397 layer1_count++;
1398 break;
1399 case 2:
1400 layer2_count++;
1401 break;
1402 }
1403 }
1404 LOG_DEBUG(
1405 "Room",
1406 "Room %03X Object Stream Summary: Main=%d, BG2Overlay=%d, BG1Overlay=%d",
1407 room_id_, layer0_count, layer1_count, layer2_count);
1408
1409 // Render room-object streams in USDASM order.
1410 // - List index 0: primary object list -> BG1 object buffer (upper tilemap)
1411 // - List index 1: BG2 overlay list -> BG2 object buffer
1412 // - List index 2: BG1 overlay list -> BG1 object buffer (BG3 enum; same draw
1413 // path as BG1 in ObjectDrawer for non-BothBG objects)
1414 // `tile_objects_[].layer_` holds the list index (0/1/2) for save/load, not
1415 // the buffer name. Map with MapRoomObjectListIndexToDrawLayer before drawing.
1416 // BothBG routines still fan out to both buffers via DrawRoutineRegistry.
1417 // Pass bg1_buffer_ as the second raw BG1 target. BG2 room objects record
1418 // deferred reveal bits on both layout and object targets without mutating
1419 // either bitmap.
1420 //
1421 // Three DrawObjectList passes match USDASM list order; the shared chest/
1422 // big-key-lock event index continues across passes (reset only on the first
1423 // non-empty pass).
1424 std::vector<std::vector<RoomObject>> by_list(3);
1425 for (const auto& obj : tile_objects_) {
1426 // Torches and pushable blocks are NOT part of the room object stream.
1427 // They come from the global tables and are drawn after the stream in
1428 // USDASM (LoadAndBuildRoom $01:873A). Draw them in a dedicated pass.
1429 if ((obj.options() & ObjectOption::Torch) != ObjectOption::Nothing) {
1430 continue;
1431 }
1432 if ((obj.options() & ObjectOption::Block) != ObjectOption::Nothing) {
1433 continue;
1434 }
1435
1436 uint8_t list_index = obj.GetLayerValue();
1437 if (list_index > 2) {
1438 list_index = 2;
1439 }
1440 RoomObject render_obj = obj;
1441 render_obj.layer_ = MapRoomObjectListIndexToDrawLayer(list_index);
1442 by_list[list_index].push_back(std::move(render_obj));
1443 }
1444
1445 absl::Status status = absl::OkStatus();
1446 bool reset_room_events_for_next_chunk = true;
1447 for (int pass = 0; pass < 3; ++pass) {
1448 if (by_list[pass].empty()) {
1449 continue;
1450 }
1451 auto chunk_status = drawer.DrawObjectList(
1452 by_list[pass], object_bg1_buffer_, object_bg2_buffer_, palette_group,
1453 dungeon_state_.get(), &bg1_buffer_, reset_room_events_for_next_chunk);
1454 reset_room_events_for_next_chunk = false;
1455 if (!chunk_status.ok() && status.ok()) {
1456 status = chunk_status;
1457 }
1458 }
1459
1460 // Render doors using DoorDef struct with enum types
1461 // Doors are drawn to the OBJECT buffer for layer visibility control
1462 // This allows doors to remain visible when toggling BG1_Layout off
1463 for (int i = 0; i < static_cast<int>(doors_.size()); ++i) {
1464 const auto& door = doors_[i];
1465 ObjectDrawer::DoorDef door_def;
1466 door_def.type = door.type;
1467 door_def.direction = door.direction;
1468 door_def.position = door.position;
1469 // Draw doors to object buffers (not layout buffers) so they remain visible
1470 // when BG1_Layout is hidden. Doors are objects, not layout tiles.
1471 drawer.DrawDoor(door_def, i, object_bg1_buffer_, object_bg2_buffer_,
1472 dungeon_state_.get());
1473 }
1474 // Mark object buffer as modified so texture gets updated
1475 if (!doors_.empty()) {
1476 object_bg1_buffer_.bitmap().set_modified(true);
1477 }
1478
1479 // Render pot items
1480 // Pot items now have their own position from ROM data
1481 // No need to match to objects - each item has exact coordinates
1482 for (const auto& pot_item : pot_items_) {
1483 if (pot_item.item != 0) { // Skip "Nothing" items
1484 // PotItem provides pixel coordinates, convert to tile coords
1485 int tile_x = pot_item.GetTileX();
1486 int tile_y = pot_item.GetTileY();
1487 drawer.DrawPotItem(pot_item.item, tile_x, tile_y, object_bg1_buffer_);
1488 }
1489 }
1490
1491 // Render sprites (for key drops)
1492 // We don't have full sprite rendering yet, but we can visualize key drops
1493 for (const auto& sprite : sprites_) {
1494 if (sprite.key_drop() > 0) {
1495 // Draw key drop visualization
1496 // Use a special item ID or just draw a key icon
1497 // We can reuse DrawPotItem with a special ID for key
1498 // Or add DrawKeyDrop to ObjectDrawer
1499 // For now, let's use DrawPotItem with ID 0xFD (Small Key) or 0xFE (Big Key)
1500 uint8_t key_item = (sprite.key_drop() == 1) ? 0xFD : 0xFE;
1501 drawer.DrawPotItem(key_item, sprite.x(), sprite.y(), object_bg1_buffer_);
1502 }
1503 }
1504
1505 // Special tables pass (USDASM-aligned):
1506 // - Pushable blocks: bank_01.asm RoomDraw_PushableBlock uses RoomDrawObjectData
1507 // offset $0E52 (bank_00.asm #obj0E52).
1508 // - Lightable torches: bank_01.asm RoomDraw_LightableTorch chooses between
1509 // offsets $0EC2 (unlit) and $0ECA (lit) (bank_00.asm #obj0EC2/#obj0ECA).
1510 constexpr uint16_t kRoomDrawObj_PushableBlock = 0x0E52;
1511 constexpr uint16_t kRoomDrawObj_TorchUnlit = 0x0EC2;
1512 constexpr uint16_t kRoomDrawObj_TorchLit = 0x0ECA;
1513 for (const auto& obj : tile_objects_) {
1514 if ((obj.options() & ObjectOption::Block) != ObjectOption::Nothing) {
1515 // SpecialUnderworldObjects bit 13 chooses the draw tilemap. Bit 14 is an
1516 // independent behavior/pit selector retained in block metadata and must
1517 // not affect rendering.
1518 (void)drawer.DrawRoomDrawObjectData2x2(
1519 static_cast<uint16_t>(obj.id_), obj.x_, obj.y_, obj.layer_,
1520 kRoomDrawObj_PushableBlock, object_bg1_buffer_, object_bg2_buffer_);
1521 continue;
1522 }
1523 if ((obj.options() & ObjectOption::Torch) != ObjectOption::Nothing) {
1524 const uint16_t off =
1525 obj.lit_ ? kRoomDrawObj_TorchLit : kRoomDrawObj_TorchUnlit;
1526 // RoomDraw_LightableTorch retains bit 13 in its masked tilemap offset,
1527 // so the stored draw layer selects upper/BG1 or lower/BG2. Reserved bit
1528 // 14 and the lit bit do not affect the draw target.
1529 (void)drawer.DrawRoomDrawObjectData2x2(
1530 static_cast<uint16_t>(obj.id_), obj.x_, obj.y_, obj.layer_, off,
1532 continue;
1533 }
1534 }
1535
1536 if (!status.ok()) {
1537 LOG_WARN(
1538 "[RenderObjectsToBackground]",
1539 "Room %03X: ObjectDrawer failed: %s (objects left dirty for retry)",
1540 room_id_,
1541 std::string(status.message().data(), status.message().size()).c_str());
1542 // Do not scribble placeholder rectangles into layout buffers; fix the
1543 // underlying draw path or ROM state instead.
1544 dirty_state_.objects = true;
1545 } else {
1546 // Mark objects as clean after successful render
1547 dirty_state_.objects = false;
1548 LOG_DEBUG("[RenderObjectsToBackground]",
1549 "Room %d: Objects rendered successfully", room_id_);
1550 }
1551}
1552
1553// LoadGraphicsSheetsIntoArena() removed - using per-room graphics instead
1554// Room rendering no longer depends on Arena graphics sheets
1555
1557 if (!rom_ || !rom_->is_loaded()) {
1558 return;
1559 }
1560
1561 if (!game_data_) {
1562 return;
1563 }
1564 auto* gfx_buffer_data = &game_data_->graphics_buffer;
1565 if (gfx_buffer_data->empty()) {
1566 return;
1567 }
1568
1569 auto rom_data = rom()->vector();
1570 if (rom_data.empty()) {
1571 return;
1572 }
1573
1574 // Validate animated_frame_ bounds
1575 if (animated_frame_ < 0 || animated_frame_ > 10) {
1576 return;
1577 }
1578
1579 // Validate background_tileset_ bounds
1580 if (background_tileset_ < 0 || background_tileset_ > 255) {
1581 return;
1582 }
1583
1584 int gfx_ptr = SnesToPc(version_constants().kGfxAnimatedPointer);
1585 if (gfx_ptr < 0 || gfx_ptr >= static_cast<int>(rom_data.size())) {
1586 return;
1587 }
1588
1589 int data = 0;
1590 while (data < 1024) {
1591 // Validate buffer access for first operation
1592 // 92 * 4096 = 376832. 1024 * 10 = 10240. Total ~387KB.
1593 int first_offset = data + (92 * 4096) + (1024 * animated_frame_);
1594 if (first_offset >= 0 &&
1595 first_offset < static_cast<int>(gfx_buffer_data->size())) {
1596 uint8_t map_byte = (*gfx_buffer_data)[first_offset];
1597
1598 // Validate current_gfx16_ access
1599 int gfx_offset = data + (7 * 4096);
1600 if (gfx_offset >= 0 &&
1601 gfx_offset < static_cast<int>(current_gfx16_.size())) {
1602 current_gfx16_[gfx_offset] = map_byte;
1603 }
1604 }
1605
1606 // Validate buffer access for second operation
1607 int tileset_index = rom_data[gfx_ptr + background_tileset_];
1608 int second_offset =
1609 data + (tileset_index * 4096) + (1024 * animated_frame_);
1610 if (second_offset >= 0 &&
1611 second_offset < static_cast<int>(gfx_buffer_data->size())) {
1612 uint8_t map_byte = (*gfx_buffer_data)[second_offset];
1613
1614 // Validate current_gfx16_ access
1615 int gfx_offset = data + (7 * 4096) - 1024;
1616 if (gfx_offset >= 0 &&
1617 gfx_offset < static_cast<int>(current_gfx16_.size())) {
1618 current_gfx16_[gfx_offset] = map_byte;
1619 }
1620 }
1621
1622 data++;
1623 }
1624}
1625
1627 LOG_DEBUG("[LoadObjects]", "Starting LoadObjects for room %d", room_id_);
1628 auto rom_data = rom()->vector();
1629
1630 // Enhanced object loading with comprehensive validation
1631 int object_pointer = (rom_data[kRoomObjectPointer + 2] << 16) +
1632 (rom_data[kRoomObjectPointer + 1] << 8) +
1633 (rom_data[kRoomObjectPointer]);
1634 object_pointer = SnesToPc(object_pointer);
1635
1636 // Enhanced bounds checking for object pointer
1637 if (object_pointer < 0 || object_pointer >= (int)rom_->size()) {
1638 return;
1639 }
1640
1641 int room_address = object_pointer + (room_id_ * 3);
1642
1643 // Enhanced bounds checking for room address
1644 if (room_address < 0 || room_address + 2 >= (int)rom_->size()) {
1645 return;
1646 }
1647
1648 int tile_address = (rom_data[room_address + 2] << 16) +
1649 (rom_data[room_address + 1] << 8) + rom_data[room_address];
1650
1651 int objects_location = SnesToPc(tile_address);
1652
1653 // Enhanced bounds checking for objects location
1654 if (objects_location < 0 || objects_location >= (int)rom_->size()) {
1655 return;
1656 }
1657
1658 // Parse floor graphics and layout with validation
1659 if (objects_location + 1 < (int)rom_->size()) {
1660 if (is_floor_) {
1662 static_cast<uint8_t>(rom_data[objects_location] & 0x0F);
1664 static_cast<uint8_t>((rom_data[objects_location] >> 4) & 0x0F);
1665 LOG_DEBUG("[LoadObjects]",
1666 "Room %d: Set floor1_graphics_=%d, floor2_graphics_=%d",
1668 }
1669
1670 layout_id_ =
1671 static_cast<uint8_t>((rom_data[objects_location + 1] >> 2) & 0x07);
1672 }
1673
1674 LoadChests();
1675
1676 // Parse objects with enhanced error handling
1677 ParseObjectsFromLocation(objects_location + 2);
1678
1679 // Load custom collision map if present
1680 if (auto res = LoadCustomCollisionMap(rom_, room_id_); res.ok()) {
1681 custom_collision_ = std::move(res.value());
1682 }
1683
1684 // Freshly loaded from ROM; not dirty until the editor mutates it.
1686 objects_loaded_ = true;
1690}
1691
1692void Room::ParseObjectsFromLocation(int objects_location) {
1693 auto rom_data = rom()->vector();
1694
1695 // Clear existing objects before parsing to prevent accumulation on reload
1696 tile_objects_.clear();
1697 doors_.clear();
1698 z3_staircases_.clear();
1699 int nbr_of_staircase = 0;
1700
1701 int pos = objects_location;
1702 uint8_t b1 = 0;
1703 uint8_t b2 = 0;
1704 uint8_t b3 = 0;
1705 int layer = 0;
1706 bool door = false;
1707 bool end_read = false;
1708
1709 // Enhanced parsing loop with bounds checking
1710 // ASM: Main object loop logic (implicit in structure)
1711 while (!end_read && pos < (int)rom_->size()) {
1712 // Check if we have enough bytes to read
1713 if (pos + 1 >= (int)rom_->size()) {
1714 break;
1715 }
1716
1717 b1 = rom_data[pos];
1718 b2 = rom_data[pos + 1];
1719
1720 // ASM Marker: 0xFF 0xFF - End of object list (next list in USDASM order).
1721 // Stored in RoomObject::layer_ as list index for EncodeObjects():
1722 // 0 = primary list (drawn to BG1/upper object buffer by default)
1723 // 1 = BG2 overlay list
1724 // 2 = BG1 overlay list (ObjectDrawer uses BG3 enum; still BG1 object path)
1725 if (b1 == 0xFF && b2 == 0xFF) {
1726 pos += 2; // Jump to next layer
1727 layer++;
1728 LOG_DEBUG(
1729 "Room", "Room %03X: Object list transition to index %d (%s)",
1730 room_id_, layer,
1731 layer == 1 ? "BG2 overlay" : (layer == 2 ? "BG1 overlay" : "END"));
1732 door = false;
1733 if (layer == 3) {
1734 break;
1735 }
1736 continue;
1737 }
1738
1739 // ASM Marker: 0xF0 0xFF - Start of Door List
1740 // See RoomDraw_DoorObject ($018916) logic
1741 if (b1 == 0xF0 && b2 == 0xFF) {
1742 pos += 2; // Jump to door section
1743 door = true;
1744 continue;
1745 }
1746
1747 // Check if we have enough bytes for object data
1748 if (pos + 2 >= (int)rom_->size()) {
1749 break;
1750 }
1751
1752 b3 = rom_data[pos + 2];
1753 if (door) {
1754 pos += 2;
1755 } else {
1756 pos += 3;
1757 }
1758
1759 if (!door) {
1760 // ASM: RoomDraw_RoomObject ($01893C)
1761 // Handles Subtype 1, 2, 3 parsing based on byte values
1763 b1, b2, b3, static_cast<uint8_t>(layer));
1764
1765 LOG_DEBUG("Room", "Room %03X: Object 0x%03X at (%d,%d) stream=%d (%s)",
1766 room_id_, r.id_, r.x_, r.y_, layer,
1767 layer == 0 ? "Primary"
1768 : (layer == 1 ? "BG2 overlay" : "BG1 overlay"));
1769
1770 // Validate object ID before adding to the room
1771 // Object IDs can be up to 12-bit (0xFFF) to support Type 3 objects
1772 if (r.id_ >= 0 && r.id_ <= 0xFFF) {
1773 r.SetRom(rom_);
1774 tile_objects_.push_back(r);
1775
1776 // Handle special object types (staircases, chests, etc.)
1777 HandleSpecialObjects(r.id_, r.x(), r.y(), nbr_of_staircase);
1778 }
1779 } else {
1780 // Handle door objects
1781 // ASM format (from RoomDraw_DoorObject):
1782 // b1: bits 4-7 = position index, bits 0-1 = direction
1783 // b2: door type (full byte)
1784 auto door = Door::FromRomBytes(b1, b2);
1785 LOG_DEBUG("Room",
1786 "ParseDoor: room=%d b1=0x%02X b2=0x%02X pos=%d dir=%d type=%d",
1787 room_id_, b1, b2, door.position,
1788 static_cast<int>(door.direction), static_cast<int>(door.type));
1789 doors_.push_back(door);
1790 }
1791 }
1792}
1793
1794// ============================================================================
1795// Object Saving Implementation (Phase 1, Task 1.3)
1796// ============================================================================
1797
1798std::vector<uint8_t> Room::EncodeObjects() const {
1799 std::vector<uint8_t> bytes;
1800
1801 // Organize objects by ROM object-stream index (0=primary, 1=BG2 overlay,
1802 // 2=BG1 overlay), stored in RoomObject::layer_ / GetLayerValue().
1803 std::vector<RoomObject> layer0_objects;
1804 std::vector<RoomObject> layer1_objects;
1805 std::vector<RoomObject> layer2_objects;
1806
1807 // IMPORTANT: Torches and pushable blocks are stored in global per-dungeon
1808 // tables (see USDASM: LoadAndBuildRoom $01:873A). They are drawn after the
1809 // room object stream passes, so they must never be encoded into the room
1810 // object stream.
1811 for (const auto& obj : tile_objects_) {
1812 if ((obj.options() & ObjectOption::Torch) != ObjectOption::Nothing) {
1813 continue;
1814 }
1815 if ((obj.options() & ObjectOption::Block) != ObjectOption::Nothing) {
1816 continue;
1817 }
1818 switch (obj.GetLayerValue()) {
1819 case 0:
1820 layer0_objects.push_back(obj);
1821 break;
1822 case 1:
1823 layer1_objects.push_back(obj);
1824 break;
1825 case 2:
1826 layer2_objects.push_back(obj);
1827 break;
1828 }
1829 }
1830
1831 // Object stream format (USDASM bank_01.asm LoadAndBuildRoom / RoomDraw_DrawAllObjects):
1832 // - List index 0 (primary) terminated by $FFFF
1833 // - List index 1 (BG2 overlay) terminated by $FFFF
1834 // - List index 2 (BG1 overlay) ends with door marker $FFF0 (bytes F0 FF), then
1835 // 2-byte door entries, and finally $FFFF which terminates both the door
1836 // list and the third object list.
1837 //
1838 // NOTE: We always emit the door marker and a terminator, even if there are
1839 // zero doors, because vanilla room data does so as well.
1840
1841 // Encode list index 0 (primary)
1842 for (const auto& obj : layer0_objects) {
1843 auto encoded = obj.EncodeObjectToBytes();
1844 bytes.push_back(encoded.b1);
1845 bytes.push_back(encoded.b2);
1846 bytes.push_back(encoded.b3);
1847 }
1848 bytes.push_back(0xFF);
1849 bytes.push_back(0xFF);
1850
1851 // Encode list index 1 (BG2 overlay)
1852 for (const auto& obj : layer1_objects) {
1853 auto encoded = obj.EncodeObjectToBytes();
1854 bytes.push_back(encoded.b1);
1855 bytes.push_back(encoded.b2);
1856 bytes.push_back(encoded.b3);
1857 }
1858 bytes.push_back(0xFF);
1859 bytes.push_back(0xFF);
1860
1861 // Encode list index 2 (BG1 overlay)
1862 for (const auto& obj : layer2_objects) {
1863 auto encoded = obj.EncodeObjectToBytes();
1864 bytes.push_back(encoded.b1);
1865 bytes.push_back(encoded.b2);
1866 bytes.push_back(encoded.b3);
1867 }
1868
1869 // ASM marker 0xF0 0xFF - start of door list (RoomDraw_DrawAllObjects checks
1870 // for word $FFF0).
1871 bytes.push_back(0xF0);
1872 bytes.push_back(0xFF);
1873 for (const auto& door : doors_) {
1874 auto [b1, b2] = door.EncodeBytes();
1875 bytes.push_back(b1);
1876 bytes.push_back(b2);
1877 }
1878
1879 // Door list terminator (word $FFFF). This is also the list-2 terminator.
1880 bytes.push_back(0xFF);
1881 bytes.push_back(0xFF);
1882
1883 return bytes;
1884}
1885
1886std::vector<uint8_t> Room::EncodeSprites() const {
1887 std::vector<uint8_t> bytes;
1888
1889 for (const auto& sprite : sprites_) {
1890 uint8_t b1, b2, b3;
1891
1892 // b3 is simply the ID
1893 b3 = sprite.id();
1894
1895 // b2 = (X & 0x1F) | ((Flags & 0x07) << 5)
1896 // Flags 0-2 come from b2 5-7
1897 b2 = (sprite.x() & 0x1F) | ((sprite.subtype() & 0x07) << 5);
1898
1899 // b1 = (Y & 0x1F) | ((Flags & 0x18) << 2) | ((Layer & 1) << 7)
1900 // Flags 3-4 come from b1 5-6. (0x18 is 00011000)
1901 // Layer bit 0 comes from b1 7
1902 b1 = (sprite.y() & 0x1F) | ((sprite.subtype() & 0x18) << 2) |
1903 ((sprite.layer() & 0x01) << 7);
1904
1905 bytes.push_back(b1);
1906 bytes.push_back(b2);
1907 bytes.push_back(b3);
1908
1909 // Key drops are stored as hidden marker sprites immediately after the
1910 // sprite that owns the drop. Keep these bytes in sync with LoadSprites().
1911 if (sprite.key_drop() == 1) {
1912 bytes.insert(bytes.end(), {0xFE, 0x00, 0xE4});
1913 } else if (sprite.key_drop() == 2) {
1914 bytes.insert(bytes.end(), {0xFD, 0x00, 0xE4});
1915 }
1916 }
1917
1918 // Terminator
1919 bytes.push_back(0xFF);
1920
1921 return bytes;
1922}
1923
1925 if (!rom || !rom->is_loaded()) {
1927 }
1928
1929 const auto& rom_data = rom->vector();
1930 int sprite_pointer = 0;
1931 if (!GetSpritePointerTablePc(rom_data, &sprite_pointer).ok()) {
1933 }
1934
1935 const int hard_end =
1936 std::min(static_cast<int>(rom_data.size()), kSpritesDataEndExclusive);
1937 if (hard_end <= 0) {
1939 }
1940
1941 int max_used = std::min(hard_end, kSpritesData);
1942 std::unordered_set<int> visited_addresses;
1943 for (int room_id = 0; room_id < kNumberOfRooms; ++room_id) {
1944 int sprite_address =
1945 ReadRoomSpriteAddressPc(rom_data, sprite_pointer, room_id);
1946 if (sprite_address < kSpritesData || sprite_address >= hard_end) {
1947 continue;
1948 }
1949 if (!visited_addresses.insert(sprite_address).second) {
1950 continue;
1951 }
1952
1953 int stream_size =
1954 MeasureSpriteStreamSize(rom_data, sprite_address, hard_end);
1955 int stream_end = sprite_address + stream_size;
1956 if (stream_end > max_used) {
1957 max_used = stream_end;
1958 }
1959 }
1960
1961 return max_used;
1962}
1963
1964absl::Status RelocateSpriteData(Rom* rom, int room_id,
1965 const std::vector<uint8_t>& encoded_bytes) {
1966 if (!rom || !rom->is_loaded()) {
1967 return absl::InvalidArgumentError("ROM not loaded");
1968 }
1969 if (room_id < 0 || room_id >= kNumberOfRooms) {
1970 return absl::OutOfRangeError("Room ID out of range");
1971 }
1972 if (encoded_bytes.empty() || encoded_bytes.back() != 0xFF ||
1973 (encoded_bytes.size() % 3) != 1) {
1974 return absl::InvalidArgumentError(
1975 "Encoded sprite payload must be N*3 bytes plus 0xFF terminator");
1976 }
1977
1978 const auto& rom_data = rom->vector();
1979 int sprite_pointer = 0;
1980 RETURN_IF_ERROR(GetSpritePointerTablePc(rom_data, &sprite_pointer));
1981
1982 int old_sprite_address =
1983 ReadRoomSpriteAddressPc(rom_data, sprite_pointer, room_id);
1984 if (old_sprite_address < 0 ||
1985 old_sprite_address >= static_cast<int>(rom_data.size())) {
1986 return absl::OutOfRangeError("Sprite address out of range");
1987 }
1988
1989 const uint8_t sort_mode = rom_data[old_sprite_address];
1990
1991 const int write_pos = FindMaxUsedSpriteAddress(rom);
1992 const size_t required_size = 1u + encoded_bytes.size();
1993 if (write_pos < kSpritesData ||
1994 static_cast<size_t>(write_pos) + required_size >
1995 static_cast<size_t>(kSpritesDataEndExclusive)) {
1996 return absl::ResourceExhaustedError(absl::StrFormat(
1997 "Not enough sprite data space. Need %d bytes at 0x%06X, "
1998 "region ends at 0x%06X",
1999 static_cast<int>(required_size), write_pos, kSpritesDataEndExclusive));
2000 }
2001 if (static_cast<size_t>(write_pos) + required_size > rom_data.size()) {
2002 const int required_end = write_pos + static_cast<int>(required_size);
2003 return absl::OutOfRangeError(
2004 absl::StrFormat("ROM too small for sprite relocation write (need "
2005 "end=0x%06X, size=0x%06X)",
2006 required_end, static_cast<int>(rom_data.size())));
2007 }
2008
2009 std::vector<uint8_t> relocated;
2010 relocated.reserve(required_size);
2011 relocated.push_back(sort_mode);
2012 relocated.insert(relocated.end(), encoded_bytes.begin(), encoded_bytes.end());
2013 RETURN_IF_ERROR(rom->WriteVector(write_pos, std::move(relocated)));
2014
2015 const uint32_t snes_addr = PcToSnes(write_pos);
2016 const int ptr_off = sprite_pointer + (room_id * 2);
2017 RETURN_IF_ERROR(rom->WriteByte(ptr_off, snes_addr & 0xFF));
2018 RETURN_IF_ERROR(rom->WriteByte(ptr_off + 1, (snes_addr >> 8) & 0xFF));
2019
2020 return absl::OkStatus();
2021}
2022
2023absl::Status Room::SaveObjects(const DungeonStreamLayout* layout) {
2024 if (rom_ == nullptr) {
2025 return absl::InvalidArgumentError("ROM pointer is null");
2026 }
2027 if (!object_stream_dirty()) {
2028 return absl::OkStatus();
2029 }
2030
2031 for (const auto& object : tile_objects_) {
2032 if (UsesRoomObjectStream(object)) {
2034 }
2035 }
2036
2037 const auto& rom_data = rom()->vector();
2038 ASSIGN_OR_RETURN(const PhysicalStreamInfo stream_info,
2039 GetObjectStreamInfo(rom_data, room_id_));
2040 const auto encoded_bytes = EncodeObjects();
2041 bool requires_copy_on_write = false;
2042 if (layout != nullptr) {
2043 ASSIGN_OR_RETURN(requires_copy_on_write,
2044 DungeonStreamRequiresCopyOnWrite(
2046 encoded_bytes.size() + 2u));
2047 }
2048 const auto relocate = [&]() -> absl::Status {
2049 if (stream_info.address + 2 > static_cast<int>(rom_data.size())) {
2050 return absl::OutOfRangeError("Object stream header is out of range");
2051 }
2052 std::vector<uint8_t> replacement = {rom_data[stream_info.address],
2053 rom_data[stream_info.address + 1]};
2054 replacement.insert(replacement.end(), encoded_bytes.begin(),
2055 encoded_bytes.end());
2056 RETURN_IF_ERROR(RelocateDungeonStream(rom_, room_id_,
2058 std::move(replacement)));
2060 return absl::OkStatus();
2061 };
2062 if (stream_info.shared || requires_copy_on_write) {
2063 if (layout != nullptr) {
2064 return relocate();
2065 }
2066 return absl::FailedPreconditionError(absl::StrFormat(
2067 "Room %d object stream at PC 0x%06X is shared; repacking is required",
2068 room_id_, stream_info.address));
2069 }
2070 if (stream_info.capacity() <= 2) {
2071 if (layout != nullptr) {
2072 return relocate();
2073 }
2074 return absl::FailedPreconditionError(absl::StrFormat(
2075 "Room %d object stream has no safe physical boundary", room_id_));
2076 }
2077
2078 // Skip graphics/layout header (2 bytes)
2079 const int write_pos = stream_info.address + 2;
2080
2081 // Encode all objects
2082 const int available_payload_size = stream_info.capacity() - 2;
2083
2084 // Validate against the nearest greater physical pointer, not the next room
2085 // ID. Pointer tables are not ordered by room ID in vanilla or expanded ROMs.
2086 if (encoded_bytes.size() > static_cast<size_t>(available_payload_size)) {
2087 if (layout != nullptr) {
2088 return relocate();
2089 }
2090 return absl::ResourceExhaustedError(absl::StrFormat(
2091 "Room %d object data too large! Size: %d, Available: %d", room_id_,
2092 static_cast<int>(encoded_bytes.size()), available_payload_size));
2093 }
2094
2095 const int door_list_offset = static_cast<int>(encoded_bytes.size()) -
2096 static_cast<int>(doors_.size()) * 2 - 2;
2097 if (door_list_offset < 0) {
2098 return absl::FailedPreconditionError("Invalid encoded door list offset");
2099 }
2100 const int door_pointer_slot = kDoorPointers + (room_id_ * 3);
2101 if (door_pointer_slot < 0 ||
2102 door_pointer_slot + 2 >= static_cast<int>(rom_data.size())) {
2103 return absl::OutOfRangeError("Door pointer slot is out of range");
2104 }
2105 const int door_pointer_pc = write_pos + door_list_offset;
2106
2107 // Write encoded bytes to ROM (includes 0xF0 0xFF + door list)
2108 RETURN_IF_ERROR(rom_->WriteVector(write_pos, encoded_bytes));
2109
2110 // Write door pointer: first byte after 0xF0 0xFF (per ZScreamDungeon Save.cs)
2112 door_pointer_slot, static_cast<uint32_t>(PcToSnes(door_pointer_pc))));
2113
2115
2116 return absl::OkStatus();
2117}
2118
2120 if (rom_ == nullptr) {
2121 return absl::InvalidArgumentError("ROM pointer is null");
2122 }
2124 return absl::OkStatus();
2125 }
2126 if (floor1_graphics_ > 0x0F || floor2_graphics_ > 0x0F) {
2127 return absl::InvalidArgumentError(
2128 "Dungeon floor graphics values must be in range 0..15");
2129 }
2130 if (layout_id_ > 0x07) {
2131 return absl::InvalidArgumentError(
2132 "Dungeon layout ID must be in range 0..7");
2133 }
2134
2135 const auto& rom_data = rom_->vector();
2136 ASSIGN_OR_RETURN(const PhysicalStreamInfo stream_info,
2137 GetObjectStreamInfo(rom_data, room_id_));
2138 if (stream_info.address < 0 ||
2139 stream_info.address + 1 >= static_cast<int>(rom_data.size())) {
2140 return absl::OutOfRangeError("Object stream header is out of range");
2141 }
2142
2143 const uint8_t dirty_mask = save_dirty_state_.object_stream_header;
2144 auto patch_header = [&](std::vector<uint8_t>* stream) -> absl::Status {
2145 if (stream == nullptr || stream->size() < 2) {
2146 return absl::DataLossError(
2147 "Object stream is missing its two-byte header");
2148 }
2149 if ((dirty_mask & kObjectHeaderFloor1Dirty) != 0) {
2150 (*stream)[0] = static_cast<uint8_t>(((*stream)[0] & 0xF0) |
2151 (floor1_graphics_ & 0x0F));
2152 }
2153 if ((dirty_mask & kObjectHeaderFloor2Dirty) != 0) {
2154 (*stream)[0] =
2155 static_cast<uint8_t>(((*stream)[0] & 0x0F) | (floor2_graphics_ << 4));
2156 }
2157 if ((dirty_mask & kObjectHeaderLayoutDirty) != 0) {
2158 (*stream)[1] = static_cast<uint8_t>(((*stream)[1] & 0xE3) |
2159 ((layout_id_ & 0x07) << 2));
2160 }
2161 return absl::OkStatus();
2162 };
2163
2164 bool requires_copy_on_write = stream_info.shared;
2165 std::vector<uint8_t> replacement;
2166 if (layout != nullptr) {
2167 if (layout->kind != DungeonStreamKind::kObject) {
2168 return absl::InvalidArgumentError(
2169 "Object-stream header save requires an object stream layout");
2170 }
2172 InventoryDungeonStreams(*rom_, *layout));
2173 if (!inventory.ok()) {
2174 return absl::FailedPreconditionError(absl::StrFormat(
2175 "Dungeon stream inventory has %zu issue(s); refusing object "
2176 "header save",
2177 inventory.issues.size()));
2178 }
2179 if (room_id_ < 0 ||
2180 static_cast<size_t>(room_id_) >= inventory.streams.size()) {
2181 return absl::OutOfRangeError(
2182 "Room ID is outside the dungeon stream layout");
2183 }
2184 replacement = inventory.streams[room_id_].encoded_stream;
2185 bool layout_requires_copy_on_write = false;
2186 ASSIGN_OR_RETURN(layout_requires_copy_on_write,
2187 DungeonStreamRequiresCopyOnWrite(
2189 replacement.size()));
2190 requires_copy_on_write =
2191 requires_copy_on_write || layout_requires_copy_on_write;
2192 }
2193
2194 if (requires_copy_on_write) {
2195 if (layout == nullptr) {
2196 return absl::FailedPreconditionError(absl::StrFormat(
2197 "Room %d object stream at PC 0x%06X is shared; a copy-on-write "
2198 "manifest is required to save its header",
2199 room_id_, stream_info.address));
2200 }
2201 RETURN_IF_ERROR(patch_header(&replacement));
2202 RETURN_IF_ERROR(RelocateDungeonStream(rom_, room_id_,
2204 std::move(replacement)));
2206 return absl::OkStatus();
2207 }
2208
2209 std::vector<uint8_t> header = {rom_data[stream_info.address],
2210 rom_data[stream_info.address + 1]};
2211 RETURN_IF_ERROR(patch_header(&header));
2212 RETURN_IF_ERROR(rom_->WriteVector(stream_info.address, std::move(header)));
2214 return absl::OkStatus();
2215}
2216
2217absl::Status Room::SaveSprites(const DungeonStreamLayout* layout) {
2218 if (rom_ == nullptr) {
2219 return absl::InvalidArgumentError("ROM pointer is null");
2220 }
2221 if (!sprites_dirty()) {
2222 return absl::OkStatus();
2223 }
2224
2225 const auto& rom_data = rom()->vector();
2226 if (room_id_ < 0 || room_id_ >= kNumberOfRooms) {
2227 return absl::OutOfRangeError("Room ID out of range");
2228 }
2229
2230 ASSIGN_OR_RETURN(const PhysicalStreamInfo stream_info,
2231 GetSpriteStreamInfo(rom_data, room_id_));
2232 const auto encoded_bytes = EncodeSprites();
2233 bool requires_copy_on_write = false;
2234 if (layout != nullptr) {
2235 ASSIGN_OR_RETURN(requires_copy_on_write,
2236 DungeonStreamRequiresCopyOnWrite(
2238 encoded_bytes.size() + 1u));
2239 }
2240 const auto relocate = [&]() -> absl::Status {
2241 std::vector<uint8_t> replacement = {rom_data[stream_info.address]};
2242 replacement.insert(replacement.end(), encoded_bytes.begin(),
2243 encoded_bytes.end());
2244 RETURN_IF_ERROR(RelocateDungeonStream(rom_, room_id_,
2246 std::move(replacement)));
2248 return absl::OkStatus();
2249 };
2250 if (stream_info.shared || requires_copy_on_write) {
2251 if (layout != nullptr) {
2252 return relocate();
2253 }
2254 return absl::FailedPreconditionError(absl::StrFormat(
2255 "Room %d sprite stream at PC 0x%06X is shared; repacking is required",
2256 room_id_, stream_info.address));
2257 }
2258 if (stream_info.capacity() <= 1) {
2259 if (layout != nullptr) {
2260 return relocate();
2261 }
2262 return absl::FailedPreconditionError(absl::StrFormat(
2263 "Room %d sprite stream has no safe physical boundary", room_id_));
2264 }
2265
2266 const int available_payload_size = stream_info.capacity() - 1;
2267 const int payload_address = stream_info.address + 1;
2268 if (payload_address < 0 ||
2269 payload_address >= static_cast<int>(rom_->size())) {
2270 return absl::OutOfRangeError(absl::StrFormat(
2271 "Room %d has invalid sprite payload address", room_id_));
2272 }
2273
2274 if (static_cast<int>(encoded_bytes.size()) > available_payload_size) {
2275 if (layout != nullptr) {
2276 return relocate();
2277 }
2278 return absl::ResourceExhaustedError(absl::StrFormat(
2279 "Room %d sprite data too large! Size: %d, Available: %d; repacking "
2280 "is required",
2281 room_id_, static_cast<int>(encoded_bytes.size()),
2282 available_payload_size));
2283 }
2284
2285 RETURN_IF_ERROR(rom_->WriteVector(payload_address, encoded_bytes));
2287 return absl::OkStatus();
2288}
2289
2290absl::Status Room::SaveRoomHeader() {
2291 if (rom_ == nullptr) {
2292 return absl::InvalidArgumentError("ROM pointer is null");
2293 }
2294
2295 const auto& rom_data = rom()->vector();
2296 if (kRoomHeaderPointer < 0 ||
2297 kRoomHeaderPointer + 2 >= static_cast<int>(rom_data.size())) {
2298 return absl::OutOfRangeError("Room header pointer out of range");
2299 }
2300 if (kRoomHeaderPointerBank < 0 ||
2301 kRoomHeaderPointerBank >= static_cast<int>(rom_data.size())) {
2302 return absl::OutOfRangeError("Room header pointer bank out of range");
2303 }
2304
2305 int header_pointer = (rom_data[kRoomHeaderPointer + 2] << 16) +
2306 (rom_data[kRoomHeaderPointer + 1] << 8) +
2307 rom_data[kRoomHeaderPointer];
2308 header_pointer = SnesToPc(header_pointer);
2309
2310 int table_offset = header_pointer + (room_id_ * 2);
2311 if (table_offset < 0 ||
2312 table_offset + 1 >= static_cast<int>(rom_data.size())) {
2313 return absl::OutOfRangeError("Room header table offset out of range");
2314 }
2315
2316 int address = (rom_data[kRoomHeaderPointerBank] << 16) +
2317 (rom_data[table_offset + 1] << 8) + rom_data[table_offset];
2318 int header_location = SnesToPc(address);
2319
2320 if (header_location < 0 ||
2321 header_location + 13 >= static_cast<int>(rom_data.size())) {
2322 return absl::OutOfRangeError("Room header location out of range");
2323 }
2324
2325 // Build 14-byte header to match LoadRoomHeaderFromRom layout. The high
2326 // three bits are the BG2/layer mode; bit 0 is the dark-room flag. DarkRoom
2327 // is an editor enum value, not a raw high-bit value.
2328 uint8_t layer2_mode_for_save = layer2_mode_ & 0x07;
2329 if (bg2() != background2::DarkRoom) {
2330 layer2_mode_for_save = static_cast<uint8_t>(bg2()) & 0x07;
2331 }
2332 const bool dark_room =
2333 IsLight() || is_dark_ || bg2() == background2::DarkRoom;
2334 uint8_t byte0 = static_cast<uint8_t>(
2335 (layer2_mode_for_save << 5) |
2336 ((static_cast<uint8_t>(collision()) & 0x07) << 2) |
2337 (rom_data[header_location] & 0x02) | (dark_room ? 1 : 0));
2338 // Preserve the full palette set ID byte (USDASM LoadRoomHeader uses 8-bit).
2339 uint8_t byte1 = palette_;
2340 // Byte 7 stores the pit target layer in bits 0-1 followed by the first
2341 // three staircase target layers in consecutive two-bit fields.
2342 uint8_t byte7 =
2343 (pits_.target_layer & 0x03) | ((staircase_plane(0) & 0x03) << 2) |
2344 ((staircase_plane(1) & 0x03) << 4) | ((staircase_plane(2) & 0x03) << 6);
2345 const uint8_t byte8 = static_cast<uint8_t>(
2346 (rom_data[header_location + 8] & 0xFC) | (staircase_plane(3) & 0x03));
2347
2348 RETURN_IF_ERROR(rom_->WriteByte(header_location + 0, byte0));
2349 RETURN_IF_ERROR(rom_->WriteByte(header_location + 1, byte1));
2350 RETURN_IF_ERROR(rom_->WriteByte(header_location + 2, blockset_));
2351 RETURN_IF_ERROR(rom_->WriteByte(header_location + 3, spriteset_));
2353 rom_->WriteByte(header_location + 4, static_cast<uint8_t>(effect())));
2355 rom_->WriteByte(header_location + 5, static_cast<uint8_t>(tag1())));
2357 rom_->WriteByte(header_location + 6, static_cast<uint8_t>(tag2())));
2358 RETURN_IF_ERROR(rom_->WriteByte(header_location + 7, byte7));
2359 RETURN_IF_ERROR(rom_->WriteByte(header_location + 8, byte8));
2360 RETURN_IF_ERROR(rom_->WriteByte(header_location + 9, holewarp_));
2361 RETURN_IF_ERROR(rom_->WriteByte(header_location + 10, staircase_room(0)));
2362 RETURN_IF_ERROR(rom_->WriteByte(header_location + 11, staircase_room(1)));
2363 RETURN_IF_ERROR(rom_->WriteByte(header_location + 12, staircase_room(2)));
2364 RETURN_IF_ERROR(rom_->WriteByte(header_location + 13, staircase_room(3)));
2365
2366 int msg_addr = kMessagesIdDungeon + (room_id_ * 2);
2367 if (msg_addr < 0 || msg_addr + 1 >= static_cast<int>(rom_data.size())) {
2368 return absl::OutOfRangeError("Message ID address out of range");
2369 }
2371
2373
2374 return absl::OkStatus();
2375}
2376
2377// ============================================================================
2378// Object Manipulation Methods (Phase 3)
2379// ============================================================================
2380
2381absl::Status Room::AddObject(const RoomObject& object) {
2382 // Validate object
2383 if (!ValidateObject(object)) {
2384 return absl::InvalidArgumentError("Invalid object parameters");
2385 }
2386
2387 // Add to internal list
2388 tile_objects_.push_back(object);
2389 objects_loaded_ = true;
2391
2392 return absl::OkStatus();
2393}
2394
2395absl::Status Room::RemoveObject(size_t index) {
2396 if (index >= tile_objects_.size()) {
2397 return absl::OutOfRangeError("Object index out of range");
2398 }
2399
2401 tile_objects_.erase(tile_objects_.begin() + index);
2402 objects_loaded_ = true;
2404
2405 return absl::OkStatus();
2406}
2407
2408absl::Status Room::UpdateObject(size_t index, const RoomObject& object) {
2409 if (index >= tile_objects_.size()) {
2410 return absl::OutOfRangeError("Object index out of range");
2411 }
2412
2413 if (!ValidateObject(object)) {
2414 return absl::InvalidArgumentError("Invalid object parameters");
2415 }
2416
2418 tile_objects_[index] = object;
2419 objects_loaded_ = true;
2421
2422 return absl::OkStatus();
2423}
2424
2425absl::StatusOr<size_t> Room::FindObjectAt(int x, int y, int layer) const {
2426 for (size_t i = 0; i < tile_objects_.size(); i++) {
2427 const auto& obj = tile_objects_[i];
2428 if (obj.x() == x && obj.y() == y && obj.GetLayerValue() == layer) {
2429 return i;
2430 }
2431 }
2432 return absl::NotFoundError("No object found at position");
2433}
2434
2435bool Room::ValidateObject(const RoomObject& object) const {
2436 // Validate position (0-63 for both X and Y)
2437 if (object.x() < 0 || object.x() > 63)
2438 return false;
2439 if (object.y() < 0 || object.y() > 63)
2440 return false;
2441
2442 // Validate layer (0-2)
2443 if (object.GetLayerValue() < 0 || object.GetLayerValue() > 2)
2444 return false;
2445
2446 // Validate object ID range
2447 if (object.id_ < 0 || object.id_ > 0xFFF)
2448 return false;
2449
2450 // Validate size for Type 1 objects
2451 if (object.id_ < 0x100 && object.size() > 15)
2452 return false;
2453
2454 return true;
2455}
2456
2457void Room::HandleSpecialObjects(short oid, uint8_t posX, uint8_t posY,
2458 int& nbr_of_staircase) {
2459 // Handle staircase objects
2460 for (short stair : kStairsObjects) {
2461 if (stair == oid) {
2462 if (nbr_of_staircase < 4) {
2463 tile_objects_.back().set_options(ObjectOption::Stairs |
2464 tile_objects_.back().options());
2465 z3_staircases_.push_back(
2466 {posX, posY,
2467 absl::StrCat("To ", staircase_rooms_[nbr_of_staircase]).data()});
2468 nbr_of_staircase++;
2469 } else {
2470 tile_objects_.back().set_options(ObjectOption::Stairs |
2471 tile_objects_.back().options());
2472 z3_staircases_.push_back({posX, posY, "To ???"});
2473 }
2474 break;
2475 }
2476 }
2477
2478 // Handle chest objects
2479 if (oid == 0xF99) {
2480 if (chests_in_room_.size() > 0) {
2481 tile_objects_.back().set_options(ObjectOption::Chest |
2482 tile_objects_.back().options());
2483 chests_in_room_.erase(chests_in_room_.begin());
2484 }
2485 } else if (oid == 0xFB1) {
2486 if (chests_in_room_.size() > 0) {
2487 tile_objects_.back().set_options(ObjectOption::Chest |
2488 tile_objects_.back().options());
2489 chests_in_room_.erase(chests_in_room_.begin());
2490 }
2491 }
2492}
2493
2495 const auto& rom_data = rom()->vector();
2496 // Avoid duplicate entries if callers reload sprite data on the same room.
2497 sprites_.clear();
2498 sprites_loaded_ = false;
2499 if (room_id_ < 0 || room_id_ >= kNumberOfRooms) {
2500 return;
2501 }
2502
2503 int sprite_pointer = 0;
2504 if (!GetSpritePointerTablePc(rom_data, &sprite_pointer).ok()) {
2505 return;
2506 }
2507
2508 int sprite_address =
2509 ReadRoomSpriteAddressPc(rom_data, sprite_pointer, room_id_);
2510 if (sprite_address < 0 ||
2511 sprite_address + 1 >= static_cast<int>(rom_data.size())) {
2512 return;
2513 }
2514
2515 // First byte is the SortSprites mode (0 or 1), not sprite data.
2516 sprite_address += 1;
2517
2518 while (sprite_address + 2 < static_cast<int>(rom_data.size())) {
2519 uint8_t b1 = rom_data[sprite_address];
2520 uint8_t b2 = rom_data[sprite_address + 1];
2521 uint8_t b3 = rom_data[sprite_address + 2];
2522
2523 if (b1 == 0xFF) {
2524 break;
2525 }
2526
2527 sprites_.emplace_back(b3, (b2 & 0x1F), (b1 & 0x1F),
2528 ((b2 & 0xE0) >> 5) + ((b1 & 0x60) >> 2),
2529 (b1 & 0x80) >> 7);
2530
2531 if (sprites_.size() > 1) {
2532 Sprite& spr = sprites_.back();
2533 Sprite& prevSprite = sprites_[sprites_.size() - 2];
2534
2535 if (spr.id() == 0xE4 && spr.x() == 0x00 && spr.y() == 0x1E &&
2536 spr.layer() == 1 && spr.subtype() == 0x18) {
2537 prevSprite.set_key_drop(1);
2538 sprites_.pop_back();
2539 }
2540
2541 if (spr.id() == 0xE4 && spr.x() == 0x00 && spr.y() == 0x1D &&
2542 spr.layer() == 1 && spr.subtype() == 0x18) {
2543 prevSprite.set_key_drop(2);
2544 sprites_.pop_back();
2545 }
2546 }
2547
2548 sprite_address += 3;
2549 }
2550
2551 sprites_loaded_ = true;
2552}
2553
2555 chests_in_room_.clear();
2556 chests_loaded_ = false;
2557 if (!rom_ || !rom_->is_loaded()) {
2558 return;
2559 }
2560 const auto& rom_data = rom()->vector();
2561 if (kChestsDataPointer1 + 2 >= static_cast<int>(rom_data.size()) ||
2562 kChestsLengthPointer + 1 >= static_cast<int>(rom_data.size())) {
2563 return;
2564 }
2565
2566 const int cpos = static_cast<int>(SnesToPc(
2567 (static_cast<uint32_t>(rom_data[kChestsDataPointer1 + 2]) << 16) |
2568 (static_cast<uint32_t>(rom_data[kChestsDataPointer1 + 1]) << 8) |
2569 rom_data[kChestsDataPointer1]));
2570 const size_t byte_length =
2571 (static_cast<size_t>(rom_data[kChestsLengthPointer + 1]) << 8) |
2572 rom_data[kChestsLengthPointer];
2573 const size_t bounded_byte_length = std::min<size_t>(
2574 byte_length, cpos >= 0 && cpos < static_cast<int>(rom_data.size())
2575 ? rom_data.size() - static_cast<size_t>(cpos)
2576 : 0);
2577 const size_t record_count = std::min<size_t>(
2578 bounded_byte_length / kChestTableRecordSize, kChestTableCapacityRecords);
2579
2580 for (size_t i = 0; i < record_count; ++i) {
2581 const size_t offset =
2582 static_cast<size_t>(cpos) + (i * kChestTableRecordSize);
2583 if ((((rom_data[offset + 1] << 8) + rom_data[offset]) & 0x7FFF) ==
2584 room_id_) {
2585 // There's a chest in that room !
2586 bool big = false;
2587 if ((((rom_data[offset + 1] << 8) + rom_data[offset]) & 0x8000) ==
2588 0x8000) {
2589 big = true;
2590 }
2591
2592 chests_in_room_.emplace_back(chest_data{rom_data[offset + 2], big});
2593 }
2594 }
2595 chests_loaded_ = true;
2596}
2597
2599 auto rom_data = rom()->vector();
2600
2601 // Doors are loaded as part of the object stream in LoadObjects()
2602 // When the parser encounters 0xF0 0xFF, it enters door mode
2603 // Door objects have format: b1 (position/direction), b2 (type)
2604 // Door encoding: b1 = (door_pos << 4) | (door_dir & 0x03)
2605 // position in bits 4-7, direction in bits 0-1
2606 // b2 = door_type (full byte, values 0x00, 0x02, 0x04, etc.)
2607 // This is already handled in ParseObjectsFromLocation()
2608
2609 LOG_DEBUG("Room",
2610 "LoadDoors for room %d - doors are loaded via object stream",
2611 room_id_);
2612}
2613
2615 auto rom_data = rom()->vector();
2616
2617 // Read torch data length
2618 int bytes_count = (rom_data[kTorchesLengthPointer + 1] << 8) |
2619 rom_data[kTorchesLengthPointer];
2620
2621 LOG_DEBUG("Room", "LoadTorches: room_id=%d, bytes_count=%d", room_id_,
2622 bytes_count);
2623
2624 // Avoid duplication if LoadTorches is called multiple times.
2625 tile_objects_.erase(
2626 std::remove_if(tile_objects_.begin(), tile_objects_.end(),
2627 [](const RoomObject& obj) {
2628 return (obj.options() & ObjectOption::Torch) !=
2629 ObjectOption::Nothing;
2630 }),
2631 tile_objects_.end());
2632
2633 // Iterate through torch data to find torches for this room
2634 for (int i = 0; i < bytes_count; i += 2) {
2635 if (i + 1 >= bytes_count)
2636 break;
2637
2638 uint8_t b1 = rom_data[kTorchData + i];
2639 uint8_t b2 = rom_data[kTorchData + i + 1];
2640
2641 // Skip 0xFFFF markers
2642 if (b1 == 0xFF && b2 == 0xFF) {
2643 continue;
2644 }
2645
2646 // Check if this entry is for our room
2647 uint16_t torch_room_id = (b2 << 8) | b1;
2648 if (torch_room_id == room_id_) {
2649 // Found torches for this room, read them
2650 i += 2;
2651 while (i < bytes_count) {
2652 if (i + 1 >= bytes_count)
2653 break;
2654
2655 b1 = rom_data[kTorchData + i];
2656 b2 = rom_data[kTorchData + i + 1];
2657
2658 // End of torch list for this room
2659 if (b1 == 0xFF && b2 == 0xFF) {
2660 break;
2661 }
2662
2663 const LightableTorchEntry entry = DecodeLightableTorchEntry({b1, b2});
2664
2665 // Create torch object (ID 0x150)
2666 RoomObject torch_obj(0x150, entry.px, entry.py, 0, entry.draw_layer);
2667 torch_obj.SetRom(rom_);
2668 torch_obj.set_options(ObjectOption::Torch);
2669 torch_obj.set_torch_reserved_bit(entry.reserved);
2670 torch_obj.lit_ = entry.lit;
2671
2672 tile_objects_.push_back(torch_obj);
2673
2674 LOG_DEBUG(
2675 "Room", "Loaded torch at (%d,%d) draw_layer=%d reserved=%d lit=%d",
2676 entry.px, entry.py, entry.draw_layer, entry.reserved, entry.lit);
2677
2678 i += 2;
2679 }
2680 break; // Found and processed our room's torches
2681 } else {
2682 // Skip to next room's torches
2683 i += 2;
2684 while (i < bytes_count) {
2685 if (i + 1 >= bytes_count)
2686 break;
2687 b1 = rom_data[kTorchData + i];
2688 b2 = rom_data[kTorchData + i + 1];
2689 if (b1 == 0xFF && b2 == 0xFF) {
2690 break;
2691 }
2692 i += 2;
2693 }
2694 }
2695 }
2696 torches_loaded_ = true;
2697}
2698
2699namespace {
2700
2701constexpr int kTorchesMaxSize = 0x120; // ZScream Constants.TorchesMaxSize
2702
2704 uint16_t room_id = 0;
2705 std::vector<uint8_t> bytes;
2706};
2707
2708// Parse current ROM torch blob in authoring order for preserve-merge.
2709std::vector<TorchSegment> ParseRomTorchSegments(
2710 const std::vector<uint8_t>& rom_data, int bytes_count) {
2711 std::vector<TorchSegment> segments;
2712 int i = 0;
2713 while (i + 1 < bytes_count && i < kTorchesMaxSize) {
2714 uint8_t b1 = rom_data[kTorchData + i];
2715 uint8_t b2 = rom_data[kTorchData + i + 1];
2716 if (b1 == 0xFF && b2 == 0xFF) {
2717 // Vanilla contains standalone $FFFF padding between two authored room
2718 // segments. Keep it as an unowned pass-through segment so a no-op save
2719 // remains byte-identical instead of compacting the table.
2720 TorchSegment padding;
2721 padding.room_id = 0xFFFF;
2722 padding.bytes = {0xFF, 0xFF};
2723 segments.push_back(std::move(padding));
2724 i += 2;
2725 continue;
2726 }
2727 uint16_t room_id = (b2 << 8) | b1;
2728 if (room_id >= kNumberOfRooms) {
2729 i += 2;
2730 continue;
2731 }
2732 TorchSegment seg;
2733 seg.room_id = room_id;
2734 seg.bytes.push_back(b1);
2735 seg.bytes.push_back(b2);
2736 i += 2;
2737 while (i + 1 < bytes_count && i < kTorchesMaxSize) {
2738 b1 = rom_data[kTorchData + i];
2739 b2 = rom_data[kTorchData + i + 1];
2740 if (b1 == 0xFF && b2 == 0xFF) {
2741 seg.bytes.push_back(0xFF);
2742 seg.bytes.push_back(0xFF);
2743 i += 2;
2744 break;
2745 }
2746 seg.bytes.push_back(b1);
2747 seg.bytes.push_back(b2);
2748 i += 2;
2749 }
2750 segments.push_back(std::move(seg));
2751 }
2752 return segments;
2753}
2754
2755std::vector<uint8_t> EncodeTorchSegmentForRoom(int room_id, const Room& room) {
2756 std::vector<uint8_t> bytes;
2757 for (const auto& obj : room.GetTileObjects()) {
2758 if ((obj.options() & ObjectOption::Torch) == ObjectOption::Nothing) {
2759 continue;
2760 }
2761 if (bytes.empty()) {
2762 bytes.push_back(room_id & 0xFF);
2763 bytes.push_back((room_id >> 8) & 0xFF);
2764 }
2766 .px = static_cast<uint8_t>(obj.x()),
2767 .py = static_cast<uint8_t>(obj.y()),
2768 .draw_layer = static_cast<uint8_t>(obj.GetLayerValue() & 1),
2769 .reserved = obj.torch_reserved_bit(),
2770 .lit = obj.lit_,
2771 });
2772 bytes.push_back(encoded.low);
2773 bytes.push_back(encoded.high);
2774 }
2775 if (!bytes.empty()) {
2776 bytes.push_back(0xFF);
2777 bytes.push_back(0xFF);
2778 }
2779 return bytes;
2780}
2781
2783 int room_id,
2784 const char* object_type) {
2785 const uint8_t selector = object.GetLayerValue();
2786 if (selector <= 1) {
2787 return absl::OkStatus();
2788 }
2789 return absl::InvalidArgumentError(absl::StrFormat(
2790 "%s in room 0x%03X has invalid special draw-layer selector %d; "
2791 "expected 0 "
2792 "(upper/BG1) or 1 (lower/BG2)",
2793 object_type, room_id, selector));
2794}
2795
2797 int room_id) {
2799 ValidateSpecialObjectDrawLayerSelector(object, room_id, "Torch"));
2800 if (object.x() <= 0x3E && object.y() <= 0x3E) {
2801 return absl::OkStatus();
2802 }
2803 return absl::InvalidArgumentError(absl::StrFormat(
2804 "Torch in room 0x%03X has invalid position (%d,%d); expected x/y in "
2805 "range 0..62",
2806 room_id, object.x(), object.y()));
2807}
2808
2809} // namespace
2810
2811template <typename RoomLookup>
2812absl::Status SaveAllTorchesImpl(Rom* rom, int room_count,
2813 RoomLookup&& room_lookup) {
2814 if (!rom || !rom->is_loaded()) {
2815 return absl::InvalidArgumentError("ROM not loaded");
2816 }
2817
2818 const auto& rom_data = rom->vector();
2819 int existing_count = (rom_data[kTorchesLengthPointer + 1] << 8) |
2820 rom_data[kTorchesLengthPointer];
2821 if (existing_count > kTorchesMaxSize) {
2822 existing_count = kTorchesMaxSize;
2823 }
2824 auto rom_segments = ParseRomTorchSegments(rom_data, existing_count);
2825
2826 std::vector<uint8_t> bytes;
2827 const int room_limit = std::min(room_count, kNumberOfRooms);
2828 std::vector<bool> owned_rooms(room_limit, false);
2829 std::vector<bool> seen_original_room(room_limit, false);
2830 std::vector<bool> emitted_owned_room(room_limit, false);
2831 std::vector<std::vector<uint8_t>> replacements(room_limit);
2832 bool any_owned_room = false;
2833 for (int room_id = 0; room_id < room_limit; ++room_id) {
2834 const Room* room = room_lookup(room_id);
2835 const bool room_owned =
2836 room != nullptr && (room->AreTorchesLoaded() || room->torches_dirty());
2837 if (!room_owned) {
2838 continue;
2839 }
2840 for (const auto& object : room->GetTileObjects()) {
2841 if ((object.options() & ObjectOption::Torch) != ObjectOption::Nothing) {
2842 RETURN_IF_ERROR(ValidateLightableTorchForSave(object, room_id));
2843 }
2844 }
2845 owned_rooms[room_id] = true;
2846 any_owned_room = true;
2847 replacements[room_id] = EncodeTorchSegmentForRoom(room_id, *room);
2848 }
2849
2850 if (!any_owned_room) {
2851 return absl::OkStatus();
2852 }
2853
2854 for (const auto& segment : rom_segments) {
2855 if (segment.room_id < room_limit) {
2856 seen_original_room[segment.room_id] = true;
2857 if (owned_rooms[segment.room_id]) {
2858 if (!emitted_owned_room[segment.room_id]) {
2859 bytes.insert(bytes.end(), replacements[segment.room_id].begin(),
2860 replacements[segment.room_id].end());
2861 emitted_owned_room[segment.room_id] = true;
2862 }
2863 continue;
2864 }
2865 }
2866 bytes.insert(bytes.end(), segment.bytes.begin(), segment.bytes.end());
2867 }
2868
2869 for (int room_id = 0; room_id < room_limit; ++room_id) {
2870 if (owned_rooms[room_id] && !seen_original_room[room_id] &&
2871 !replacements[room_id].empty()) {
2872 bytes.insert(bytes.end(), replacements[room_id].begin(),
2873 replacements[room_id].end());
2874 }
2875 }
2876
2877 if (bytes.size() > kTorchesMaxSize) {
2878 return absl::ResourceExhaustedError(
2879 absl::StrFormat("Torch data too large: %d bytes (max %d)", bytes.size(),
2880 kTorchesMaxSize));
2881 }
2882
2883 const uint16_t current_len =
2884 static_cast<uint16_t>(rom_data[kTorchesLengthPointer]) |
2885 (static_cast<uint16_t>(rom_data[kTorchesLengthPointer + 1]) << 8);
2886 if (current_len == bytes.size() &&
2887 kTorchData + static_cast<int>(bytes.size()) <=
2888 static_cast<int>(rom_data.size()) &&
2889 std::equal(bytes.begin(), bytes.end(), rom_data.begin() + kTorchData)) {
2890 for (int room_id = 0; room_id < room_limit; ++room_id) {
2891 if (const Room* room = room_lookup(room_id);
2892 room != nullptr && room->torches_dirty()) {
2893 const_cast<Room*>(room)->ClearTorchesDirty();
2894 }
2895 }
2896 return absl::OkStatus();
2897 }
2898
2900 static_cast<uint16_t>(bytes.size())));
2902 for (int room_id = 0; room_id < room_limit; ++room_id) {
2903 if (const Room* room = room_lookup(room_id);
2904 room != nullptr && room->torches_dirty()) {
2905 const_cast<Room*>(room)->ClearTorchesDirty();
2906 }
2907 }
2908 return absl::OkStatus();
2909}
2910
2911absl::Status SaveAllTorches(Rom* rom, absl::Span<const Room> rooms) {
2912 return SaveAllTorchesImpl(rom, static_cast<int>(rooms.size()),
2913 [&rooms](int room_id) { return &rooms[room_id]; });
2914}
2915
2916absl::Status SaveAllTorches(
2917 Rom* rom, int room_count,
2918 const std::function<const Room*(int)>& room_lookup) {
2919 return SaveAllTorchesImpl(rom, room_count, room_lookup);
2920}
2921
2922// Region preservation for `RoomsWithPitDamage` when no edited table is supplied.
2923// When `pit_damage_table` is non-null and dirty, encode the in-memory membership
2924// list through `PitDamageTable::SaveToRom` instead of blind preservation.
2925absl::Status SaveAllPits(Rom* rom) {
2926 return SaveAllPits(rom, nullptr);
2927}
2928
2929absl::Status SaveAllPits(Rom* rom, PitDamageTable* pit_damage_table) {
2930 if (pit_damage_table != nullptr && pit_damage_table->dirty()) {
2931 RETURN_IF_ERROR(pit_damage_table->SaveToRom(rom));
2932 pit_damage_table->ClearDirty();
2933 return absl::OkStatus();
2934 }
2935 if (!rom || !rom->is_loaded()) {
2936 return absl::InvalidArgumentError("ROM not loaded");
2937 }
2938 const auto& rom_data = rom->vector();
2939 if (kPitCount < 0 || kPitCount >= static_cast<int>(rom_data.size()) ||
2940 kPitPointer + 2 >= static_cast<int>(rom_data.size())) {
2941 return absl::OutOfRangeError("Pit count/pointer out of range");
2942 }
2943 int max_offset = rom_data[kPitCount];
2944 // Total bytes = max_offset + 2 (covers offsets 0..max_offset
2945 // inclusive, with each entry being a 2-byte word). When max_offset
2946 // is 0, there's still 1 word to preserve (the entry at offset 0).
2947 int data_len = max_offset + 2;
2948 int pit_ptr_snes = (rom_data[kPitPointer + 2] << 16) |
2949 (rom_data[kPitPointer + 1] << 8) | rom_data[kPitPointer];
2950 int pit_data_pc = SnesToPc(pit_ptr_snes);
2951 if (pit_data_pc < 0 ||
2952 pit_data_pc + data_len > static_cast<int>(rom_data.size())) {
2953 return absl::OutOfRangeError("Pit data region out of range");
2954 }
2955 std::vector<uint8_t> data(rom_data.begin() + pit_data_pc,
2956 rom_data.begin() + pit_data_pc + data_len);
2957 RETURN_IF_ERROR(rom->WriteByte(kPitCount, max_offset));
2958 RETURN_IF_ERROR(rom->WriteByte(kPitPointer, pit_ptr_snes & 0xFF));
2959 RETURN_IF_ERROR(rom->WriteByte(kPitPointer + 1, (pit_ptr_snes >> 8) & 0xFF));
2960 RETURN_IF_ERROR(rom->WriteByte(kPitPointer + 2, (pit_ptr_snes >> 16) & 0xFF));
2961 return rom->WriteVector(pit_data_pc, data);
2962}
2963
2964namespace {
2965
2966constexpr int kBlocksRegionSize = 0x80;
2969
2970bool HalfOpenRangesOverlap(int first_begin, int first_end, int second_begin,
2971 int second_end) {
2972 return first_begin < second_end && second_begin < first_end;
2973}
2974
2976 const std::vector<uint8_t>& rom_data, int operand_pc) {
2977 if (operand_pc <= 0 || operand_pc + 5 >= static_cast<int>(rom_data.size())) {
2978 return absl::OutOfRangeError("Blocks pointer operand out of range");
2979 }
2980 // The block table pointers are the 3-byte operands in the US USDASM
2981 // bank_02 loader shape (#_02DAF9..#_02DB12):
2982 // BF ll hh bb LDA.l table+N*0x80,X
2983 // 9D ll hh STA.w $7EF940+N*0x80,X
2984 // The data table starts at bank_04's
2985 // SpecialUnderworldObjects_pushable_block (#_04F1DE). Pinned against a real
2986 // vanilla ROM by
2987 // DungeonSaveRegionTest.BlocksLoaderPointerOperandsMatchUsdasmShape.
2988 //
2989 // Guard both sides before dereferencing or future repointing so a bad
2990 // constant or already-patched ROM cannot make the saver treat unrelated
2991 // instruction bytes as data pointers.
2992 if (rom_data[operand_pc - 1] != 0xBF || rom_data[operand_pc + 3] != 0x9D) {
2993 return absl::FailedPreconditionError(absl::StrFormat(
2994 "Blocks pointer operand at PC 0x%05X is not in the expected "
2995 "LDA.l ...,X / STA.w loader sequence",
2996 operand_pc));
2997 }
2998 return absl::OkStatus();
2999}
3000
3002 const std::vector<uint8_t>& rom_data, std::array<int, 4>* destination_pcs) {
3003 if (kBlocksLength < 0 ||
3004 kBlocksLength + 1 >= static_cast<int>(rom_data.size())) {
3005 return absl::OutOfRangeError("Blocks length out of range");
3006 }
3007
3008 for (size_t page = 0; page < kBlocksPointerSlots.size(); ++page) {
3009 const int operand_pc = kBlocksPointerSlots[page];
3011 const int snes = (rom_data[operand_pc + 2] << 16) |
3012 (rom_data[operand_pc + 1] << 8) | rom_data[operand_pc];
3013 const int data_pc = SnesToPc(snes);
3014 if (data_pc < 0 ||
3015 data_pc + kBlocksRegionSize > static_cast<int>(rom_data.size())) {
3016 return absl::OutOfRangeError(absl::StrFormat(
3017 "Blocks data region out of range for loader page %d", page + 1));
3018 }
3019 (*destination_pcs)[page] = data_pc;
3020 }
3021
3022 constexpr int kLengthMetadataEnd = kBlocksLength + 2;
3023 for (size_t page = 0; page < destination_pcs->size(); ++page) {
3024 const int page_begin = (*destination_pcs)[page];
3025 const int page_end = page_begin + kBlocksRegionSize;
3026 if (HalfOpenRangesOverlap(page_begin, page_end, kBlocksLength,
3027 kLengthMetadataEnd)) {
3028 return absl::FailedPreconditionError(absl::StrFormat(
3029 "Blocks data page %d at PC [0x%05X, 0x%05X) overlaps block-table "
3030 "length metadata [0x%05X, 0x%05X)",
3031 page + 1, page_begin, page_end, kBlocksLength, kLengthMetadataEnd));
3032 }
3033
3034 for (size_t loader = 0; loader < kBlocksPointerSlots.size(); ++loader) {
3035 // Each destination operand is embedded in a seven-byte loader
3036 // instruction: BF ll hh bb 9D ll hh. Treat the complete instruction as
3037 // metadata so a table write cannot corrupt either opcode or operand.
3038 const int loader_begin = kBlocksPointerSlots[loader] - 1;
3039 const int loader_end = kBlocksPointerSlots[loader] + 6;
3040 if (HalfOpenRangesOverlap(page_begin, page_end, loader_begin,
3041 loader_end)) {
3042 return absl::FailedPreconditionError(absl::StrFormat(
3043 "Blocks data page %d at PC [0x%05X, 0x%05X) overlaps loader %d "
3044 "opcode/operand metadata [0x%05X, 0x%05X)",
3045 page + 1, page_begin, page_end, loader + 1, loader_begin,
3046 loader_end));
3047 }
3048 }
3049
3050 for (size_t previous = 0; previous < page; ++previous) {
3051 const int previous_begin = (*destination_pcs)[previous];
3052 const int previous_end = previous_begin + kBlocksRegionSize;
3053 if (HalfOpenRangesOverlap(page_begin, page_end, previous_begin,
3054 previous_end)) {
3055 return absl::FailedPreconditionError(absl::StrFormat(
3056 "Blocks data pages %d and %d overlap at PC ranges [0x%05X, "
3057 "0x%05X) and [0x%05X, 0x%05X)",
3058 previous + 1, page + 1, previous_begin, previous_end, page_begin,
3059 page_end));
3060 }
3061 }
3062 }
3063
3064 return absl::OkStatus();
3065}
3066
3067} // namespace
3068
3069absl::Status SaveAllBlocks(Rom* rom) {
3070 if (!rom || !rom->is_loaded()) {
3071 return absl::InvalidArgumentError("ROM not loaded");
3072 }
3073 const auto& rom_data = rom->vector();
3074 if (kBlocksLength + 1 >= static_cast<int>(rom_data.size())) {
3075 return absl::OutOfRangeError("Blocks length out of range");
3076 }
3077 int blocks_count =
3078 (rom_data[kBlocksLength + 1] << 8) | rom_data[kBlocksLength];
3079 std::array<int, 4> destination_pcs{};
3081 PreflightBlocksLoaderDestinations(rom_data, &destination_pcs));
3082 if (blocks_count <= 0) {
3083 return absl::OkStatus();
3084 }
3085 for (int r = 0; r < 4; ++r) {
3086 const int pc = destination_pcs[r];
3087 int off = r * kBlocksRegionSize;
3088 int len = std::min(kBlocksRegionSize, blocks_count - off);
3089 if (len <= 0)
3090 break;
3091 std::vector<uint8_t> chunk(rom_data.begin() + pc,
3092 rom_data.begin() + pc + len);
3093 RETURN_IF_ERROR(rom->WriteVector(pc, chunk));
3094 }
3096 rom->WriteWord(kBlocksLength, static_cast<uint16_t>(blocks_count)));
3097 return absl::OkStatus();
3098}
3099
3100absl::Status SaveAllBlocks(Rom* rom, int room_count,
3101 const std::function<const Room*(int)>& room_lookup) {
3102 if (!rom || !rom->is_loaded()) {
3103 return absl::InvalidArgumentError("ROM not loaded");
3104 }
3105 const auto& rom_data = rom->vector();
3106 if (kBlocksLength + 1 >= static_cast<int>(rom_data.size())) {
3107 return absl::OutOfRangeError("Blocks length out of range");
3108 }
3109
3110 std::array<int, 4> destination_pcs{};
3112 PreflightBlocksLoaderDestinations(rom_data, &destination_pcs));
3113
3114 // Read the original block buffer by dereferencing the four pointer
3115 // slots. We need this so unmaterialized / header-only rooms can have
3116 // their entries preserved verbatim — only rooms whose blocks were
3117 // actually loaded into memory get re-encoded from `tile_objects_`.
3118 // This prevents the editor migration from silently dropping vanilla
3119 // blocks for any room the user hasn't materialized yet.
3120 const int original_count_word =
3121 (rom_data[kBlocksLength + 1] << 8) | rom_data[kBlocksLength];
3122 const int original_byte_len = std::max(0, original_count_word);
3123 std::vector<uint8_t> original_buffer(original_byte_len, 0);
3124 for (int r = 0; r < 4; ++r) {
3125 const int pc = destination_pcs[r];
3126 const int off = r * kBlocksRegionSize;
3127 const int len = std::min(kBlocksRegionSize, original_byte_len - off);
3128 if (len <= 0)
3129 break;
3130 std::copy_n(rom_data.begin() + pc, len, original_buffer.begin() + off);
3131 }
3132 const int original_slot_count = original_byte_len / 4;
3133
3134 // Build:
3135 // - `slot_replacements`: for each existing slot whose room_id is
3136 // "owned" by an editor-loaded room, the re-encoded bytes (or
3137 // absent if the block was deleted in memory).
3138 // - `owned_room_ids`: the set of room_ids whose blocks were
3139 // materialized (so unmaterialized / header-only rooms can be
3140 // preserved verbatim from `original_buffer`).
3141 // - `appended`: blocks with `load_order == kBlockLoadOrderNew`,
3142 // appended to the end in creation order.
3143 struct EncodedBlock {
3144 PushableBlockBytes bytes;
3145 const RoomObject* source_object;
3146 };
3147 std::unordered_set<uint16_t> owned_room_ids;
3148 std::unordered_set<int> claimed_load_orders;
3149 std::unordered_map<int, EncodedBlock> slot_replacements;
3150 std::vector<EncodedBlock> appended;
3151 for (int rid = 0; rid < room_count; ++rid) {
3152 const Room* room = room_lookup(rid);
3153 if (room == nullptr)
3154 continue;
3155 if (!room->AreBlocksLoaded()) {
3156 if (room->blocks_dirty()) {
3157 return absl::FailedPreconditionError(absl::StrFormat(
3158 "Room 0x%03X has unsaved pushable-block edits, but its block "
3159 "table is not loaded. Load the room's blocks before saving.",
3160 rid));
3161 }
3162 continue; // Header-only — preserve its slots verbatim from ROM.
3163 }
3164 owned_room_ids.insert(static_cast<uint16_t>(rid));
3165 for (const auto& obj : room->GetTileObjects()) {
3166 if ((obj.options() & ObjectOption::Block) != ObjectOption::Block)
3167 continue;
3169 ValidateSpecialObjectDrawLayerSelector(obj, rid, "Pushable block"));
3170 PushableBlockEntry encoded_entry;
3171 encoded_entry.room_id = static_cast<uint16_t>(rid);
3172 encoded_entry.px = obj.x();
3173 encoded_entry.py = obj.y();
3174 encoded_entry.draw_layer = obj.GetLayerValue();
3175 encoded_entry.behavior_layer = obj.block_behavior_layer();
3176 const PushableBlockBytes encoded =
3177 EncodePushableBlockEntry(encoded_entry);
3178 const EncodedBlock encoded_block{encoded, &obj};
3179 const int load_order = obj.block_load_order();
3180 if (load_order >= 0 && !claimed_load_orders.insert(load_order).second) {
3181 return absl::FailedPreconditionError(absl::StrFormat(
3182 "Room 0x%03X has multiple pushable blocks claiming non-new "
3183 "load-order slot %d",
3184 rid, load_order));
3185 }
3186 if (load_order == RoomObject::kBlockLoadOrderNew) {
3187 appended.push_back(encoded_block);
3188 } else if (load_order >= 0 && load_order < original_slot_count) {
3189 const int original_offset = load_order * 4;
3190 const uint16_t original_room_id =
3191 static_cast<uint16_t>(original_buffer[original_offset] |
3192 (original_buffer[original_offset + 1] << 8));
3193 if (original_room_id != static_cast<uint16_t>(rid)) {
3194 // Undo/redo snapshots can restore the load order that was valid
3195 // before a prior save compacted the global table. Never let that
3196 // stale identity replace a different room's entry; preserve the
3197 // object by appending it as a newly reconciled entry instead.
3198 appended.push_back(encoded_block);
3199 continue;
3200 }
3201 slot_replacements.emplace(load_order, encoded_block);
3202 } else {
3203 // load_order points outside the original buffer (e.g. ROM
3204 // changed under us). Treat as new.
3205 appended.push_back(encoded_block);
3206 }
3207 }
3208 }
3209
3210 // Walk the original buffer slot-by-slot, replacing entries owned by
3211 // materialized rooms and preserving the rest verbatim.
3212 std::vector<uint8_t> output;
3213 output.reserve(original_byte_len + appended.size() * 4);
3214 std::vector<std::pair<const RoomObject*, int>> load_order_updates;
3215 load_order_updates.reserve(slot_replacements.size() + appended.size());
3216 const auto append_encoded_block =
3217 [&output, &load_order_updates](const EncodedBlock& block) {
3218 const int output_slot = static_cast<int>(output.size() / 4);
3219 output.push_back(block.bytes.b1);
3220 output.push_back(block.bytes.b2);
3221 output.push_back(block.bytes.b3);
3222 output.push_back(block.bytes.b4);
3223 load_order_updates.emplace_back(block.source_object, output_slot);
3224 };
3225 for (int slot = 0; slot < original_slot_count; ++slot) {
3226 const uint8_t b1 = original_buffer[slot * 4 + 0];
3227 const uint8_t b2 = original_buffer[slot * 4 + 1];
3228 const uint16_t slot_room_id = static_cast<uint16_t>(b1 | (b2 << 8));
3229 if (owned_room_ids.contains(slot_room_id)) {
3230 const auto it = slot_replacements.find(slot);
3231 if (it == slot_replacements.end()) {
3232 // The block at this slot was deleted in memory. Skip it,
3233 // shrinking the output.
3234 continue;
3235 }
3236 append_encoded_block(it->second);
3237 } else {
3238 // Unmaterialized / header-only room: keep the original bytes.
3239 output.push_back(b1);
3240 output.push_back(b2);
3241 output.push_back(original_buffer[slot * 4 + 2]);
3242 output.push_back(original_buffer[slot * 4 + 3]);
3243 }
3244 }
3245 // Append newly-added blocks (load_order == kBlockLoadOrderNew) at the
3246 // tail in creation order. Anything in slot_replacements that didn't
3247 // match a slot was already routed to `appended` above.
3248 for (const auto& block : appended) {
3249 append_encoded_block(block);
3250 }
3251
3252 // LoadAndBuildRoom's block scan is a do-while loop: it always reads the
3253 // entry at $7EF940 before adding four and comparing against this byte
3254 // length. A zero limit can therefore never terminate at the first boundary;
3255 // the 16-bit index walks beyond the 0x200-byte WRAM table until it wraps.
3256 // Fail before the first ROM write and keep edited rooms dirty rather than
3257 // emitting a runtime-unsafe empty table.
3258 if (output.empty()) {
3259 return absl::FailedPreconditionError(
3260 "Pushable-block table cannot be empty: ALTTP's runtime scan reads one "
3261 "entry before comparing the byte-length limit. Keep at least one "
3262 "pushable block, or patch the runtime loop before removing the last "
3263 "entry.");
3264 }
3265
3266 // Capacity check against the vanilla 128-entry cap.
3267 const int kMaxEntries = (4 * kBlocksRegionSize) / 4;
3268 if (static_cast<int>(output.size() / 4) > kMaxEntries) {
3269 return absl::FailedPreconditionError(absl::StrCat(
3270 "Pushable-block table overflow: ", output.size() / 4,
3271 " entries exceeds the vanilla cap of ", kMaxEntries,
3272 " (expand layout requires repointing all 4 LDA.l operand slots; "
3273 "out of scope for this encoder)."));
3274 }
3275
3276 // Build the write plan from the four destinations preflighted above. Doing
3277 // the topology check before encoding means direct callers that do not wrap
3278 // this public API in a transaction cannot discover a bad later page only
3279 // after an earlier page has already been written.
3280 const int total_bytes = static_cast<int>(output.size());
3281 struct BlockWriteDestination {
3282 int pc;
3283 int output_offset;
3284 int length;
3285 };
3286 std::vector<BlockWriteDestination> write_destinations;
3287 write_destinations.reserve(4);
3288 for (int r = 0; r < 4; ++r) {
3289 const int off = r * kBlocksRegionSize;
3290 const int len = std::min(kBlocksRegionSize, total_bytes - off);
3291 if (len <= 0)
3292 break;
3293 write_destinations.push_back({destination_pcs[r], off, len});
3294 }
3295
3296 // Write each prevalidated region. We do not relocate the data — the four
3297 // operand slots keep pointing at their existing SNES addresses.
3298 for (const auto& destination : write_destinations) {
3299 std::vector<uint8_t> chunk(
3300 output.begin() + destination.output_offset,
3301 output.begin() + destination.output_offset + destination.length);
3302 RETURN_IF_ERROR(rom->WriteVector(destination.pc, chunk));
3303 }
3304
3306 rom->WriteWord(kBlocksLength, static_cast<uint16_t>(total_bytes)));
3307
3308 // Deleting an entry compacts every following slot. Rebase each loaded
3309 // object's identity to its committed output slot so a later no-op save does
3310 // not try to replace the stale pre-compaction slot and silently drop it.
3311 // This metadata stays untouched until every ROM write succeeds, matching
3312 // the dirty-state failure contract below.
3313 for (const auto& [object, load_order] : load_order_updates) {
3314 const_cast<RoomObject*>(object)->set_block_load_order(load_order);
3315 }
3316 for (int room_id = 0; room_id < room_count; ++room_id) {
3317 if (const Room* room = room_lookup(room_id);
3318 room != nullptr && room->AreBlocksLoaded() && room->blocks_dirty()) {
3319 const_cast<Room*>(room)->ClearBlocksDirty();
3320 }
3321 }
3322 return absl::OkStatus();
3323}
3324
3325template <typename RoomLookup>
3326absl::Status SaveAllCollisionImpl(Rom* rom, int room_count,
3327 RoomLookup&& room_lookup) {
3328 if (!rom || !rom->is_loaded()) {
3329 return absl::InvalidArgumentError("ROM not loaded");
3330 }
3331
3332 // If the custom collision region doesn't exist (vanilla ROM), treat as a noop
3333 // only when there are no pending custom collision edits. This avoids silently
3334 // dropping user-authored collision changes on ROMs that don't support the
3335 // expanded collision bank.
3336 const auto& rom_data = rom->vector();
3337 const int ptrs_size = kNumberOfRooms * 3;
3338 const bool has_ptr_table = HasCustomCollisionPointerTable(rom_data.size());
3339 const bool has_data_region = HasCustomCollisionDataRegion(rom_data.size());
3340
3341 if (!has_ptr_table) {
3342 for (int room_id = 0; room_id < room_count; ++room_id) {
3343 const Room* room = room_lookup(room_id);
3344 if (room != nullptr && room->custom_collision_dirty()) {
3345 return absl::FailedPreconditionError(
3346 "Custom collision region not present in this ROM");
3347 }
3348 }
3349 return absl::OkStatus();
3350 }
3351
3352 if (!has_data_region) {
3353 for (int room_id = 0; room_id < room_count; ++room_id) {
3354 const Room* room = room_lookup(room_id);
3355 if (room != nullptr && room->custom_collision_dirty()) {
3356 return absl::FailedPreconditionError(
3357 "Custom collision data region not present in this ROM");
3358 }
3359 }
3360 return absl::OkStatus();
3361 }
3362
3363 // Save-time guardrails: custom collision writes must never clobber the
3364 // reserved WaterFill tail region (Oracle of Secrets).
3366 RETURN_IF_ERROR(fence.Allow(
3367 static_cast<uint32_t>(kCustomCollisionRoomPointers),
3368 static_cast<uint32_t>(kCustomCollisionRoomPointers + ptrs_size),
3369 "CustomCollisionPointers"));
3371 fence.Allow(static_cast<uint32_t>(kCustomCollisionDataPosition),
3372 static_cast<uint32_t>(kCustomCollisionDataSoftEnd),
3373 "CustomCollisionData"));
3374 yaze::rom::ScopedWriteFence scope(rom, &fence);
3375
3376 const int room_limit = std::min(room_count, kNumberOfRooms);
3377 for (int room_id = 0; room_id < room_limit; ++room_id) {
3378 const Room* room = room_lookup(room_id);
3379 if (room == nullptr || !room->custom_collision_dirty()) {
3380 continue;
3381 }
3382
3383 const int actual_room_id = room->id();
3384 const int ptr_offset = kCustomCollisionRoomPointers + (actual_room_id * 3);
3385 if (ptr_offset + 2 >= static_cast<int>(rom_data.size())) {
3386 return absl::OutOfRangeError("Custom collision pointer out of range");
3387 }
3388
3389 if (!room->has_custom_collision()) {
3390 // Disable: clear the pointer entry.
3391 RETURN_IF_ERROR(rom->WriteByte(ptr_offset, 0));
3392 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 1, 0));
3393 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 2, 0));
3394 const_cast<Room*>(room)->ClearCustomCollisionDirty();
3395 continue;
3396 }
3397
3398 // Treat an all-zero map as disabled to avoid wasting space.
3399 bool any = false;
3400 for (uint8_t v : room->custom_collision().tiles) {
3401 if (v != 0) {
3402 any = true;
3403 break;
3404 }
3405 }
3406 if (!any) {
3407 RETURN_IF_ERROR(rom->WriteByte(ptr_offset, 0));
3408 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 1, 0));
3409 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 2, 0));
3410 const_cast<Room*>(room)->ClearCustomCollisionDirty();
3411 continue;
3412 }
3413
3415 WriteTrackCollision(rom, actual_room_id, room->custom_collision()));
3416 const_cast<Room*>(room)->ClearCustomCollisionDirty();
3417 }
3418
3419 return absl::OkStatus();
3420}
3421
3422absl::Status SaveAllCollision(Rom* rom, absl::Span<Room> rooms) {
3423 return SaveAllCollisionImpl(
3424 rom, static_cast<int>(rooms.size()),
3425 [&rooms](int room_id) { return &rooms[room_id]; });
3426}
3427
3428absl::Status SaveAllCollision(Rom* rom, int room_count,
3429 const std::function<Room*(int)>& room_lookup) {
3430 return SaveAllCollisionImpl(rom, room_count, room_lookup);
3431}
3432
3433absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
3435 if (rom == nullptr || !rom->is_loaded()) {
3436 return absl::InvalidArgumentError("ROM not loaded");
3437 }
3438 const auto& rom_data = rom->vector();
3439 if (kChestsLengthPointer + 1 >= static_cast<int>(rom_data.size()) ||
3440 kChestsDataPointer1 + 2 >= static_cast<int>(rom_data.size())) {
3441 return absl::OutOfRangeError("Chest pointers out of range");
3442 }
3443
3444 const uint32_t data_pointer =
3445 (static_cast<uint32_t>(rom_data[kChestsDataPointer1 + 2]) << 16) |
3446 (static_cast<uint32_t>(rom_data[kChestsDataPointer1 + 1]) << 8) |
3447 rom_data[kChestsDataPointer1];
3448 const uint32_t data_pc = SnesToPc(data_pointer);
3449 if (data_pc > rom_data.size() ||
3450 static_cast<size_t>(kChestTableCapacityBytes) >
3451 rom_data.size() - static_cast<size_t>(data_pc)) {
3452 return absl::OutOfRangeError("Chest data region out of range");
3453 }
3454 const uint32_t data_end =
3455 data_pc + static_cast<uint32_t>(kChestTableCapacityBytes);
3456 const auto overlaps = [](uint32_t begin, uint32_t end, uint32_t other_begin,
3457 uint32_t other_end) {
3458 return begin < other_end && other_begin < end;
3459 };
3460 if (overlaps(data_pc, data_end, kChestsLengthPointer,
3461 kChestsLengthPointer + 2) ||
3462 overlaps(data_pc, data_end, kChestsDataPointer1,
3463 kChestsDataPointer1 + 3)) {
3464 return absl::FailedPreconditionError(
3465 "Chest data region overlaps chest metadata operands");
3466 }
3467
3468 return std::vector<std::pair<uint32_t, uint32_t>>{
3469 {static_cast<uint32_t>(kChestsLengthPointer),
3470 static_cast<uint32_t>(kChestsLengthPointer + 2)},
3471 {data_pc, data_end},
3472 };
3473}
3474
3475namespace {
3476
3478 uint16_t word = 0;
3479 uint8_t item = 0;
3480
3481 uint16_t room_id() const { return word & 0x7FFF; }
3482};
3483
3484// Parse current ROM chest data without grouping or normalizing records.
3485// `byte_length` is the runtime byte count at kChestsLengthPointer.
3486std::vector<PhysicalChestRecord> ParsePhysicalRomChests(
3487 const std::vector<uint8_t>& rom_data, int cpos, int byte_length) {
3488 std::vector<PhysicalChestRecord> records;
3489 const int record_count = byte_length / kChestTableRecordSize;
3490 records.reserve(record_count);
3491 for (int i = 0; i < record_count; ++i) {
3492 const int off = cpos + i * kChestTableRecordSize;
3493 if (off < 0 ||
3494 off + kChestTableRecordSize > static_cast<int>(rom_data.size())) {
3495 break;
3496 }
3497 const uint16_t word =
3498 (static_cast<uint16_t>(rom_data[off + 1]) << 8) | rom_data[off];
3499 records.push_back(PhysicalChestRecord{word, rom_data[off + 2]});
3500 }
3501 return records;
3502}
3503
3504void AppendChestRecord(std::vector<uint8_t>* bytes, uint16_t word,
3505 uint8_t item) {
3506 bytes->push_back(word & 0xFF);
3507 bytes->push_back((word >> 8) & 0xFF);
3508 bytes->push_back(item);
3509}
3510
3511void AppendEditedChestRecord(std::vector<uint8_t>* bytes, int room_id,
3512 const chest_data& chest) {
3513 const uint16_t word = static_cast<uint16_t>(room_id) |
3514 (chest.size ? static_cast<uint16_t>(0x8000) : 0);
3515 AppendChestRecord(bytes, word, chest.id);
3516}
3517
3518void AppendChangedChestRuns(uint32_t pc, absl::Span<const uint8_t> expected,
3519 absl::Span<const uint8_t> replacement,
3520 std::vector<ChestWriteRun>* writes) {
3521 size_t cursor = 0;
3522 while (cursor < replacement.size()) {
3523 if (replacement[cursor] == expected[cursor]) {
3524 ++cursor;
3525 continue;
3526 }
3527 const size_t begin = cursor;
3528 do {
3529 ++cursor;
3530 } while (cursor < replacement.size() &&
3531 replacement[cursor] != expected[cursor]);
3532
3533 ChestWriteRun run;
3534 run.pc = pc + static_cast<uint32_t>(begin);
3535 run.expected_bytes.assign(expected.begin() + begin,
3536 expected.begin() + cursor);
3537 run.replacement_bytes.assign(replacement.begin() + begin,
3538 replacement.begin() + cursor);
3539 if (!writes->empty() && writes->back().end() == run.pc) {
3540 writes->back().expected_bytes.insert(writes->back().expected_bytes.end(),
3541 run.expected_bytes.begin(),
3542 run.expected_bytes.end());
3543 writes->back().replacement_bytes.insert(
3544 writes->back().replacement_bytes.end(), run.replacement_bytes.begin(),
3545 run.replacement_bytes.end());
3546 } else {
3547 writes->push_back(std::move(run));
3548 }
3549 }
3550}
3551
3552void SortAndCoalesceChestRuns(std::vector<ChestWriteRun>* writes) {
3553 std::sort(writes->begin(), writes->end(),
3554 [](const ChestWriteRun& lhs, const ChestWriteRun& rhs) {
3555 return lhs.pc < rhs.pc;
3556 });
3557 std::vector<ChestWriteRun> merged;
3558 merged.reserve(writes->size());
3559 for (ChestWriteRun& write : *writes) {
3560 if (!merged.empty() && merged.back().end() == write.pc) {
3561 merged.back().expected_bytes.insert(merged.back().expected_bytes.end(),
3562 write.expected_bytes.begin(),
3563 write.expected_bytes.end());
3564 merged.back().replacement_bytes.insert(
3565 merged.back().replacement_bytes.end(),
3566 write.replacement_bytes.begin(), write.replacement_bytes.end());
3567 continue;
3568 }
3569 merged.push_back(std::move(write));
3570 }
3571 *writes = std::move(merged);
3572}
3573
3574std::vector<uint8_t> EncodeChestRoomState(int room_id, const Room& room) {
3575 std::vector<uint8_t> bytes;
3576 bytes.reserve(room.GetChests().size() * kChestTableRecordSize);
3577 for (const chest_data& chest : room.GetChests()) {
3578 AppendEditedChestRecord(&bytes, room_id, chest);
3579 }
3580 return bytes;
3581}
3582
3583absl::StatusOr<ChestSavePlan> BuildChestSavePlanImpl(
3584 const Rom* rom, int room_count,
3585 const std::function<const Room*(int)>& room_lookup) {
3586 if (rom == nullptr || !rom->is_loaded()) {
3587 return absl::InvalidArgumentError("ROM not loaded");
3588 }
3589 const auto& rom_data = rom->vector();
3590 if (kChestsLengthPointer + 1 >= static_cast<int>(rom_data.size()) ||
3591 kChestsDataPointer1 + 2 >= static_cast<int>(rom_data.size())) {
3592 return absl::OutOfRangeError("Chest pointers out of range");
3593 }
3594
3595 ChestSavePlan plan;
3596 plan.room_limit = std::min(room_count, kNumberOfRooms);
3597 std::vector<const Room*> dirty_rooms(kNumberOfRooms, nullptr);
3598 for (int room_id = 0; room_id < plan.room_limit; ++room_id) {
3599 const Room* room = room_lookup(room_id);
3600 if (room != nullptr && room->chests_dirty()) {
3601 dirty_rooms[room_id] = room;
3602 plan.any_dirty = true;
3603 plan.dirty_rooms.push_back(
3604 ChestDirtyRoomState{room_id, EncodeChestRoomState(room_id, *room)});
3605 }
3606 }
3607 if (!plan.any_dirty) {
3608 return plan;
3609 }
3610
3611 ASSIGN_OR_RETURN(auto potential_ranges, GetChestTableWriteRanges(rom));
3612 plan.data_pc = potential_ranges[1].first;
3613 std::copy_n(rom_data.begin() + kChestsDataPointer1,
3614 plan.pointer_operand.size(), plan.pointer_operand.begin());
3616 static_cast<uint16_t>((rom_data[kChestsLengthPointer + 1] << 8) |
3617 rom_data[kChestsLengthPointer]);
3618 plan.original_capacity_bytes.assign(
3619 rom_data.begin() + plan.data_pc,
3620 rom_data.begin() + plan.data_pc + kChestTableCapacityBytes);
3623 return absl::FailedPreconditionError(absl::StrFormat(
3624 "Chest table byte length %d is invalid (capacity %d)",
3625 static_cast<int>(plan.original_byte_length), kChestTableCapacityBytes));
3626 }
3627
3628 const auto physical_records = ParsePhysicalRomChests(
3629 rom_data, static_cast<int>(plan.data_pc), plan.original_byte_length);
3630 if (physical_records.size() !=
3631 static_cast<size_t>(plan.original_byte_length / kChestTableRecordSize)) {
3632 return absl::OutOfRangeError("Chest data region is truncated");
3633 }
3634
3635 std::vector<size_t> old_counts(kNumberOfRooms, 0);
3636 for (const PhysicalChestRecord& record : physical_records) {
3637 if (record.room_id() < kNumberOfRooms) {
3638 ++old_counts[record.room_id()];
3639 }
3640 }
3641
3642 size_t final_record_count = physical_records.size();
3643 for (int room_id = 0; room_id < plan.room_limit; ++room_id) {
3644 if (dirty_rooms[room_id] == nullptr) {
3645 continue;
3646 }
3647 final_record_count -= old_counts[room_id];
3648 final_record_count += dirty_rooms[room_id]->GetChests().size();
3649 }
3650 if (final_record_count > static_cast<size_t>(kChestTableCapacityRecords)) {
3651 return absl::ResourceExhaustedError(absl::StrFormat(
3652 "Chest table has %d records; capacity is %d",
3653 static_cast<int>(final_record_count), kChestTableCapacityRecords));
3654 }
3655
3656 std::vector<uint8_t> replacement_bytes;
3657 replacement_bytes.reserve(final_record_count * kChestTableRecordSize);
3658 std::vector<size_t> seen_counts(kNumberOfRooms, 0);
3659 for (const PhysicalChestRecord& record : physical_records) {
3660 const uint16_t room_id = record.room_id();
3661 const Room* dirty_room =
3662 room_id < kNumberOfRooms ? dirty_rooms[room_id] : nullptr;
3663 if (dirty_room == nullptr) {
3664 AppendChestRecord(&replacement_bytes, record.word, record.item);
3665 continue;
3666 }
3667
3668 const size_t occurrence = seen_counts[room_id]++;
3669 const auto& replacements = dirty_room->GetChests();
3670 if (occurrence < replacements.size()) {
3671 AppendEditedChestRecord(&replacement_bytes, room_id,
3672 replacements[occurrence]);
3673 }
3674 }
3675
3676 // Growth has no existing physical slot. Append extras in room-ID order so
3677 // repeated saves are deterministic while every pre-existing record keeps its
3678 // relative position.
3679 for (int room_id = 0; room_id < plan.room_limit; ++room_id) {
3680 const Room* dirty_room = dirty_rooms[room_id];
3681 if (dirty_room == nullptr) {
3682 continue;
3683 }
3684 const auto& replacements = dirty_room->GetChests();
3685 for (size_t i = seen_counts[room_id]; i < replacements.size(); ++i) {
3686 AppendEditedChestRecord(&replacement_bytes, room_id, replacements[i]);
3687 }
3688 }
3689
3690 if (replacement_bytes.size() != final_record_count * kChestTableRecordSize) {
3691 return absl::InternalError("Chest save plan size mismatch");
3692 }
3693
3694 const std::array<uint8_t, 2> expected_length = {
3695 static_cast<uint8_t>(plan.original_byte_length & 0xFF),
3696 static_cast<uint8_t>((plan.original_byte_length >> 8) & 0xFF)};
3697 const uint16_t replacement_length =
3698 static_cast<uint16_t>(replacement_bytes.size());
3699 const std::array<uint8_t, 2> encoded_replacement_length = {
3700 static_cast<uint8_t>(replacement_length & 0xFF),
3701 static_cast<uint8_t>((replacement_length >> 8) & 0xFF)};
3703 encoded_replacement_length, &plan.writes);
3705 absl::MakeConstSpan(plan.original_capacity_bytes)
3706 .subspan(0, replacement_bytes.size()),
3707 replacement_bytes, &plan.writes);
3709 return plan;
3710}
3711
3712int ReadRoomPotItemAddressPc(const std::vector<uint8_t>& rom_data,
3713 int room_id) {
3714 if (room_id < 0 || room_id >= kNumberOfRooms) {
3715 return -1;
3716 }
3717 const int ptr_off = kRoomItemsPointers + (room_id * 2);
3718 if (ptr_off < 0 || ptr_off + 1 >= static_cast<int>(rom_data.size())) {
3719 return -1;
3720 }
3721 const uint16_t item_ptr =
3722 (static_cast<uint16_t>(rom_data[ptr_off + 1]) << 8) | rom_data[ptr_off];
3723 if (item_ptr < 0x8000) {
3724 return -1;
3725 }
3726 const int item_addr = static_cast<int>(SnesToPc(0x010000 | item_ptr));
3727 return item_addr >= 0 && item_addr < static_cast<int>(rom_data.size())
3728 ? item_addr
3729 : -1;
3730}
3731
3732absl::StatusOr<PhysicalStreamInfo> GetPotItemStreamInfo(
3733 const std::vector<uint8_t>& rom_data, int room_id) {
3735 static_cast<int>(rom_data.size())) {
3736 return absl::OutOfRangeError("Room items pointer table out of range");
3737 }
3738 if (room_id < 0 || room_id >= kNumberOfRooms) {
3739 return absl::OutOfRangeError("Room ID out of range");
3740 }
3741
3742 std::vector<int> addresses(kNumberOfRooms, -1);
3743 for (int id = 0; id < kNumberOfRooms; ++id) {
3744 addresses[id] = ReadRoomPotItemAddressPc(rom_data, id);
3745 }
3746 const int hard_end =
3747 std::min(static_cast<int>(rom_data.size()), kRoomItemsDataEnd);
3748 PhysicalStreamInfo info = AnalyzePhysicalStream(addresses, room_id, hard_end);
3749 if (info.address < 0 || info.address >= hard_end) {
3750 return absl::FailedPreconditionError(
3751 "Room pot item pointer is null, invalid, or outside the item region");
3752 }
3753 return info;
3754}
3755
3756} // namespace
3757
3758absl::StatusOr<ChestSavePlan> BuildChestSavePlan(
3759 const Rom* rom, int room_count,
3760 const std::function<const Room*(int)>& room_lookup) {
3761 return BuildChestSavePlanImpl(rom, room_count, room_lookup);
3762}
3763
3764absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
3765GetDirtyChestWriteRanges(const Rom* rom, int room_count,
3766 const std::function<const Room*(int)>& room_lookup) {
3768 BuildChestSavePlan(rom, room_count, room_lookup));
3769 return plan.write_ranges();
3770}
3771
3773 Rom* rom, const ChestSavePlan& plan,
3774 const std::function<const Room*(int)>& room_lookup) {
3775 if (rom == nullptr || !rom->is_loaded()) {
3776 return absl::InvalidArgumentError("ROM not loaded");
3777 }
3778 if (!plan.any_dirty) {
3779 ASSIGN_OR_RETURN(ChestSavePlan canonical_plan,
3780 BuildChestSavePlan(rom, plan.room_limit, room_lookup));
3781 if (canonical_plan != plan) {
3782 return absl::FailedPreconditionError(
3783 "Chest save plan does not match canonical serialization");
3784 }
3785 return absl::OkStatus();
3786 }
3787
3788 const auto& rom_data = rom->vector();
3789 if (kChestsLengthPointer + 1 >= static_cast<int>(rom_data.size()) ||
3790 kChestsDataPointer1 + 2 >= static_cast<int>(rom_data.size()) ||
3791 plan.data_pc > rom_data.size() ||
3792 plan.original_capacity_bytes.size() > rom_data.size() - plan.data_pc) {
3793 return absl::FailedPreconditionError(
3794 "Chest save plan source is no longer addressable");
3795 }
3796 if (!std::equal(plan.pointer_operand.begin(), plan.pointer_operand.end(),
3797 rom_data.begin() + kChestsDataPointer1)) {
3798 return absl::FailedPreconditionError(
3799 "Chest save plan is stale: data pointer changed");
3800 }
3801 const uint16_t current_length =
3802 static_cast<uint16_t>((rom_data[kChestsLengthPointer + 1] << 8) |
3803 rom_data[kChestsLengthPointer]);
3804 if (current_length != plan.original_byte_length) {
3805 return absl::FailedPreconditionError(
3806 "Chest save plan is stale: runtime length changed");
3807 }
3808 if (!std::equal(plan.original_capacity_bytes.begin(),
3809 plan.original_capacity_bytes.end(),
3810 rom_data.begin() + plan.data_pc)) {
3811 return absl::FailedPreconditionError(
3812 "Chest save plan is stale: table bytes changed");
3813 }
3814 for (const ChestDirtyRoomState& state : plan.dirty_rooms) {
3815 const Room* room = room_lookup(state.room_id);
3816 if (room == nullptr || !room->chests_dirty() ||
3817 EncodeChestRoomState(state.room_id, *room) != state.encoded_chests) {
3818 return absl::FailedPreconditionError(absl::StrFormat(
3819 "Chest save plan is stale for room 0x%03X", state.room_id));
3820 }
3821 }
3822 ASSIGN_OR_RETURN(ChestSavePlan canonical_plan,
3823 BuildChestSavePlan(rom, plan.room_limit, room_lookup));
3824 if (canonical_plan != plan) {
3825 return absl::FailedPreconditionError(
3826 "Chest save plan does not match canonical serialization");
3827 }
3828
3829 yaze::ScopedRomTransaction transaction(*rom);
3831 for (const ChestWriteRun& write : plan.writes) {
3832 RETURN_IF_ERROR(fence.Allow(write.pc, write.end(), "ChestTableExactDelta"));
3833 }
3834 yaze::rom::ScopedWriteFence scope(rom, &fence);
3835 for (const ChestWriteRun& write : plan.writes) {
3836 RETURN_IF_ERROR(rom->WriteVector(write.pc, write.replacement_bytes));
3837 }
3838 for (const ChestDirtyRoomState& state : plan.dirty_rooms) {
3839 const_cast<Room*>(room_lookup(state.room_id))->ClearChestsDirty();
3840 }
3841 transaction.Commit();
3842 return absl::OkStatus();
3843}
3844
3845template <typename RoomLookup>
3846absl::Status SaveAllChestsImpl(Rom* rom, int room_count,
3847 RoomLookup&& room_lookup) {
3848 const std::function<const Room*(int)> lookup =
3849 std::forward<RoomLookup>(room_lookup);
3851 BuildChestSavePlan(rom, room_count, lookup));
3852 return ApplyChestSavePlan(rom, plan, lookup);
3853}
3854
3855absl::Status SaveAllChests(Rom* rom, absl::Span<const Room> rooms) {
3856 return SaveAllChestsImpl(rom, static_cast<int>(rooms.size()),
3857 [&rooms](int room_id) { return &rooms[room_id]; });
3858}
3859
3860absl::Status SaveAllChests(Rom* rom, int room_count,
3861 const std::function<const Room*(int)>& room_lookup) {
3862 return SaveAllChestsImpl(rom, room_count, room_lookup);
3863}
3864
3865template <typename RoomLookup>
3867 Rom* rom, int room_count, RoomLookup&& room_lookup,
3868 const DungeonStreamLayout* repack_layout = nullptr) {
3869 if (!rom || !rom->is_loaded()) {
3870 return absl::InvalidArgumentError("ROM not loaded");
3871 }
3872 const auto& rom_data = rom->vector();
3874 static_cast<int>(rom_data.size())) {
3875 return absl::OutOfRangeError("Room items pointer table out of range");
3876 }
3877
3878 const int room_limit = std::min(room_count, kNumberOfRooms);
3879 if (repack_layout != nullptr) {
3880 std::vector<DungeonStreamReplacement> replacements;
3881 for (int room_id = 0; room_id < room_limit; ++room_id) {
3882 const Room* room = room_lookup(room_id);
3883 if (room == nullptr || !room->pot_items_dirty()) {
3884 continue;
3885 }
3886
3887 DungeonStreamReplacement replacement;
3888 replacement.room_id = static_cast<uint32_t>(room_id);
3889 replacement.encoded_stream.reserve(room->GetPotItems().size() * 3 + 2);
3890 for (const PotItem& item : room->GetPotItems()) {
3891 replacement.encoded_stream.push_back(item.position & 0xFF);
3892 replacement.encoded_stream.push_back((item.position >> 8) & 0xFF);
3893 replacement.encoded_stream.push_back(item.item);
3894 }
3895 replacement.encoded_stream.push_back(0xFF);
3896 replacement.encoded_stream.push_back(0xFF);
3897 replacements.push_back(std::move(replacement));
3898 }
3899 if (replacements.empty()) {
3900 return absl::OkStatus();
3901 }
3902
3904 InventoryDungeonStreams(*rom, *repack_layout));
3906 PlanDungeonStreamRepack(inventory, replacements));
3908 for (const DungeonStreamReplacement& replacement : replacements) {
3909 if (const Room* room = room_lookup(replacement.room_id);
3910 room != nullptr) {
3911 const_cast<Room*>(room)->ClearPotItemsDirty();
3912 }
3913 }
3914 return absl::OkStatus();
3915 }
3916
3917 struct PendingPotItemWrite {
3918 int room_id = -1;
3919 int address = -1;
3920 std::vector<uint8_t> bytes;
3921 };
3922
3923 // Build and validate every dirty write before touching the ROM. A later
3924 // shared/overfull stream must not leave earlier rooms partially written.
3925 std::vector<PendingPotItemWrite> pending_writes;
3926 for (int room_id = 0; room_id < room_limit; ++room_id) {
3927 const Room* room = room_lookup(room_id);
3928 if (room == nullptr || !room->pot_items_dirty()) {
3929 continue;
3930 }
3931
3932 ASSIGN_OR_RETURN(const PhysicalStreamInfo stream_info,
3933 GetPotItemStreamInfo(rom_data, room_id));
3934 if (stream_info.shared) {
3935 return absl::FailedPreconditionError(absl::StrFormat(
3936 "Room %d pot item stream at PC 0x%06X is shared; repacking is "
3937 "required",
3938 room_id, stream_info.address));
3939 }
3940 if (stream_info.capacity() <= 0) {
3941 return absl::FailedPreconditionError(absl::StrFormat(
3942 "Room %d pot item stream has no safe physical boundary", room_id));
3943 }
3944
3945 PendingPotItemWrite pending;
3946 pending.room_id = room_id;
3947 pending.address = stream_info.address;
3948 for (const auto& pi : room->GetPotItems()) {
3949 pending.bytes.push_back(pi.position & 0xFF);
3950 pending.bytes.push_back((pi.position >> 8) & 0xFF);
3951 pending.bytes.push_back(pi.item);
3952 }
3953 pending.bytes.push_back(0xFF);
3954 pending.bytes.push_back(0xFF);
3955 if (static_cast<int>(pending.bytes.size()) > stream_info.capacity()) {
3956 return absl::ResourceExhaustedError(absl::StrFormat(
3957 "Room %d pot item data too large! Size: %d, Available: %d", room_id,
3958 static_cast<int>(pending.bytes.size()), stream_info.capacity()));
3959 }
3960 pending_writes.push_back(std::move(pending));
3961 }
3962
3963 for (const auto& pending : pending_writes) {
3964 const bool data_changed =
3965 !std::equal(pending.bytes.begin(), pending.bytes.end(),
3966 rom_data.begin() + pending.address);
3967 if (data_changed) {
3968 RETURN_IF_ERROR(rom->WriteVector(pending.address, pending.bytes));
3969 }
3970 }
3971
3972 for (const auto& pending : pending_writes) {
3973 if (const Room* room = room_lookup(pending.room_id); room != nullptr) {
3974 const_cast<Room*>(room)->ClearPotItemsDirty();
3975 }
3976 }
3977 return absl::OkStatus();
3978}
3979
3980absl::Status SaveAllPotItems(Rom* rom, absl::Span<const Room> rooms) {
3981 return SaveAllPotItemsImpl(rom, static_cast<int>(rooms.size()),
3982 [&rooms](int room_id) { return &rooms[room_id]; });
3983}
3984
3985absl::Status SaveAllPotItems(Rom* rom, absl::Span<const Room> rooms,
3986 const DungeonStreamLayout* repack_layout) {
3987 return SaveAllPotItemsImpl(
3988 rom, static_cast<int>(rooms.size()),
3989 [&rooms](int room_id) { return &rooms[room_id]; }, repack_layout);
3990}
3991
3992absl::Status SaveAllPotItems(
3993 Rom* rom, int room_count,
3994 const std::function<const Room*(int)>& room_lookup) {
3995 return SaveAllPotItemsImpl(rom, room_count, room_lookup);
3996}
3997
3998absl::Status SaveAllPotItems(Rom* rom, int room_count,
3999 const std::function<const Room*(int)>& room_lookup,
4000 const DungeonStreamLayout* repack_layout) {
4001 return SaveAllPotItemsImpl(rom, room_count, room_lookup, repack_layout);
4002}
4003
4005 auto rom_data = rom()->vector();
4006
4007 // Read blocks length
4008 int blocks_count =
4009 (rom_data[kBlocksLength + 1] << 8) | rom_data[kBlocksLength];
4010
4011 LOG_DEBUG("Room", "LoadBlocks: room_id=%d, blocks_count=%d", room_id_,
4012 blocks_count);
4013
4014 // Load block data from the four data regions.
4015 //
4016 // `kBlocksPointer1..4` are 3-byte SNES long-address operand slots
4017 // embedded in bank_02's LDA.l instructions (`$02:DAF9..$02:DB2E`),
4018 // not inline data offsets. Each operand encodes
4019 // `data_base + region_offset` where data_base is the SNES address
4020 // of `SpecialUnderworldObjects_pushable_block` ($04:F1DE in vanilla)
4021 // and region_offset is `r * 0x80`. The previous code read directly
4022 // from the operand slots, so it was decoding the LDA.l opcode
4023 // operand bytes as block data — silently corrupting every block on
4024 // load. `SaveAllBlocks` has always dereferenced these correctly;
4025 // load now matches.
4026 const int kRegionSize = 0x80;
4027 const int kPointerSlots[4] = {kBlocksPointer1, kBlocksPointer2,
4029 std::vector<uint8_t> blocks_data(blocks_count, 0);
4030 for (int r = 0; r < 4; ++r) {
4031 const int slot = kPointerSlots[r];
4032 if (slot + 2 >= static_cast<int>(rom_data.size())) {
4033 LOG_WARN("Room", "LoadBlocks: pointer slot %d out of range", r);
4034 return;
4035 }
4036 const absl::Status operand_status =
4037 ValidateBlocksLoaderPointerOperand(rom_data, slot);
4038 if (!operand_status.ok()) {
4039 LOG_WARN("Room", "LoadBlocks: %s",
4040 std::string(operand_status.message()).c_str());
4041 return;
4042 }
4043 const int snes =
4044 (rom_data[slot + 2] << 16) | (rom_data[slot + 1] << 8) | rom_data[slot];
4045 const int pc = SnesToPc(snes);
4046 const int off = r * kRegionSize;
4047 const int len = std::min(kRegionSize, blocks_count - off);
4048 if (len <= 0)
4049 break;
4050 if (pc < 0 || pc + len > static_cast<int>(rom_data.size())) {
4051 LOG_WARN("Room", "LoadBlocks: region %d data out of range", r);
4052 return;
4053 }
4054 std::copy_n(rom_data.begin() + pc, len, blocks_data.begin() + off);
4055 }
4056
4057 // Avoid duplication if LoadBlocks is called multiple times. Do this only
4058 // after the ROM pointer operands and data regions are known-good so a guard
4059 // failure cannot make existing in-memory block objects vanish.
4060 tile_objects_.erase(
4061 std::remove_if(tile_objects_.begin(), tile_objects_.end(),
4062 [](const RoomObject& obj) {
4063 return (obj.options() & ObjectOption::Block) !=
4064 ObjectOption::Nothing;
4065 }),
4066 tile_objects_.end());
4067
4068 // Parse blocks for this room (4 bytes per block entry).
4069 //
4070 // Vanilla scan (bank_01.asm:1162) walks the flat 396-byte table linearly
4071 // matching on room_id; there is no per-room 0xFFFF terminator. The
4072 // previous "break on b3==0xFF && b4==0xFF after room_id match" guard was
4073 // a phantom — it never fired in vanilla and would have prematurely
4074 // truncated a room's block list if a future tombstone happened to share
4075 // its room_id. Removed alongside the decoder fix.
4076 for (int i = 0; i + 3 < blocks_count; i += 4) {
4077 PushableBlockBytes bytes{blocks_data[i], blocks_data[i + 1],
4078 blocks_data[i + 2], blocks_data[i + 3]};
4079 const PushableBlockEntry entry = DecodePushableBlockEntry(bytes);
4080 if (entry.room_id != room_id_)
4081 continue;
4082
4083 RoomObject block_obj(0x0E00, entry.px, entry.py, 0, entry.draw_layer);
4084 block_obj.SetRom(rom_);
4087 // Capture the entry's slot index in the global buffer so
4088 // SaveAllBlocks can emit entries in vanilla authoring order
4089 // (interleaved across rooms; sorting by room_id would reshuffle
4090 // bytes and break byte equality on no-op saves).
4091 block_obj.set_block_load_order(i / 4);
4092 tile_objects_.push_back(block_obj);
4093
4094 LOG_DEBUG("Room", "Loaded block at (%d,%d) draw_layer=%d behavior_layer=%d",
4095 entry.px, entry.py, entry.draw_layer, entry.behavior_layer);
4096 }
4097 blocks_loaded_ = true;
4098}
4099
4101 if (!rom_ || !rom_->is_loaded())
4102 return;
4103 auto rom_data = rom()->vector();
4104 pot_items_.clear();
4105 pot_items_loaded_ = false;
4106
4107 // Load pot items
4108 // Format per ASM analysis (bank_01.asm):
4109 // - Pointer table at kRoomItemsPointers (0x01DB69)
4110 // - Each room has a pointer to item data
4111 // - Item data format: 3 bytes per item
4112 // - 2 bytes: position word (Y_hi, X_lo encoding)
4113 // - 1 byte: item type
4114 // - Terminated by 0xFFFF position word
4115
4116 int table_addr = kRoomItemsPointers; // 0x01DB69
4117
4118 // Read pointer for this room
4119 int ptr_addr = table_addr + (room_id_ * 2);
4120 if (ptr_addr + 1 >= static_cast<int>(rom_data.size()))
4121 return;
4122
4123 uint16_t item_ptr = (rom_data[ptr_addr + 1] << 8) | rom_data[ptr_addr];
4124
4125 // Convert to PC address (Bank 01 offset)
4126 int item_addr = SnesToPc(0x010000 | item_ptr);
4127
4128 // Read 3-byte entries until 0xFFFF terminator
4129 while (item_addr + 2 < static_cast<int>(rom_data.size())) {
4130 // Read position word (little endian)
4131 uint16_t position = (rom_data[item_addr + 1] << 8) | rom_data[item_addr];
4132
4133 // Check for terminator
4134 if (position == 0xFFFF)
4135 break;
4136
4137 // Read item type (3rd byte)
4138 uint8_t item_type = rom_data[item_addr + 2];
4139
4140 PotItem pot_item;
4141 pot_item.position = position;
4142 pot_item.item = item_type;
4143 pot_items_.push_back(pot_item);
4144
4145 item_addr += 3; // Move to next entry
4146 }
4147
4148 pot_items_loaded_ = true;
4149}
4150
4152 auto rom_data = rom()->vector();
4153
4154 // The legacy symbol `kPitCount` is the LDX.w immediate at PC 0x394A6
4155 // — the **maximum X offset** in the runtime CMP loop, not an entry
4156 // count. Total entries = `(max_offset / 2) + 1`. This function does
4157 // not actually consume the table contents (yaze has no editable
4158 // surface for pit-damage gating); it just resolves the dereferenced
4159 // address for diagnostic logging. See
4160 // `test/integration/zelda3/dungeon_save_region_test.cc` for the
4161 // format-pinning tests and `memory/project_dungeon_pit_audit.md`
4162 // for the audit conclusion.
4163 const int max_offset = rom_data[kPitCount];
4164 const int pit_entries = max_offset / 2 + 1;
4165
4166 const int pit_ptr = (rom_data[kPitPointer + 2] << 16) |
4167 (rom_data[kPitPointer + 1] << 8) | rom_data[kPitPointer];
4168
4169 LOG_DEBUG("Room",
4170 "LoadPits: room_id=%d, RoomsWithPitDamage entries=%d, "
4171 "table_snes=0x%06X",
4172 room_id_, pit_entries, pit_ptr);
4173
4174 // The per-room pit DESTINATION (where Link goes when falling through
4175 // a non-damaging pit) is unrelated to the global RoomsWithPitDamage
4176 // table read above. It lives in the room header and was loaded into
4177 // `pits_` (target room + target_layer) by `LoadRoomFromRom`. The
4178 // round-trip for that state goes through the room header save path,
4179 // not `SaveAllPits`.
4180 LOG_DEBUG("Room", "Per-room pit destination: target=%d, target_layer=%d",
4182}
4183
4184// ============================================================================
4185// Object Limit Counting (ZScream Feature Parity)
4186// ============================================================================
4187
4188std::map<DungeonLimit, int> Room::GetLimitedObjectCounts() const {
4189 auto counts = CreateLimitCounter();
4190
4191 // Count sprites
4192 counts[DungeonLimit::kSprites] = static_cast<int>(sprites_.size());
4193
4194 // Count overlords (sprites with ID > 0x40 are overlords in ALTTP)
4195 for (const auto& sprite : sprites_) {
4196 if (sprite.IsOverlord()) {
4197 counts[DungeonLimit::Overlords]++;
4198 }
4199 }
4200
4201 // Count chests
4202 counts[DungeonLimit::kChests] = static_cast<int>(chests_in_room_.size());
4203
4204 // Count doors (total and special)
4205 counts[DungeonLimit::kDoors] = static_cast<int>(doors_.size());
4206 for (const auto& door : doors_) {
4207 // Special doors: shutters and key-locked doors.
4208 const bool is_special = [&]() -> bool {
4209 switch (door.type) {
4224 return true;
4225 default:
4226 return false;
4227 }
4228 }();
4229 if (is_special) {
4231 }
4232 }
4233
4234 // Count stairs
4236 static_cast<int>(z3_staircases_.size());
4237
4238 // Count objects with specific options
4239 for (const auto& obj : tile_objects_) {
4240 auto options = obj.options();
4241
4242 // Count blocks
4243 if ((options & ObjectOption::Block) != ObjectOption::Nothing) {
4244 counts[DungeonLimit::Blocks]++;
4245 }
4246
4247 // Count torches
4248 if ((options & ObjectOption::Torch) != ObjectOption::Nothing) {
4249 counts[DungeonLimit::Torches]++;
4250 }
4251
4252 // Count star tiles (object IDs 0x11E and 0x11F)
4253 if (obj.id_ == 0x11E || obj.id_ == 0x11F) {
4254 counts[DungeonLimit::StarTiles]++;
4255 }
4256
4257 // Count somaria paths (object IDs in 0xF83-0xF8F range)
4258 if (obj.id_ >= 0xF83 && obj.id_ <= 0xF8F) {
4259 counts[DungeonLimit::SomariaLine]++;
4260 }
4261
4262 // Count staircase objects based on direction
4263 if ((options & ObjectOption::Stairs) != ObjectOption::Nothing) {
4264 // North-facing stairs: IDs 0x130-0x135
4265 if ((obj.id_ >= 0x130 && obj.id_ <= 0x135) || obj.id_ == 0x139 ||
4266 obj.id_ == 0x13A || obj.id_ == 0x13B) {
4267 counts[DungeonLimit::StairsNorth]++;
4268 }
4269 // South-facing stairs: IDs 0x13B-0x13D
4270 else if (obj.id_ >= 0x13C && obj.id_ <= 0x13F) {
4271 counts[DungeonLimit::StairsSouth]++;
4272 }
4273 }
4274
4275 // Count general manipulable objects
4276 if ((options & ObjectOption::Block) != ObjectOption::Nothing ||
4280 }
4281 }
4282
4283 return counts;
4284}
4285
4287 auto counts = GetLimitedObjectCounts();
4288 return yaze::zelda3::HasExceededLimits(counts);
4289}
4290
4291std::vector<DungeonLimitInfo> Room::GetExceededLimitDetails() const {
4292 auto counts = GetLimitedObjectCounts();
4293 return GetExceededLimits(counts);
4294}
4295
4296} // namespace zelda3
4297} // 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:586
const auto & vector() const
Definition rom.h:155
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
Definition rom.cc:658
absl::StatusOr< uint16_t > ReadWord(int offset) const
Definition rom.cc:526
auto data() const
Definition rom.h:151
auto size() const
Definition rom.h:150
bool is_loaded() const
Definition rom.h:144
absl::Status WriteWord(int addr, uint16_t value)
Definition rom.cc:605
absl::Status WriteLong(uint32_t addr, uint32_t value)
Definition rom.cc:632
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
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 SetBG1RevealMaskSource(gfx::BG1RevealMaskSource source)
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_room_event_indices=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)
void SetCurrentRenderPalette(const std::vector< SDL_Color > &palette)
absl::Status SaveToRom(Rom *rom) const
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 set_block_behavior_layer(uint8_t layer)
void SetRom(Rom *rom)
Definition room_object.h:80
static constexpr int kBlockLoadOrderNew
void set_block_load_order(int order)
void set_options(ObjectOption options)
bool ValidateObject(const RoomObject &object) const
Definition room.cc:2435
SaveDirtyState save_dirty_state_
Definition room.h:1036
destination pits_
Definition room.h:1103
std::vector< RoomObject > tile_objects_
Definition room.h:1087
uint8_t cached_blockset_
Definition room.h:1051
EffectKey effect_
Definition room.h:1098
gfx::BackgroundBuffer object_bg1_buffer_
Definition room.h:1005
uint8_t palette_
Definition room.h:1072
void SetTag2Direct(TagKey tag2)
Definition room.h:804
bool HasExceededLimits() const
Check if any object limits are exceeded.
Definition room.cc:4286
void ClearObjectStreamHeaderDirty()
Definition room.h:447
uint8_t render_entrance_blockset_
Definition room.h:1069
uint8_t cached_layout_
Definition room.h:1054
const CustomCollisionMap & custom_collision() const
Definition room.h:497
absl::Status UpdateObject(size_t index, const RoomObject &object)
Definition room.cc:2408
void ClearSaveDirtyState()
Definition room.h:598
TagKey cached_tag2_
Definition room.h:1059
void MarkLayoutDirty()
Definition room.h:455
void SetStair4Target(uint8_t target)
Definition room.h:864
void SetPitsTarget(uint8_t target)
Definition room.h:840
void SetIsLight(bool is_light)
Definition room.h:669
void LoadChests()
Definition room.cc:2554
void EnsureSpritesLoaded()
Definition room.cc:828
void MarkObjectsDirty()
Definition room.h:409
gfx::BackgroundBuffer bg2_buffer_
Definition room.h:1004
uint8_t resolved_main_blockset_
Definition room.h:1070
const std::vector< chest_data > & GetChests() const
Definition room.h:261
uint8_t cached_floor2_graphics_
Definition room.h:1056
std::vector< zelda3::Sprite > sprites_
Definition room.h:1089
CustomCollisionMap custom_collision_
Definition room.h:1109
GameData * game_data_
Definition room.h:997
void MarkSaveDirtyForTileObject(const RoomObject &object)
Definition room.h:418
void SetLoaded(bool loaded)
Definition room.h:873
void ClearObjectStreamDirty()
Definition room.h:443
void ClearCustomCollisionDirty()
Definition room.h:524
void CopyRoomGraphicsToBuffer()
Definition room.cc:877
bool custom_collision_dirty() const
Definition room.h:523
uint8_t cached_palette_
Definition room.h:1053
void ClearHeaderDirty()
Definition room.h:587
zelda3_version_pointers version_constants() const
Definition room.h:967
uint8_t cached_effect_
Definition room.h:1057
std::vector< Door > doors_
Definition room.h:1092
void SetStaircaseRoom(int index, uint8_t room)
Definition room.h:737
static constexpr uint8_t kObjectHeaderFloor1Dirty
Definition room.h:1027
absl::Status RemoveObject(size_t index)
Definition room.cc:2395
void SetStair1TargetLayer(uint8_t layer)
Definition room.h:816
void MarkGraphicsDirty()
Definition room.h:450
void LoadBlocks()
Definition room.cc:4004
void SetLayer2Mode(uint8_t mode)
Definition room.h:762
RoomLayout layout_
Definition room.h:1094
uint8_t layer2_mode_
Definition room.h:1082
void LoadLayoutTilesToBuffer()
Definition room.cc:1245
uint8_t staircase_room(int index) const
Definition room.h:896
bool sprites_loaded_
Definition room.h:1041
void SetTag2(TagKey tag2)
Definition room.h:718
std::vector< DungeonLimitInfo > GetExceededLimitDetails() const
Get list of exceeded limits with details.
Definition room.cc:4291
void ParseObjectsFromLocation(int objects_location)
Definition room.cc:1692
uint8_t cached_floor1_graphics_
Definition room.h:1055
bool custom_collision_dirty_
Definition room.h:1110
static constexpr uint8_t kObjectHeaderFloor2Dirty
Definition room.h:1028
void ReloadGraphics(std::optional< uint8_t > entrance_blockset=std::nullopt)
Definition room.cc:842
bool pot_items_dirty() const
Definition room.h:369
void SetTag1Direct(TagKey tag1)
Definition room.h:798
void LoadTorches()
Definition room.cc:2614
void SetHolewarp(uint8_t hw)
Definition room.h:731
TagKey tag2() const
Definition room.h:890
void SetStair2Target(uint8_t target)
Definition room.h:852
bool object_stream_dirty() const
Definition room.h:442
bool sprites_dirty() const
Definition room.h:256
void SetCollision(CollisionKey collision)
Definition room.h:663
bool object_stream_header_dirty() const
Definition room.h:444
gfx::BackgroundBuffer bg1_buffer_
Definition room.h:1003
bool torches_loaded_
Definition room.h:1044
uint8_t palette() const
Definition room.h:906
void SetStaircasePlane(int index, uint8_t plane)
Definition room.h:725
absl::Status SaveObjects(const DungeonStreamLayout *layout=nullptr)
Definition room.cc:2023
void SetIsDark(bool is_dark)
Definition room.h:774
bool objects_loaded_
Definition room.h:1040
auto rom() const
Definition room.h:959
std::map< DungeonLimit, int > GetLimitedObjectCounts() const
Count limited objects in this room.
Definition room.cc:4188
void RenderRoomGraphics()
Definition room.cc:990
absl::Status SaveRoomHeader()
Definition room.cc:2290
gfx::Bitmap composite_bitmap_
Definition room.h:1032
Room & operator=(Room &&)
bool chests_dirty() const
Definition room.h:263
uint64_t composite_signature_
Definition room.h:1033
uint16_t message_id_
Definition room.h:1075
TagKey tag1() const
Definition room.h:889
CollisionKey collision() const
Definition room.h:891
void LoadRoomGraphics(std::optional< uint8_t > entrance_blockset=std::nullopt)
Definition room.cc:749
uint8_t staircase_rooms_[4]
Definition room.h:1065
gfx::Bitmap & GetCompositeBitmap(RoomLayerManager &layer_mgr)
Get a composite bitmap of all layers merged.
Definition room.cc:977
std::vector< uint8_t > EncodeObjects() const
Definition room.cc:1798
void SetEffect(EffectKey effect)
Definition room.h:704
absl::Status SaveObjectStreamHeader(const DungeonStreamLayout *layout=nullptr)
Definition room.cc:2119
gfx::BackgroundBuffer object_bg2_buffer_
Definition room.h:1006
void ClearWaterFillDirty()
Definition room.h:582
uint8_t holewarp_
Definition room.h:1074
uint8_t layout_id_
Definition room.h:1073
std::array< uint8_t, 16 > blocks_
Definition room.h:1084
void SetTag1(TagKey tag1)
Definition room.h:711
void SetBackgroundTileset(uint8_t tileset)
Definition room.h:780
void SetStair3TargetLayer(uint8_t layer)
Definition room.h:828
void SetRenderEntranceBlockset(uint8_t entrance_blockset)
Definition room.h:697
uint8_t floor2_graphics_
Definition room.h:1081
absl::Status SaveSprites(const DungeonStreamLayout *layout=nullptr)
Definition room.cc:2217
void PrepareForRender(std::optional< uint8_t > entrance_blockset=std::nullopt)
Definition room.cc:853
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:383
bool IsLight() const
Definition room.h:753
uint8_t floor1_graphics_
Definition room.h:1080
void SetLayerMerging(LayerMergeType merging)
Definition room.h:768
void SetPitsTargetLayer(uint8_t layer)
Definition room.h:810
void LoadObjects()
Definition room.cc:1626
void LoadPotItems()
Definition room.cc:4100
void ClearSpritesDirty()
Definition room.h:258
bool blocks_dirty() const
Definition room.h:376
uint8_t blockset_
Definition room.h:1068
EffectKey effect() const
Definition room.h:888
void SetSpriteTileset(uint8_t tileset)
Definition room.h:786
void SetStair1Target(uint8_t target)
Definition room.h:846
bool pot_items_loaded_
Definition room.h:1043
void SetBg2(background2 bg2)
Definition room.h:657
static constexpr uint8_t kObjectHeaderLayoutDirty
Definition room.h:1029
bool torches_dirty() const
Definition room.h:373
void EnsureObjectsLoaded()
Definition room.cc:821
void SetSpriteset(uint8_t ss)
Definition room.h:690
int ResolveDungeonPaletteId() const
Definition room.cc:727
std::unique_ptr< DungeonState > dungeon_state_
Definition room.h:1115
void LoadAnimatedGraphics()
Definition room.cc:1556
std::vector< uint8_t > EncodeSprites() const
Definition room.cc:1886
std::vector< chest_data > chests_in_room_
Definition room.h:1091
void SetBlockset(uint8_t bs)
Definition room.h:682
uint8_t spriteset_
Definition room.h:1071
LayerMergeType layer_merging_
Definition room.h:1096
background2 bg2() const
Definition room.h:887
bool chests_loaded_
Definition room.h:1042
void EnsurePotItemsLoaded()
Definition room.cc:835
uint8_t staircase_plane(int index) const
Definition room.h:893
bool has_composite_signature_
Definition room.h:1034
bool AreTorchesLoaded() const
Definition room.h:878
uint8_t cached_spriteset_
Definition room.h:1052
std::array< uint8_t, 0x10000 > current_gfx16_
Definition room.h:999
std::vector< staircase > z3_staircases_
Definition room.h:1090
std::vector< PotItem > pot_items_
Definition room.h:1093
bool blocks_loaded_
Definition room.h:1045
void LoadSprites()
Definition room.cc:2494
bool AreBlocksLoaded() const
Definition room.h:884
TagKey cached_tag1_
Definition room.h:1058
uint8_t background_tileset_
Definition room.h:1077
void SetStair3Target(uint8_t target)
Definition room.h:858
DirtyState dirty_state_
Definition room.h:1035
void HandleSpecialObjects(short oid, uint8_t posX, uint8_t posY, int &nbr_of_staircase)
Definition room.cc:2457
void SetStair4TargetLayer(uint8_t layer)
Definition room.h:834
absl::Status AddObject(const RoomObject &object)
Definition room.cc:2381
absl::StatusOr< size_t > FindObjectAt(int x, int y, int layer) const
Definition room.cc:2425
void SetPalette(uint8_t pal)
Definition room.h:675
bool has_custom_collision() const
Definition room.h:501
const std::vector< PotItem > & GetPotItems() const
Definition room.h:367
void SetStair2TargetLayer(uint8_t layer)
Definition room.h:822
void RenderObjectsToBackground()
Definition room.cc:1310
void SetLayer2Behavior(uint8_t behavior)
Definition room.h:792
void SetMessageId(uint16_t mid)
Definition room.h:745
int id() const
Definition room.h:900
A class for managing sprites in the overworld and underworld.
Definition sprite.h:37
auto id() const
Definition sprite.h:99
auto layer() const
Definition sprite.h:110
auto set_key_drop(int key)
Definition sprite.h:118
auto subtype() const
Definition sprite.h:111
auto y() const
Definition sprite.h:102
auto x() const
Definition sprite.h:101
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
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
SDL_Palette * GetSurfacePalette(SDL_Surface *surface)
Get the palette attached to a surface.
Definition sdl_compat.h:392
absl::Status GetObjectPointerTablePc(const std::vector< uint8_t > &rom_data, int *table_pc)
Definition room.cc:270
void AppendChestRecord(std::vector< uint8_t > *bytes, uint16_t word, uint8_t item)
Definition room.cc:3504
absl::Status ValidateSpecialObjectDrawLayerSelector(const RoomObject &object, int room_id, const char *object_type)
Definition room.cc:2782
int ReadRoomPotItemAddressPc(const std::vector< uint8_t > &rom_data, int room_id)
Definition room.cc:3712
absl::Status RelocateDungeonStream(Rom *rom, int room_id, DungeonStreamKind expected_kind, const DungeonStreamLayout &layout, std::vector< uint8_t > encoded_stream)
Definition room.cc:424
void SortAndCoalesceChestRuns(std::vector< ChestWriteRun > *writes)
Definition room.cc:3552
bool RoomUsesTrackCornerAliases(const std::vector< RoomObject > &objects)
Definition room.cc:45
const LayerMergeType & LayerMergeFromHeaderByte(uint8_t byte0)
Definition room.cc:58
void AppendChangedChestRuns(uint32_t pc, absl::Span< const uint8_t > expected, absl::Span< const uint8_t > replacement, std::vector< ChestWriteRun > *writes)
Definition room.cc:3518
std::vector< TorchSegment > ParseRomTorchSegments(const std::vector< uint8_t > &rom_data, int bytes_count)
Definition room.cc:2709
absl::Status GetSpritePointerTablePc(const std::vector< uint8_t > &rom_data, int *table_pc)
Definition room.cc:337
absl::StatusOr< bool > DungeonStreamRequiresCopyOnWrite(const Rom &rom, int room_id, DungeonStreamKind expected_kind, const DungeonStreamLayout &layout, size_t replacement_size)
Definition room.cc:443
int ReadRoomObjectAddressPc(const std::vector< uint8_t > &rom_data, int table_pc, int room_id)
Definition room.cc:307
int ReadRoomSpriteAddressPc(const std::vector< uint8_t > &rom_data, int table_pc, int room_id)
Definition room.cc:358
absl::Status PreflightBlocksLoaderDestinations(const std::vector< uint8_t > &rom_data, std::array< int, 4 > *destination_pcs)
Definition room.cc:3001
absl::StatusOr< PhysicalStreamInfo > GetObjectStreamInfo(const std::vector< uint8_t > &rom_data, int room_id)
Definition room.cc:317
std::vector< uint8_t > EncodeChestRoomState(int room_id, const Room &room)
Definition room.cc:3574
absl::StatusOr< PhysicalStreamInfo > GetSpriteStreamInfo(const std::vector< uint8_t > &rom_data, int room_id)
Definition room.cc:380
PhysicalStreamInfo AnalyzePhysicalStream(const std::vector< int > &room_addresses, int room_id, int known_region_end=-1)
Definition room.cc:222
std::vector< uint8_t > EncodeTorchSegmentForRoom(int room_id, const Room &room)
Definition room.cc:2755
bool HalfOpenRangesOverlap(int first_begin, int first_end, int second_begin, int second_end)
Definition room.cc:2970
int MeasureSpriteStreamSize(const std::vector< uint8_t > &rom_data, int sprite_address, int hard_end)
Definition room.cc:401
void PopulateDungeonRenderPaletteRows(const gfx::SnesPalette &dungeon_palette, const gfx::SnesPalette *hud_palette, WriteColor write_color)
Definition room.cc:72
absl::Status ValidateBlocksLoaderPointerOperand(const std::vector< uint8_t > &rom_data, int operand_pc)
Definition room.cc:2975
bool IsDarkRoomHeaderByte(uint8_t byte0)
Definition room.cc:54
absl::StatusOr< ChestSavePlan > BuildChestSavePlanImpl(const Rom *rom, int room_count, const std::function< const Room *(int)> &room_lookup)
Definition room.cc:3583
void AppendEditedChestRecord(std::vector< uint8_t > *bytes, int room_id, const chest_data &chest)
Definition room.cc:3511
constexpr std::array< int, 4 > kBlocksPointerSlots
Definition room.cc:2967
std::vector< PhysicalChestRecord > ParsePhysicalRomChests(const std::vector< uint8_t > &rom_data, int cpos, int byte_length)
Definition room.cc:3486
background2 Background2FromHeaderByte(uint8_t byte0)
Definition room.cc:64
uint32_t ReadRoomObjectAddressSnes(const std::vector< uint8_t > &rom_data, int table_pc, int room_id)
Definition room.cc:293
uint8_t Layer2ModeFromHeaderByte(uint8_t byte0)
Definition room.cc:50
absl::StatusOr< PhysicalStreamInfo > GetPotItemStreamInfo(const std::vector< uint8_t > &rom_data, int room_id)
Definition room.cc:3732
absl::Status ValidateLightableTorchForSave(const RoomObject &object, int room_id)
Definition room.cc:2796
constexpr int kBlocksPointer4
constexpr int kSpritesDataEndExclusive
absl::Status SaveAllChests(Rom *rom, absl::Span< const Room > rooms)
Definition room.cc:3855
constexpr int kDoorPointers
constexpr int kGfxBufferAnimatedFrameStride
Definition room.cc:871
const std::string RoomTag[65]
Definition room.cc:145
absl::Status WriteTrackCollision(Rom *rom, int room_id, const CustomCollisionMap &map)
constexpr int kGfxBufferAnimatedFrameOffset
Definition room.cc:870
@ 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.
PushableBlockBytes EncodePushableBlockEntry(const PushableBlockEntry &entry)
constexpr int kTorchesLengthPointer
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
Definition room.cc:546
constexpr int kCustomCollisionDataSoftEnd
std::vector< DungeonLimitInfo > GetExceededLimits(const std::map< DungeonLimit, int > &counts)
absl::StatusOr< ChestSavePlan > BuildChestSavePlan(const Rom *rom, int room_count, const std::function< const Room *(int)> &room_lookup)
Definition room.cc:3758
constexpr int kChestsLengthPointer
int FindMaxUsedSpriteAddress(Rom *rom)
Definition room.cc:1924
constexpr int kMessagesIdDungeon
absl::Status RelocateSpriteData(Rom *rom, int room_id, const std::vector< uint8_t > &encoded_bytes)
Definition room.cc:1964
constexpr int kGfxBufferRoomOffset
Definition room.cc:872
RoomObject::LayerType MapRoomObjectListIndexToDrawLayer(uint8_t list_index)
absl::Status SaveAllPotItems(Rom *rom, absl::Span< const Room > rooms)
Definition room.cc:3980
constexpr int kGfxBufferRoomSpriteOffset
Definition room.cc:873
RoomSize CalculateRoomSize(Rom *rom, int room_id)
Definition room.cc:496
absl::Status SaveAllPotItemsImpl(Rom *rom, int room_count, RoomLookup &&room_lookup, const DungeonStreamLayout *repack_layout=nullptr)
Definition room.cc:3866
constexpr int kPitPointer
constexpr int kDungeonPaletteBytes
Definition game_data.h:46
constexpr int kSpritesData
void LoadDungeonRenderPaletteToCgram(std::span< uint16_t > cgram, const gfx::SnesPalette &dungeon_palette, const gfx::SnesPalette *hud_palette)
Definition room.cc:121
LightableTorchEntry DecodeLightableTorchEntry(const LightableTorchBytes &bytes)
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > GetChestTableWriteRanges(const Rom *rom)
Definition room.cc:3434
constexpr int kRoomItemsDataEnd
absl::Status SaveAllTorches(Rom *rom, absl::Span< const Room > rooms)
Definition room.cc:2911
constexpr int kTileAddress
constexpr int kPitCount
LightableTorchBytes EncodeLightableTorchEntry(const LightableTorchEntry &entry)
std::vector< SDL_Color > BuildDungeonRenderPalette(const gfx::SnesPalette &dungeon_palette, const gfx::SnesPalette *hud_palette)
Definition room.cc:102
absl::StatusOr< DungeonStreamWritePlan > PlanDungeonStreamRepack(const DungeonStreamInventory &inventory, const std::vector< DungeonStreamReplacement > &requested_replacements)
constexpr int kRoomsSpritePointer
constexpr int kTileAddressFloor
constexpr int GetDungeonObjectDataRegionEnd(int pc_address)
absl::Status SaveAllPits(Rom *rom)
Definition room.cc:2925
constexpr int kChestsDataPointer1
constexpr int kBlocksLength
absl::Status SaveAllBlocks(Rom *rom)
Definition room.cc:3069
constexpr int kBlocksPointer1
constexpr int kChestTableCapacityRecords
constexpr int kRoomItemsPointers
constexpr int kGfxBufferRoomSpriteStride
Definition room.cc:874
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
constexpr int kChestTableRecordSize
constexpr bool HasCustomCollisionPointerTable(std::size_t rom_size)
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > GetDirtyChestWriteRanges(const Rom *rom, int room_count, const std::function< const Room *(int)> &room_lookup)
Definition room.cc:3765
absl::Status SaveAllCollisionImpl(Rom *rom, int room_count, RoomLookup &&room_lookup)
Definition room.cc:3326
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:521
constexpr int kCustomCollisionDataPosition
absl::Status ApplyDungeonStreamWritePlan(Rom *rom, const DungeonStreamWritePlan &plan)
bool HasExceededLimits(const std::map< DungeonLimit, int > &counts)
constexpr uint32_t kDungeonPalettePointerTable
Definition game_data.h:45
bool UsesRoomObjectStream(const RoomObject &object)
constexpr uint16_t kStairsObjects[]
constexpr int kChestTableCapacityBytes
absl::Status SaveAllChestsImpl(Rom *rom, int room_count, RoomLookup &&room_lookup)
Definition room.cc:3846
absl::StatusOr< DungeonStreamWritePlan > PlanDungeonStreamWrites(const DungeonStreamInventory &inventory, const std::vector< DungeonStreamReplacement > &requested_replacements)
absl::StatusOr< DungeonStreamInventory > InventoryDungeonStreams(const Rom &rom, const DungeonStreamLayout &requested_layout)
absl::Status SaveAllTorchesImpl(Rom *rom, int room_count, RoomLookup &&room_lookup)
Definition room.cc:2812
constexpr int kNumberOfRooms
absl::Status ApplyChestSavePlan(Rom *rom, const ChestSavePlan &plan, const std::function< const Room *(int)> &room_lookup)
Definition room.cc:3772
const std::string RoomEffect[8]
Definition room.cc:135
constexpr int kRoomHeaderPointer
constexpr bool HasCustomCollisionDataRegion(std::size_t rom_size)
constexpr int kBlocksPointer3
constexpr int kRoomHeaderPointerBank
absl::Status ValidateRoomObjectStreamEntryForSave(const RoomObject &object)
constexpr int kCustomCollisionRoomPointers
constexpr int kGfxBufferStride
Definition room.cc:869
std::map< DungeonLimit, int > CreateLimitCounter()
constexpr int kTorchData
absl::Status SaveAllCollision(Rom *rom, absl::Span< Room > rooms)
Definition room.cc:3422
constexpr int kGfxBufferRoomSpriteLastLineOffset
Definition room.cc:875
PushableBlockEntry DecodePushableBlockEntry(const PushableBlockBytes &bytes)
constexpr int kBlocksPointer2
constexpr int kRoomObjectPointer
constexpr int kGfxBufferOffset
Definition room.cc:868
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
Treasure chest.
Definition zelda.h:425
Room transition destination.
Definition zelda.h:448
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::vector< uint8_t > original_capacity_bytes
Definition room.h:1212
std::vector< std::pair< uint32_t, uint32_t > > write_ranges() const
Definition room.h:1218
uint16_t original_byte_length
Definition room.h:1211
std::vector< ChestWriteRun > writes
Definition room.h:1213
std::array< uint8_t, 3 > pointer_operand
Definition room.h:1210
std::vector< ChestDirtyRoomState > dirty_rooms
Definition room.h:1214
std::vector< uint8_t > expected_bytes
Definition room.h:1185
std::vector< uint8_t > replacement_bytes
Definition room.h:1186
std::array< uint8_t, 64 *64 > tiles
std::vector< DungeonStreamAliasGroup > aliases
std::vector< DungeonStreamIssue > issues
std::vector< DungeonStreamOverlap > overlaps
std::vector< DungeonStreamRecord > streams
std::vector< DungeonStreamPcRange > data_ranges
std::array< std::array< uint8_t, 4 >, kNumSpritesets > spriteset_ids
Definition game_data.h:96
std::array< std::array< uint8_t, 4 >, kNumRoomBlocksets > room_blockset_ids
Definition game_data.h:95
std::array< std::array< uint8_t, 4 >, kNumPalettesets > paletteset_ids
Definition game_data.h:102
gfx::PaletteGroupMap palette_groups
Definition game_data.h:92
std::array< std::array< uint8_t, 8 >, kNumMainBlocksets > main_blockset_ids
Definition game_data.h:94
std::vector< uint8_t > graphics_buffer
Definition game_data.h:84
uint16_t position
Definition room.h:109
static Door FromRomBytes(uint8_t b1, uint8_t b2)
Definition room.h:335
Public YAZE API umbrella header.