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 marked draws_to_both_bgs explicitly write both tilemaps.
1359 // Object-specific stair routing is handled inside the registered routines.
1360 // The room object stream is split here as primary -> BG2 overlay -> BG1
1361 // overlay, while the layout pass is rendered separately by RoomLayout::Draw.
1362
1363 // Clear object buffers before rendering
1364 // IMPORTANT: Fill with 255 (transparent color key) so objects overlay correctly
1365 // on the floor. We use index 255 as transparent since palette has 90 colors (0-89).
1368 object_bg1_buffer_.bitmap().Fill(255);
1369 object_bg2_buffer_.bitmap().Fill(255);
1370
1371 // IMPORTANT: Clear priority buffers when clearing object buffers
1372 // Otherwise, old priority values persist and cause incorrect Z-ordering
1375
1376 // IMPORTANT: Clear coverage buffers when clearing object buffers.
1377 // Coverage distinguishes "no draw" vs "drew transparent", so stale values
1378 // can cause objects to incorrectly clear the layout.
1381
1382 // Room-object masks target both raw BG1 stacks. Clear only their source bit
1383 // so layout-owned reveals survive an object-only rerender.
1386
1387 // Log stream distribution for this room.
1388 // USDASM order is: main list -> BG2 overlay list -> BG1 overlay list.
1389 int layer0_count = 0, layer1_count = 0, layer2_count = 0;
1390 for (const auto& obj : tile_objects_) {
1391 switch (obj.GetLayerValue()) {
1392 case 0:
1393 layer0_count++;
1394 break;
1395 case 1:
1396 layer1_count++;
1397 break;
1398 case 2:
1399 layer2_count++;
1400 break;
1401 }
1402 }
1403 LOG_DEBUG(
1404 "Room",
1405 "Room %03X Object Stream Summary: Main=%d, BG2Overlay=%d, BG1Overlay=%d",
1406 room_id_, layer0_count, layer1_count, layer2_count);
1407
1408 // Render room-object streams in USDASM order.
1409 // - List index 0: primary object list -> BG1 object buffer (upper tilemap)
1410 // - List index 1: BG2 overlay list -> BG2 object buffer
1411 // - List index 2: BG1 overlay list -> BG1 object buffer (BG3 enum; same draw
1412 // path as BG1 in ObjectDrawer for non-BothBG objects)
1413 // `tile_objects_[].layer_` holds the list index (0/1/2) for save/load, not
1414 // the buffer name. Map with MapRoomObjectListIndexToDrawLayer before drawing.
1415 // BothBG routines still fan out to both buffers via DrawRoutineRegistry.
1416 // Pass bg1_buffer_ as the second raw BG1 target. BG2 room objects record
1417 // deferred reveal bits on both layout and object targets without mutating
1418 // either bitmap.
1419 //
1420 // Three DrawObjectList passes match USDASM list order; the shared chest/
1421 // big-key-lock event index continues across passes (reset only on the first
1422 // non-empty pass).
1423 std::vector<std::vector<RoomObject>> by_list(3);
1424 for (const auto& obj : tile_objects_) {
1425 // Torches and pushable blocks are NOT part of the room object stream.
1426 // They come from the global tables and are drawn after the stream in
1427 // USDASM (LoadAndBuildRoom $01:873A). Draw them in a dedicated pass.
1428 if ((obj.options() & ObjectOption::Torch) != ObjectOption::Nothing) {
1429 continue;
1430 }
1431 if ((obj.options() & ObjectOption::Block) != ObjectOption::Nothing) {
1432 continue;
1433 }
1434
1435 uint8_t list_index = obj.GetLayerValue();
1436 if (list_index > 2) {
1437 list_index = 2;
1438 }
1439 RoomObject render_obj = obj;
1440 render_obj.layer_ = MapRoomObjectListIndexToDrawLayer(list_index);
1441 by_list[list_index].push_back(std::move(render_obj));
1442 }
1443
1444 absl::Status status = absl::OkStatus();
1445 bool reset_room_events_for_next_chunk = true;
1446 for (int pass = 0; pass < 3; ++pass) {
1447 if (by_list[pass].empty()) {
1448 continue;
1449 }
1450 auto chunk_status = drawer.DrawObjectList(
1451 by_list[pass], object_bg1_buffer_, object_bg2_buffer_, palette_group,
1452 dungeon_state_.get(), &bg1_buffer_, reset_room_events_for_next_chunk);
1453 reset_room_events_for_next_chunk = false;
1454 if (!chunk_status.ok() && status.ok()) {
1455 status = chunk_status;
1456 }
1457 }
1458
1459 // Render doors using DoorDef struct with enum types
1460 // Doors are drawn to the OBJECT buffer for layer visibility control
1461 // This allows doors to remain visible when toggling BG1_Layout off
1462 for (int i = 0; i < static_cast<int>(doors_.size()); ++i) {
1463 const auto& door = doors_[i];
1464 ObjectDrawer::DoorDef door_def;
1465 door_def.type = door.type;
1466 door_def.direction = door.direction;
1467 door_def.position = door.position;
1468 // Draw doors to object buffers (not layout buffers) so they remain visible
1469 // when BG1_Layout is hidden. Doors are objects, not layout tiles.
1470 drawer.DrawDoor(door_def, i, object_bg1_buffer_, object_bg2_buffer_,
1471 dungeon_state_.get());
1472 }
1473 // Mark object buffer as modified so texture gets updated
1474 if (!doors_.empty()) {
1475 object_bg1_buffer_.bitmap().set_modified(true);
1476 }
1477
1478 // Render pot items
1479 // Pot items now have their own position from ROM data
1480 // No need to match to objects - each item has exact coordinates
1481 for (const auto& pot_item : pot_items_) {
1482 if (pot_item.item != 0) { // Skip "Nothing" items
1483 // PotItem provides pixel coordinates, convert to tile coords
1484 int tile_x = pot_item.GetTileX();
1485 int tile_y = pot_item.GetTileY();
1486 drawer.DrawPotItem(pot_item.item, tile_x, tile_y, object_bg1_buffer_);
1487 }
1488 }
1489
1490 // Render sprites (for key drops)
1491 // We don't have full sprite rendering yet, but we can visualize key drops
1492 for (const auto& sprite : sprites_) {
1493 if (sprite.key_drop() > 0) {
1494 // Draw key drop visualization
1495 // Use a special item ID or just draw a key icon
1496 // We can reuse DrawPotItem with a special ID for key
1497 // Or add DrawKeyDrop to ObjectDrawer
1498 // For now, let's use DrawPotItem with ID 0xFD (Small Key) or 0xFE (Big Key)
1499 uint8_t key_item = (sprite.key_drop() == 1) ? 0xFD : 0xFE;
1500 drawer.DrawPotItem(key_item, sprite.x(), sprite.y(), object_bg1_buffer_);
1501 }
1502 }
1503
1504 // Special tables pass (USDASM-aligned):
1505 // - Pushable blocks: bank_01.asm RoomDraw_PushableBlock uses RoomDrawObjectData
1506 // offset $0E52 (bank_00.asm #obj0E52).
1507 // - Lightable torches: bank_01.asm RoomDraw_LightableTorch chooses between
1508 // offsets $0EC2 (unlit) and $0ECA (lit) (bank_00.asm #obj0EC2/#obj0ECA).
1509 constexpr uint16_t kRoomDrawObj_PushableBlock = 0x0E52;
1510 constexpr uint16_t kRoomDrawObj_TorchUnlit = 0x0EC2;
1511 constexpr uint16_t kRoomDrawObj_TorchLit = 0x0ECA;
1512 for (const auto& obj : tile_objects_) {
1513 if ((obj.options() & ObjectOption::Block) != ObjectOption::Nothing) {
1514 // SpecialUnderworldObjects bit 13 chooses the draw tilemap. Bit 14 is an
1515 // independent behavior/pit selector retained in block metadata and must
1516 // not affect rendering.
1517 (void)drawer.DrawRoomDrawObjectData2x2(
1518 static_cast<uint16_t>(obj.id_), obj.x_, obj.y_, obj.layer_,
1519 kRoomDrawObj_PushableBlock, object_bg1_buffer_, object_bg2_buffer_);
1520 continue;
1521 }
1522 if ((obj.options() & ObjectOption::Torch) != ObjectOption::Nothing) {
1523 const uint16_t off =
1524 obj.lit_ ? kRoomDrawObj_TorchLit : kRoomDrawObj_TorchUnlit;
1525 // RoomDraw_LightableTorch retains bit 13 in its masked tilemap offset,
1526 // so the stored draw layer selects upper/BG1 or lower/BG2. Reserved bit
1527 // 14 and the lit bit do not affect the draw target.
1528 (void)drawer.DrawRoomDrawObjectData2x2(
1529 static_cast<uint16_t>(obj.id_), obj.x_, obj.y_, obj.layer_, off,
1531 continue;
1532 }
1533 }
1534
1535 if (!status.ok()) {
1536 LOG_WARN(
1537 "[RenderObjectsToBackground]",
1538 "Room %03X: ObjectDrawer failed: %s (objects left dirty for retry)",
1539 room_id_,
1540 std::string(status.message().data(), status.message().size()).c_str());
1541 // Do not scribble placeholder rectangles into layout buffers; fix the
1542 // underlying draw path or ROM state instead.
1543 dirty_state_.objects = true;
1544 } else {
1545 // Mark objects as clean after successful render
1546 dirty_state_.objects = false;
1547 LOG_DEBUG("[RenderObjectsToBackground]",
1548 "Room %d: Objects rendered successfully", room_id_);
1549 }
1550}
1551
1552// LoadGraphicsSheetsIntoArena() removed - using per-room graphics instead
1553// Room rendering no longer depends on Arena graphics sheets
1554
1556 if (!rom_ || !rom_->is_loaded()) {
1557 return;
1558 }
1559
1560 if (!game_data_) {
1561 return;
1562 }
1563 auto* gfx_buffer_data = &game_data_->graphics_buffer;
1564 if (gfx_buffer_data->empty()) {
1565 return;
1566 }
1567
1568 auto rom_data = rom()->vector();
1569 if (rom_data.empty()) {
1570 return;
1571 }
1572
1573 // Validate animated_frame_ bounds
1574 if (animated_frame_ < 0 || animated_frame_ > 10) {
1575 return;
1576 }
1577
1578 // Validate background_tileset_ bounds
1579 if (background_tileset_ < 0 || background_tileset_ > 255) {
1580 return;
1581 }
1582
1583 int gfx_ptr = SnesToPc(version_constants().kGfxAnimatedPointer);
1584 if (gfx_ptr < 0 || gfx_ptr >= static_cast<int>(rom_data.size())) {
1585 return;
1586 }
1587
1588 int data = 0;
1589 while (data < 1024) {
1590 // Validate buffer access for first operation
1591 // 92 * 4096 = 376832. 1024 * 10 = 10240. Total ~387KB.
1592 int first_offset = data + (92 * 4096) + (1024 * animated_frame_);
1593 if (first_offset >= 0 &&
1594 first_offset < static_cast<int>(gfx_buffer_data->size())) {
1595 uint8_t map_byte = (*gfx_buffer_data)[first_offset];
1596
1597 // Validate current_gfx16_ access
1598 int gfx_offset = data + (7 * 4096);
1599 if (gfx_offset >= 0 &&
1600 gfx_offset < static_cast<int>(current_gfx16_.size())) {
1601 current_gfx16_[gfx_offset] = map_byte;
1602 }
1603 }
1604
1605 // Validate buffer access for second operation
1606 int tileset_index = rom_data[gfx_ptr + background_tileset_];
1607 int second_offset =
1608 data + (tileset_index * 4096) + (1024 * animated_frame_);
1609 if (second_offset >= 0 &&
1610 second_offset < static_cast<int>(gfx_buffer_data->size())) {
1611 uint8_t map_byte = (*gfx_buffer_data)[second_offset];
1612
1613 // Validate current_gfx16_ access
1614 int gfx_offset = data + (7 * 4096) - 1024;
1615 if (gfx_offset >= 0 &&
1616 gfx_offset < static_cast<int>(current_gfx16_.size())) {
1617 current_gfx16_[gfx_offset] = map_byte;
1618 }
1619 }
1620
1621 data++;
1622 }
1623}
1624
1626 LOG_DEBUG("[LoadObjects]", "Starting LoadObjects for room %d", room_id_);
1627 auto rom_data = rom()->vector();
1628
1629 // Enhanced object loading with comprehensive validation
1630 int object_pointer = (rom_data[kRoomObjectPointer + 2] << 16) +
1631 (rom_data[kRoomObjectPointer + 1] << 8) +
1632 (rom_data[kRoomObjectPointer]);
1633 object_pointer = SnesToPc(object_pointer);
1634
1635 // Enhanced bounds checking for object pointer
1636 if (object_pointer < 0 || object_pointer >= (int)rom_->size()) {
1637 return;
1638 }
1639
1640 int room_address = object_pointer + (room_id_ * 3);
1641
1642 // Enhanced bounds checking for room address
1643 if (room_address < 0 || room_address + 2 >= (int)rom_->size()) {
1644 return;
1645 }
1646
1647 int tile_address = (rom_data[room_address + 2] << 16) +
1648 (rom_data[room_address + 1] << 8) + rom_data[room_address];
1649
1650 int objects_location = SnesToPc(tile_address);
1651
1652 // Enhanced bounds checking for objects location
1653 if (objects_location < 0 || objects_location >= (int)rom_->size()) {
1654 return;
1655 }
1656
1657 // Parse floor graphics and layout with validation
1658 if (objects_location + 1 < (int)rom_->size()) {
1659 if (is_floor_) {
1661 static_cast<uint8_t>(rom_data[objects_location] & 0x0F);
1663 static_cast<uint8_t>((rom_data[objects_location] >> 4) & 0x0F);
1664 LOG_DEBUG("[LoadObjects]",
1665 "Room %d: Set floor1_graphics_=%d, floor2_graphics_=%d",
1667 }
1668
1669 layout_id_ =
1670 static_cast<uint8_t>((rom_data[objects_location + 1] >> 2) & 0x07);
1671 }
1672
1673 LoadChests();
1674
1675 // Parse objects with enhanced error handling
1676 ParseObjectsFromLocation(objects_location + 2);
1677
1678 // Load custom collision map if present
1679 if (auto res = LoadCustomCollisionMap(rom_, room_id_); res.ok()) {
1680 custom_collision_ = std::move(res.value());
1681 }
1682
1683 // Freshly loaded from ROM; not dirty until the editor mutates it.
1685 objects_loaded_ = true;
1689}
1690
1691void Room::ParseObjectsFromLocation(int objects_location) {
1692 auto rom_data = rom()->vector();
1693
1694 // Clear existing objects before parsing to prevent accumulation on reload
1695 tile_objects_.clear();
1696 doors_.clear();
1697 z3_staircases_.clear();
1698 int nbr_of_staircase = 0;
1699
1700 int pos = objects_location;
1701 uint8_t b1 = 0;
1702 uint8_t b2 = 0;
1703 uint8_t b3 = 0;
1704 int layer = 0;
1705 bool door = false;
1706 bool end_read = false;
1707
1708 // Enhanced parsing loop with bounds checking
1709 // ASM: Main object loop logic (implicit in structure)
1710 while (!end_read && pos < (int)rom_->size()) {
1711 // Check if we have enough bytes to read
1712 if (pos + 1 >= (int)rom_->size()) {
1713 break;
1714 }
1715
1716 b1 = rom_data[pos];
1717 b2 = rom_data[pos + 1];
1718
1719 // ASM Marker: 0xFF 0xFF - End of object list (next list in USDASM order).
1720 // Stored in RoomObject::layer_ as list index for EncodeObjects():
1721 // 0 = primary list (drawn to BG1/upper object buffer by default)
1722 // 1 = BG2 overlay list
1723 // 2 = BG1 overlay list (ObjectDrawer uses BG3 enum; still BG1 object path)
1724 if (b1 == 0xFF && b2 == 0xFF) {
1725 pos += 2; // Jump to next layer
1726 layer++;
1727 LOG_DEBUG(
1728 "Room", "Room %03X: Object list transition to index %d (%s)",
1729 room_id_, layer,
1730 layer == 1 ? "BG2 overlay" : (layer == 2 ? "BG1 overlay" : "END"));
1731 door = false;
1732 if (layer == 3) {
1733 break;
1734 }
1735 continue;
1736 }
1737
1738 // ASM Marker: 0xF0 0xFF - Start of Door List
1739 // See RoomDraw_DoorObject ($018916) logic
1740 if (b1 == 0xF0 && b2 == 0xFF) {
1741 pos += 2; // Jump to door section
1742 door = true;
1743 continue;
1744 }
1745
1746 // Check if we have enough bytes for object data
1747 if (pos + 2 >= (int)rom_->size()) {
1748 break;
1749 }
1750
1751 b3 = rom_data[pos + 2];
1752 if (door) {
1753 pos += 2;
1754 } else {
1755 pos += 3;
1756 }
1757
1758 if (!door) {
1759 // ASM: RoomDraw_RoomObject ($01893C)
1760 // Handles Subtype 1, 2, 3 parsing based on byte values
1762 b1, b2, b3, static_cast<uint8_t>(layer));
1763
1764 LOG_DEBUG("Room", "Room %03X: Object 0x%03X at (%d,%d) stream=%d (%s)",
1765 room_id_, r.id_, r.x_, r.y_, layer,
1766 layer == 0 ? "Primary"
1767 : (layer == 1 ? "BG2 overlay" : "BG1 overlay"));
1768
1769 // Validate object ID before adding to the room
1770 // Object IDs can be up to 12-bit (0xFFF) to support Type 3 objects
1771 if (r.id_ >= 0 && r.id_ <= 0xFFF) {
1772 r.SetRom(rom_);
1773 tile_objects_.push_back(r);
1774
1775 // Handle special object types (staircases, chests, etc.)
1776 HandleSpecialObjects(r.id_, r.x(), r.y(), nbr_of_staircase);
1777 }
1778 } else {
1779 // Handle door objects
1780 // ASM format (from RoomDraw_DoorObject):
1781 // b1: bits 4-7 = position index, bits 0-1 = direction
1782 // b2: door type (full byte)
1783 auto door = Door::FromRomBytes(b1, b2);
1784 LOG_DEBUG("Room",
1785 "ParseDoor: room=%d b1=0x%02X b2=0x%02X pos=%d dir=%d type=%d",
1786 room_id_, b1, b2, door.position,
1787 static_cast<int>(door.direction), static_cast<int>(door.type));
1788 doors_.push_back(door);
1789 }
1790 }
1791}
1792
1793// ============================================================================
1794// Object Saving Implementation (Phase 1, Task 1.3)
1795// ============================================================================
1796
1797std::vector<uint8_t> Room::EncodeObjects() const {
1798 std::vector<uint8_t> bytes;
1799
1800 // Organize objects by ROM object-stream index (0=primary, 1=BG2 overlay,
1801 // 2=BG1 overlay), stored in RoomObject::layer_ / GetLayerValue().
1802 std::vector<RoomObject> layer0_objects;
1803 std::vector<RoomObject> layer1_objects;
1804 std::vector<RoomObject> layer2_objects;
1805
1806 // IMPORTANT: Torches and pushable blocks are stored in global per-dungeon
1807 // tables (see USDASM: LoadAndBuildRoom $01:873A). They are drawn after the
1808 // room object stream passes, so they must never be encoded into the room
1809 // object stream.
1810 for (const auto& obj : tile_objects_) {
1811 if ((obj.options() & ObjectOption::Torch) != ObjectOption::Nothing) {
1812 continue;
1813 }
1814 if ((obj.options() & ObjectOption::Block) != ObjectOption::Nothing) {
1815 continue;
1816 }
1817 switch (obj.GetLayerValue()) {
1818 case 0:
1819 layer0_objects.push_back(obj);
1820 break;
1821 case 1:
1822 layer1_objects.push_back(obj);
1823 break;
1824 case 2:
1825 layer2_objects.push_back(obj);
1826 break;
1827 }
1828 }
1829
1830 // Object stream format (USDASM bank_01.asm LoadAndBuildRoom / RoomDraw_DrawAllObjects):
1831 // - List index 0 (primary) terminated by $FFFF
1832 // - List index 1 (BG2 overlay) terminated by $FFFF
1833 // - List index 2 (BG1 overlay) ends with door marker $FFF0 (bytes F0 FF), then
1834 // 2-byte door entries, and finally $FFFF which terminates both the door
1835 // list and the third object list.
1836 //
1837 // NOTE: We always emit the door marker and a terminator, even if there are
1838 // zero doors, because vanilla room data does so as well.
1839
1840 // Encode list index 0 (primary)
1841 for (const auto& obj : layer0_objects) {
1842 auto encoded = obj.EncodeObjectToBytes();
1843 bytes.push_back(encoded.b1);
1844 bytes.push_back(encoded.b2);
1845 bytes.push_back(encoded.b3);
1846 }
1847 bytes.push_back(0xFF);
1848 bytes.push_back(0xFF);
1849
1850 // Encode list index 1 (BG2 overlay)
1851 for (const auto& obj : layer1_objects) {
1852 auto encoded = obj.EncodeObjectToBytes();
1853 bytes.push_back(encoded.b1);
1854 bytes.push_back(encoded.b2);
1855 bytes.push_back(encoded.b3);
1856 }
1857 bytes.push_back(0xFF);
1858 bytes.push_back(0xFF);
1859
1860 // Encode list index 2 (BG1 overlay)
1861 for (const auto& obj : layer2_objects) {
1862 auto encoded = obj.EncodeObjectToBytes();
1863 bytes.push_back(encoded.b1);
1864 bytes.push_back(encoded.b2);
1865 bytes.push_back(encoded.b3);
1866 }
1867
1868 // ASM marker 0xF0 0xFF - start of door list (RoomDraw_DrawAllObjects checks
1869 // for word $FFF0).
1870 bytes.push_back(0xF0);
1871 bytes.push_back(0xFF);
1872 for (const auto& door : doors_) {
1873 auto [b1, b2] = door.EncodeBytes();
1874 bytes.push_back(b1);
1875 bytes.push_back(b2);
1876 }
1877
1878 // Door list terminator (word $FFFF). This is also the list-2 terminator.
1879 bytes.push_back(0xFF);
1880 bytes.push_back(0xFF);
1881
1882 return bytes;
1883}
1884
1885std::vector<uint8_t> Room::EncodeSprites() const {
1886 std::vector<uint8_t> bytes;
1887
1888 for (const auto& sprite : sprites_) {
1889 uint8_t b1, b2, b3;
1890
1891 // b3 is simply the ID
1892 b3 = sprite.id();
1893
1894 // b2 = (X & 0x1F) | ((Flags & 0x07) << 5)
1895 // Flags 0-2 come from b2 5-7
1896 b2 = (sprite.x() & 0x1F) | ((sprite.subtype() & 0x07) << 5);
1897
1898 // b1 = (Y & 0x1F) | ((Flags & 0x18) << 2) | ((Layer & 1) << 7)
1899 // Flags 3-4 come from b1 5-6. (0x18 is 00011000)
1900 // Layer bit 0 comes from b1 7
1901 b1 = (sprite.y() & 0x1F) | ((sprite.subtype() & 0x18) << 2) |
1902 ((sprite.layer() & 0x01) << 7);
1903
1904 bytes.push_back(b1);
1905 bytes.push_back(b2);
1906 bytes.push_back(b3);
1907
1908 // Key drops are stored as hidden marker sprites immediately after the
1909 // sprite that owns the drop. Keep these bytes in sync with LoadSprites().
1910 if (sprite.key_drop() == 1) {
1911 bytes.insert(bytes.end(), {0xFE, 0x00, 0xE4});
1912 } else if (sprite.key_drop() == 2) {
1913 bytes.insert(bytes.end(), {0xFD, 0x00, 0xE4});
1914 }
1915 }
1916
1917 // Terminator
1918 bytes.push_back(0xFF);
1919
1920 return bytes;
1921}
1922
1924 if (!rom || !rom->is_loaded()) {
1926 }
1927
1928 const auto& rom_data = rom->vector();
1929 int sprite_pointer = 0;
1930 if (!GetSpritePointerTablePc(rom_data, &sprite_pointer).ok()) {
1932 }
1933
1934 const int hard_end =
1935 std::min(static_cast<int>(rom_data.size()), kSpritesDataEndExclusive);
1936 if (hard_end <= 0) {
1938 }
1939
1940 int max_used = std::min(hard_end, kSpritesData);
1941 std::unordered_set<int> visited_addresses;
1942 for (int room_id = 0; room_id < kNumberOfRooms; ++room_id) {
1943 int sprite_address =
1944 ReadRoomSpriteAddressPc(rom_data, sprite_pointer, room_id);
1945 if (sprite_address < kSpritesData || sprite_address >= hard_end) {
1946 continue;
1947 }
1948 if (!visited_addresses.insert(sprite_address).second) {
1949 continue;
1950 }
1951
1952 int stream_size =
1953 MeasureSpriteStreamSize(rom_data, sprite_address, hard_end);
1954 int stream_end = sprite_address + stream_size;
1955 if (stream_end > max_used) {
1956 max_used = stream_end;
1957 }
1958 }
1959
1960 return max_used;
1961}
1962
1963absl::Status RelocateSpriteData(Rom* rom, int room_id,
1964 const std::vector<uint8_t>& encoded_bytes) {
1965 if (!rom || !rom->is_loaded()) {
1966 return absl::InvalidArgumentError("ROM not loaded");
1967 }
1968 if (room_id < 0 || room_id >= kNumberOfRooms) {
1969 return absl::OutOfRangeError("Room ID out of range");
1970 }
1971 if (encoded_bytes.empty() || encoded_bytes.back() != 0xFF ||
1972 (encoded_bytes.size() % 3) != 1) {
1973 return absl::InvalidArgumentError(
1974 "Encoded sprite payload must be N*3 bytes plus 0xFF terminator");
1975 }
1976
1977 const auto& rom_data = rom->vector();
1978 int sprite_pointer = 0;
1979 RETURN_IF_ERROR(GetSpritePointerTablePc(rom_data, &sprite_pointer));
1980
1981 int old_sprite_address =
1982 ReadRoomSpriteAddressPc(rom_data, sprite_pointer, room_id);
1983 if (old_sprite_address < 0 ||
1984 old_sprite_address >= static_cast<int>(rom_data.size())) {
1985 return absl::OutOfRangeError("Sprite address out of range");
1986 }
1987
1988 const uint8_t sort_mode = rom_data[old_sprite_address];
1989
1990 const int write_pos = FindMaxUsedSpriteAddress(rom);
1991 const size_t required_size = 1u + encoded_bytes.size();
1992 if (write_pos < kSpritesData ||
1993 static_cast<size_t>(write_pos) + required_size >
1994 static_cast<size_t>(kSpritesDataEndExclusive)) {
1995 return absl::ResourceExhaustedError(absl::StrFormat(
1996 "Not enough sprite data space. Need %d bytes at 0x%06X, "
1997 "region ends at 0x%06X",
1998 static_cast<int>(required_size), write_pos, kSpritesDataEndExclusive));
1999 }
2000 if (static_cast<size_t>(write_pos) + required_size > rom_data.size()) {
2001 const int required_end = write_pos + static_cast<int>(required_size);
2002 return absl::OutOfRangeError(
2003 absl::StrFormat("ROM too small for sprite relocation write (need "
2004 "end=0x%06X, size=0x%06X)",
2005 required_end, static_cast<int>(rom_data.size())));
2006 }
2007
2008 std::vector<uint8_t> relocated;
2009 relocated.reserve(required_size);
2010 relocated.push_back(sort_mode);
2011 relocated.insert(relocated.end(), encoded_bytes.begin(), encoded_bytes.end());
2012 RETURN_IF_ERROR(rom->WriteVector(write_pos, std::move(relocated)));
2013
2014 const uint32_t snes_addr = PcToSnes(write_pos);
2015 const int ptr_off = sprite_pointer + (room_id * 2);
2016 RETURN_IF_ERROR(rom->WriteByte(ptr_off, snes_addr & 0xFF));
2017 RETURN_IF_ERROR(rom->WriteByte(ptr_off + 1, (snes_addr >> 8) & 0xFF));
2018
2019 return absl::OkStatus();
2020}
2021
2022absl::Status Room::SaveObjects(const DungeonStreamLayout* layout) {
2023 if (rom_ == nullptr) {
2024 return absl::InvalidArgumentError("ROM pointer is null");
2025 }
2026 if (!object_stream_dirty()) {
2027 return absl::OkStatus();
2028 }
2029
2030 for (const auto& object : tile_objects_) {
2031 if (UsesRoomObjectStream(object)) {
2033 }
2034 }
2035
2036 const auto& rom_data = rom()->vector();
2037 ASSIGN_OR_RETURN(const PhysicalStreamInfo stream_info,
2038 GetObjectStreamInfo(rom_data, room_id_));
2039 const auto encoded_bytes = EncodeObjects();
2040 bool requires_copy_on_write = false;
2041 if (layout != nullptr) {
2042 ASSIGN_OR_RETURN(requires_copy_on_write,
2043 DungeonStreamRequiresCopyOnWrite(
2045 encoded_bytes.size() + 2u));
2046 }
2047 const auto relocate = [&]() -> absl::Status {
2048 if (stream_info.address + 2 > static_cast<int>(rom_data.size())) {
2049 return absl::OutOfRangeError("Object stream header is out of range");
2050 }
2051 std::vector<uint8_t> replacement = {rom_data[stream_info.address],
2052 rom_data[stream_info.address + 1]};
2053 replacement.insert(replacement.end(), encoded_bytes.begin(),
2054 encoded_bytes.end());
2055 RETURN_IF_ERROR(RelocateDungeonStream(rom_, room_id_,
2057 std::move(replacement)));
2059 return absl::OkStatus();
2060 };
2061 if (stream_info.shared || requires_copy_on_write) {
2062 if (layout != nullptr) {
2063 return relocate();
2064 }
2065 return absl::FailedPreconditionError(absl::StrFormat(
2066 "Room %d object stream at PC 0x%06X is shared; repacking is required",
2067 room_id_, stream_info.address));
2068 }
2069 if (stream_info.capacity() <= 2) {
2070 if (layout != nullptr) {
2071 return relocate();
2072 }
2073 return absl::FailedPreconditionError(absl::StrFormat(
2074 "Room %d object stream has no safe physical boundary", room_id_));
2075 }
2076
2077 // Skip graphics/layout header (2 bytes)
2078 const int write_pos = stream_info.address + 2;
2079
2080 // Encode all objects
2081 const int available_payload_size = stream_info.capacity() - 2;
2082
2083 // Validate against the nearest greater physical pointer, not the next room
2084 // ID. Pointer tables are not ordered by room ID in vanilla or expanded ROMs.
2085 if (encoded_bytes.size() > static_cast<size_t>(available_payload_size)) {
2086 if (layout != nullptr) {
2087 return relocate();
2088 }
2089 return absl::ResourceExhaustedError(absl::StrFormat(
2090 "Room %d object data too large! Size: %d, Available: %d", room_id_,
2091 static_cast<int>(encoded_bytes.size()), available_payload_size));
2092 }
2093
2094 const int door_list_offset = static_cast<int>(encoded_bytes.size()) -
2095 static_cast<int>(doors_.size()) * 2 - 2;
2096 if (door_list_offset < 0) {
2097 return absl::FailedPreconditionError("Invalid encoded door list offset");
2098 }
2099 const int door_pointer_slot = kDoorPointers + (room_id_ * 3);
2100 if (door_pointer_slot < 0 ||
2101 door_pointer_slot + 2 >= static_cast<int>(rom_data.size())) {
2102 return absl::OutOfRangeError("Door pointer slot is out of range");
2103 }
2104 const int door_pointer_pc = write_pos + door_list_offset;
2105 ASSIGN_OR_RETURN(const uint32_t source_door_pointer,
2106 rom_->ReadLong(door_pointer_slot));
2107
2108 // Write encoded bytes to ROM (includes 0xF0 0xFF + door list)
2109 RETURN_IF_ERROR(rom_->WriteVector(write_pos, encoded_bytes));
2110
2111 // Write door pointer: first byte after 0xF0 0xFF (per ZScreamDungeon
2112 // Save.cs). Preserve the source pointer's slow/fast ROM bank mirror; both
2113 // encodings address the same bytes, but normalizing an unchanged pointer
2114 // creates an unrelated ROM diff.
2115 uint32_t encoded_door_pointer = PcToSnes(door_pointer_pc);
2116 encoded_door_pointer |= source_door_pointer & 0x800000u;
2117 RETURN_IF_ERROR(rom_->WriteLong(door_pointer_slot, encoded_door_pointer));
2118
2120
2121 return absl::OkStatus();
2122}
2123
2125 if (rom_ == nullptr) {
2126 return absl::InvalidArgumentError("ROM pointer is null");
2127 }
2129 return absl::OkStatus();
2130 }
2131 if (floor1_graphics_ > 0x0F || floor2_graphics_ > 0x0F) {
2132 return absl::InvalidArgumentError(
2133 "Dungeon floor graphics values must be in range 0..15");
2134 }
2135 if (layout_id_ > 0x07) {
2136 return absl::InvalidArgumentError(
2137 "Dungeon layout ID must be in range 0..7");
2138 }
2139
2140 const auto& rom_data = rom_->vector();
2141 ASSIGN_OR_RETURN(const PhysicalStreamInfo stream_info,
2142 GetObjectStreamInfo(rom_data, room_id_));
2143 if (stream_info.address < 0 ||
2144 stream_info.address + 1 >= static_cast<int>(rom_data.size())) {
2145 return absl::OutOfRangeError("Object stream header is out of range");
2146 }
2147
2148 const uint8_t dirty_mask = save_dirty_state_.object_stream_header;
2149 auto patch_header = [&](std::vector<uint8_t>* stream) -> absl::Status {
2150 if (stream == nullptr || stream->size() < 2) {
2151 return absl::DataLossError(
2152 "Object stream is missing its two-byte header");
2153 }
2154 if ((dirty_mask & kObjectHeaderFloor1Dirty) != 0) {
2155 (*stream)[0] = static_cast<uint8_t>(((*stream)[0] & 0xF0) |
2156 (floor1_graphics_ & 0x0F));
2157 }
2158 if ((dirty_mask & kObjectHeaderFloor2Dirty) != 0) {
2159 (*stream)[0] =
2160 static_cast<uint8_t>(((*stream)[0] & 0x0F) | (floor2_graphics_ << 4));
2161 }
2162 if ((dirty_mask & kObjectHeaderLayoutDirty) != 0) {
2163 (*stream)[1] = static_cast<uint8_t>(((*stream)[1] & 0xE3) |
2164 ((layout_id_ & 0x07) << 2));
2165 }
2166 return absl::OkStatus();
2167 };
2168
2169 bool requires_copy_on_write = stream_info.shared;
2170 std::vector<uint8_t> replacement;
2171 if (layout != nullptr) {
2172 if (layout->kind != DungeonStreamKind::kObject) {
2173 return absl::InvalidArgumentError(
2174 "Object-stream header save requires an object stream layout");
2175 }
2177 InventoryDungeonStreams(*rom_, *layout));
2178 if (!inventory.ok()) {
2179 return absl::FailedPreconditionError(absl::StrFormat(
2180 "Dungeon stream inventory has %zu issue(s); refusing object "
2181 "header save",
2182 inventory.issues.size()));
2183 }
2184 if (room_id_ < 0 ||
2185 static_cast<size_t>(room_id_) >= inventory.streams.size()) {
2186 return absl::OutOfRangeError(
2187 "Room ID is outside the dungeon stream layout");
2188 }
2189 replacement = inventory.streams[room_id_].encoded_stream;
2190 bool layout_requires_copy_on_write = false;
2191 ASSIGN_OR_RETURN(layout_requires_copy_on_write,
2192 DungeonStreamRequiresCopyOnWrite(
2194 replacement.size()));
2195 requires_copy_on_write =
2196 requires_copy_on_write || layout_requires_copy_on_write;
2197 }
2198
2199 if (requires_copy_on_write) {
2200 if (layout == nullptr) {
2201 return absl::FailedPreconditionError(absl::StrFormat(
2202 "Room %d object stream at PC 0x%06X is shared; a copy-on-write "
2203 "manifest is required to save its header",
2204 room_id_, stream_info.address));
2205 }
2206 RETURN_IF_ERROR(patch_header(&replacement));
2207 RETURN_IF_ERROR(RelocateDungeonStream(rom_, room_id_,
2209 std::move(replacement)));
2211 return absl::OkStatus();
2212 }
2213
2214 std::vector<uint8_t> header = {rom_data[stream_info.address],
2215 rom_data[stream_info.address + 1]};
2216 RETURN_IF_ERROR(patch_header(&header));
2217 RETURN_IF_ERROR(rom_->WriteVector(stream_info.address, std::move(header)));
2219 return absl::OkStatus();
2220}
2221
2222absl::Status Room::SaveSprites(const DungeonStreamLayout* layout) {
2223 if (rom_ == nullptr) {
2224 return absl::InvalidArgumentError("ROM pointer is null");
2225 }
2226 if (!sprites_dirty()) {
2227 return absl::OkStatus();
2228 }
2229
2230 const auto& rom_data = rom()->vector();
2231 if (room_id_ < 0 || room_id_ >= kNumberOfRooms) {
2232 return absl::OutOfRangeError("Room ID out of range");
2233 }
2234
2235 ASSIGN_OR_RETURN(const PhysicalStreamInfo stream_info,
2236 GetSpriteStreamInfo(rom_data, room_id_));
2237 const auto encoded_bytes = EncodeSprites();
2238 bool requires_copy_on_write = false;
2239 if (layout != nullptr) {
2240 ASSIGN_OR_RETURN(requires_copy_on_write,
2241 DungeonStreamRequiresCopyOnWrite(
2243 encoded_bytes.size() + 1u));
2244 }
2245 const auto relocate = [&]() -> absl::Status {
2246 std::vector<uint8_t> replacement = {rom_data[stream_info.address]};
2247 replacement.insert(replacement.end(), encoded_bytes.begin(),
2248 encoded_bytes.end());
2249 RETURN_IF_ERROR(RelocateDungeonStream(rom_, room_id_,
2251 std::move(replacement)));
2253 return absl::OkStatus();
2254 };
2255 if (stream_info.shared || requires_copy_on_write) {
2256 if (layout != nullptr) {
2257 return relocate();
2258 }
2259 return absl::FailedPreconditionError(absl::StrFormat(
2260 "Room %d sprite stream at PC 0x%06X is shared; repacking is required",
2261 room_id_, stream_info.address));
2262 }
2263 if (stream_info.capacity() <= 1) {
2264 if (layout != nullptr) {
2265 return relocate();
2266 }
2267 return absl::FailedPreconditionError(absl::StrFormat(
2268 "Room %d sprite stream has no safe physical boundary", room_id_));
2269 }
2270
2271 const int available_payload_size = stream_info.capacity() - 1;
2272 const int payload_address = stream_info.address + 1;
2273 if (payload_address < 0 ||
2274 payload_address >= static_cast<int>(rom_->size())) {
2275 return absl::OutOfRangeError(absl::StrFormat(
2276 "Room %d has invalid sprite payload address", room_id_));
2277 }
2278
2279 if (static_cast<int>(encoded_bytes.size()) > available_payload_size) {
2280 if (layout != nullptr) {
2281 return relocate();
2282 }
2283 return absl::ResourceExhaustedError(absl::StrFormat(
2284 "Room %d sprite data too large! Size: %d, Available: %d; repacking "
2285 "is required",
2286 room_id_, static_cast<int>(encoded_bytes.size()),
2287 available_payload_size));
2288 }
2289
2290 RETURN_IF_ERROR(rom_->WriteVector(payload_address, encoded_bytes));
2292 return absl::OkStatus();
2293}
2294
2295absl::Status Room::SaveRoomHeader() {
2296 if (rom_ == nullptr) {
2297 return absl::InvalidArgumentError("ROM pointer is null");
2298 }
2299
2300 const auto& rom_data = rom()->vector();
2301 if (kRoomHeaderPointer < 0 ||
2302 kRoomHeaderPointer + 2 >= static_cast<int>(rom_data.size())) {
2303 return absl::OutOfRangeError("Room header pointer out of range");
2304 }
2305 if (kRoomHeaderPointerBank < 0 ||
2306 kRoomHeaderPointerBank >= static_cast<int>(rom_data.size())) {
2307 return absl::OutOfRangeError("Room header pointer bank out of range");
2308 }
2309
2310 int header_pointer = (rom_data[kRoomHeaderPointer + 2] << 16) +
2311 (rom_data[kRoomHeaderPointer + 1] << 8) +
2312 rom_data[kRoomHeaderPointer];
2313 header_pointer = SnesToPc(header_pointer);
2314
2315 int table_offset = header_pointer + (room_id_ * 2);
2316 if (table_offset < 0 ||
2317 table_offset + 1 >= static_cast<int>(rom_data.size())) {
2318 return absl::OutOfRangeError("Room header table offset out of range");
2319 }
2320
2321 int address = (rom_data[kRoomHeaderPointerBank] << 16) +
2322 (rom_data[table_offset + 1] << 8) + rom_data[table_offset];
2323 int header_location = SnesToPc(address);
2324
2325 if (header_location < 0 ||
2326 header_location + 13 >= static_cast<int>(rom_data.size())) {
2327 return absl::OutOfRangeError("Room header location out of range");
2328 }
2329
2330 // Build 14-byte header to match LoadRoomHeaderFromRom layout. The high
2331 // three bits are the BG2/layer mode; bit 0 is the dark-room flag. DarkRoom
2332 // is an editor enum value, not a raw high-bit value.
2333 uint8_t layer2_mode_for_save = layer2_mode_ & 0x07;
2334 if (bg2() != background2::DarkRoom) {
2335 layer2_mode_for_save = static_cast<uint8_t>(bg2()) & 0x07;
2336 }
2337 const bool dark_room =
2338 IsLight() || is_dark_ || bg2() == background2::DarkRoom;
2339 uint8_t byte0 = static_cast<uint8_t>(
2340 (layer2_mode_for_save << 5) |
2341 ((static_cast<uint8_t>(collision()) & 0x07) << 2) |
2342 (rom_data[header_location] & 0x02) | (dark_room ? 1 : 0));
2343 // Preserve the full palette set ID byte (USDASM LoadRoomHeader uses 8-bit).
2344 uint8_t byte1 = palette_;
2345 // Byte 7 stores the pit target layer in bits 0-1 followed by the first
2346 // three staircase target layers in consecutive two-bit fields.
2347 uint8_t byte7 =
2348 (pits_.target_layer & 0x03) | ((staircase_plane(0) & 0x03) << 2) |
2349 ((staircase_plane(1) & 0x03) << 4) | ((staircase_plane(2) & 0x03) << 6);
2350 const uint8_t byte8 = static_cast<uint8_t>(
2351 (rom_data[header_location + 8] & 0xFC) | (staircase_plane(3) & 0x03));
2352
2353 RETURN_IF_ERROR(rom_->WriteByte(header_location + 0, byte0));
2354 RETURN_IF_ERROR(rom_->WriteByte(header_location + 1, byte1));
2355 RETURN_IF_ERROR(rom_->WriteByte(header_location + 2, blockset_));
2356 RETURN_IF_ERROR(rom_->WriteByte(header_location + 3, spriteset_));
2358 rom_->WriteByte(header_location + 4, static_cast<uint8_t>(effect())));
2360 rom_->WriteByte(header_location + 5, static_cast<uint8_t>(tag1())));
2362 rom_->WriteByte(header_location + 6, static_cast<uint8_t>(tag2())));
2363 RETURN_IF_ERROR(rom_->WriteByte(header_location + 7, byte7));
2364 RETURN_IF_ERROR(rom_->WriteByte(header_location + 8, byte8));
2365 RETURN_IF_ERROR(rom_->WriteByte(header_location + 9, holewarp_));
2366 RETURN_IF_ERROR(rom_->WriteByte(header_location + 10, staircase_room(0)));
2367 RETURN_IF_ERROR(rom_->WriteByte(header_location + 11, staircase_room(1)));
2368 RETURN_IF_ERROR(rom_->WriteByte(header_location + 12, staircase_room(2)));
2369 RETURN_IF_ERROR(rom_->WriteByte(header_location + 13, staircase_room(3)));
2370
2371 int msg_addr = kMessagesIdDungeon + (room_id_ * 2);
2372 if (msg_addr < 0 || msg_addr + 1 >= static_cast<int>(rom_data.size())) {
2373 return absl::OutOfRangeError("Message ID address out of range");
2374 }
2376
2378
2379 return absl::OkStatus();
2380}
2381
2382// ============================================================================
2383// Object Manipulation Methods (Phase 3)
2384// ============================================================================
2385
2386absl::Status Room::AddObject(const RoomObject& object) {
2387 // Validate object
2388 if (!ValidateObject(object)) {
2389 return absl::InvalidArgumentError("Invalid object parameters");
2390 }
2391
2392 // Add to internal list
2393 tile_objects_.push_back(object);
2394 objects_loaded_ = true;
2396
2397 return absl::OkStatus();
2398}
2399
2400absl::Status Room::RemoveObject(size_t index) {
2401 if (index >= tile_objects_.size()) {
2402 return absl::OutOfRangeError("Object index out of range");
2403 }
2404
2406 tile_objects_.erase(tile_objects_.begin() + index);
2407 objects_loaded_ = true;
2409
2410 return absl::OkStatus();
2411}
2412
2413absl::Status Room::UpdateObject(size_t index, const RoomObject& object) {
2414 if (index >= tile_objects_.size()) {
2415 return absl::OutOfRangeError("Object index out of range");
2416 }
2417
2418 if (!ValidateObject(object)) {
2419 return absl::InvalidArgumentError("Invalid object parameters");
2420 }
2421
2423 tile_objects_[index] = object;
2424 objects_loaded_ = true;
2426
2427 return absl::OkStatus();
2428}
2429
2430absl::StatusOr<size_t> Room::FindObjectAt(int x, int y, int layer) const {
2431 for (size_t i = 0; i < tile_objects_.size(); i++) {
2432 const auto& obj = tile_objects_[i];
2433 if (obj.x() == x && obj.y() == y && obj.GetLayerValue() == layer) {
2434 return i;
2435 }
2436 }
2437 return absl::NotFoundError("No object found at position");
2438}
2439
2440bool Room::ValidateObject(const RoomObject& object) const {
2441 // Validate position (0-63 for both X and Y)
2442 if (object.x() < 0 || object.x() > 63)
2443 return false;
2444 if (object.y() < 0 || object.y() > 63)
2445 return false;
2446
2447 // Validate layer (0-2)
2448 if (object.GetLayerValue() < 0 || object.GetLayerValue() > 2)
2449 return false;
2450
2451 // Validate object ID range
2452 if (object.id_ < 0 || object.id_ > 0xFFF)
2453 return false;
2454
2455 // Validate size for Type 1 objects
2456 if (object.id_ < 0x100 && object.size() > 15)
2457 return false;
2458
2459 return true;
2460}
2461
2462void Room::HandleSpecialObjects(short oid, uint8_t posX, uint8_t posY,
2463 int& nbr_of_staircase) {
2464 // Handle staircase objects
2465 for (short stair : kStairsObjects) {
2466 if (stair == oid) {
2467 if (nbr_of_staircase < 4) {
2468 tile_objects_.back().set_options(ObjectOption::Stairs |
2469 tile_objects_.back().options());
2470 z3_staircases_.push_back(
2471 {posX, posY,
2472 absl::StrCat("To ", staircase_rooms_[nbr_of_staircase]).data()});
2473 nbr_of_staircase++;
2474 } else {
2475 tile_objects_.back().set_options(ObjectOption::Stairs |
2476 tile_objects_.back().options());
2477 z3_staircases_.push_back({posX, posY, "To ???"});
2478 }
2479 break;
2480 }
2481 }
2482
2483 // Handle chest objects
2484 if (oid == 0xF99) {
2485 if (chests_in_room_.size() > 0) {
2486 tile_objects_.back().set_options(ObjectOption::Chest |
2487 tile_objects_.back().options());
2488 chests_in_room_.erase(chests_in_room_.begin());
2489 }
2490 } else if (oid == 0xFB1) {
2491 if (chests_in_room_.size() > 0) {
2492 tile_objects_.back().set_options(ObjectOption::Chest |
2493 tile_objects_.back().options());
2494 chests_in_room_.erase(chests_in_room_.begin());
2495 }
2496 }
2497}
2498
2500 const auto& rom_data = rom()->vector();
2501 // Avoid duplicate entries if callers reload sprite data on the same room.
2502 sprites_.clear();
2503 sprites_loaded_ = false;
2504 if (room_id_ < 0 || room_id_ >= kNumberOfRooms) {
2505 return;
2506 }
2507
2508 int sprite_pointer = 0;
2509 if (!GetSpritePointerTablePc(rom_data, &sprite_pointer).ok()) {
2510 return;
2511 }
2512
2513 int sprite_address =
2514 ReadRoomSpriteAddressPc(rom_data, sprite_pointer, room_id_);
2515 if (sprite_address < 0 ||
2516 sprite_address + 1 >= static_cast<int>(rom_data.size())) {
2517 return;
2518 }
2519
2520 // First byte is the SortSprites mode (0 or 1), not sprite data.
2521 sprite_address += 1;
2522
2523 while (sprite_address + 2 < static_cast<int>(rom_data.size())) {
2524 uint8_t b1 = rom_data[sprite_address];
2525 uint8_t b2 = rom_data[sprite_address + 1];
2526 uint8_t b3 = rom_data[sprite_address + 2];
2527
2528 if (b1 == 0xFF) {
2529 break;
2530 }
2531
2532 sprites_.emplace_back(b3, (b2 & 0x1F), (b1 & 0x1F),
2533 ((b2 & 0xE0) >> 5) + ((b1 & 0x60) >> 2),
2534 (b1 & 0x80) >> 7);
2535
2536 if (sprites_.size() > 1) {
2537 Sprite& spr = sprites_.back();
2538 Sprite& prevSprite = sprites_[sprites_.size() - 2];
2539
2540 if (spr.id() == 0xE4 && spr.x() == 0x00 && spr.y() == 0x1E &&
2541 spr.layer() == 1 && spr.subtype() == 0x18) {
2542 prevSprite.set_key_drop(1);
2543 sprites_.pop_back();
2544 }
2545
2546 if (spr.id() == 0xE4 && spr.x() == 0x00 && spr.y() == 0x1D &&
2547 spr.layer() == 1 && spr.subtype() == 0x18) {
2548 prevSprite.set_key_drop(2);
2549 sprites_.pop_back();
2550 }
2551 }
2552
2553 sprite_address += 3;
2554 }
2555
2556 sprites_loaded_ = true;
2557}
2558
2560 chests_in_room_.clear();
2561 chests_loaded_ = false;
2562 if (!rom_ || !rom_->is_loaded()) {
2563 return;
2564 }
2565 const auto& rom_data = rom()->vector();
2566 if (kChestsDataPointer1 + 2 >= static_cast<int>(rom_data.size()) ||
2567 kChestsLengthPointer + 1 >= static_cast<int>(rom_data.size())) {
2568 return;
2569 }
2570
2571 const int cpos = static_cast<int>(SnesToPc(
2572 (static_cast<uint32_t>(rom_data[kChestsDataPointer1 + 2]) << 16) |
2573 (static_cast<uint32_t>(rom_data[kChestsDataPointer1 + 1]) << 8) |
2574 rom_data[kChestsDataPointer1]));
2575 const size_t byte_length =
2576 (static_cast<size_t>(rom_data[kChestsLengthPointer + 1]) << 8) |
2577 rom_data[kChestsLengthPointer];
2578 const size_t bounded_byte_length = std::min<size_t>(
2579 byte_length, cpos >= 0 && cpos < static_cast<int>(rom_data.size())
2580 ? rom_data.size() - static_cast<size_t>(cpos)
2581 : 0);
2582 const size_t record_count = std::min<size_t>(
2583 bounded_byte_length / kChestTableRecordSize, kChestTableCapacityRecords);
2584
2585 for (size_t i = 0; i < record_count; ++i) {
2586 const size_t offset =
2587 static_cast<size_t>(cpos) + (i * kChestTableRecordSize);
2588 if ((((rom_data[offset + 1] << 8) + rom_data[offset]) & 0x7FFF) ==
2589 room_id_) {
2590 // There's a chest in that room !
2591 bool big = false;
2592 if ((((rom_data[offset + 1] << 8) + rom_data[offset]) & 0x8000) ==
2593 0x8000) {
2594 big = true;
2595 }
2596
2597 chests_in_room_.emplace_back(chest_data{rom_data[offset + 2], big});
2598 }
2599 }
2600 chests_loaded_ = true;
2601}
2602
2604 auto rom_data = rom()->vector();
2605
2606 // Doors are loaded as part of the object stream in LoadObjects()
2607 // When the parser encounters 0xF0 0xFF, it enters door mode
2608 // Door objects have format: b1 (position/direction), b2 (type)
2609 // Door encoding: b1 = (door_pos << 4) | (door_dir & 0x03)
2610 // position in bits 4-7, direction in bits 0-1
2611 // b2 = door_type (full byte, values 0x00, 0x02, 0x04, etc.)
2612 // This is already handled in ParseObjectsFromLocation()
2613
2614 LOG_DEBUG("Room",
2615 "LoadDoors for room %d - doors are loaded via object stream",
2616 room_id_);
2617}
2618
2620 auto rom_data = rom()->vector();
2621
2622 // Read torch data length
2623 int bytes_count = (rom_data[kTorchesLengthPointer + 1] << 8) |
2624 rom_data[kTorchesLengthPointer];
2625
2626 LOG_DEBUG("Room", "LoadTorches: room_id=%d, bytes_count=%d", room_id_,
2627 bytes_count);
2628
2629 // Avoid duplication if LoadTorches is called multiple times.
2630 tile_objects_.erase(
2631 std::remove_if(tile_objects_.begin(), tile_objects_.end(),
2632 [](const RoomObject& obj) {
2633 return (obj.options() & ObjectOption::Torch) !=
2634 ObjectOption::Nothing;
2635 }),
2636 tile_objects_.end());
2637
2638 // Iterate through torch data to find torches for this room
2639 for (int i = 0; i < bytes_count; i += 2) {
2640 if (i + 1 >= bytes_count)
2641 break;
2642
2643 uint8_t b1 = rom_data[kTorchData + i];
2644 uint8_t b2 = rom_data[kTorchData + i + 1];
2645
2646 // Skip 0xFFFF markers
2647 if (b1 == 0xFF && b2 == 0xFF) {
2648 continue;
2649 }
2650
2651 // Check if this entry is for our room
2652 uint16_t torch_room_id = (b2 << 8) | b1;
2653 if (torch_room_id == room_id_) {
2654 // Found torches for this room, read them
2655 i += 2;
2656 while (i < bytes_count) {
2657 if (i + 1 >= bytes_count)
2658 break;
2659
2660 b1 = rom_data[kTorchData + i];
2661 b2 = rom_data[kTorchData + i + 1];
2662
2663 // End of torch list for this room
2664 if (b1 == 0xFF && b2 == 0xFF) {
2665 break;
2666 }
2667
2668 const LightableTorchEntry entry = DecodeLightableTorchEntry({b1, b2});
2669
2670 // Create torch object (ID 0x150)
2671 RoomObject torch_obj(0x150, entry.px, entry.py, 0, entry.draw_layer);
2672 torch_obj.SetRom(rom_);
2673 torch_obj.set_options(ObjectOption::Torch);
2674 torch_obj.set_torch_reserved_bit(entry.reserved);
2675 torch_obj.lit_ = entry.lit;
2676
2677 tile_objects_.push_back(torch_obj);
2678
2679 LOG_DEBUG(
2680 "Room", "Loaded torch at (%d,%d) draw_layer=%d reserved=%d lit=%d",
2681 entry.px, entry.py, entry.draw_layer, entry.reserved, entry.lit);
2682
2683 i += 2;
2684 }
2685 break; // Found and processed our room's torches
2686 } else {
2687 // Skip to next room's torches
2688 i += 2;
2689 while (i < bytes_count) {
2690 if (i + 1 >= bytes_count)
2691 break;
2692 b1 = rom_data[kTorchData + i];
2693 b2 = rom_data[kTorchData + i + 1];
2694 if (b1 == 0xFF && b2 == 0xFF) {
2695 break;
2696 }
2697 i += 2;
2698 }
2699 }
2700 }
2701 torches_loaded_ = true;
2702}
2703
2704namespace {
2705
2706constexpr int kTorchesMaxSize = 0x120; // ZScream Constants.TorchesMaxSize
2707
2709 uint16_t room_id = 0;
2710 std::vector<uint8_t> bytes;
2711};
2712
2713// Parse current ROM torch blob in authoring order for preserve-merge.
2714std::vector<TorchSegment> ParseRomTorchSegments(
2715 const std::vector<uint8_t>& rom_data, int bytes_count) {
2716 std::vector<TorchSegment> segments;
2717 int i = 0;
2718 while (i + 1 < bytes_count && i < kTorchesMaxSize) {
2719 uint8_t b1 = rom_data[kTorchData + i];
2720 uint8_t b2 = rom_data[kTorchData + i + 1];
2721 if (b1 == 0xFF && b2 == 0xFF) {
2722 // Vanilla contains standalone $FFFF padding between two authored room
2723 // segments. Keep it as an unowned pass-through segment so a no-op save
2724 // remains byte-identical instead of compacting the table.
2725 TorchSegment padding;
2726 padding.room_id = 0xFFFF;
2727 padding.bytes = {0xFF, 0xFF};
2728 segments.push_back(std::move(padding));
2729 i += 2;
2730 continue;
2731 }
2732 uint16_t room_id = (b2 << 8) | b1;
2733 if (room_id >= kNumberOfRooms) {
2734 i += 2;
2735 continue;
2736 }
2737 TorchSegment seg;
2738 seg.room_id = room_id;
2739 seg.bytes.push_back(b1);
2740 seg.bytes.push_back(b2);
2741 i += 2;
2742 while (i + 1 < bytes_count && i < kTorchesMaxSize) {
2743 b1 = rom_data[kTorchData + i];
2744 b2 = rom_data[kTorchData + i + 1];
2745 if (b1 == 0xFF && b2 == 0xFF) {
2746 seg.bytes.push_back(0xFF);
2747 seg.bytes.push_back(0xFF);
2748 i += 2;
2749 break;
2750 }
2751 seg.bytes.push_back(b1);
2752 seg.bytes.push_back(b2);
2753 i += 2;
2754 }
2755 segments.push_back(std::move(seg));
2756 }
2757 return segments;
2758}
2759
2760std::vector<uint8_t> EncodeTorchSegmentForRoom(int room_id, const Room& room) {
2761 std::vector<uint8_t> bytes;
2762 for (const auto& obj : room.GetTileObjects()) {
2763 if ((obj.options() & ObjectOption::Torch) == ObjectOption::Nothing) {
2764 continue;
2765 }
2766 if (bytes.empty()) {
2767 bytes.push_back(room_id & 0xFF);
2768 bytes.push_back((room_id >> 8) & 0xFF);
2769 }
2771 .px = static_cast<uint8_t>(obj.x()),
2772 .py = static_cast<uint8_t>(obj.y()),
2773 .draw_layer = static_cast<uint8_t>(obj.GetLayerValue() & 1),
2774 .reserved = obj.torch_reserved_bit(),
2775 .lit = obj.lit_,
2776 });
2777 bytes.push_back(encoded.low);
2778 bytes.push_back(encoded.high);
2779 }
2780 if (!bytes.empty()) {
2781 bytes.push_back(0xFF);
2782 bytes.push_back(0xFF);
2783 }
2784 return bytes;
2785}
2786
2788 int room_id,
2789 const char* object_type) {
2790 const uint8_t selector = object.GetLayerValue();
2791 if (selector <= 1) {
2792 return absl::OkStatus();
2793 }
2794 return absl::InvalidArgumentError(absl::StrFormat(
2795 "%s in room 0x%03X has invalid special draw-layer selector %d; "
2796 "expected 0 "
2797 "(upper/BG1) or 1 (lower/BG2)",
2798 object_type, room_id, selector));
2799}
2800
2802 int room_id) {
2804 ValidateSpecialObjectDrawLayerSelector(object, room_id, "Torch"));
2805 if (object.x() <= 0x3E && object.y() <= 0x3E) {
2806 return absl::OkStatus();
2807 }
2808 return absl::InvalidArgumentError(absl::StrFormat(
2809 "Torch in room 0x%03X has invalid position (%d,%d); expected x/y in "
2810 "range 0..62",
2811 room_id, object.x(), object.y()));
2812}
2813
2814} // namespace
2815
2816template <typename RoomLookup>
2817absl::Status SaveAllTorchesImpl(Rom* rom, int room_count,
2818 RoomLookup&& room_lookup) {
2819 if (!rom || !rom->is_loaded()) {
2820 return absl::InvalidArgumentError("ROM not loaded");
2821 }
2822
2823 const auto& rom_data = rom->vector();
2824 int existing_count = (rom_data[kTorchesLengthPointer + 1] << 8) |
2825 rom_data[kTorchesLengthPointer];
2826 if (existing_count > kTorchesMaxSize) {
2827 existing_count = kTorchesMaxSize;
2828 }
2829 auto rom_segments = ParseRomTorchSegments(rom_data, existing_count);
2830
2831 std::vector<uint8_t> bytes;
2832 const int room_limit = std::min(room_count, kNumberOfRooms);
2833 std::vector<bool> owned_rooms(room_limit, false);
2834 std::vector<bool> seen_original_room(room_limit, false);
2835 std::vector<bool> emitted_owned_room(room_limit, false);
2836 std::vector<std::vector<uint8_t>> replacements(room_limit);
2837 bool any_owned_room = false;
2838 for (int room_id = 0; room_id < room_limit; ++room_id) {
2839 const Room* room = room_lookup(room_id);
2840 const bool room_owned =
2841 room != nullptr && (room->AreTorchesLoaded() || room->torches_dirty());
2842 if (!room_owned) {
2843 continue;
2844 }
2845 for (const auto& object : room->GetTileObjects()) {
2846 if ((object.options() & ObjectOption::Torch) != ObjectOption::Nothing) {
2847 RETURN_IF_ERROR(ValidateLightableTorchForSave(object, room_id));
2848 }
2849 }
2850 owned_rooms[room_id] = true;
2851 any_owned_room = true;
2852 replacements[room_id] = EncodeTorchSegmentForRoom(room_id, *room);
2853 }
2854
2855 if (!any_owned_room) {
2856 return absl::OkStatus();
2857 }
2858
2859 for (const auto& segment : rom_segments) {
2860 if (segment.room_id < room_limit) {
2861 seen_original_room[segment.room_id] = true;
2862 if (owned_rooms[segment.room_id]) {
2863 if (!emitted_owned_room[segment.room_id]) {
2864 bytes.insert(bytes.end(), replacements[segment.room_id].begin(),
2865 replacements[segment.room_id].end());
2866 emitted_owned_room[segment.room_id] = true;
2867 }
2868 continue;
2869 }
2870 }
2871 bytes.insert(bytes.end(), segment.bytes.begin(), segment.bytes.end());
2872 }
2873
2874 for (int room_id = 0; room_id < room_limit; ++room_id) {
2875 if (owned_rooms[room_id] && !seen_original_room[room_id] &&
2876 !replacements[room_id].empty()) {
2877 bytes.insert(bytes.end(), replacements[room_id].begin(),
2878 replacements[room_id].end());
2879 }
2880 }
2881
2882 if (bytes.size() > kTorchesMaxSize) {
2883 return absl::ResourceExhaustedError(
2884 absl::StrFormat("Torch data too large: %d bytes (max %d)", bytes.size(),
2885 kTorchesMaxSize));
2886 }
2887
2888 const uint16_t current_len =
2889 static_cast<uint16_t>(rom_data[kTorchesLengthPointer]) |
2890 (static_cast<uint16_t>(rom_data[kTorchesLengthPointer + 1]) << 8);
2891 if (current_len == bytes.size() &&
2892 kTorchData + static_cast<int>(bytes.size()) <=
2893 static_cast<int>(rom_data.size()) &&
2894 std::equal(bytes.begin(), bytes.end(), rom_data.begin() + kTorchData)) {
2895 for (int room_id = 0; room_id < room_limit; ++room_id) {
2896 if (const Room* room = room_lookup(room_id);
2897 room != nullptr && room->torches_dirty()) {
2898 const_cast<Room*>(room)->ClearTorchesDirty();
2899 }
2900 }
2901 return absl::OkStatus();
2902 }
2903
2905 static_cast<uint16_t>(bytes.size())));
2907 for (int room_id = 0; room_id < room_limit; ++room_id) {
2908 if (const Room* room = room_lookup(room_id);
2909 room != nullptr && room->torches_dirty()) {
2910 const_cast<Room*>(room)->ClearTorchesDirty();
2911 }
2912 }
2913 return absl::OkStatus();
2914}
2915
2916absl::Status SaveAllTorches(Rom* rom, absl::Span<const Room> rooms) {
2917 return SaveAllTorchesImpl(rom, static_cast<int>(rooms.size()),
2918 [&rooms](int room_id) { return &rooms[room_id]; });
2919}
2920
2921absl::Status SaveAllTorches(
2922 Rom* rom, int room_count,
2923 const std::function<const Room*(int)>& room_lookup) {
2924 return SaveAllTorchesImpl(rom, room_count, room_lookup);
2925}
2926
2927// Region preservation for `RoomsWithPitDamage` when no edited table is supplied.
2928// When `pit_damage_table` is non-null and dirty, encode the in-memory membership
2929// list through `PitDamageTable::SaveToRom` instead of blind preservation.
2930absl::Status SaveAllPits(Rom* rom) {
2931 return SaveAllPits(rom, nullptr);
2932}
2933
2934absl::Status SaveAllPits(Rom* rom, PitDamageTable* pit_damage_table) {
2935 if (pit_damage_table != nullptr && pit_damage_table->dirty()) {
2936 RETURN_IF_ERROR(pit_damage_table->SaveToRom(rom));
2937 pit_damage_table->ClearDirty();
2938 return absl::OkStatus();
2939 }
2940 if (!rom || !rom->is_loaded()) {
2941 return absl::InvalidArgumentError("ROM not loaded");
2942 }
2943 const auto& rom_data = rom->vector();
2944 if (kPitCount < 0 || kPitCount >= static_cast<int>(rom_data.size()) ||
2945 kPitPointer + 2 >= static_cast<int>(rom_data.size())) {
2946 return absl::OutOfRangeError("Pit count/pointer out of range");
2947 }
2948 int max_offset = rom_data[kPitCount];
2949 // Total bytes = max_offset + 2 (covers offsets 0..max_offset
2950 // inclusive, with each entry being a 2-byte word). When max_offset
2951 // is 0, there's still 1 word to preserve (the entry at offset 0).
2952 int data_len = max_offset + 2;
2953 int pit_ptr_snes = (rom_data[kPitPointer + 2] << 16) |
2954 (rom_data[kPitPointer + 1] << 8) | rom_data[kPitPointer];
2955 int pit_data_pc = SnesToPc(pit_ptr_snes);
2956 if (pit_data_pc < 0 ||
2957 pit_data_pc + data_len > static_cast<int>(rom_data.size())) {
2958 return absl::OutOfRangeError("Pit data region out of range");
2959 }
2960 std::vector<uint8_t> data(rom_data.begin() + pit_data_pc,
2961 rom_data.begin() + pit_data_pc + data_len);
2962 RETURN_IF_ERROR(rom->WriteByte(kPitCount, max_offset));
2963 RETURN_IF_ERROR(rom->WriteByte(kPitPointer, pit_ptr_snes & 0xFF));
2964 RETURN_IF_ERROR(rom->WriteByte(kPitPointer + 1, (pit_ptr_snes >> 8) & 0xFF));
2965 RETURN_IF_ERROR(rom->WriteByte(kPitPointer + 2, (pit_ptr_snes >> 16) & 0xFF));
2966 return rom->WriteVector(pit_data_pc, data);
2967}
2968
2969namespace {
2970
2971constexpr int kBlocksRegionSize = 0x80;
2974
2975bool HalfOpenRangesOverlap(int first_begin, int first_end, int second_begin,
2976 int second_end) {
2977 return first_begin < second_end && second_begin < first_end;
2978}
2979
2981 const std::vector<uint8_t>& rom_data, int operand_pc) {
2982 if (operand_pc <= 0 || operand_pc + 5 >= static_cast<int>(rom_data.size())) {
2983 return absl::OutOfRangeError("Blocks pointer operand out of range");
2984 }
2985 // The block table pointers are the 3-byte operands in the US USDASM
2986 // bank_02 loader shape (#_02DAF9..#_02DB12):
2987 // BF ll hh bb LDA.l table+N*0x80,X
2988 // 9D ll hh STA.w $7EF940+N*0x80,X
2989 // The data table starts at bank_04's
2990 // SpecialUnderworldObjects_pushable_block (#_04F1DE). Pinned against a real
2991 // vanilla ROM by
2992 // DungeonSaveRegionTest.BlocksLoaderPointerOperandsMatchUsdasmShape.
2993 //
2994 // Guard both sides before dereferencing or future repointing so a bad
2995 // constant or already-patched ROM cannot make the saver treat unrelated
2996 // instruction bytes as data pointers.
2997 if (rom_data[operand_pc - 1] != 0xBF || rom_data[operand_pc + 3] != 0x9D) {
2998 return absl::FailedPreconditionError(absl::StrFormat(
2999 "Blocks pointer operand at PC 0x%05X is not in the expected "
3000 "LDA.l ...,X / STA.w loader sequence",
3001 operand_pc));
3002 }
3003 return absl::OkStatus();
3004}
3005
3007 const std::vector<uint8_t>& rom_data, std::array<int, 4>* destination_pcs) {
3008 if (kBlocksLength < 0 ||
3009 kBlocksLength + 1 >= static_cast<int>(rom_data.size())) {
3010 return absl::OutOfRangeError("Blocks length out of range");
3011 }
3012
3013 for (size_t page = 0; page < kBlocksPointerSlots.size(); ++page) {
3014 const int operand_pc = kBlocksPointerSlots[page];
3016 const int snes = (rom_data[operand_pc + 2] << 16) |
3017 (rom_data[operand_pc + 1] << 8) | rom_data[operand_pc];
3018 const int data_pc = SnesToPc(snes);
3019 if (data_pc < 0 ||
3020 data_pc + kBlocksRegionSize > static_cast<int>(rom_data.size())) {
3021 return absl::OutOfRangeError(absl::StrFormat(
3022 "Blocks data region out of range for loader page %d", page + 1));
3023 }
3024 (*destination_pcs)[page] = data_pc;
3025 }
3026
3027 constexpr int kLengthMetadataEnd = kBlocksLength + 2;
3028 for (size_t page = 0; page < destination_pcs->size(); ++page) {
3029 const int page_begin = (*destination_pcs)[page];
3030 const int page_end = page_begin + kBlocksRegionSize;
3031 if (HalfOpenRangesOverlap(page_begin, page_end, kBlocksLength,
3032 kLengthMetadataEnd)) {
3033 return absl::FailedPreconditionError(absl::StrFormat(
3034 "Blocks data page %d at PC [0x%05X, 0x%05X) overlaps block-table "
3035 "length metadata [0x%05X, 0x%05X)",
3036 page + 1, page_begin, page_end, kBlocksLength, kLengthMetadataEnd));
3037 }
3038
3039 for (size_t loader = 0; loader < kBlocksPointerSlots.size(); ++loader) {
3040 // Each destination operand is embedded in a seven-byte loader
3041 // instruction: BF ll hh bb 9D ll hh. Treat the complete instruction as
3042 // metadata so a table write cannot corrupt either opcode or operand.
3043 const int loader_begin = kBlocksPointerSlots[loader] - 1;
3044 const int loader_end = kBlocksPointerSlots[loader] + 6;
3045 if (HalfOpenRangesOverlap(page_begin, page_end, loader_begin,
3046 loader_end)) {
3047 return absl::FailedPreconditionError(absl::StrFormat(
3048 "Blocks data page %d at PC [0x%05X, 0x%05X) overlaps loader %d "
3049 "opcode/operand metadata [0x%05X, 0x%05X)",
3050 page + 1, page_begin, page_end, loader + 1, loader_begin,
3051 loader_end));
3052 }
3053 }
3054
3055 for (size_t previous = 0; previous < page; ++previous) {
3056 const int previous_begin = (*destination_pcs)[previous];
3057 const int previous_end = previous_begin + kBlocksRegionSize;
3058 if (HalfOpenRangesOverlap(page_begin, page_end, previous_begin,
3059 previous_end)) {
3060 return absl::FailedPreconditionError(absl::StrFormat(
3061 "Blocks data pages %d and %d overlap at PC ranges [0x%05X, "
3062 "0x%05X) and [0x%05X, 0x%05X)",
3063 previous + 1, page + 1, previous_begin, previous_end, page_begin,
3064 page_end));
3065 }
3066 }
3067 }
3068
3069 return absl::OkStatus();
3070}
3071
3072} // namespace
3073
3074absl::Status SaveAllBlocks(Rom* rom) {
3075 if (!rom || !rom->is_loaded()) {
3076 return absl::InvalidArgumentError("ROM not loaded");
3077 }
3078 const auto& rom_data = rom->vector();
3079 if (kBlocksLength + 1 >= static_cast<int>(rom_data.size())) {
3080 return absl::OutOfRangeError("Blocks length out of range");
3081 }
3082 int blocks_count =
3083 (rom_data[kBlocksLength + 1] << 8) | rom_data[kBlocksLength];
3084 std::array<int, 4> destination_pcs{};
3086 PreflightBlocksLoaderDestinations(rom_data, &destination_pcs));
3087 if (blocks_count <= 0) {
3088 return absl::OkStatus();
3089 }
3090 for (int r = 0; r < 4; ++r) {
3091 const int pc = destination_pcs[r];
3092 int off = r * kBlocksRegionSize;
3093 int len = std::min(kBlocksRegionSize, blocks_count - off);
3094 if (len <= 0)
3095 break;
3096 std::vector<uint8_t> chunk(rom_data.begin() + pc,
3097 rom_data.begin() + pc + len);
3098 RETURN_IF_ERROR(rom->WriteVector(pc, chunk));
3099 }
3101 rom->WriteWord(kBlocksLength, static_cast<uint16_t>(blocks_count)));
3102 return absl::OkStatus();
3103}
3104
3105absl::Status SaveAllBlocks(Rom* rom, int room_count,
3106 const std::function<const Room*(int)>& room_lookup) {
3107 if (!rom || !rom->is_loaded()) {
3108 return absl::InvalidArgumentError("ROM not loaded");
3109 }
3110 const auto& rom_data = rom->vector();
3111 if (kBlocksLength + 1 >= static_cast<int>(rom_data.size())) {
3112 return absl::OutOfRangeError("Blocks length out of range");
3113 }
3114
3115 std::array<int, 4> destination_pcs{};
3117 PreflightBlocksLoaderDestinations(rom_data, &destination_pcs));
3118
3119 // Read the original block buffer by dereferencing the four pointer
3120 // slots. We need this so unmaterialized / header-only rooms can have
3121 // their entries preserved verbatim — only rooms whose blocks were
3122 // actually loaded into memory get re-encoded from `tile_objects_`.
3123 // This prevents the editor migration from silently dropping vanilla
3124 // blocks for any room the user hasn't materialized yet.
3125 const int original_count_word =
3126 (rom_data[kBlocksLength + 1] << 8) | rom_data[kBlocksLength];
3127 const int original_byte_len = std::max(0, original_count_word);
3128 std::vector<uint8_t> original_buffer(original_byte_len, 0);
3129 for (int r = 0; r < 4; ++r) {
3130 const int pc = destination_pcs[r];
3131 const int off = r * kBlocksRegionSize;
3132 const int len = std::min(kBlocksRegionSize, original_byte_len - off);
3133 if (len <= 0)
3134 break;
3135 std::copy_n(rom_data.begin() + pc, len, original_buffer.begin() + off);
3136 }
3137 const int original_slot_count = original_byte_len / 4;
3138
3139 // Build:
3140 // - `slot_replacements`: for each existing slot whose room_id is
3141 // "owned" by an editor-loaded room, the re-encoded bytes (or
3142 // absent if the block was deleted in memory).
3143 // - `owned_room_ids`: the set of room_ids whose blocks were
3144 // materialized (so unmaterialized / header-only rooms can be
3145 // preserved verbatim from `original_buffer`).
3146 // - `appended`: blocks with `load_order == kBlockLoadOrderNew`,
3147 // appended to the end in creation order.
3148 struct EncodedBlock {
3149 PushableBlockBytes bytes;
3150 const RoomObject* source_object;
3151 };
3152 std::unordered_set<uint16_t> owned_room_ids;
3153 std::unordered_set<int> claimed_load_orders;
3154 std::unordered_map<int, EncodedBlock> slot_replacements;
3155 std::vector<EncodedBlock> appended;
3156 for (int rid = 0; rid < room_count; ++rid) {
3157 const Room* room = room_lookup(rid);
3158 if (room == nullptr)
3159 continue;
3160 if (!room->AreBlocksLoaded()) {
3161 if (room->blocks_dirty()) {
3162 return absl::FailedPreconditionError(absl::StrFormat(
3163 "Room 0x%03X has unsaved pushable-block edits, but its block "
3164 "table is not loaded. Load the room's blocks before saving.",
3165 rid));
3166 }
3167 continue; // Header-only — preserve its slots verbatim from ROM.
3168 }
3169 owned_room_ids.insert(static_cast<uint16_t>(rid));
3170 for (const auto& obj : room->GetTileObjects()) {
3171 if ((obj.options() & ObjectOption::Block) != ObjectOption::Block)
3172 continue;
3174 ValidateSpecialObjectDrawLayerSelector(obj, rid, "Pushable block"));
3175 PushableBlockEntry encoded_entry;
3176 encoded_entry.room_id = static_cast<uint16_t>(rid);
3177 encoded_entry.px = obj.x();
3178 encoded_entry.py = obj.y();
3179 encoded_entry.draw_layer = obj.GetLayerValue();
3180 encoded_entry.behavior_layer = obj.block_behavior_layer();
3181 const PushableBlockBytes encoded =
3182 EncodePushableBlockEntry(encoded_entry);
3183 const EncodedBlock encoded_block{encoded, &obj};
3184 const int load_order = obj.block_load_order();
3185 if (load_order >= 0 && !claimed_load_orders.insert(load_order).second) {
3186 return absl::FailedPreconditionError(absl::StrFormat(
3187 "Room 0x%03X has multiple pushable blocks claiming non-new "
3188 "load-order slot %d",
3189 rid, load_order));
3190 }
3191 if (load_order == RoomObject::kBlockLoadOrderNew) {
3192 appended.push_back(encoded_block);
3193 } else if (load_order >= 0 && load_order < original_slot_count) {
3194 const int original_offset = load_order * 4;
3195 const uint16_t original_room_id =
3196 static_cast<uint16_t>(original_buffer[original_offset] |
3197 (original_buffer[original_offset + 1] << 8));
3198 if (original_room_id != static_cast<uint16_t>(rid)) {
3199 // Undo/redo snapshots can restore the load order that was valid
3200 // before a prior save compacted the global table. Never let that
3201 // stale identity replace a different room's entry; preserve the
3202 // object by appending it as a newly reconciled entry instead.
3203 appended.push_back(encoded_block);
3204 continue;
3205 }
3206 slot_replacements.emplace(load_order, encoded_block);
3207 } else {
3208 // load_order points outside the original buffer (e.g. ROM
3209 // changed under us). Treat as new.
3210 appended.push_back(encoded_block);
3211 }
3212 }
3213 }
3214
3215 // Walk the original buffer slot-by-slot, replacing entries owned by
3216 // materialized rooms and preserving the rest verbatim.
3217 std::vector<uint8_t> output;
3218 output.reserve(original_byte_len + appended.size() * 4);
3219 std::vector<std::pair<const RoomObject*, int>> load_order_updates;
3220 load_order_updates.reserve(slot_replacements.size() + appended.size());
3221 const auto append_encoded_block =
3222 [&output, &load_order_updates](const EncodedBlock& block) {
3223 const int output_slot = static_cast<int>(output.size() / 4);
3224 output.push_back(block.bytes.b1);
3225 output.push_back(block.bytes.b2);
3226 output.push_back(block.bytes.b3);
3227 output.push_back(block.bytes.b4);
3228 load_order_updates.emplace_back(block.source_object, output_slot);
3229 };
3230 for (int slot = 0; slot < original_slot_count; ++slot) {
3231 const uint8_t b1 = original_buffer[slot * 4 + 0];
3232 const uint8_t b2 = original_buffer[slot * 4 + 1];
3233 const uint16_t slot_room_id = static_cast<uint16_t>(b1 | (b2 << 8));
3234 if (owned_room_ids.contains(slot_room_id)) {
3235 const auto it = slot_replacements.find(slot);
3236 if (it == slot_replacements.end()) {
3237 // The block at this slot was deleted in memory. Skip it,
3238 // shrinking the output.
3239 continue;
3240 }
3241 append_encoded_block(it->second);
3242 } else {
3243 // Unmaterialized / header-only room: keep the original bytes.
3244 output.push_back(b1);
3245 output.push_back(b2);
3246 output.push_back(original_buffer[slot * 4 + 2]);
3247 output.push_back(original_buffer[slot * 4 + 3]);
3248 }
3249 }
3250 // Append newly-added blocks (load_order == kBlockLoadOrderNew) at the
3251 // tail in creation order. Anything in slot_replacements that didn't
3252 // match a slot was already routed to `appended` above.
3253 for (const auto& block : appended) {
3254 append_encoded_block(block);
3255 }
3256
3257 // LoadAndBuildRoom's block scan is a do-while loop: it always reads the
3258 // entry at $7EF940 before adding four and comparing against this byte
3259 // length. A zero limit can therefore never terminate at the first boundary;
3260 // the 16-bit index walks beyond the 0x200-byte WRAM table until it wraps.
3261 // Fail before the first ROM write and keep edited rooms dirty rather than
3262 // emitting a runtime-unsafe empty table.
3263 if (output.empty()) {
3264 return absl::FailedPreconditionError(
3265 "Pushable-block table cannot be empty: ALTTP's runtime scan reads one "
3266 "entry before comparing the byte-length limit. Keep at least one "
3267 "pushable block, or patch the runtime loop before removing the last "
3268 "entry.");
3269 }
3270
3271 // Capacity check against the vanilla 128-entry cap.
3272 const int kMaxEntries = (4 * kBlocksRegionSize) / 4;
3273 if (static_cast<int>(output.size() / 4) > kMaxEntries) {
3274 return absl::FailedPreconditionError(absl::StrCat(
3275 "Pushable-block table overflow: ", output.size() / 4,
3276 " entries exceeds the vanilla cap of ", kMaxEntries,
3277 " (expand layout requires repointing all 4 LDA.l operand slots; "
3278 "out of scope for this encoder)."));
3279 }
3280
3281 // Build the write plan from the four destinations preflighted above. Doing
3282 // the topology check before encoding means direct callers that do not wrap
3283 // this public API in a transaction cannot discover a bad later page only
3284 // after an earlier page has already been written.
3285 const int total_bytes = static_cast<int>(output.size());
3286 struct BlockWriteDestination {
3287 int pc;
3288 int output_offset;
3289 int length;
3290 };
3291 std::vector<BlockWriteDestination> write_destinations;
3292 write_destinations.reserve(4);
3293 for (int r = 0; r < 4; ++r) {
3294 const int off = r * kBlocksRegionSize;
3295 const int len = std::min(kBlocksRegionSize, total_bytes - off);
3296 if (len <= 0)
3297 break;
3298 write_destinations.push_back({destination_pcs[r], off, len});
3299 }
3300
3301 // Write each prevalidated region. We do not relocate the data — the four
3302 // operand slots keep pointing at their existing SNES addresses.
3303 for (const auto& destination : write_destinations) {
3304 std::vector<uint8_t> chunk(
3305 output.begin() + destination.output_offset,
3306 output.begin() + destination.output_offset + destination.length);
3307 RETURN_IF_ERROR(rom->WriteVector(destination.pc, chunk));
3308 }
3309
3311 rom->WriteWord(kBlocksLength, static_cast<uint16_t>(total_bytes)));
3312
3313 // Deleting an entry compacts every following slot. Rebase each loaded
3314 // object's identity to its committed output slot so a later no-op save does
3315 // not try to replace the stale pre-compaction slot and silently drop it.
3316 // This metadata stays untouched until every ROM write succeeds, matching
3317 // the dirty-state failure contract below.
3318 for (const auto& [object, load_order] : load_order_updates) {
3319 const_cast<RoomObject*>(object)->set_block_load_order(load_order);
3320 }
3321 for (int room_id = 0; room_id < room_count; ++room_id) {
3322 if (const Room* room = room_lookup(room_id);
3323 room != nullptr && room->AreBlocksLoaded() && room->blocks_dirty()) {
3324 const_cast<Room*>(room)->ClearBlocksDirty();
3325 }
3326 }
3327 return absl::OkStatus();
3328}
3329
3330template <typename RoomLookup>
3331absl::Status SaveAllCollisionImpl(Rom* rom, int room_count,
3332 RoomLookup&& room_lookup) {
3333 if (!rom || !rom->is_loaded()) {
3334 return absl::InvalidArgumentError("ROM not loaded");
3335 }
3336
3337 // If the custom collision region doesn't exist (vanilla ROM), treat as a noop
3338 // only when there are no pending custom collision edits. This avoids silently
3339 // dropping user-authored collision changes on ROMs that don't support the
3340 // expanded collision bank.
3341 const auto& rom_data = rom->vector();
3342 const int ptrs_size = kNumberOfRooms * 3;
3343 const bool has_ptr_table = HasCustomCollisionPointerTable(rom_data.size());
3344 const bool has_data_region = HasCustomCollisionDataRegion(rom_data.size());
3345
3346 if (!has_ptr_table) {
3347 for (int room_id = 0; room_id < room_count; ++room_id) {
3348 const Room* room = room_lookup(room_id);
3349 if (room != nullptr && room->custom_collision_dirty()) {
3350 return absl::FailedPreconditionError(
3351 "Custom collision region not present in this ROM");
3352 }
3353 }
3354 return absl::OkStatus();
3355 }
3356
3357 if (!has_data_region) {
3358 for (int room_id = 0; room_id < room_count; ++room_id) {
3359 const Room* room = room_lookup(room_id);
3360 if (room != nullptr && room->custom_collision_dirty()) {
3361 return absl::FailedPreconditionError(
3362 "Custom collision data region not present in this ROM");
3363 }
3364 }
3365 return absl::OkStatus();
3366 }
3367
3368 // Save-time guardrails: custom collision writes must never clobber the
3369 // reserved WaterFill tail region (Oracle of Secrets).
3371 RETURN_IF_ERROR(fence.Allow(
3372 static_cast<uint32_t>(kCustomCollisionRoomPointers),
3373 static_cast<uint32_t>(kCustomCollisionRoomPointers + ptrs_size),
3374 "CustomCollisionPointers"));
3376 fence.Allow(static_cast<uint32_t>(kCustomCollisionDataPosition),
3377 static_cast<uint32_t>(kCustomCollisionDataSoftEnd),
3378 "CustomCollisionData"));
3379 yaze::rom::ScopedWriteFence scope(rom, &fence);
3380
3381 const int room_limit = std::min(room_count, kNumberOfRooms);
3382 for (int room_id = 0; room_id < room_limit; ++room_id) {
3383 const Room* room = room_lookup(room_id);
3384 if (room == nullptr || !room->custom_collision_dirty()) {
3385 continue;
3386 }
3387
3388 const int actual_room_id = room->id();
3389 const int ptr_offset = kCustomCollisionRoomPointers + (actual_room_id * 3);
3390 if (ptr_offset + 2 >= static_cast<int>(rom_data.size())) {
3391 return absl::OutOfRangeError("Custom collision pointer out of range");
3392 }
3393
3394 if (!room->has_custom_collision()) {
3395 // Disable: clear the pointer entry.
3396 RETURN_IF_ERROR(rom->WriteByte(ptr_offset, 0));
3397 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 1, 0));
3398 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 2, 0));
3399 const_cast<Room*>(room)->ClearCustomCollisionDirty();
3400 continue;
3401 }
3402
3403 // Treat an all-zero map as disabled to avoid wasting space.
3404 bool any = false;
3405 for (uint8_t v : room->custom_collision().tiles) {
3406 if (v != 0) {
3407 any = true;
3408 break;
3409 }
3410 }
3411 if (!any) {
3412 RETURN_IF_ERROR(rom->WriteByte(ptr_offset, 0));
3413 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 1, 0));
3414 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 2, 0));
3415 const_cast<Room*>(room)->ClearCustomCollisionDirty();
3416 continue;
3417 }
3418
3420 WriteTrackCollision(rom, actual_room_id, room->custom_collision()));
3421 const_cast<Room*>(room)->ClearCustomCollisionDirty();
3422 }
3423
3424 return absl::OkStatus();
3425}
3426
3427absl::Status SaveAllCollision(Rom* rom, absl::Span<Room> rooms) {
3428 return SaveAllCollisionImpl(
3429 rom, static_cast<int>(rooms.size()),
3430 [&rooms](int room_id) { return &rooms[room_id]; });
3431}
3432
3433absl::Status SaveAllCollision(Rom* rom, int room_count,
3434 const std::function<Room*(int)>& room_lookup) {
3435 return SaveAllCollisionImpl(rom, room_count, room_lookup);
3436}
3437
3438absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
3440 if (rom == nullptr || !rom->is_loaded()) {
3441 return absl::InvalidArgumentError("ROM not loaded");
3442 }
3443 const auto& rom_data = rom->vector();
3444 if (kChestsLengthPointer + 1 >= static_cast<int>(rom_data.size()) ||
3445 kChestsDataPointer1 + 2 >= static_cast<int>(rom_data.size())) {
3446 return absl::OutOfRangeError("Chest pointers out of range");
3447 }
3448
3449 const uint32_t data_pointer =
3450 (static_cast<uint32_t>(rom_data[kChestsDataPointer1 + 2]) << 16) |
3451 (static_cast<uint32_t>(rom_data[kChestsDataPointer1 + 1]) << 8) |
3452 rom_data[kChestsDataPointer1];
3453 const uint32_t data_pc = SnesToPc(data_pointer);
3454 if (data_pc > rom_data.size() ||
3455 static_cast<size_t>(kChestTableCapacityBytes) >
3456 rom_data.size() - static_cast<size_t>(data_pc)) {
3457 return absl::OutOfRangeError("Chest data region out of range");
3458 }
3459 const uint32_t data_end =
3460 data_pc + static_cast<uint32_t>(kChestTableCapacityBytes);
3461 const auto overlaps = [](uint32_t begin, uint32_t end, uint32_t other_begin,
3462 uint32_t other_end) {
3463 return begin < other_end && other_begin < end;
3464 };
3465 if (overlaps(data_pc, data_end, kChestsLengthPointer,
3466 kChestsLengthPointer + 2) ||
3467 overlaps(data_pc, data_end, kChestsDataPointer1,
3468 kChestsDataPointer1 + 3)) {
3469 return absl::FailedPreconditionError(
3470 "Chest data region overlaps chest metadata operands");
3471 }
3472
3473 return std::vector<std::pair<uint32_t, uint32_t>>{
3474 {static_cast<uint32_t>(kChestsLengthPointer),
3475 static_cast<uint32_t>(kChestsLengthPointer + 2)},
3476 {data_pc, data_end},
3477 };
3478}
3479
3480namespace {
3481
3483 uint16_t word = 0;
3484 uint8_t item = 0;
3485
3486 uint16_t room_id() const { return word & 0x7FFF; }
3487};
3488
3489// Parse current ROM chest data without grouping or normalizing records.
3490// `byte_length` is the runtime byte count at kChestsLengthPointer.
3491std::vector<PhysicalChestRecord> ParsePhysicalRomChests(
3492 const std::vector<uint8_t>& rom_data, int cpos, int byte_length) {
3493 std::vector<PhysicalChestRecord> records;
3494 const int record_count = byte_length / kChestTableRecordSize;
3495 records.reserve(record_count);
3496 for (int i = 0; i < record_count; ++i) {
3497 const int off = cpos + i * kChestTableRecordSize;
3498 if (off < 0 ||
3499 off + kChestTableRecordSize > static_cast<int>(rom_data.size())) {
3500 break;
3501 }
3502 const uint16_t word =
3503 (static_cast<uint16_t>(rom_data[off + 1]) << 8) | rom_data[off];
3504 records.push_back(PhysicalChestRecord{word, rom_data[off + 2]});
3505 }
3506 return records;
3507}
3508
3509void AppendChestRecord(std::vector<uint8_t>* bytes, uint16_t word,
3510 uint8_t item) {
3511 bytes->push_back(word & 0xFF);
3512 bytes->push_back((word >> 8) & 0xFF);
3513 bytes->push_back(item);
3514}
3515
3516void AppendEditedChestRecord(std::vector<uint8_t>* bytes, int room_id,
3517 const chest_data& chest) {
3518 const uint16_t word = static_cast<uint16_t>(room_id) |
3519 (chest.size ? static_cast<uint16_t>(0x8000) : 0);
3520 AppendChestRecord(bytes, word, chest.id);
3521}
3522
3523void AppendChangedChestRuns(uint32_t pc, absl::Span<const uint8_t> expected,
3524 absl::Span<const uint8_t> replacement,
3525 std::vector<ChestWriteRun>* writes) {
3526 size_t cursor = 0;
3527 while (cursor < replacement.size()) {
3528 if (replacement[cursor] == expected[cursor]) {
3529 ++cursor;
3530 continue;
3531 }
3532 const size_t begin = cursor;
3533 do {
3534 ++cursor;
3535 } while (cursor < replacement.size() &&
3536 replacement[cursor] != expected[cursor]);
3537
3538 ChestWriteRun run;
3539 run.pc = pc + static_cast<uint32_t>(begin);
3540 run.expected_bytes.assign(expected.begin() + begin,
3541 expected.begin() + cursor);
3542 run.replacement_bytes.assign(replacement.begin() + begin,
3543 replacement.begin() + cursor);
3544 if (!writes->empty() && writes->back().end() == run.pc) {
3545 writes->back().expected_bytes.insert(writes->back().expected_bytes.end(),
3546 run.expected_bytes.begin(),
3547 run.expected_bytes.end());
3548 writes->back().replacement_bytes.insert(
3549 writes->back().replacement_bytes.end(), run.replacement_bytes.begin(),
3550 run.replacement_bytes.end());
3551 } else {
3552 writes->push_back(std::move(run));
3553 }
3554 }
3555}
3556
3557void SortAndCoalesceChestRuns(std::vector<ChestWriteRun>* writes) {
3558 std::sort(writes->begin(), writes->end(),
3559 [](const ChestWriteRun& lhs, const ChestWriteRun& rhs) {
3560 return lhs.pc < rhs.pc;
3561 });
3562 std::vector<ChestWriteRun> merged;
3563 merged.reserve(writes->size());
3564 for (ChestWriteRun& write : *writes) {
3565 if (!merged.empty() && merged.back().end() == write.pc) {
3566 merged.back().expected_bytes.insert(merged.back().expected_bytes.end(),
3567 write.expected_bytes.begin(),
3568 write.expected_bytes.end());
3569 merged.back().replacement_bytes.insert(
3570 merged.back().replacement_bytes.end(),
3571 write.replacement_bytes.begin(), write.replacement_bytes.end());
3572 continue;
3573 }
3574 merged.push_back(std::move(write));
3575 }
3576 *writes = std::move(merged);
3577}
3578
3579std::vector<uint8_t> EncodeChestRoomState(int room_id, const Room& room) {
3580 std::vector<uint8_t> bytes;
3581 bytes.reserve(room.GetChests().size() * kChestTableRecordSize);
3582 for (const chest_data& chest : room.GetChests()) {
3583 AppendEditedChestRecord(&bytes, room_id, chest);
3584 }
3585 return bytes;
3586}
3587
3588absl::StatusOr<ChestSavePlan> BuildChestSavePlanImpl(
3589 const Rom* rom, int room_count,
3590 const std::function<const Room*(int)>& room_lookup) {
3591 if (rom == nullptr || !rom->is_loaded()) {
3592 return absl::InvalidArgumentError("ROM not loaded");
3593 }
3594 const auto& rom_data = rom->vector();
3595 if (kChestsLengthPointer + 1 >= static_cast<int>(rom_data.size()) ||
3596 kChestsDataPointer1 + 2 >= static_cast<int>(rom_data.size())) {
3597 return absl::OutOfRangeError("Chest pointers out of range");
3598 }
3599
3600 ChestSavePlan plan;
3601 plan.room_limit = std::min(room_count, kNumberOfRooms);
3602 std::vector<const Room*> dirty_rooms(kNumberOfRooms, nullptr);
3603 for (int room_id = 0; room_id < plan.room_limit; ++room_id) {
3604 const Room* room = room_lookup(room_id);
3605 if (room != nullptr && room->chests_dirty()) {
3606 dirty_rooms[room_id] = room;
3607 plan.any_dirty = true;
3608 plan.dirty_rooms.push_back(
3609 ChestDirtyRoomState{room_id, EncodeChestRoomState(room_id, *room)});
3610 }
3611 }
3612 if (!plan.any_dirty) {
3613 return plan;
3614 }
3615
3616 ASSIGN_OR_RETURN(auto potential_ranges, GetChestTableWriteRanges(rom));
3617 plan.data_pc = potential_ranges[1].first;
3618 std::copy_n(rom_data.begin() + kChestsDataPointer1,
3619 plan.pointer_operand.size(), plan.pointer_operand.begin());
3621 static_cast<uint16_t>((rom_data[kChestsLengthPointer + 1] << 8) |
3622 rom_data[kChestsLengthPointer]);
3623 plan.original_capacity_bytes.assign(
3624 rom_data.begin() + plan.data_pc,
3625 rom_data.begin() + plan.data_pc + kChestTableCapacityBytes);
3628 return absl::FailedPreconditionError(absl::StrFormat(
3629 "Chest table byte length %d is invalid (capacity %d)",
3630 static_cast<int>(plan.original_byte_length), kChestTableCapacityBytes));
3631 }
3632
3633 const auto physical_records = ParsePhysicalRomChests(
3634 rom_data, static_cast<int>(plan.data_pc), plan.original_byte_length);
3635 if (physical_records.size() !=
3636 static_cast<size_t>(plan.original_byte_length / kChestTableRecordSize)) {
3637 return absl::OutOfRangeError("Chest data region is truncated");
3638 }
3639
3640 std::vector<size_t> old_counts(kNumberOfRooms, 0);
3641 for (const PhysicalChestRecord& record : physical_records) {
3642 if (record.room_id() < kNumberOfRooms) {
3643 ++old_counts[record.room_id()];
3644 }
3645 }
3646
3647 size_t final_record_count = physical_records.size();
3648 for (int room_id = 0; room_id < plan.room_limit; ++room_id) {
3649 if (dirty_rooms[room_id] == nullptr) {
3650 continue;
3651 }
3652 final_record_count -= old_counts[room_id];
3653 final_record_count += dirty_rooms[room_id]->GetChests().size();
3654 }
3655 if (final_record_count > static_cast<size_t>(kChestTableCapacityRecords)) {
3656 return absl::ResourceExhaustedError(absl::StrFormat(
3657 "Chest table has %d records; capacity is %d",
3658 static_cast<int>(final_record_count), kChestTableCapacityRecords));
3659 }
3660
3661 std::vector<uint8_t> replacement_bytes;
3662 replacement_bytes.reserve(final_record_count * kChestTableRecordSize);
3663 std::vector<size_t> seen_counts(kNumberOfRooms, 0);
3664 for (const PhysicalChestRecord& record : physical_records) {
3665 const uint16_t room_id = record.room_id();
3666 const Room* dirty_room =
3667 room_id < kNumberOfRooms ? dirty_rooms[room_id] : nullptr;
3668 if (dirty_room == nullptr) {
3669 AppendChestRecord(&replacement_bytes, record.word, record.item);
3670 continue;
3671 }
3672
3673 const size_t occurrence = seen_counts[room_id]++;
3674 const auto& replacements = dirty_room->GetChests();
3675 if (occurrence < replacements.size()) {
3676 AppendEditedChestRecord(&replacement_bytes, room_id,
3677 replacements[occurrence]);
3678 }
3679 }
3680
3681 // Growth has no existing physical slot. Append extras in room-ID order so
3682 // repeated saves are deterministic while every pre-existing record keeps its
3683 // relative position.
3684 for (int room_id = 0; room_id < plan.room_limit; ++room_id) {
3685 const Room* dirty_room = dirty_rooms[room_id];
3686 if (dirty_room == nullptr) {
3687 continue;
3688 }
3689 const auto& replacements = dirty_room->GetChests();
3690 for (size_t i = seen_counts[room_id]; i < replacements.size(); ++i) {
3691 AppendEditedChestRecord(&replacement_bytes, room_id, replacements[i]);
3692 }
3693 }
3694
3695 if (replacement_bytes.size() != final_record_count * kChestTableRecordSize) {
3696 return absl::InternalError("Chest save plan size mismatch");
3697 }
3698
3699 const std::array<uint8_t, 2> expected_length = {
3700 static_cast<uint8_t>(plan.original_byte_length & 0xFF),
3701 static_cast<uint8_t>((plan.original_byte_length >> 8) & 0xFF)};
3702 const uint16_t replacement_length =
3703 static_cast<uint16_t>(replacement_bytes.size());
3704 const std::array<uint8_t, 2> encoded_replacement_length = {
3705 static_cast<uint8_t>(replacement_length & 0xFF),
3706 static_cast<uint8_t>((replacement_length >> 8) & 0xFF)};
3708 encoded_replacement_length, &plan.writes);
3710 absl::MakeConstSpan(plan.original_capacity_bytes)
3711 .subspan(0, replacement_bytes.size()),
3712 replacement_bytes, &plan.writes);
3714 return plan;
3715}
3716
3717int ReadRoomPotItemAddressPc(const std::vector<uint8_t>& rom_data,
3718 int room_id) {
3719 if (room_id < 0 || room_id >= kNumberOfRooms) {
3720 return -1;
3721 }
3722 const int ptr_off = kRoomItemsPointers + (room_id * 2);
3723 if (ptr_off < 0 || ptr_off + 1 >= static_cast<int>(rom_data.size())) {
3724 return -1;
3725 }
3726 const uint16_t item_ptr =
3727 (static_cast<uint16_t>(rom_data[ptr_off + 1]) << 8) | rom_data[ptr_off];
3728 if (item_ptr < 0x8000) {
3729 return -1;
3730 }
3731 const int item_addr = static_cast<int>(SnesToPc(0x010000 | item_ptr));
3732 return item_addr >= 0 && item_addr < static_cast<int>(rom_data.size())
3733 ? item_addr
3734 : -1;
3735}
3736
3737absl::StatusOr<PhysicalStreamInfo> GetPotItemStreamInfo(
3738 const std::vector<uint8_t>& rom_data, int room_id) {
3740 static_cast<int>(rom_data.size())) {
3741 return absl::OutOfRangeError("Room items pointer table out of range");
3742 }
3743 if (room_id < 0 || room_id >= kNumberOfRooms) {
3744 return absl::OutOfRangeError("Room ID out of range");
3745 }
3746
3747 std::vector<int> addresses(kNumberOfRooms, -1);
3748 for (int id = 0; id < kNumberOfRooms; ++id) {
3749 addresses[id] = ReadRoomPotItemAddressPc(rom_data, id);
3750 }
3751 const int hard_end =
3752 std::min(static_cast<int>(rom_data.size()), kRoomItemsDataEnd);
3753 PhysicalStreamInfo info = AnalyzePhysicalStream(addresses, room_id, hard_end);
3754 if (info.address < 0 || info.address >= hard_end) {
3755 return absl::FailedPreconditionError(
3756 "Room pot item pointer is null, invalid, or outside the item region");
3757 }
3758 return info;
3759}
3760
3761} // namespace
3762
3763absl::StatusOr<ChestSavePlan> BuildChestSavePlan(
3764 const Rom* rom, int room_count,
3765 const std::function<const Room*(int)>& room_lookup) {
3766 return BuildChestSavePlanImpl(rom, room_count, room_lookup);
3767}
3768
3769absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
3770GetDirtyChestWriteRanges(const Rom* rom, int room_count,
3771 const std::function<const Room*(int)>& room_lookup) {
3773 BuildChestSavePlan(rom, room_count, room_lookup));
3774 return plan.write_ranges();
3775}
3776
3778 Rom* rom, const ChestSavePlan& plan,
3779 const std::function<const Room*(int)>& room_lookup) {
3780 if (rom == nullptr || !rom->is_loaded()) {
3781 return absl::InvalidArgumentError("ROM not loaded");
3782 }
3783 if (!plan.any_dirty) {
3784 ASSIGN_OR_RETURN(ChestSavePlan canonical_plan,
3785 BuildChestSavePlan(rom, plan.room_limit, room_lookup));
3786 if (canonical_plan != plan) {
3787 return absl::FailedPreconditionError(
3788 "Chest save plan does not match canonical serialization");
3789 }
3790 return absl::OkStatus();
3791 }
3792
3793 const auto& rom_data = rom->vector();
3794 if (kChestsLengthPointer + 1 >= static_cast<int>(rom_data.size()) ||
3795 kChestsDataPointer1 + 2 >= static_cast<int>(rom_data.size()) ||
3796 plan.data_pc > rom_data.size() ||
3797 plan.original_capacity_bytes.size() > rom_data.size() - plan.data_pc) {
3798 return absl::FailedPreconditionError(
3799 "Chest save plan source is no longer addressable");
3800 }
3801 if (!std::equal(plan.pointer_operand.begin(), plan.pointer_operand.end(),
3802 rom_data.begin() + kChestsDataPointer1)) {
3803 return absl::FailedPreconditionError(
3804 "Chest save plan is stale: data pointer changed");
3805 }
3806 const uint16_t current_length =
3807 static_cast<uint16_t>((rom_data[kChestsLengthPointer + 1] << 8) |
3808 rom_data[kChestsLengthPointer]);
3809 if (current_length != plan.original_byte_length) {
3810 return absl::FailedPreconditionError(
3811 "Chest save plan is stale: runtime length changed");
3812 }
3813 if (!std::equal(plan.original_capacity_bytes.begin(),
3814 plan.original_capacity_bytes.end(),
3815 rom_data.begin() + plan.data_pc)) {
3816 return absl::FailedPreconditionError(
3817 "Chest save plan is stale: table bytes changed");
3818 }
3819 for (const ChestDirtyRoomState& state : plan.dirty_rooms) {
3820 const Room* room = room_lookup(state.room_id);
3821 if (room == nullptr || !room->chests_dirty() ||
3822 EncodeChestRoomState(state.room_id, *room) != state.encoded_chests) {
3823 return absl::FailedPreconditionError(absl::StrFormat(
3824 "Chest save plan is stale for room 0x%03X", state.room_id));
3825 }
3826 }
3827 ASSIGN_OR_RETURN(ChestSavePlan canonical_plan,
3828 BuildChestSavePlan(rom, plan.room_limit, room_lookup));
3829 if (canonical_plan != plan) {
3830 return absl::FailedPreconditionError(
3831 "Chest save plan does not match canonical serialization");
3832 }
3833
3834 yaze::ScopedRomTransaction transaction(*rom);
3836 for (const ChestWriteRun& write : plan.writes) {
3837 RETURN_IF_ERROR(fence.Allow(write.pc, write.end(), "ChestTableExactDelta"));
3838 }
3839 yaze::rom::ScopedWriteFence scope(rom, &fence);
3840 for (const ChestWriteRun& write : plan.writes) {
3841 RETURN_IF_ERROR(rom->WriteVector(write.pc, write.replacement_bytes));
3842 }
3843 for (const ChestDirtyRoomState& state : plan.dirty_rooms) {
3844 const_cast<Room*>(room_lookup(state.room_id))->ClearChestsDirty();
3845 }
3846 transaction.Commit();
3847 return absl::OkStatus();
3848}
3849
3850template <typename RoomLookup>
3851absl::Status SaveAllChestsImpl(Rom* rom, int room_count,
3852 RoomLookup&& room_lookup) {
3853 const std::function<const Room*(int)> lookup =
3854 std::forward<RoomLookup>(room_lookup);
3856 BuildChestSavePlan(rom, room_count, lookup));
3857 return ApplyChestSavePlan(rom, plan, lookup);
3858}
3859
3860absl::Status SaveAllChests(Rom* rom, absl::Span<const Room> rooms) {
3861 return SaveAllChestsImpl(rom, static_cast<int>(rooms.size()),
3862 [&rooms](int room_id) { return &rooms[room_id]; });
3863}
3864
3865absl::Status SaveAllChests(Rom* rom, int room_count,
3866 const std::function<const Room*(int)>& room_lookup) {
3867 return SaveAllChestsImpl(rom, room_count, room_lookup);
3868}
3869
3870template <typename RoomLookup>
3872 Rom* rom, int room_count, RoomLookup&& room_lookup,
3873 const DungeonStreamLayout* repack_layout = nullptr) {
3874 if (!rom || !rom->is_loaded()) {
3875 return absl::InvalidArgumentError("ROM not loaded");
3876 }
3877 const auto& rom_data = rom->vector();
3879 static_cast<int>(rom_data.size())) {
3880 return absl::OutOfRangeError("Room items pointer table out of range");
3881 }
3882
3883 const int room_limit = std::min(room_count, kNumberOfRooms);
3884 if (repack_layout != nullptr) {
3885 std::vector<DungeonStreamReplacement> replacements;
3886 for (int room_id = 0; room_id < room_limit; ++room_id) {
3887 const Room* room = room_lookup(room_id);
3888 if (room == nullptr || !room->pot_items_dirty()) {
3889 continue;
3890 }
3891
3892 DungeonStreamReplacement replacement;
3893 replacement.room_id = static_cast<uint32_t>(room_id);
3894 replacement.encoded_stream.reserve(room->GetPotItems().size() * 3 + 2);
3895 for (const PotItem& item : room->GetPotItems()) {
3896 replacement.encoded_stream.push_back(item.position & 0xFF);
3897 replacement.encoded_stream.push_back((item.position >> 8) & 0xFF);
3898 replacement.encoded_stream.push_back(item.item);
3899 }
3900 replacement.encoded_stream.push_back(0xFF);
3901 replacement.encoded_stream.push_back(0xFF);
3902 replacements.push_back(std::move(replacement));
3903 }
3904 if (replacements.empty()) {
3905 return absl::OkStatus();
3906 }
3907
3909 InventoryDungeonStreams(*rom, *repack_layout));
3911 PlanDungeonStreamRepack(inventory, replacements));
3913 for (const DungeonStreamReplacement& replacement : replacements) {
3914 if (const Room* room = room_lookup(replacement.room_id);
3915 room != nullptr) {
3916 const_cast<Room*>(room)->ClearPotItemsDirty();
3917 }
3918 }
3919 return absl::OkStatus();
3920 }
3921
3922 struct PendingPotItemWrite {
3923 int room_id = -1;
3924 int address = -1;
3925 std::vector<uint8_t> bytes;
3926 };
3927
3928 // Build and validate every dirty write before touching the ROM. A later
3929 // shared/overfull stream must not leave earlier rooms partially written.
3930 std::vector<PendingPotItemWrite> pending_writes;
3931 for (int room_id = 0; room_id < room_limit; ++room_id) {
3932 const Room* room = room_lookup(room_id);
3933 if (room == nullptr || !room->pot_items_dirty()) {
3934 continue;
3935 }
3936
3937 ASSIGN_OR_RETURN(const PhysicalStreamInfo stream_info,
3938 GetPotItemStreamInfo(rom_data, room_id));
3939 if (stream_info.shared) {
3940 return absl::FailedPreconditionError(absl::StrFormat(
3941 "Room %d pot item stream at PC 0x%06X is shared; repacking is "
3942 "required",
3943 room_id, stream_info.address));
3944 }
3945 if (stream_info.capacity() <= 0) {
3946 return absl::FailedPreconditionError(absl::StrFormat(
3947 "Room %d pot item stream has no safe physical boundary", room_id));
3948 }
3949
3950 PendingPotItemWrite pending;
3951 pending.room_id = room_id;
3952 pending.address = stream_info.address;
3953 for (const auto& pi : room->GetPotItems()) {
3954 pending.bytes.push_back(pi.position & 0xFF);
3955 pending.bytes.push_back((pi.position >> 8) & 0xFF);
3956 pending.bytes.push_back(pi.item);
3957 }
3958 pending.bytes.push_back(0xFF);
3959 pending.bytes.push_back(0xFF);
3960 if (static_cast<int>(pending.bytes.size()) > stream_info.capacity()) {
3961 return absl::ResourceExhaustedError(absl::StrFormat(
3962 "Room %d pot item data too large! Size: %d, Available: %d", room_id,
3963 static_cast<int>(pending.bytes.size()), stream_info.capacity()));
3964 }
3965 pending_writes.push_back(std::move(pending));
3966 }
3967
3968 for (const auto& pending : pending_writes) {
3969 const bool data_changed =
3970 !std::equal(pending.bytes.begin(), pending.bytes.end(),
3971 rom_data.begin() + pending.address);
3972 if (data_changed) {
3973 RETURN_IF_ERROR(rom->WriteVector(pending.address, pending.bytes));
3974 }
3975 }
3976
3977 for (const auto& pending : pending_writes) {
3978 if (const Room* room = room_lookup(pending.room_id); room != nullptr) {
3979 const_cast<Room*>(room)->ClearPotItemsDirty();
3980 }
3981 }
3982 return absl::OkStatus();
3983}
3984
3985absl::Status SaveAllPotItems(Rom* rom, absl::Span<const Room> rooms) {
3986 return SaveAllPotItemsImpl(rom, static_cast<int>(rooms.size()),
3987 [&rooms](int room_id) { return &rooms[room_id]; });
3988}
3989
3990absl::Status SaveAllPotItems(Rom* rom, absl::Span<const Room> rooms,
3991 const DungeonStreamLayout* repack_layout) {
3992 return SaveAllPotItemsImpl(
3993 rom, static_cast<int>(rooms.size()),
3994 [&rooms](int room_id) { return &rooms[room_id]; }, repack_layout);
3995}
3996
3997absl::Status SaveAllPotItems(
3998 Rom* rom, int room_count,
3999 const std::function<const Room*(int)>& room_lookup) {
4000 return SaveAllPotItemsImpl(rom, room_count, room_lookup);
4001}
4002
4003absl::Status SaveAllPotItems(Rom* rom, int room_count,
4004 const std::function<const Room*(int)>& room_lookup,
4005 const DungeonStreamLayout* repack_layout) {
4006 return SaveAllPotItemsImpl(rom, room_count, room_lookup, repack_layout);
4007}
4008
4010 auto rom_data = rom()->vector();
4011
4012 // Read blocks length
4013 int blocks_count =
4014 (rom_data[kBlocksLength + 1] << 8) | rom_data[kBlocksLength];
4015
4016 LOG_DEBUG("Room", "LoadBlocks: room_id=%d, blocks_count=%d", room_id_,
4017 blocks_count);
4018
4019 // Load block data from the four data regions.
4020 //
4021 // `kBlocksPointer1..4` are 3-byte SNES long-address operand slots
4022 // embedded in bank_02's LDA.l instructions (`$02:DAF9..$02:DB2E`),
4023 // not inline data offsets. Each operand encodes
4024 // `data_base + region_offset` where data_base is the SNES address
4025 // of `SpecialUnderworldObjects_pushable_block` ($04:F1DE in vanilla)
4026 // and region_offset is `r * 0x80`. The previous code read directly
4027 // from the operand slots, so it was decoding the LDA.l opcode
4028 // operand bytes as block data — silently corrupting every block on
4029 // load. `SaveAllBlocks` has always dereferenced these correctly;
4030 // load now matches.
4031 const int kRegionSize = 0x80;
4032 const int kPointerSlots[4] = {kBlocksPointer1, kBlocksPointer2,
4034 std::vector<uint8_t> blocks_data(blocks_count, 0);
4035 for (int r = 0; r < 4; ++r) {
4036 const int slot = kPointerSlots[r];
4037 if (slot + 2 >= static_cast<int>(rom_data.size())) {
4038 LOG_WARN("Room", "LoadBlocks: pointer slot %d out of range", r);
4039 return;
4040 }
4041 const absl::Status operand_status =
4042 ValidateBlocksLoaderPointerOperand(rom_data, slot);
4043 if (!operand_status.ok()) {
4044 LOG_WARN("Room", "LoadBlocks: %s",
4045 std::string(operand_status.message()).c_str());
4046 return;
4047 }
4048 const int snes =
4049 (rom_data[slot + 2] << 16) | (rom_data[slot + 1] << 8) | rom_data[slot];
4050 const int pc = SnesToPc(snes);
4051 const int off = r * kRegionSize;
4052 const int len = std::min(kRegionSize, blocks_count - off);
4053 if (len <= 0)
4054 break;
4055 if (pc < 0 || pc + len > static_cast<int>(rom_data.size())) {
4056 LOG_WARN("Room", "LoadBlocks: region %d data out of range", r);
4057 return;
4058 }
4059 std::copy_n(rom_data.begin() + pc, len, blocks_data.begin() + off);
4060 }
4061
4062 // Avoid duplication if LoadBlocks is called multiple times. Do this only
4063 // after the ROM pointer operands and data regions are known-good so a guard
4064 // failure cannot make existing in-memory block objects vanish.
4065 tile_objects_.erase(
4066 std::remove_if(tile_objects_.begin(), tile_objects_.end(),
4067 [](const RoomObject& obj) {
4068 return (obj.options() & ObjectOption::Block) !=
4069 ObjectOption::Nothing;
4070 }),
4071 tile_objects_.end());
4072
4073 // Parse blocks for this room (4 bytes per block entry).
4074 //
4075 // Vanilla scan (bank_01.asm:1162) walks the flat 396-byte table linearly
4076 // matching on room_id; there is no per-room 0xFFFF terminator. The
4077 // previous "break on b3==0xFF && b4==0xFF after room_id match" guard was
4078 // a phantom — it never fired in vanilla and would have prematurely
4079 // truncated a room's block list if a future tombstone happened to share
4080 // its room_id. Removed alongside the decoder fix.
4081 for (int i = 0; i + 3 < blocks_count; i += 4) {
4082 PushableBlockBytes bytes{blocks_data[i], blocks_data[i + 1],
4083 blocks_data[i + 2], blocks_data[i + 3]};
4084 const PushableBlockEntry entry = DecodePushableBlockEntry(bytes);
4085 if (entry.room_id != room_id_)
4086 continue;
4087
4088 RoomObject block_obj(0x0E00, entry.px, entry.py, 0, entry.draw_layer);
4089 block_obj.SetRom(rom_);
4092 // Capture the entry's slot index in the global buffer so
4093 // SaveAllBlocks can emit entries in vanilla authoring order
4094 // (interleaved across rooms; sorting by room_id would reshuffle
4095 // bytes and break byte equality on no-op saves).
4096 block_obj.set_block_load_order(i / 4);
4097 tile_objects_.push_back(block_obj);
4098
4099 LOG_DEBUG("Room", "Loaded block at (%d,%d) draw_layer=%d behavior_layer=%d",
4100 entry.px, entry.py, entry.draw_layer, entry.behavior_layer);
4101 }
4102 blocks_loaded_ = true;
4103}
4104
4106 if (!rom_ || !rom_->is_loaded())
4107 return;
4108 auto rom_data = rom()->vector();
4109 pot_items_.clear();
4110 pot_items_loaded_ = false;
4111
4112 // Load pot items
4113 // Format per ASM analysis (bank_01.asm):
4114 // - Pointer table at kRoomItemsPointers (0x01DB69)
4115 // - Each room has a pointer to item data
4116 // - Item data format: 3 bytes per item
4117 // - 2 bytes: position word (Y_hi, X_lo encoding)
4118 // - 1 byte: item type
4119 // - Terminated by 0xFFFF position word
4120
4121 int table_addr = kRoomItemsPointers; // 0x01DB69
4122
4123 // Read pointer for this room
4124 int ptr_addr = table_addr + (room_id_ * 2);
4125 if (ptr_addr + 1 >= static_cast<int>(rom_data.size()))
4126 return;
4127
4128 uint16_t item_ptr = (rom_data[ptr_addr + 1] << 8) | rom_data[ptr_addr];
4129
4130 // Convert to PC address (Bank 01 offset)
4131 int item_addr = SnesToPc(0x010000 | item_ptr);
4132
4133 // Read 3-byte entries until 0xFFFF terminator
4134 while (item_addr + 2 < static_cast<int>(rom_data.size())) {
4135 // Read position word (little endian)
4136 uint16_t position = (rom_data[item_addr + 1] << 8) | rom_data[item_addr];
4137
4138 // Check for terminator
4139 if (position == 0xFFFF)
4140 break;
4141
4142 // Read item type (3rd byte)
4143 uint8_t item_type = rom_data[item_addr + 2];
4144
4145 PotItem pot_item;
4146 pot_item.position = position;
4147 pot_item.item = item_type;
4148 pot_items_.push_back(pot_item);
4149
4150 item_addr += 3; // Move to next entry
4151 }
4152
4153 pot_items_loaded_ = true;
4154}
4155
4157 auto rom_data = rom()->vector();
4158
4159 // The legacy symbol `kPitCount` is the LDX.w immediate at PC 0x394A6
4160 // — the **maximum X offset** in the runtime CMP loop, not an entry
4161 // count. Total entries = `(max_offset / 2) + 1`. This function does
4162 // not actually consume the table contents (yaze has no editable
4163 // surface for pit-damage gating); it just resolves the dereferenced
4164 // address for diagnostic logging. See
4165 // `test/integration/zelda3/dungeon_save_region_test.cc` for the
4166 // format-pinning tests and `memory/project_dungeon_pit_audit.md`
4167 // for the audit conclusion.
4168 const int max_offset = rom_data[kPitCount];
4169 const int pit_entries = max_offset / 2 + 1;
4170
4171 const int pit_ptr = (rom_data[kPitPointer + 2] << 16) |
4172 (rom_data[kPitPointer + 1] << 8) | rom_data[kPitPointer];
4173
4174 LOG_DEBUG("Room",
4175 "LoadPits: room_id=%d, RoomsWithPitDamage entries=%d, "
4176 "table_snes=0x%06X",
4177 room_id_, pit_entries, pit_ptr);
4178
4179 // The per-room pit DESTINATION (where Link goes when falling through
4180 // a non-damaging pit) is unrelated to the global RoomsWithPitDamage
4181 // table read above. It lives in the room header and was loaded into
4182 // `pits_` (target room + target_layer) by `LoadRoomFromRom`. The
4183 // round-trip for that state goes through the room header save path,
4184 // not `SaveAllPits`.
4185 LOG_DEBUG("Room", "Per-room pit destination: target=%d, target_layer=%d",
4187}
4188
4189// ============================================================================
4190// Object Limit Counting (ZScream Feature Parity)
4191// ============================================================================
4192
4193std::map<DungeonLimit, int> Room::GetLimitedObjectCounts() const {
4194 auto counts = CreateLimitCounter();
4195
4196 // Count sprites
4197 counts[DungeonLimit::kSprites] = static_cast<int>(sprites_.size());
4198
4199 // Count overlords (sprites with ID > 0x40 are overlords in ALTTP)
4200 for (const auto& sprite : sprites_) {
4201 if (sprite.IsOverlord()) {
4202 counts[DungeonLimit::Overlords]++;
4203 }
4204 }
4205
4206 // Count chests
4207 counts[DungeonLimit::kChests] = static_cast<int>(chests_in_room_.size());
4208
4209 // Count doors (total and special)
4210 counts[DungeonLimit::kDoors] = static_cast<int>(doors_.size());
4211 for (const auto& door : doors_) {
4212 // Special doors: shutters and key-locked doors.
4213 const bool is_special = [&]() -> bool {
4214 switch (door.type) {
4229 return true;
4230 default:
4231 return false;
4232 }
4233 }();
4234 if (is_special) {
4236 }
4237 }
4238
4239 // Count stairs
4241 static_cast<int>(z3_staircases_.size());
4242
4243 // Count objects with specific options
4244 for (const auto& obj : tile_objects_) {
4245 auto options = obj.options();
4246
4247 // Count blocks
4248 if ((options & ObjectOption::Block) != ObjectOption::Nothing) {
4249 counts[DungeonLimit::Blocks]++;
4250 }
4251
4252 // Count torches
4253 if ((options & ObjectOption::Torch) != ObjectOption::Nothing) {
4254 counts[DungeonLimit::Torches]++;
4255 }
4256
4257 // Count star tiles (object IDs 0x11E and 0x11F)
4258 if (obj.id_ == 0x11E || obj.id_ == 0x11F) {
4259 counts[DungeonLimit::StarTiles]++;
4260 }
4261
4262 // Count somaria paths (object IDs in 0xF83-0xF8F range)
4263 if (obj.id_ >= 0xF83 && obj.id_ <= 0xF8F) {
4264 counts[DungeonLimit::SomariaLine]++;
4265 }
4266
4267 // Count staircase objects based on direction
4268 if ((options & ObjectOption::Stairs) != ObjectOption::Nothing) {
4269 // North-facing stairs: IDs 0x130-0x135
4270 if ((obj.id_ >= 0x130 && obj.id_ <= 0x135) || obj.id_ == 0x139 ||
4271 obj.id_ == 0x13A || obj.id_ == 0x13B) {
4272 counts[DungeonLimit::StairsNorth]++;
4273 }
4274 // South-facing stairs: IDs 0x13B-0x13D
4275 else if (obj.id_ >= 0x13C && obj.id_ <= 0x13F) {
4276 counts[DungeonLimit::StairsSouth]++;
4277 }
4278 }
4279
4280 // Count general manipulable objects
4281 if ((options & ObjectOption::Block) != ObjectOption::Nothing ||
4285 }
4286 }
4287
4288 return counts;
4289}
4290
4292 auto counts = GetLimitedObjectCounts();
4293 return yaze::zelda3::HasExceededLimits(counts);
4294}
4295
4296std::vector<DungeonLimitInfo> Room::GetExceededLimitDetails() const {
4297 auto counts = GetLimitedObjectCounts();
4298 return GetExceededLimits(counts);
4299}
4300
4301} // namespace zelda3
4302} // 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:631
const auto & vector() const
Definition rom.h:173
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
Definition rom.cc:703
absl::StatusOr< uint16_t > ReadWord(int offset) const
Definition rom.cc:571
auto data() const
Definition rom.h:169
auto size() const
Definition rom.h:168
bool is_loaded() const
Definition rom.h:155
absl::Status WriteWord(int addr, uint16_t value)
Definition rom.cc:650
absl::Status WriteLong(uint32_t addr, uint32_t value)
Definition rom.cc:677
absl::StatusOr< uint32_t > ReadLong(int offset) const
Definition rom.cc:578
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:2440
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:4291
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:2413
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:2559
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:2400
void SetStair1TargetLayer(uint8_t layer)
Definition room.h:816
void MarkGraphicsDirty()
Definition room.h:450
void LoadBlocks()
Definition room.cc:4009
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:4296
void ParseObjectsFromLocation(int objects_location)
Definition room.cc:1691
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:2619
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:2022
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:4193
void RenderRoomGraphics()
Definition room.cc:990
absl::Status SaveRoomHeader()
Definition room.cc:2295
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:1797
void SetEffect(EffectKey effect)
Definition room.h:704
absl::Status SaveObjectStreamHeader(const DungeonStreamLayout *layout=nullptr)
Definition room.cc:2124
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:2222
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:1625
void LoadPotItems()
Definition room.cc:4105
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:1555
std::vector< uint8_t > EncodeSprites() const
Definition room.cc:1885
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:2499
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:2462
void SetStair4TargetLayer(uint8_t layer)
Definition room.h:834
absl::Status AddObject(const RoomObject &object)
Definition room.cc:2386
absl::StatusOr< size_t > FindObjectAt(int x, int y, int layer) const
Definition room.cc:2430
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:3509
absl::Status ValidateSpecialObjectDrawLayerSelector(const RoomObject &object, int room_id, const char *object_type)
Definition room.cc:2787
int ReadRoomPotItemAddressPc(const std::vector< uint8_t > &rom_data, int room_id)
Definition room.cc:3717
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:3557
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:3523
std::vector< TorchSegment > ParseRomTorchSegments(const std::vector< uint8_t > &rom_data, int bytes_count)
Definition room.cc:2714
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:3006
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:3579
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:2760
bool HalfOpenRangesOverlap(int first_begin, int first_end, int second_begin, int second_end)
Definition room.cc:2975
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:2980
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:3588
void AppendEditedChestRecord(std::vector< uint8_t > *bytes, int room_id, const chest_data &chest)
Definition room.cc:3516
constexpr std::array< int, 4 > kBlocksPointerSlots
Definition room.cc:2972
std::vector< PhysicalChestRecord > ParsePhysicalRomChests(const std::vector< uint8_t > &rom_data, int cpos, int byte_length)
Definition room.cc:3491
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:3737
absl::Status ValidateLightableTorchForSave(const RoomObject &object, int room_id)
Definition room.cc:2801
constexpr int kBlocksPointer4
constexpr int kSpritesDataEndExclusive
absl::Status SaveAllChests(Rom *rom, absl::Span< const Room > rooms)
Definition room.cc:3860
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:3763
constexpr int kChestsLengthPointer
int FindMaxUsedSpriteAddress(Rom *rom)
Definition room.cc:1923
constexpr int kMessagesIdDungeon
absl::Status RelocateSpriteData(Rom *rom, int room_id, const std::vector< uint8_t > &encoded_bytes)
Definition room.cc:1963
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:3985
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:3871
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:3439
constexpr int kRoomItemsDataEnd
absl::Status SaveAllTorches(Rom *rom, absl::Span< const Room > rooms)
Definition room.cc:2916
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:2930
constexpr int kChestsDataPointer1
constexpr int kBlocksLength
absl::Status SaveAllBlocks(Rom *rom)
Definition room.cc:3074
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:3770
absl::Status SaveAllCollisionImpl(Rom *rom, int room_count, RoomLookup &&room_lookup)
Definition room.cc:3331
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:3851
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:2817
constexpr int kNumberOfRooms
absl::Status ApplyChestSavePlan(Rom *rom, const ChestSavePlan &plan, const std::function< const Room *(int)> &room_lookup)
Definition room.cc:3777
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:3427
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.