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