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