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