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
1949 if (auto res = LoadCustomCollisionMap(rom_, room_id_); res.ok()) {
1950 custom_collision_ = std::move(res.value());
1951 }
1952
1953 // Freshly loaded from ROM; not dirty until the editor mutates it.
1955 objects_loaded_ = true;
1959}
1960
1961void Room::ParseObjectsFromLocation(int objects_location) {
1962 const auto& rom_data = rom()->vector();
1963
1964 // Clear existing objects before parsing to prevent accumulation on reload
1965 tile_objects_.clear();
1966 doors_.clear();
1967 z3_staircases_.clear();
1968 int nbr_of_staircase = 0;
1969
1970 int pos = objects_location;
1971 uint8_t b1 = 0;
1972 uint8_t b2 = 0;
1973 uint8_t b3 = 0;
1974 int layer = 0;
1975 bool door = false;
1976 bool end_read = false;
1977
1978 // Enhanced parsing loop with bounds checking
1979 // ASM: Main object loop logic (implicit in structure)
1980 while (!end_read && pos < (int)rom_->size()) {
1981 // Check if we have enough bytes to read
1982 if (pos + 1 >= (int)rom_->size()) {
1983 break;
1984 }
1985
1986 b1 = rom_data[pos];
1987 b2 = rom_data[pos + 1];
1988
1989 // ASM Marker: 0xFF 0xFF - End of object list (next list in USDASM order).
1990 // Stored in RoomObject::layer_ as list index for EncodeObjects():
1991 // 0 = primary list (drawn to BG1/upper object buffer by default)
1992 // 1 = BG2 overlay list
1993 // 2 = BG1 overlay list (ObjectDrawer uses BG3 enum; still BG1 object path)
1994 if (b1 == 0xFF && b2 == 0xFF) {
1995 pos += 2; // Jump to next layer
1996 layer++;
1997 LOG_DEBUG(
1998 "Room", "Room %03X: Object list transition to index %d (%s)",
1999 room_id_, layer,
2000 layer == 1 ? "BG2 overlay" : (layer == 2 ? "BG1 overlay" : "END"));
2001 door = false;
2002 if (layer == 3) {
2003 break;
2004 }
2005 continue;
2006 }
2007
2008 // ASM Marker: 0xF0 0xFF - Start of Door List
2009 // See RoomDraw_DoorObject ($018916) logic
2010 if (b1 == 0xF0 && b2 == 0xFF) {
2011 pos += 2; // Jump to door section
2012 door = true;
2013 continue;
2014 }
2015
2016 // Check if we have enough bytes for object data
2017 if (pos + 2 >= (int)rom_->size()) {
2018 break;
2019 }
2020
2021 b3 = rom_data[pos + 2];
2022 if (door) {
2023 pos += 2;
2024 } else {
2025 pos += 3;
2026 }
2027
2028 if (!door) {
2029 // ASM: RoomDraw_RoomObject ($01893C)
2030 // Handles Subtype 1, 2, 3 parsing based on byte values
2032 b1, b2, b3, static_cast<uint8_t>(layer));
2033
2034 LOG_DEBUG("Room", "Room %03X: Object 0x%03X at (%d,%d) stream=%d (%s)",
2035 room_id_, r.id_, r.x_, r.y_, layer,
2036 layer == 0 ? "Primary"
2037 : (layer == 1 ? "BG2 overlay" : "BG1 overlay"));
2038
2039 // Validate object ID before adding to the room
2040 // Object IDs can be up to 12-bit (0xFFF) to support Type 3 objects
2041 if (r.id_ >= 0 && r.id_ <= 0xFFF) {
2042 r.SetRom(rom_);
2043 tile_objects_.push_back(r);
2044
2045 // Handle special object types (staircases, chests, etc.)
2046 HandleSpecialObjects(r.id_, r.x(), r.y(), nbr_of_staircase);
2047 }
2048 } else {
2049 // Handle door objects
2050 // ASM format (from RoomDraw_DoorObject):
2051 // b1: bits 4-7 = position index, bits 0-1 = direction
2052 // b2: door type (full byte)
2053 auto door = Door::FromRomBytes(b1, b2);
2054 LOG_DEBUG("Room",
2055 "ParseDoor: room=%d b1=0x%02X b2=0x%02X pos=%d dir=%d type=%d",
2056 room_id_, b1, b2, door.position,
2057 static_cast<int>(door.direction), static_cast<int>(door.type));
2058 doors_.push_back(door);
2059 }
2060 }
2061}
2062
2063// ============================================================================
2064// Object Saving Implementation (Phase 1, Task 1.3)
2065// ============================================================================
2066
2067std::vector<uint8_t> Room::EncodeObjects() const {
2068 std::vector<uint8_t> bytes;
2069
2070 // Organize objects by ROM object-stream index (0=primary, 1=BG2 overlay,
2071 // 2=BG1 overlay), stored in RoomObject::layer_ / GetLayerValue().
2072 std::vector<RoomObject> layer0_objects;
2073 std::vector<RoomObject> layer1_objects;
2074 std::vector<RoomObject> layer2_objects;
2075
2076 // IMPORTANT: Torches and pushable blocks are stored in global per-dungeon
2077 // tables (see USDASM: LoadAndBuildRoom $01:873A). They are drawn after the
2078 // room object stream passes, so they must never be encoded into the room
2079 // object stream.
2080 for (const auto& obj : tile_objects_) {
2081 if ((obj.options() & ObjectOption::Torch) != ObjectOption::Nothing) {
2082 continue;
2083 }
2084 if ((obj.options() & ObjectOption::Block) != ObjectOption::Nothing) {
2085 continue;
2086 }
2087 switch (obj.GetLayerValue()) {
2088 case 0:
2089 layer0_objects.push_back(obj);
2090 break;
2091 case 1:
2092 layer1_objects.push_back(obj);
2093 break;
2094 case 2:
2095 layer2_objects.push_back(obj);
2096 break;
2097 }
2098 }
2099
2100 // Object stream format (USDASM bank_01.asm LoadAndBuildRoom / RoomDraw_DrawAllObjects):
2101 // - List index 0 (primary) terminated by $FFFF
2102 // - List index 1 (BG2 overlay) terminated by $FFFF
2103 // - List index 2 (BG1 overlay) ends with door marker $FFF0 (bytes F0 FF), then
2104 // 2-byte door entries, and finally $FFFF which terminates both the door
2105 // list and the third object list.
2106 //
2107 // NOTE: We always emit the door marker and a terminator, even if there are
2108 // zero doors, because vanilla room data does so as well.
2109
2110 // Encode list index 0 (primary)
2111 for (const auto& obj : layer0_objects) {
2112 auto encoded = obj.EncodeObjectToBytes();
2113 bytes.push_back(encoded.b1);
2114 bytes.push_back(encoded.b2);
2115 bytes.push_back(encoded.b3);
2116 }
2117 bytes.push_back(0xFF);
2118 bytes.push_back(0xFF);
2119
2120 // Encode list index 1 (BG2 overlay)
2121 for (const auto& obj : layer1_objects) {
2122 auto encoded = obj.EncodeObjectToBytes();
2123 bytes.push_back(encoded.b1);
2124 bytes.push_back(encoded.b2);
2125 bytes.push_back(encoded.b3);
2126 }
2127 bytes.push_back(0xFF);
2128 bytes.push_back(0xFF);
2129
2130 // Encode list index 2 (BG1 overlay)
2131 for (const auto& obj : layer2_objects) {
2132 auto encoded = obj.EncodeObjectToBytes();
2133 bytes.push_back(encoded.b1);
2134 bytes.push_back(encoded.b2);
2135 bytes.push_back(encoded.b3);
2136 }
2137
2138 // ASM marker 0xF0 0xFF - start of door list (RoomDraw_DrawAllObjects checks
2139 // for word $FFF0).
2140 bytes.push_back(0xF0);
2141 bytes.push_back(0xFF);
2142 for (const auto& door : doors_) {
2143 auto [b1, b2] = door.EncodeBytes();
2144 bytes.push_back(b1);
2145 bytes.push_back(b2);
2146 }
2147
2148 // Door list terminator (word $FFFF). This is also the list-2 terminator.
2149 bytes.push_back(0xFF);
2150 bytes.push_back(0xFF);
2151
2152 return bytes;
2153}
2154
2155std::vector<uint8_t> Room::EncodeSprites() const {
2156 std::vector<uint8_t> bytes;
2157
2158 for (const auto& sprite : sprites_) {
2159 uint8_t b1, b2, b3;
2160
2161 // b3 is simply the ID
2162 b3 = sprite.id();
2163
2164 // b2 = (X & 0x1F) | ((Flags & 0x07) << 5)
2165 // Flags 0-2 come from b2 5-7
2166 b2 = (sprite.x() & 0x1F) | ((sprite.subtype() & 0x07) << 5);
2167
2168 // b1 = (Y & 0x1F) | ((Flags & 0x18) << 2) | ((Layer & 1) << 7)
2169 // Flags 3-4 come from b1 5-6. (0x18 is 00011000)
2170 // Layer bit 0 comes from b1 7
2171 b1 = (sprite.y() & 0x1F) | ((sprite.subtype() & 0x18) << 2) |
2172 ((sprite.layer() & 0x01) << 7);
2173
2174 bytes.push_back(b1);
2175 bytes.push_back(b2);
2176 bytes.push_back(b3);
2177
2178 // Key drops are stored as hidden marker sprites immediately after the
2179 // sprite that owns the drop. Keep these bytes in sync with LoadSprites().
2180 if (sprite.key_drop() == 1) {
2181 bytes.insert(bytes.end(), {0xFE, 0x00, 0xE4});
2182 } else if (sprite.key_drop() == 2) {
2183 bytes.insert(bytes.end(), {0xFD, 0x00, 0xE4});
2184 }
2185 }
2186
2187 // Terminator
2188 bytes.push_back(0xFF);
2189
2190 return bytes;
2191}
2192
2194 if (!rom || !rom->is_loaded()) {
2196 }
2197
2198 const auto& rom_data = rom->vector();
2199 int sprite_pointer = 0;
2200 if (!GetSpritePointerTablePc(rom_data, &sprite_pointer).ok()) {
2202 }
2203
2204 const int hard_end =
2205 std::min(static_cast<int>(rom_data.size()), kSpritesDataEndExclusive);
2206 if (hard_end <= 0) {
2208 }
2209
2210 int max_used = std::min(hard_end, kSpritesData);
2211 std::unordered_set<int> visited_addresses;
2212 for (int room_id = 0; room_id < kNumberOfRooms; ++room_id) {
2213 int sprite_address =
2214 ReadRoomSpriteAddressPc(rom_data, sprite_pointer, room_id);
2215 if (sprite_address < kSpritesData || sprite_address >= hard_end) {
2216 continue;
2217 }
2218 if (!visited_addresses.insert(sprite_address).second) {
2219 continue;
2220 }
2221
2222 int stream_size =
2223 MeasureSpriteStreamSize(rom_data, sprite_address, hard_end);
2224 int stream_end = sprite_address + stream_size;
2225 if (stream_end > max_used) {
2226 max_used = stream_end;
2227 }
2228 }
2229
2230 return max_used;
2231}
2232
2233absl::Status RelocateSpriteData(Rom* rom, int room_id,
2234 const std::vector<uint8_t>& encoded_bytes) {
2235 if (!rom || !rom->is_loaded()) {
2236 return absl::InvalidArgumentError("ROM not loaded");
2237 }
2238 if (room_id < 0 || room_id >= kNumberOfRooms) {
2239 return absl::OutOfRangeError("Room ID out of range");
2240 }
2241 if (encoded_bytes.empty() || encoded_bytes.back() != 0xFF ||
2242 (encoded_bytes.size() % 3) != 1) {
2243 return absl::InvalidArgumentError(
2244 "Encoded sprite payload must be N*3 bytes plus 0xFF terminator");
2245 }
2246
2247 const auto& rom_data = rom->vector();
2248 int sprite_pointer = 0;
2249 RETURN_IF_ERROR(GetSpritePointerTablePc(rom_data, &sprite_pointer));
2250
2251 int old_sprite_address =
2252 ReadRoomSpriteAddressPc(rom_data, sprite_pointer, room_id);
2253 if (old_sprite_address < 0 ||
2254 old_sprite_address >= static_cast<int>(rom_data.size())) {
2255 return absl::OutOfRangeError("Sprite address out of range");
2256 }
2257
2258 const uint8_t sort_mode = rom_data[old_sprite_address];
2259
2260 const int write_pos = FindMaxUsedSpriteAddress(rom);
2261 const size_t required_size = 1u + encoded_bytes.size();
2262 if (write_pos < kSpritesData ||
2263 static_cast<size_t>(write_pos) + required_size >
2264 static_cast<size_t>(kSpritesDataEndExclusive)) {
2265 return absl::ResourceExhaustedError(absl::StrFormat(
2266 "Not enough sprite data space. Need %d bytes at 0x%06X, "
2267 "region ends at 0x%06X",
2268 static_cast<int>(required_size), write_pos, kSpritesDataEndExclusive));
2269 }
2270 if (static_cast<size_t>(write_pos) + required_size > rom_data.size()) {
2271 const int required_end = write_pos + static_cast<int>(required_size);
2272 return absl::OutOfRangeError(
2273 absl::StrFormat("ROM too small for sprite relocation write (need "
2274 "end=0x%06X, size=0x%06X)",
2275 required_end, static_cast<int>(rom_data.size())));
2276 }
2277
2278 std::vector<uint8_t> relocated;
2279 relocated.reserve(required_size);
2280 relocated.push_back(sort_mode);
2281 relocated.insert(relocated.end(), encoded_bytes.begin(), encoded_bytes.end());
2282 RETURN_IF_ERROR(rom->WriteVector(write_pos, std::move(relocated)));
2283
2284 const uint32_t snes_addr = PcToSnes(write_pos);
2285 const int ptr_off = sprite_pointer + (room_id * 2);
2286 RETURN_IF_ERROR(rom->WriteByte(ptr_off, snes_addr & 0xFF));
2287 RETURN_IF_ERROR(rom->WriteByte(ptr_off + 1, (snes_addr >> 8) & 0xFF));
2288
2289 return absl::OkStatus();
2290}
2291
2292std::vector<std::pair<uint32_t, uint32_t>> Room::StreamSavePlan::write_ranges()
2293 const {
2294 std::vector<std::pair<uint32_t, uint32_t>> ranges;
2295 const auto add = [&](uint32_t address, size_t size) {
2296 ranges.emplace_back(address, address + static_cast<uint32_t>(size));
2297 };
2298 for (const auto& [address, bytes] : direct_writes) {
2299 add(address, bytes.size());
2300 }
2301 if (direct_long_write.has_value()) {
2302 add(direct_long_write->first, 3);
2303 }
2304 if (relocation.has_value()) {
2305 for (const auto& write : relocation->payload_writes) {
2306 add(write.address, write.bytes.size());
2307 }
2308 for (const auto& write : relocation->pointer_writes) {
2309 add(write.address, write.bytes.size());
2310 }
2311 for (const auto& write : relocation->auxiliary_pointer_writes) {
2312 add(write.address, write.bytes.size());
2313 }
2314 }
2315 return ranges;
2316}
2317
2319 if (plan.relocation.has_value()) {
2321 }
2322 for (const auto& [address, bytes] : plan.direct_writes) {
2323 RETURN_IF_ERROR(rom_->WriteVector(address, bytes));
2324 }
2325 if (plan.direct_long_write.has_value()) {
2327 plan.direct_long_write->second));
2328 }
2329 return absl::OkStatus();
2330}
2331
2332absl::StatusOr<Room::StreamSavePlan> Room::PlanObjectSave(
2333 const DungeonStreamLayout* layout) const {
2334 StreamSavePlan plan;
2335 if (rom_ == nullptr) {
2336 return absl::InvalidArgumentError("ROM pointer is null");
2337 }
2338 if (!object_stream_dirty()) {
2339 return plan;
2340 }
2341 for (const auto& object : tile_objects_) {
2342 if (UsesRoomObjectStream(object)) {
2344 }
2345 }
2346
2347 const auto& rom_data = rom_->vector();
2348 ASSIGN_OR_RETURN(const PhysicalStreamInfo stream_info,
2349 GetObjectStreamInfo(rom_data, room_id_));
2350 const auto encoded_bytes = EncodeObjects();
2351 bool requires_copy_on_write = false;
2352 if (layout != nullptr) {
2353 ASSIGN_OR_RETURN(requires_copy_on_write,
2354 DungeonStreamRequiresCopyOnWrite(
2356 encoded_bytes.size() + 2u));
2357 }
2358 const int write_pos = stream_info.address + 2;
2359 const int door_list_offset = static_cast<int>(encoded_bytes.size()) -
2360 static_cast<int>(doors_.size()) * 2 - 2;
2361 if (door_list_offset < 0) {
2362 return absl::FailedPreconditionError("Invalid encoded door list offset");
2363 }
2364 const int door_pointer_slot = kDoorPointers + (room_id_ * 3);
2365 if (door_pointer_slot < 0 ||
2366 door_pointer_slot + 2 >= static_cast<int>(rom_data.size())) {
2367 return absl::OutOfRangeError("Door pointer slot is out of range");
2368 }
2369 ASSIGN_OR_RETURN(const uint32_t source_door_pointer,
2370 rom_->ReadLong(door_pointer_slot));
2371 // Preserve the source slow/fast bank mirror when updating the door pointer.
2372 uint32_t encoded_door_pointer = PcToSnes(write_pos + door_list_offset);
2373 encoded_door_pointer |= source_door_pointer & 0x800000u;
2374
2375 // Undo can restore the original payload while leaving the stream dirty.
2376 // Compare the complete bounded stream, not a prefix, and require the runtime
2377 // door pointer to agree before treating this as an allocation-free no-op.
2378 // Layout validation above still applies even when no bytes need writing.
2379 if (source_door_pointer == encoded_door_pointer) {
2380 const auto source = ReadDungeonObjectStream(*rom_, room_id_);
2381 if (source.ok() &&
2382 source->data_pc == static_cast<uint32_t>(stream_info.address) &&
2383 source->encoded_stream.size() == encoded_bytes.size() + 2u &&
2384 std::equal(encoded_bytes.begin(), encoded_bytes.end(),
2385 source->encoded_stream.begin() + 2)) {
2386 return plan;
2387 }
2388 }
2389 const auto relocate = [&]() -> absl::StatusOr<StreamSavePlan> {
2390 if (stream_info.address + 2 > static_cast<int>(rom_data.size())) {
2391 return absl::OutOfRangeError("Object stream header is out of range");
2392 }
2393 std::vector<uint8_t> replacement = {rom_data[stream_info.address],
2394 rom_data[stream_info.address + 1]};
2395 replacement.insert(replacement.end(), encoded_bytes.begin(),
2396 encoded_bytes.end());
2398 auto relocation,
2399 PlanRelocateDungeonStream(*rom_, room_id_, DungeonStreamKind::kObject,
2400 *layout, std::move(replacement)));
2401 plan.relocation = std::move(relocation);
2402 return plan;
2403 };
2404 if (stream_info.shared || requires_copy_on_write) {
2405 if (layout != nullptr) {
2406 return relocate();
2407 }
2408 return absl::FailedPreconditionError(absl::StrFormat(
2409 "Room %d object stream at PC 0x%06X is shared; repacking is required",
2410 room_id_, stream_info.address));
2411 }
2412 if (stream_info.capacity() <= 2) {
2413 if (layout != nullptr) {
2414 return relocate();
2415 }
2416 return absl::FailedPreconditionError(absl::StrFormat(
2417 "Room %d object stream has no safe physical boundary", room_id_));
2418 }
2419
2420 const int available_payload_size = stream_info.capacity() - 2;
2421 if (encoded_bytes.size() > static_cast<size_t>(available_payload_size)) {
2422 if (layout != nullptr) {
2423 return relocate();
2424 }
2425 return absl::ResourceExhaustedError(absl::StrFormat(
2426 "Room %d object data too large! Size: %d, Available: %d", room_id_,
2427 static_cast<int>(encoded_bytes.size()), available_payload_size));
2428 }
2429
2430 plan.direct_writes.emplace_back(write_pos, encoded_bytes);
2431 plan.direct_long_write = std::make_pair(
2432 static_cast<uint32_t>(door_pointer_slot), encoded_door_pointer);
2433 return plan;
2434}
2435
2436absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
2438 ASSIGN_OR_RETURN(const StreamSavePlan plan, PlanObjectSave(layout));
2439 return plan.write_ranges();
2440}
2441
2442absl::Status Room::SaveObjects(const DungeonStreamLayout* layout) {
2443 ASSIGN_OR_RETURN(const StreamSavePlan plan, PlanObjectSave(layout));
2445 if (object_stream_dirty()) {
2447 }
2448 return absl::OkStatus();
2449}
2450
2451absl::StatusOr<Room::StreamSavePlan> Room::PlanObjectStreamHeaderSave(
2452 const DungeonStreamLayout* layout) const {
2453 StreamSavePlan plan;
2454 if (rom_ == nullptr) {
2455 return absl::InvalidArgumentError("ROM pointer is null");
2456 }
2458 return plan;
2459 }
2460 if (floor1_graphics_ > 0x0F || floor2_graphics_ > 0x0F) {
2461 return absl::InvalidArgumentError(
2462 "Dungeon floor graphics values must be in range 0..15");
2463 }
2464 if (layout_id_ > 0x07) {
2465 return absl::InvalidArgumentError(
2466 "Dungeon layout ID must be in range 0..7");
2467 }
2468
2469 const auto& rom_data = rom_->vector();
2470 ASSIGN_OR_RETURN(const PhysicalStreamInfo stream_info,
2471 GetObjectStreamInfo(rom_data, room_id_));
2472 if (stream_info.address < 0 ||
2473 stream_info.address + 1 >= static_cast<int>(rom_data.size())) {
2474 return absl::OutOfRangeError("Object stream header is out of range");
2475 }
2476
2477 const uint8_t dirty_mask = save_dirty_state_.object_stream_header;
2478 auto patch_header = [&](std::vector<uint8_t>* stream) -> absl::Status {
2479 if (stream == nullptr || stream->size() < 2) {
2480 return absl::DataLossError(
2481 "Object stream is missing its two-byte header");
2482 }
2483 if ((dirty_mask & kObjectHeaderFloor1Dirty) != 0) {
2484 (*stream)[0] = static_cast<uint8_t>(((*stream)[0] & 0xF0) |
2485 (floor1_graphics_ & 0x0F));
2486 }
2487 if ((dirty_mask & kObjectHeaderFloor2Dirty) != 0) {
2488 (*stream)[0] =
2489 static_cast<uint8_t>(((*stream)[0] & 0x0F) | (floor2_graphics_ << 4));
2490 }
2491 if ((dirty_mask & kObjectHeaderLayoutDirty) != 0) {
2492 (*stream)[1] = static_cast<uint8_t>(((*stream)[1] & 0xE3) |
2493 ((layout_id_ & 0x07) << 2));
2494 }
2495 return absl::OkStatus();
2496 };
2497
2498 bool requires_copy_on_write = stream_info.shared;
2499 std::vector<uint8_t> replacement;
2500 if (layout != nullptr) {
2501 if (layout->kind != DungeonStreamKind::kObject) {
2502 return absl::InvalidArgumentError(
2503 "Object-stream header save requires an object stream layout");
2504 }
2506 InventoryDungeonStreams(*rom_, *layout));
2507 if (!inventory.ok()) {
2508 return absl::FailedPreconditionError(absl::StrFormat(
2509 "Dungeon stream inventory has %zu issue(s); refusing object "
2510 "header save",
2511 inventory.issues.size()));
2512 }
2513 if (room_id_ < 0 ||
2514 static_cast<size_t>(room_id_) >= inventory.streams.size()) {
2515 return absl::OutOfRangeError(
2516 "Room ID is outside the dungeon stream layout");
2517 }
2518 replacement = inventory.streams[room_id_].encoded_stream;
2519 bool layout_requires_copy_on_write = false;
2520 ASSIGN_OR_RETURN(layout_requires_copy_on_write,
2521 DungeonStreamRequiresCopyOnWrite(
2523 replacement.size()));
2524 requires_copy_on_write =
2525 requires_copy_on_write || layout_requires_copy_on_write;
2526 }
2527
2528 std::vector<uint8_t> header = {rom_data[stream_info.address],
2529 rom_data[stream_info.address + 1]};
2530 RETURN_IF_ERROR(patch_header(&header));
2531 if (header[0] == rom_data[stream_info.address] &&
2532 header[1] == rom_data[stream_info.address + 1]) {
2533 return plan;
2534 }
2535
2536 if (requires_copy_on_write) {
2537 if (layout == nullptr) {
2538 return absl::FailedPreconditionError(absl::StrFormat(
2539 "Room %d object stream at PC 0x%06X is shared; a copy-on-write "
2540 "manifest is required to save its header",
2541 room_id_, stream_info.address));
2542 }
2543 RETURN_IF_ERROR(patch_header(&replacement));
2545 auto relocation,
2546 PlanRelocateDungeonStream(*rom_, room_id_, DungeonStreamKind::kObject,
2547 *layout, std::move(replacement)));
2548 plan.relocation = std::move(relocation);
2549 return plan;
2550 }
2551
2552 plan.direct_writes.emplace_back(stream_info.address, std::move(header));
2553 return plan;
2554}
2555
2556absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
2558 const DungeonStreamLayout* layout) const {
2561 return plan.write_ranges();
2562}
2563
2570 }
2571 return absl::OkStatus();
2572}
2573
2574absl::StatusOr<Room::StreamSavePlan> Room::PlanSpriteSave(
2575 const DungeonStreamLayout* layout) const {
2576 StreamSavePlan plan;
2577 if (rom_ == nullptr) {
2578 return absl::InvalidArgumentError("ROM pointer is null");
2579 }
2580 if (!sprites_dirty()) {
2581 return plan;
2582 }
2583
2584 const auto& rom_data = rom_->vector();
2585 if (room_id_ < 0 || room_id_ >= kNumberOfRooms) {
2586 return absl::OutOfRangeError("Room ID out of range");
2587 }
2588
2589 ASSIGN_OR_RETURN(const PhysicalStreamInfo stream_info,
2590 GetSpriteStreamInfo(rom_data, room_id_));
2591 const auto encoded_bytes = EncodeSprites();
2592 bool requires_copy_on_write = false;
2593 if (layout != nullptr) {
2594 ASSIGN_OR_RETURN(requires_copy_on_write,
2595 DungeonStreamRequiresCopyOnWrite(
2597 encoded_bytes.size() + 1u));
2598 }
2599 // Undo can restore a stream byte-for-byte while leaving its dirty bit set.
2600 // A no-op needs neither a new allocation nor an in-place write. Parse the
2601 // complete live stream first so a matching prefix in a longer or malformed
2602 // stream cannot be mistaken for an unchanged payload.
2603 const uint64_t source_payload_address =
2604 static_cast<uint64_t>(stream_info.address) + 1u;
2605 if (source_payload_address <= rom_data.size() &&
2606 encoded_bytes.size() <= rom_data.size() - source_payload_address) {
2607 const auto source = ReadDungeonSpriteStream(*rom_, room_id_);
2608 if (source.ok() &&
2609 source->data_pc == static_cast<uint32_t>(stream_info.address) &&
2610 source->encoded_stream.size() == encoded_bytes.size() + 1u &&
2611 std::equal(encoded_bytes.begin(), encoded_bytes.end(),
2612 source->encoded_stream.begin() + 1)) {
2613 return plan;
2614 }
2615 }
2616 const auto relocate = [&]() -> absl::StatusOr<StreamSavePlan> {
2617 std::vector<uint8_t> replacement = {rom_data[stream_info.address]};
2618 replacement.insert(replacement.end(), encoded_bytes.begin(),
2619 encoded_bytes.end());
2621 auto relocation,
2622 PlanRelocateDungeonStream(*rom_, room_id_, DungeonStreamKind::kSprite,
2623 *layout, std::move(replacement)));
2624 plan.relocation = std::move(relocation);
2625 return plan;
2626 };
2627 if (stream_info.shared || requires_copy_on_write) {
2628 if (layout != nullptr) {
2629 return relocate();
2630 }
2631 return absl::FailedPreconditionError(absl::StrFormat(
2632 "Room %d sprite stream at PC 0x%06X is shared; repacking is required",
2633 room_id_, stream_info.address));
2634 }
2635 if (stream_info.capacity() <= 1) {
2636 if (layout != nullptr) {
2637 return relocate();
2638 }
2639 return absl::FailedPreconditionError(absl::StrFormat(
2640 "Room %d sprite stream has no safe physical boundary", room_id_));
2641 }
2642
2643 const int available_payload_size = stream_info.capacity() - 1;
2644 const int payload_address = stream_info.address + 1;
2645 if (payload_address < 0 ||
2646 payload_address >= static_cast<int>(rom_->size())) {
2647 return absl::OutOfRangeError(absl::StrFormat(
2648 "Room %d has invalid sprite payload address", room_id_));
2649 }
2650
2651 if (static_cast<int>(encoded_bytes.size()) > available_payload_size) {
2652 if (layout != nullptr) {
2653 return relocate();
2654 }
2655 return absl::ResourceExhaustedError(absl::StrFormat(
2656 "Room %d sprite data too large! Size: %d, Available: %d; repacking "
2657 "is required",
2658 room_id_, static_cast<int>(encoded_bytes.size()),
2659 available_payload_size));
2660 }
2661
2662 plan.direct_writes.emplace_back(payload_address, encoded_bytes);
2663 return plan;
2664}
2665
2666absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
2668 ASSIGN_OR_RETURN(const StreamSavePlan plan, PlanSpriteSave(layout));
2669 return plan.write_ranges();
2670}
2671
2672absl::Status Room::SaveSprites(const DungeonStreamLayout* layout) {
2673 ASSIGN_OR_RETURN(const StreamSavePlan plan, PlanSpriteSave(layout));
2675 if (sprites_dirty()) {
2677 }
2678 return absl::OkStatus();
2679}
2680
2681absl::Status Room::SaveRoomHeader() {
2682 if (rom_ == nullptr) {
2683 return absl::InvalidArgumentError("ROM pointer is null");
2684 }
2685
2686 const auto& rom_data = rom()->vector();
2687 if (kRoomHeaderPointer < 0 ||
2688 kRoomHeaderPointer + 2 >= static_cast<int>(rom_data.size())) {
2689 return absl::OutOfRangeError("Room header pointer out of range");
2690 }
2691 if (kRoomHeaderPointerBank < 0 ||
2692 kRoomHeaderPointerBank >= static_cast<int>(rom_data.size())) {
2693 return absl::OutOfRangeError("Room header pointer bank out of range");
2694 }
2695
2696 int header_pointer = (rom_data[kRoomHeaderPointer + 2] << 16) +
2697 (rom_data[kRoomHeaderPointer + 1] << 8) +
2698 rom_data[kRoomHeaderPointer];
2699 header_pointer = SnesToPc(header_pointer);
2700
2701 int table_offset = header_pointer + (room_id_ * 2);
2702 if (table_offset < 0 ||
2703 table_offset + 1 >= static_cast<int>(rom_data.size())) {
2704 return absl::OutOfRangeError("Room header table offset out of range");
2705 }
2706
2707 int address = (rom_data[kRoomHeaderPointerBank] << 16) +
2708 (rom_data[table_offset + 1] << 8) + rom_data[table_offset];
2709 int header_location = SnesToPc(address);
2710
2711 if (header_location < 0 ||
2712 header_location + 13 >= static_cast<int>(rom_data.size())) {
2713 return absl::OutOfRangeError("Room header location out of range");
2714 }
2715
2716 // Build 14-byte header to match LoadRoomHeaderFromRom layout. The high
2717 // three bits are the BG2/layer mode; bit 0 is the dark-room flag. DarkRoom
2718 // is an editor enum value, not a raw high-bit value.
2719 uint8_t layer2_mode_for_save = layer2_mode_ & 0x07;
2720 if (bg2() != background2::DarkRoom) {
2721 layer2_mode_for_save = static_cast<uint8_t>(bg2()) & 0x07;
2722 }
2723 const bool dark_room =
2724 IsLight() || is_dark_ || bg2() == background2::DarkRoom;
2725 uint8_t byte0 = static_cast<uint8_t>(
2726 (layer2_mode_for_save << 5) |
2727 ((static_cast<uint8_t>(collision()) & 0x07) << 2) |
2728 (rom_data[header_location] & 0x02) | (dark_room ? 1 : 0));
2729 // Preserve the full palette set ID byte (USDASM LoadRoomHeader uses 8-bit).
2730 uint8_t byte1 = palette_;
2731 // Byte 7 stores the pit target layer in bits 0-1 followed by the first
2732 // three staircase target layers in consecutive two-bit fields.
2733 uint8_t byte7 =
2734 (pits_.target_layer & 0x03) | ((staircase_plane(0) & 0x03) << 2) |
2735 ((staircase_plane(1) & 0x03) << 4) | ((staircase_plane(2) & 0x03) << 6);
2736 const uint8_t byte8 = static_cast<uint8_t>(
2737 (rom_data[header_location + 8] & 0xFC) | (staircase_plane(3) & 0x03));
2738
2739 RETURN_IF_ERROR(rom_->WriteByte(header_location + 0, byte0));
2740 RETURN_IF_ERROR(rom_->WriteByte(header_location + 1, byte1));
2741 RETURN_IF_ERROR(rom_->WriteByte(header_location + 2, blockset_));
2742 RETURN_IF_ERROR(rom_->WriteByte(header_location + 3, spriteset_));
2744 rom_->WriteByte(header_location + 4, static_cast<uint8_t>(effect())));
2746 rom_->WriteByte(header_location + 5, static_cast<uint8_t>(tag1())));
2748 rom_->WriteByte(header_location + 6, static_cast<uint8_t>(tag2())));
2749 RETURN_IF_ERROR(rom_->WriteByte(header_location + 7, byte7));
2750 RETURN_IF_ERROR(rom_->WriteByte(header_location + 8, byte8));
2751 RETURN_IF_ERROR(rom_->WriteByte(header_location + 9, holewarp_));
2752 RETURN_IF_ERROR(rom_->WriteByte(header_location + 10, staircase_room(0)));
2753 RETURN_IF_ERROR(rom_->WriteByte(header_location + 11, staircase_room(1)));
2754 RETURN_IF_ERROR(rom_->WriteByte(header_location + 12, staircase_room(2)));
2755 RETURN_IF_ERROR(rom_->WriteByte(header_location + 13, staircase_room(3)));
2756
2757 int msg_addr = kMessagesIdDungeon + (room_id_ * 2);
2758 if (msg_addr < 0 || msg_addr + 1 >= static_cast<int>(rom_data.size())) {
2759 return absl::OutOfRangeError("Message ID address out of range");
2760 }
2762
2764
2765 return absl::OkStatus();
2766}
2767
2768// ============================================================================
2769// Object Manipulation Methods (Phase 3)
2770// ============================================================================
2771
2772absl::Status Room::AddObject(const RoomObject& object) {
2773 // Validate object
2774 if (!ValidateObject(object)) {
2775 return absl::InvalidArgumentError("Invalid object parameters");
2776 }
2777
2778 // Add to internal list
2779 tile_objects_.push_back(object);
2780 objects_loaded_ = true;
2782
2783 return absl::OkStatus();
2784}
2785
2786absl::Status Room::RemoveObject(size_t index) {
2787 if (index >= tile_objects_.size()) {
2788 return absl::OutOfRangeError("Object index out of range");
2789 }
2790
2792 tile_objects_.erase(tile_objects_.begin() + index);
2793 objects_loaded_ = true;
2795
2796 return absl::OkStatus();
2797}
2798
2799absl::Status Room::UpdateObject(size_t index, const RoomObject& object) {
2800 if (index >= tile_objects_.size()) {
2801 return absl::OutOfRangeError("Object index out of range");
2802 }
2803
2804 if (!ValidateObject(object)) {
2805 return absl::InvalidArgumentError("Invalid object parameters");
2806 }
2807
2809 tile_objects_[index] = object;
2810 objects_loaded_ = true;
2812
2813 return absl::OkStatus();
2814}
2815
2816absl::StatusOr<size_t> Room::FindObjectAt(int x, int y, int layer) const {
2817 for (size_t i = 0; i < tile_objects_.size(); i++) {
2818 const auto& obj = tile_objects_[i];
2819 if (obj.x() == x && obj.y() == y && obj.GetLayerValue() == layer) {
2820 return i;
2821 }
2822 }
2823 return absl::NotFoundError("No object found at position");
2824}
2825
2826bool Room::ValidateObject(const RoomObject& object) const {
2827 // Validate position (0-63 for both X and Y)
2828 if (object.x() < 0 || object.x() > 63)
2829 return false;
2830 if (object.y() < 0 || object.y() > 63)
2831 return false;
2832
2833 // Validate layer (0-2)
2834 if (object.GetLayerValue() < 0 || object.GetLayerValue() > 2)
2835 return false;
2836
2837 // Validate object ID range
2838 if (object.id_ < 0 || object.id_ > 0xFFF)
2839 return false;
2840
2841 // Validate size for Type 1 objects
2842 if (object.id_ < 0x100 && object.size() > 15)
2843 return false;
2844
2845 return true;
2846}
2847
2848void Room::HandleSpecialObjects(short oid, uint8_t posX, uint8_t posY,
2849 int& nbr_of_staircase) {
2850 // The public C staircase record borrows its label. Keep destination labels
2851 // alive independently of Room copies, moves, reloads, and destruction.
2852 static const auto kStaircaseLabels = [] {
2853 std::array<std::string, 256> labels;
2854 for (size_t room = 0; room < labels.size(); ++room) {
2855 labels[room] = absl::StrCat("To ", room);
2856 }
2857 return labels;
2858 }();
2859
2860 // Handle staircase objects
2861 for (short stair : kStairsObjects) {
2862 if (stair == oid) {
2863 if (nbr_of_staircase < 4) {
2864 tile_objects_.back().set_options(ObjectOption::Stairs |
2865 tile_objects_.back().options());
2866 z3_staircases_.push_back(
2867 {posX, posY,
2868 kStaircaseLabels[staircase_rooms_[nbr_of_staircase]].c_str()});
2869 nbr_of_staircase++;
2870 } else {
2871 tile_objects_.back().set_options(ObjectOption::Stairs |
2872 tile_objects_.back().options());
2873 z3_staircases_.push_back({posX, posY, "To ???"});
2874 }
2875 break;
2876 }
2877 }
2878
2879 // Handle chest objects
2880 if (IsStatefulChestObjectId(oid)) {
2881 tile_objects_.back().set_options(ObjectOption::Chest |
2882 tile_objects_.back().options());
2883 }
2884}
2885
2887 const auto& rom_data = rom()->vector();
2888 // Avoid duplicate entries if callers reload sprite data on the same room.
2889 sprites_.clear();
2890 sprites_loaded_ = false;
2891 if (room_id_ < 0 || room_id_ >= kNumberOfRooms) {
2892 return;
2893 }
2894
2895 int sprite_pointer = 0;
2896 if (!GetSpritePointerTablePc(rom_data, &sprite_pointer).ok()) {
2897 return;
2898 }
2899
2900 int sprite_address =
2901 ReadRoomSpriteAddressPc(rom_data, sprite_pointer, room_id_);
2902 if (sprite_address < 0 ||
2903 sprite_address + 1 >= static_cast<int>(rom_data.size())) {
2904 return;
2905 }
2906
2907 // First byte is the SortSprites mode (0 or 1), not sprite data.
2908 sprite_address += 1;
2909
2910 while (sprite_address + 2 < static_cast<int>(rom_data.size())) {
2911 uint8_t b1 = rom_data[sprite_address];
2912 uint8_t b2 = rom_data[sprite_address + 1];
2913 uint8_t b3 = rom_data[sprite_address + 2];
2914
2915 if (b1 == 0xFF) {
2916 break;
2917 }
2918
2919 sprites_.emplace_back(b3, (b2 & 0x1F), (b1 & 0x1F),
2920 ((b2 & 0xE0) >> 5) + ((b1 & 0x60) >> 2),
2921 (b1 & 0x80) >> 7);
2922
2923 if (sprites_.size() > 1) {
2924 Sprite& spr = sprites_.back();
2925 Sprite& prevSprite = sprites_[sprites_.size() - 2];
2926
2927 if (spr.id() == 0xE4 && spr.x() == 0x00 && spr.y() == 0x1E &&
2928 spr.layer() == 1 && spr.subtype() == 0x18) {
2929 prevSprite.set_key_drop(1);
2930 sprites_.pop_back();
2931 }
2932
2933 if (spr.id() == 0xE4 && spr.x() == 0x00 && spr.y() == 0x1D &&
2934 spr.layer() == 1 && spr.subtype() == 0x18) {
2935 prevSprite.set_key_drop(2);
2936 sprites_.pop_back();
2937 }
2938 }
2939
2940 sprite_address += 3;
2941 }
2942
2943 sprites_loaded_ = true;
2944}
2945
2947 chests_in_room_.clear();
2948 chests_loaded_ = false;
2949 if (!rom_ || !rom_->is_loaded()) {
2950 return;
2951 }
2952 const auto& rom_data = rom()->vector();
2953 if (kChestsDataPointer1 + 2 >= static_cast<int>(rom_data.size()) ||
2954 kChestsLengthPointer + 1 >= static_cast<int>(rom_data.size())) {
2955 return;
2956 }
2957
2958 const int cpos = static_cast<int>(SnesToPc(
2959 (static_cast<uint32_t>(rom_data[kChestsDataPointer1 + 2]) << 16) |
2960 (static_cast<uint32_t>(rom_data[kChestsDataPointer1 + 1]) << 8) |
2961 rom_data[kChestsDataPointer1]));
2962 const size_t byte_length =
2963 (static_cast<size_t>(rom_data[kChestsLengthPointer + 1]) << 8) |
2964 rom_data[kChestsLengthPointer];
2965 const size_t bounded_byte_length = std::min<size_t>(
2966 byte_length, cpos >= 0 && cpos < static_cast<int>(rom_data.size())
2967 ? rom_data.size() - static_cast<size_t>(cpos)
2968 : 0);
2969 const size_t record_count = std::min<size_t>(
2970 bounded_byte_length / kChestTableRecordSize, kChestTableCapacityRecords);
2971
2972 for (size_t i = 0; i < record_count; ++i) {
2973 const size_t offset =
2974 static_cast<size_t>(cpos) + (i * kChestTableRecordSize);
2975 if ((((rom_data[offset + 1] << 8) + rom_data[offset]) & 0x7FFF) ==
2976 room_id_) {
2977 // There's a chest in that room !
2978 bool big = false;
2979 if ((((rom_data[offset + 1] << 8) + rom_data[offset]) & 0x8000) ==
2980 0x8000) {
2981 big = true;
2982 }
2983
2984 chests_in_room_.emplace_back(chest_data{rom_data[offset + 2], big});
2985 }
2986 }
2987 chests_loaded_ = true;
2988}
2989
2991 auto rom_data = rom()->vector();
2992
2993 // Doors are loaded as part of the object stream in LoadObjects()
2994 // When the parser encounters 0xF0 0xFF, it enters door mode
2995 // Door objects have format: b1 (position/direction), b2 (type)
2996 // Door encoding: b1 = (door_pos << 4) | (door_dir & 0x03)
2997 // position in bits 4-7, direction in bits 0-1
2998 // b2 = door_type (full byte, values 0x00, 0x02, 0x04, etc.)
2999 // This is already handled in ParseObjectsFromLocation()
3000
3001 LOG_DEBUG("Room",
3002 "LoadDoors for room %d - doors are loaded via object stream",
3003 room_id_);
3004}
3005
3007 auto rom_data = rom()->vector();
3008
3009 // Read torch data length
3010 int bytes_count = (rom_data[kTorchesLengthPointer + 1] << 8) |
3011 rom_data[kTorchesLengthPointer];
3012
3013 LOG_DEBUG("Room", "LoadTorches: room_id=%d, bytes_count=%d", room_id_,
3014 bytes_count);
3015
3016 // Avoid duplication if LoadTorches is called multiple times.
3017 tile_objects_.erase(
3018 std::remove_if(tile_objects_.begin(), tile_objects_.end(),
3019 [](const RoomObject& obj) {
3020 return (obj.options() & ObjectOption::Torch) !=
3021 ObjectOption::Nothing;
3022 }),
3023 tile_objects_.end());
3024
3025 // Iterate through torch data to find torches for this room
3026 for (int i = 0; i < bytes_count; i += 2) {
3027 if (i + 1 >= bytes_count)
3028 break;
3029
3030 uint8_t b1 = rom_data[kTorchData + i];
3031 uint8_t b2 = rom_data[kTorchData + i + 1];
3032
3033 // Skip 0xFFFF markers
3034 if (b1 == 0xFF && b2 == 0xFF) {
3035 continue;
3036 }
3037
3038 // Check if this entry is for our room
3039 uint16_t torch_room_id = (b2 << 8) | b1;
3040 if (torch_room_id == room_id_) {
3041 // Found torches for this room, read them
3042 i += 2;
3043 while (i < bytes_count) {
3044 if (i + 1 >= bytes_count)
3045 break;
3046
3047 b1 = rom_data[kTorchData + i];
3048 b2 = rom_data[kTorchData + i + 1];
3049
3050 // End of torch list for this room
3051 if (b1 == 0xFF && b2 == 0xFF) {
3052 break;
3053 }
3054
3055 const LightableTorchEntry entry = DecodeLightableTorchEntry({b1, b2});
3056
3057 // Create torch object (ID 0x150)
3058 RoomObject torch_obj(0x150, entry.px, entry.py, 0, entry.draw_layer);
3059 torch_obj.SetRom(rom_);
3060 torch_obj.set_options(ObjectOption::Torch);
3061 torch_obj.set_torch_reserved_bit(entry.reserved);
3062 torch_obj.lit_ = entry.lit;
3063
3064 tile_objects_.push_back(torch_obj);
3065
3066 LOG_DEBUG(
3067 "Room", "Loaded torch at (%d,%d) draw_layer=%d reserved=%d lit=%d",
3068 entry.px, entry.py, entry.draw_layer, entry.reserved, entry.lit);
3069
3070 i += 2;
3071 }
3072 break; // Found and processed our room's torches
3073 } else {
3074 // Skip to next room's torches
3075 i += 2;
3076 while (i < bytes_count) {
3077 if (i + 1 >= bytes_count)
3078 break;
3079 b1 = rom_data[kTorchData + i];
3080 b2 = rom_data[kTorchData + i + 1];
3081 if (b1 == 0xFF && b2 == 0xFF) {
3082 break;
3083 }
3084 i += 2;
3085 }
3086 }
3087 }
3088 torches_loaded_ = true;
3089}
3090
3091namespace {
3092
3093constexpr int kTorchesMaxSize = 0x120; // ZScream Constants.TorchesMaxSize
3094
3096 uint16_t room_id = 0;
3097 std::vector<uint8_t> bytes;
3098};
3099
3100// Parse current ROM torch blob in authoring order for preserve-merge.
3101std::vector<TorchSegment> ParseRomTorchSegments(
3102 const std::vector<uint8_t>& rom_data, int bytes_count) {
3103 std::vector<TorchSegment> segments;
3104 int i = 0;
3105 while (i + 1 < bytes_count && i < kTorchesMaxSize) {
3106 uint8_t b1 = rom_data[kTorchData + i];
3107 uint8_t b2 = rom_data[kTorchData + i + 1];
3108 if (b1 == 0xFF && b2 == 0xFF) {
3109 // Vanilla contains standalone $FFFF padding between two authored room
3110 // segments. Keep it as an unowned pass-through segment so a no-op save
3111 // remains byte-identical instead of compacting the table.
3112 TorchSegment padding;
3113 padding.room_id = 0xFFFF;
3114 padding.bytes = {0xFF, 0xFF};
3115 segments.push_back(std::move(padding));
3116 i += 2;
3117 continue;
3118 }
3119 uint16_t room_id = (b2 << 8) | b1;
3120 if (room_id >= kNumberOfRooms) {
3121 i += 2;
3122 continue;
3123 }
3124 TorchSegment seg;
3125 seg.room_id = room_id;
3126 seg.bytes.push_back(b1);
3127 seg.bytes.push_back(b2);
3128 i += 2;
3129 while (i + 1 < bytes_count && i < kTorchesMaxSize) {
3130 b1 = rom_data[kTorchData + i];
3131 b2 = rom_data[kTorchData + i + 1];
3132 if (b1 == 0xFF && b2 == 0xFF) {
3133 seg.bytes.push_back(0xFF);
3134 seg.bytes.push_back(0xFF);
3135 i += 2;
3136 break;
3137 }
3138 seg.bytes.push_back(b1);
3139 seg.bytes.push_back(b2);
3140 i += 2;
3141 }
3142 segments.push_back(std::move(seg));
3143 }
3144 return segments;
3145}
3146
3147std::vector<uint8_t> EncodeTorchSegmentForRoom(int room_id, const Room& room) {
3148 std::vector<uint8_t> bytes;
3149 for (const auto& obj : room.GetTileObjects()) {
3150 if ((obj.options() & ObjectOption::Torch) == ObjectOption::Nothing) {
3151 continue;
3152 }
3153 if (bytes.empty()) {
3154 bytes.push_back(room_id & 0xFF);
3155 bytes.push_back((room_id >> 8) & 0xFF);
3156 }
3158 .px = static_cast<uint8_t>(obj.x()),
3159 .py = static_cast<uint8_t>(obj.y()),
3160 .draw_layer = static_cast<uint8_t>(obj.GetLayerValue() & 1),
3161 .reserved = obj.torch_reserved_bit(),
3162 .lit = obj.lit_,
3163 });
3164 bytes.push_back(encoded.low);
3165 bytes.push_back(encoded.high);
3166 }
3167 if (!bytes.empty()) {
3168 bytes.push_back(0xFF);
3169 bytes.push_back(0xFF);
3170 }
3171 return bytes;
3172}
3173
3175 int room_id,
3176 const char* object_type) {
3177 const uint8_t selector = object.GetLayerValue();
3178 if (selector <= 1) {
3179 return absl::OkStatus();
3180 }
3181 return absl::InvalidArgumentError(absl::StrFormat(
3182 "%s in room 0x%03X has invalid special draw-layer selector %d; "
3183 "expected 0 "
3184 "(upper/BG1) or 1 (lower/BG2)",
3185 object_type, room_id, selector));
3186}
3187
3189 int room_id) {
3191 ValidateSpecialObjectDrawLayerSelector(object, room_id, "Torch"));
3192 if (object.x() <= 0x3E && object.y() <= 0x3E) {
3193 return absl::OkStatus();
3194 }
3195 return absl::InvalidArgumentError(absl::StrFormat(
3196 "Torch in room 0x%03X has invalid position (%d,%d); expected x/y in "
3197 "range 0..62",
3198 room_id, object.x(), object.y()));
3199}
3200
3201} // namespace
3202
3203namespace {
3204
3206 std::vector<uint8_t> bytes;
3207 int room_limit = 0;
3208 bool any_owned_room = false;
3209 bool needs_write = false;
3210};
3211
3212template <typename RoomLookup>
3213absl::StatusOr<TorchSavePlan> BuildTorchSavePlan(const Rom* rom, int room_count,
3214 RoomLookup&& room_lookup) {
3215 if (!rom || !rom->is_loaded()) {
3216 return absl::InvalidArgumentError("ROM not loaded");
3217 }
3218
3219 const auto& rom_data = rom->vector();
3220 if (kTorchesLengthPointer < 0 ||
3221 kTorchesLengthPointer + 1 >= static_cast<int>(rom_data.size())) {
3222 return absl::OutOfRangeError("Torch length pointer is out of range");
3223 }
3224 int existing_count = (rom_data[kTorchesLengthPointer + 1] << 8) |
3225 rom_data[kTorchesLengthPointer];
3226 if (existing_count > kTorchesMaxSize) {
3227 existing_count = kTorchesMaxSize;
3228 }
3229 if (kTorchData < 0 ||
3230 static_cast<uint64_t>(kTorchData) + existing_count > rom_data.size()) {
3231 return absl::OutOfRangeError("Torch data is out of range");
3232 }
3233 auto rom_segments = ParseRomTorchSegments(rom_data, existing_count);
3234
3235 TorchSavePlan plan;
3236 auto& bytes = plan.bytes;
3237 const int room_limit = std::clamp(room_count, 0, kNumberOfRooms);
3238 plan.room_limit = room_limit;
3239 std::vector<bool> owned_rooms(room_limit, false);
3240 std::vector<bool> seen_original_room(room_limit, false);
3241 std::vector<bool> emitted_owned_room(room_limit, false);
3242 std::vector<std::vector<uint8_t>> replacements(room_limit);
3243 bool any_owned_room = false;
3244 for (int room_id = 0; room_id < room_limit; ++room_id) {
3245 const Room* room = room_lookup(room_id);
3246 const bool room_owned =
3247 room != nullptr && (room->AreTorchesLoaded() || room->torches_dirty());
3248 if (!room_owned) {
3249 continue;
3250 }
3251 for (const auto& object : room->GetTileObjects()) {
3252 if ((object.options() & ObjectOption::Torch) != ObjectOption::Nothing) {
3254 }
3255 }
3256 owned_rooms[room_id] = true;
3257 any_owned_room = true;
3258 replacements[room_id] = EncodeTorchSegmentForRoom(room_id, *room);
3259 }
3260
3261 if (!any_owned_room) {
3262 return plan;
3263 }
3264 plan.any_owned_room = true;
3265
3266 for (const auto& segment : rom_segments) {
3267 if (segment.room_id < room_limit) {
3268 seen_original_room[segment.room_id] = true;
3269 if (owned_rooms[segment.room_id]) {
3270 if (!emitted_owned_room[segment.room_id]) {
3271 bytes.insert(bytes.end(), replacements[segment.room_id].begin(),
3272 replacements[segment.room_id].end());
3273 emitted_owned_room[segment.room_id] = true;
3274 }
3275 continue;
3276 }
3277 }
3278 bytes.insert(bytes.end(), segment.bytes.begin(), segment.bytes.end());
3279 }
3280
3281 for (int room_id = 0; room_id < room_limit; ++room_id) {
3282 if (owned_rooms[room_id] && !seen_original_room[room_id] &&
3283 !replacements[room_id].empty()) {
3284 bytes.insert(bytes.end(), replacements[room_id].begin(),
3285 replacements[room_id].end());
3286 }
3287 }
3288
3289 if (bytes.size() > kTorchesMaxSize) {
3290 return absl::ResourceExhaustedError(
3291 absl::StrFormat("Torch data too large: %d bytes (max %d)", bytes.size(),
3293 }
3294 if (static_cast<uint64_t>(kTorchData) + bytes.size() > rom_data.size()) {
3295 return absl::OutOfRangeError("Torch save data is out of range");
3296 }
3297
3298 const uint16_t current_len =
3299 static_cast<uint16_t>(rom_data[kTorchesLengthPointer]) |
3300 (static_cast<uint16_t>(rom_data[kTorchesLengthPointer + 1]) << 8);
3301 plan.needs_write =
3302 current_len != bytes.size() ||
3303 !std::equal(bytes.begin(), bytes.end(), rom_data.begin() + kTorchData);
3304 return plan;
3305}
3306
3307template <typename RoomLookup>
3308absl::Status SaveAllTorchesImpl(Rom* rom, int room_count,
3309 RoomLookup&& room_lookup) {
3310 ASSIGN_OR_RETURN(auto plan, BuildTorchSavePlan(rom, room_count, room_lookup));
3311 if (!plan.any_owned_room) {
3312 return absl::OkStatus();
3313 }
3314
3315 if (plan.needs_write) {
3317 static_cast<uint16_t>(plan.bytes.size())));
3318 RETURN_IF_ERROR(rom->WriteVector(kTorchData, plan.bytes));
3319 }
3320 for (int room_id = 0; room_id < plan.room_limit; ++room_id) {
3321 if (const Room* room = room_lookup(room_id);
3322 room != nullptr && room->torches_dirty()) {
3323 const_cast<Room*>(room)->ClearTorchesDirty();
3324 }
3325 }
3326 return absl::OkStatus();
3327}
3328
3329} // namespace
3330
3331absl::Status SaveAllTorches(Rom* rom, absl::Span<const Room> rooms) {
3332 return SaveAllTorchesImpl(rom, static_cast<int>(rooms.size()),
3333 [&rooms](int room_id) { return &rooms[room_id]; });
3334}
3335
3336absl::Status SaveAllTorches(
3337 Rom* rom, int room_count,
3338 const std::function<const Room*(int)>& room_lookup) {
3339 return SaveAllTorchesImpl(rom, room_count, room_lookup);
3340}
3341
3342absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
3343GetTorchSaveWriteRanges(const Rom* rom, int room_count,
3344 const std::function<const Room*(int)>& room_lookup) {
3345 ASSIGN_OR_RETURN(const auto plan,
3346 BuildTorchSavePlan(rom, room_count, room_lookup));
3347 if (!plan.needs_write) {
3348 return std::vector<std::pair<uint32_t, uint32_t>>{};
3349 }
3350 return std::vector<std::pair<uint32_t, uint32_t>>{
3352 {kTorchData, kTorchData + static_cast<uint32_t>(plan.bytes.size())}};
3353}
3354
3355// Region preservation for `RoomsWithPitDamage` when no edited table is supplied.
3356// When `pit_damage_table` is non-null and dirty, encode the in-memory membership
3357// list through `PitDamageTable::SaveToRom` instead of blind preservation.
3358absl::Status SaveAllPits(Rom* rom) {
3359 return SaveAllPits(rom, nullptr);
3360}
3361
3362absl::Status SaveAllPits(Rom* rom, PitDamageTable* pit_damage_table) {
3363 if (pit_damage_table != nullptr && pit_damage_table->dirty()) {
3364 RETURN_IF_ERROR(pit_damage_table->SaveToRom(rom));
3365 pit_damage_table->ClearDirty();
3366 return absl::OkStatus();
3367 }
3368 if (!rom || !rom->is_loaded()) {
3369 return absl::InvalidArgumentError("ROM not loaded");
3370 }
3371 const auto& rom_data = rom->vector();
3372 if (kPitCount < 0 || kPitCount >= static_cast<int>(rom_data.size()) ||
3373 kPitPointer + 2 >= static_cast<int>(rom_data.size())) {
3374 return absl::OutOfRangeError("Pit count/pointer out of range");
3375 }
3376 int max_offset = rom_data[kPitCount];
3377 // Total bytes = max_offset + 2 (covers offsets 0..max_offset
3378 // inclusive, with each entry being a 2-byte word). When max_offset
3379 // is 0, there's still 1 word to preserve (the entry at offset 0).
3380 int data_len = max_offset + 2;
3381 int pit_ptr_snes = (rom_data[kPitPointer + 2] << 16) |
3382 (rom_data[kPitPointer + 1] << 8) | rom_data[kPitPointer];
3383 int pit_data_pc = SnesToPc(pit_ptr_snes);
3384 if (pit_data_pc < 0 ||
3385 pit_data_pc + data_len > static_cast<int>(rom_data.size())) {
3386 return absl::OutOfRangeError("Pit data region out of range");
3387 }
3388 std::vector<uint8_t> data(rom_data.begin() + pit_data_pc,
3389 rom_data.begin() + pit_data_pc + data_len);
3390 RETURN_IF_ERROR(rom->WriteByte(kPitCount, max_offset));
3391 RETURN_IF_ERROR(rom->WriteByte(kPitPointer, pit_ptr_snes & 0xFF));
3392 RETURN_IF_ERROR(rom->WriteByte(kPitPointer + 1, (pit_ptr_snes >> 8) & 0xFF));
3393 RETURN_IF_ERROR(rom->WriteByte(kPitPointer + 2, (pit_ptr_snes >> 16) & 0xFF));
3394 return rom->WriteVector(pit_data_pc, data);
3395}
3396
3397namespace {
3398
3399constexpr int kBlocksRegionSize = 0x80;
3402
3403bool HalfOpenRangesOverlap(int first_begin, int first_end, int second_begin,
3404 int second_end) {
3405 return first_begin < second_end && second_begin < first_end;
3406}
3407
3409 const std::vector<uint8_t>& rom_data, int operand_pc) {
3410 if (operand_pc <= 0 || operand_pc + 5 >= static_cast<int>(rom_data.size())) {
3411 return absl::OutOfRangeError("Blocks pointer operand out of range");
3412 }
3413 // The block table pointers are the 3-byte operands in the US USDASM
3414 // bank_02 loader shape (#_02DAF9..#_02DB12):
3415 // BF ll hh bb LDA.l table+N*0x80,X
3416 // 9D ll hh STA.w $7EF940+N*0x80,X
3417 // The data table starts at bank_04's
3418 // SpecialUnderworldObjects_pushable_block (#_04F1DE). Pinned against a real
3419 // vanilla ROM by
3420 // DungeonSaveRegionTest.BlocksLoaderPointerOperandsMatchUsdasmShape.
3421 //
3422 // Guard both sides before dereferencing or future repointing so a bad
3423 // constant or already-patched ROM cannot make the saver treat unrelated
3424 // instruction bytes as data pointers.
3425 if (rom_data[operand_pc - 1] != 0xBF || rom_data[operand_pc + 3] != 0x9D) {
3426 return absl::FailedPreconditionError(absl::StrFormat(
3427 "Blocks pointer operand at PC 0x%05X is not in the expected "
3428 "LDA.l ...,X / STA.w loader sequence",
3429 operand_pc));
3430 }
3431 return absl::OkStatus();
3432}
3433
3435 const std::vector<uint8_t>& rom_data, std::array<int, 4>* destination_pcs) {
3436 if (kBlocksLength < 0 ||
3437 kBlocksLength + 1 >= static_cast<int>(rom_data.size())) {
3438 return absl::OutOfRangeError("Blocks length out of range");
3439 }
3440
3441 for (size_t page = 0; page < kBlocksPointerSlots.size(); ++page) {
3442 const int operand_pc = kBlocksPointerSlots[page];
3444 const int snes = (rom_data[operand_pc + 2] << 16) |
3445 (rom_data[operand_pc + 1] << 8) | rom_data[operand_pc];
3446 const int data_pc = SnesToPc(snes);
3447 if (data_pc < 0 ||
3448 data_pc + kBlocksRegionSize > static_cast<int>(rom_data.size())) {
3449 return absl::OutOfRangeError(absl::StrFormat(
3450 "Blocks data region out of range for loader page %d", page + 1));
3451 }
3452 (*destination_pcs)[page] = data_pc;
3453 }
3454
3455 constexpr int kLengthMetadataEnd = kBlocksLength + 2;
3456 for (size_t page = 0; page < destination_pcs->size(); ++page) {
3457 const int page_begin = (*destination_pcs)[page];
3458 const int page_end = page_begin + kBlocksRegionSize;
3459 if (HalfOpenRangesOverlap(page_begin, page_end, kBlocksLength,
3460 kLengthMetadataEnd)) {
3461 return absl::FailedPreconditionError(absl::StrFormat(
3462 "Blocks data page %d at PC [0x%05X, 0x%05X) overlaps block-table "
3463 "length metadata [0x%05X, 0x%05X)",
3464 page + 1, page_begin, page_end, kBlocksLength, kLengthMetadataEnd));
3465 }
3466
3467 for (size_t loader = 0; loader < kBlocksPointerSlots.size(); ++loader) {
3468 // Each destination operand is embedded in a seven-byte loader
3469 // instruction: BF ll hh bb 9D ll hh. Treat the complete instruction as
3470 // metadata so a table write cannot corrupt either opcode or operand.
3471 const int loader_begin = kBlocksPointerSlots[loader] - 1;
3472 const int loader_end = kBlocksPointerSlots[loader] + 6;
3473 if (HalfOpenRangesOverlap(page_begin, page_end, loader_begin,
3474 loader_end)) {
3475 return absl::FailedPreconditionError(absl::StrFormat(
3476 "Blocks data page %d at PC [0x%05X, 0x%05X) overlaps loader %d "
3477 "opcode/operand metadata [0x%05X, 0x%05X)",
3478 page + 1, page_begin, page_end, loader + 1, loader_begin,
3479 loader_end));
3480 }
3481 }
3482
3483 for (size_t previous = 0; previous < page; ++previous) {
3484 const int previous_begin = (*destination_pcs)[previous];
3485 const int previous_end = previous_begin + kBlocksRegionSize;
3486 if (HalfOpenRangesOverlap(page_begin, page_end, previous_begin,
3487 previous_end)) {
3488 return absl::FailedPreconditionError(absl::StrFormat(
3489 "Blocks data pages %d and %d overlap at PC ranges [0x%05X, "
3490 "0x%05X) and [0x%05X, 0x%05X)",
3491 previous + 1, page + 1, previous_begin, previous_end, page_begin,
3492 page_end));
3493 }
3494 }
3495 }
3496
3497 return absl::OkStatus();
3498}
3499
3500// Loader pages describe a 0x200-byte WRAM buffer, not 0x200 bytes of owned
3501// ROM storage. USDASM bank_04 places the vanilla block table at $04F1DE
3502// (PC 0x271DE) and the torch table at $04F36A (PC 0x2736A): only 99 four-byte
3503// block records fit. The fourth 128-byte loader read deliberately includes
3504// unused torch bytes; writes must stop before those bytes. Repointed block
3505// pages retain the full WRAM capacity only when their actual payload spans
3506// avoid the fixed torch allocation ($04F36A..$04F48A) and its length operand.
3508 const std::array<int, 4>& destination_pcs, int byte_length) {
3509 if (byte_length < 0 || byte_length > 4 * kBlocksRegionSize ||
3510 byte_length % 4 != 0) {
3511 return absl::FailedPreconditionError(
3512 "Pushable-block length must contain whole four-byte records within "
3513 "the 0x200-byte WRAM buffer");
3514 }
3515 for (size_t page = 0; page < destination_pcs.size(); ++page) {
3516 const int offset = static_cast<int>(page) * kBlocksRegionSize;
3517 const int length = std::min(kBlocksRegionSize, byte_length - offset);
3518 if (length <= 0)
3519 break;
3520 const int begin = destination_pcs[page];
3521 const int end = begin + length;
3522 if (HalfOpenRangesOverlap(begin, end, kTorchData,
3524 return absl::FailedPreconditionError(absl::StrFormat(
3525 "Pushable-block ROM capacity exceeded: loader page %d payload "
3526 "[0x%05X, 0x%05X) overlaps torch data [0x%05X, 0x%05X). "
3527 "Repoint the block data to independent ROM storage before growing "
3528 "this table.",
3529 page + 1, begin, end, kTorchData, kTorchData + kTorchesMaxSize));
3530 }
3532 kTorchesLengthPointer + 2)) {
3533 return absl::FailedPreconditionError(absl::StrFormat(
3534 "Blocks loader page %d payload overlaps torch length metadata",
3535 page + 1));
3536 }
3537 }
3538 return absl::OkStatus();
3539}
3540
3541} // namespace
3542
3543absl::Status SaveAllBlocks(Rom* rom) {
3544 if (!rom || !rom->is_loaded()) {
3545 return absl::InvalidArgumentError("ROM not loaded");
3546 }
3547 const auto& rom_data = rom->vector();
3548 if (kBlocksLength + 1 >= static_cast<int>(rom_data.size())) {
3549 return absl::OutOfRangeError("Blocks length out of range");
3550 }
3551 int blocks_count =
3552 (rom_data[kBlocksLength + 1] << 8) | rom_data[kBlocksLength];
3553 std::array<int, 4> destination_pcs{};
3555 PreflightBlocksLoaderDestinations(rom_data, &destination_pcs));
3557 PreflightBlocksPayloadDestinations(destination_pcs, blocks_count));
3558 if (blocks_count <= 0) {
3559 return absl::OkStatus();
3560 }
3561 for (int r = 0; r < 4; ++r) {
3562 const int pc = destination_pcs[r];
3563 int off = r * kBlocksRegionSize;
3564 int len = std::min(kBlocksRegionSize, blocks_count - off);
3565 if (len <= 0)
3566 break;
3567 std::vector<uint8_t> chunk(rom_data.begin() + pc,
3568 rom_data.begin() + pc + len);
3569 RETURN_IF_ERROR(rom->WriteVector(pc, chunk));
3570 }
3572 rom->WriteWord(kBlocksLength, static_cast<uint16_t>(blocks_count)));
3573 return absl::OkStatus();
3574}
3575
3576namespace {
3577
3583
3585 std::vector<uint8_t> bytes;
3586 std::vector<BlockWriteDestination> destinations;
3587 std::vector<std::pair<const RoomObject*, int>> load_order_updates;
3588};
3589
3590absl::StatusOr<BlockSavePlan> BuildBlockSavePlan(
3591 const Rom* rom, int room_count,
3592 const std::function<const Room*(int)>& room_lookup) {
3593 if (!rom || !rom->is_loaded()) {
3594 return absl::InvalidArgumentError("ROM not loaded");
3595 }
3596 const auto& rom_data = rom->vector();
3597 if (kBlocksLength + 1 >= static_cast<int>(rom_data.size())) {
3598 return absl::OutOfRangeError("Blocks length out of range");
3599 }
3600
3601 std::array<int, 4> destination_pcs{};
3603 PreflightBlocksLoaderDestinations(rom_data, &destination_pcs));
3604
3605 // Read the original block buffer by dereferencing the four pointer
3606 // slots. We need this so unmaterialized / header-only rooms can have
3607 // their entries preserved verbatim — only rooms whose blocks were
3608 // actually loaded into memory get re-encoded from `tile_objects_`.
3609 // This prevents the editor migration from silently dropping vanilla
3610 // blocks for any room the user hasn't materialized yet.
3611 const int original_count_word =
3612 (rom_data[kBlocksLength + 1] << 8) | rom_data[kBlocksLength];
3613 const int original_byte_len = original_count_word;
3615 PreflightBlocksPayloadDestinations(destination_pcs, original_byte_len));
3616 std::vector<uint8_t> original_buffer(original_byte_len, 0);
3617 for (int r = 0; r < 4; ++r) {
3618 const int pc = destination_pcs[r];
3619 const int off = r * kBlocksRegionSize;
3620 const int len = std::min(kBlocksRegionSize, original_byte_len - off);
3621 if (len <= 0)
3622 break;
3623 std::copy_n(rom_data.begin() + pc, len, original_buffer.begin() + off);
3624 }
3625 const int original_slot_count = original_byte_len / 4;
3626
3627 // Build:
3628 // - `slot_replacements`: for each existing slot whose room_id is
3629 // "owned" by an editor-loaded room, the re-encoded bytes (or
3630 // absent if the block was deleted in memory).
3631 // - `owned_room_ids`: the set of room_ids whose blocks were
3632 // materialized (so unmaterialized / header-only rooms can be
3633 // preserved verbatim from `original_buffer`).
3634 // - `appended`: blocks with `load_order == kBlockLoadOrderNew`,
3635 // appended to the end in creation order.
3636 struct EncodedBlock {
3637 PushableBlockBytes bytes;
3638 const RoomObject* source_object;
3639 };
3640 std::unordered_set<uint16_t> owned_room_ids;
3641 std::unordered_map<int, EncodedBlock> slot_replacements;
3642 std::vector<EncodedBlock> appended;
3643 for (int rid = 0; rid < room_count; ++rid) {
3644 const Room* room = room_lookup(rid);
3645 if (room == nullptr)
3646 continue;
3647 if (!room->AreBlocksLoaded()) {
3648 if (room->blocks_dirty()) {
3649 return absl::FailedPreconditionError(absl::StrFormat(
3650 "Room 0x%03X has unsaved pushable-block edits, but its block "
3651 "table is not loaded. Load the room's blocks before saving.",
3652 rid));
3653 }
3654 continue; // Header-only — preserve its slots verbatim from ROM.
3655 }
3656 owned_room_ids.insert(static_cast<uint16_t>(rid));
3657 // Historical slots are only identities within their original room. Undo
3658 // may restore a slot now occupied by another room after table compaction.
3659 // Still reject two blocks in this room claiming the same historical slot.
3660 std::unordered_set<int> claimed_load_orders;
3661 for (const auto& obj : room->GetTileObjects()) {
3662 if ((obj.options() & ObjectOption::Block) != ObjectOption::Block)
3663 continue;
3665 ValidateSpecialObjectDrawLayerSelector(obj, rid, "Pushable block"));
3666 PushableBlockEntry encoded_entry;
3667 encoded_entry.room_id = static_cast<uint16_t>(rid);
3668 encoded_entry.px = obj.x();
3669 encoded_entry.py = obj.y();
3670 encoded_entry.draw_layer = obj.GetLayerValue();
3671 encoded_entry.behavior_layer = obj.block_behavior_layer();
3672 const PushableBlockBytes encoded =
3673 EncodePushableBlockEntry(encoded_entry);
3674 const EncodedBlock encoded_block{encoded, &obj};
3675 const int load_order = obj.block_load_order();
3676 if (load_order >= 0 && !claimed_load_orders.insert(load_order).second) {
3677 return absl::FailedPreconditionError(absl::StrFormat(
3678 "Room 0x%03X has multiple pushable blocks claiming non-new "
3679 "load-order slot %d",
3680 rid, load_order));
3681 }
3682 if (load_order == RoomObject::kBlockLoadOrderNew) {
3683 appended.push_back(encoded_block);
3684 } else if (load_order >= 0 && load_order < original_slot_count) {
3685 const int original_offset = load_order * 4;
3686 const uint16_t original_room_id =
3687 static_cast<uint16_t>(original_buffer[original_offset] |
3688 (original_buffer[original_offset + 1] << 8));
3689 if (original_room_id != static_cast<uint16_t>(rid)) {
3690 // Undo/redo snapshots can restore the load order that was valid
3691 // before a prior save compacted the global table. Never let that
3692 // stale identity replace a different room's entry; preserve the
3693 // object by appending it as a newly reconciled entry instead.
3694 appended.push_back(encoded_block);
3695 continue;
3696 }
3697 slot_replacements.emplace(load_order, encoded_block);
3698 } else {
3699 // load_order points outside the original buffer (e.g. ROM
3700 // changed under us). Treat as new.
3701 appended.push_back(encoded_block);
3702 }
3703 }
3704 }
3705
3706 // Walk the original buffer slot-by-slot, replacing entries owned by
3707 // materialized rooms and preserving the rest verbatim.
3708 std::vector<uint8_t> output;
3709 output.reserve(original_byte_len + appended.size() * 4);
3710 std::vector<std::pair<const RoomObject*, int>> load_order_updates;
3711 load_order_updates.reserve(slot_replacements.size() + appended.size());
3712 const auto append_encoded_block =
3713 [&output, &load_order_updates](const EncodedBlock& block) {
3714 const int output_slot = static_cast<int>(output.size() / 4);
3715 output.push_back(block.bytes.b1);
3716 output.push_back(block.bytes.b2);
3717 output.push_back(block.bytes.b3);
3718 output.push_back(block.bytes.b4);
3719 load_order_updates.emplace_back(block.source_object, output_slot);
3720 };
3721 for (int slot = 0; slot < original_slot_count; ++slot) {
3722 const uint8_t b1 = original_buffer[slot * 4 + 0];
3723 const uint8_t b2 = original_buffer[slot * 4 + 1];
3724 const uint16_t slot_room_id = static_cast<uint16_t>(b1 | (b2 << 8));
3725 if (owned_room_ids.contains(slot_room_id)) {
3726 const auto it = slot_replacements.find(slot);
3727 if (it == slot_replacements.end()) {
3728 // The block at this slot was deleted in memory. Skip it,
3729 // shrinking the output.
3730 continue;
3731 }
3732 append_encoded_block(it->second);
3733 } else {
3734 // Unmaterialized / header-only room: keep the original bytes.
3735 output.push_back(b1);
3736 output.push_back(b2);
3737 output.push_back(original_buffer[slot * 4 + 2]);
3738 output.push_back(original_buffer[slot * 4 + 3]);
3739 }
3740 }
3741 // Append newly-added blocks (load_order == kBlockLoadOrderNew) at the
3742 // tail in creation order. Anything in slot_replacements that didn't
3743 // match a slot was already routed to `appended` above.
3744 for (const auto& block : appended) {
3745 append_encoded_block(block);
3746 }
3747
3748 // LoadAndBuildRoom's block scan is a do-while loop: it always reads the
3749 // entry at $7EF940 before adding four and comparing against this byte
3750 // length. A zero limit can therefore never terminate at the first boundary;
3751 // the 16-bit index walks beyond the 0x200-byte WRAM table until it wraps.
3752 // Fail before the first ROM write and keep edited rooms dirty rather than
3753 // emitting a runtime-unsafe empty table.
3754 if (output.empty()) {
3755 return absl::FailedPreconditionError(
3756 "Pushable-block table cannot be empty: ALTTP's runtime scan reads one "
3757 "entry before comparing the byte-length limit. Keep at least one "
3758 "pushable block, or patch the runtime loop before removing the last "
3759 "entry.");
3760 }
3761
3762 // The runtime buffer has 128 slots; the ROM layout may have fewer.
3763 const int kMaxEntries = (4 * kBlocksRegionSize) / 4;
3764 if (static_cast<int>(output.size() / 4) > kMaxEntries) {
3765 return absl::FailedPreconditionError(absl::StrCat(
3766 "Pushable-block table overflow: ", output.size() / 4,
3767 " entries exceeds the runtime WRAM cap of ", kMaxEntries,
3768 " (a larger table requires runtime buffer and loader changes)."));
3769 }
3770
3771 // Build the write plan from the four destinations preflighted above. Doing
3772 // the topology check before encoding means direct callers that do not wrap
3773 // this public API in a transaction cannot discover a bad later page only
3774 // after an earlier page has already been written.
3775 const int total_bytes = static_cast<int>(output.size());
3777 PreflightBlocksPayloadDestinations(destination_pcs, total_bytes));
3778 BlockSavePlan plan;
3779 auto& write_destinations = plan.destinations;
3780 write_destinations.reserve(4);
3781 for (int r = 0; r < 4; ++r) {
3782 const int off = r * kBlocksRegionSize;
3783 const int len = std::min(kBlocksRegionSize, total_bytes - off);
3784 if (len <= 0)
3785 break;
3786 write_destinations.push_back({destination_pcs[r], off, len});
3787 }
3788
3789 plan.bytes = std::move(output);
3790 plan.load_order_updates = std::move(load_order_updates);
3791 return plan;
3792}
3793
3794} // namespace
3795
3796absl::Status SaveAllBlocks(Rom* rom, int room_count,
3797 const std::function<const Room*(int)>& room_lookup) {
3798 ASSIGN_OR_RETURN(auto plan, BuildBlockSavePlan(rom, room_count, room_lookup));
3799
3800 // Write each prevalidated region. We do not relocate the data — the four
3801 // operand slots keep pointing at their existing SNES addresses.
3802 for (const auto& destination : plan.destinations) {
3803 std::vector<uint8_t> chunk(
3804 plan.bytes.begin() + destination.output_offset,
3805 plan.bytes.begin() + destination.output_offset + destination.length);
3806 RETURN_IF_ERROR(rom->WriteVector(destination.pc, chunk));
3807 }
3808
3810 rom->WriteWord(kBlocksLength, static_cast<uint16_t>(plan.bytes.size())));
3811
3812 // Deleting an entry compacts every following slot. Rebase each loaded
3813 // object's identity to its committed output slot so a later no-op save does
3814 // not try to replace the stale pre-compaction slot and silently drop it.
3815 // This metadata stays untouched until every ROM write succeeds, matching
3816 // the dirty-state failure contract below.
3817 for (const auto& [object, load_order] : plan.load_order_updates) {
3818 const_cast<RoomObject*>(object)->set_block_load_order(load_order);
3819 }
3820 for (int room_id = 0; room_id < room_count; ++room_id) {
3821 if (const Room* room = room_lookup(room_id);
3822 room != nullptr && room->AreBlocksLoaded() && room->blocks_dirty()) {
3823 const_cast<Room*>(room)->ClearBlocksDirty();
3824 }
3825 }
3826 return absl::OkStatus();
3827}
3828
3829absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
3830GetBlockSaveWriteRanges(const Rom* rom, int room_count,
3831 const std::function<const Room*(int)>& room_lookup) {
3832 ASSIGN_OR_RETURN(const auto plan,
3833 BuildBlockSavePlan(rom, room_count, room_lookup));
3834 std::vector<std::pair<uint32_t, uint32_t>> ranges;
3835 ranges.reserve(plan.destinations.size() + 1);
3836 for (const auto& destination : plan.destinations) {
3837 ranges.emplace_back(destination.pc, destination.pc + destination.length);
3838 }
3839 ranges.emplace_back(kBlocksLength, kBlocksLength + 2);
3840 return ranges;
3841}
3842
3843template <typename RoomLookup>
3844absl::Status SaveAllCollisionImpl(Rom* rom, int room_count,
3845 RoomLookup&& room_lookup) {
3846 if (!rom || !rom->is_loaded()) {
3847 return absl::InvalidArgumentError("ROM not loaded");
3848 }
3849
3850 // If the custom collision region doesn't exist (vanilla ROM), treat as a noop
3851 // only when there are no pending custom collision edits. This avoids silently
3852 // dropping user-authored collision changes on ROMs that don't support the
3853 // expanded collision bank.
3854 const auto& rom_data = rom->vector();
3855 const int ptrs_size = kNumberOfRooms * 3;
3856 const bool has_ptr_table = HasCustomCollisionPointerTable(rom_data.size());
3857 const bool has_data_region = HasCustomCollisionDataRegion(rom_data.size());
3858
3859 if (!has_ptr_table) {
3860 for (int room_id = 0; room_id < room_count; ++room_id) {
3861 const Room* room = room_lookup(room_id);
3862 if (room != nullptr && room->custom_collision_dirty()) {
3863 return absl::FailedPreconditionError(
3864 "Custom collision region not present in this ROM");
3865 }
3866 }
3867 return absl::OkStatus();
3868 }
3869
3870 if (!has_data_region) {
3871 for (int room_id = 0; room_id < room_count; ++room_id) {
3872 const Room* room = room_lookup(room_id);
3873 if (room != nullptr && room->custom_collision_dirty()) {
3874 return absl::FailedPreconditionError(
3875 "Custom collision data region not present in this ROM");
3876 }
3877 }
3878 return absl::OkStatus();
3879 }
3880
3881 // Save-time guardrails: custom collision writes must never clobber the
3882 // reserved WaterFill tail region (Oracle of Secrets).
3884 RETURN_IF_ERROR(fence.Allow(
3885 static_cast<uint32_t>(kCustomCollisionRoomPointers),
3886 static_cast<uint32_t>(kCustomCollisionRoomPointers + ptrs_size),
3887 "CustomCollisionPointers"));
3889 fence.Allow(static_cast<uint32_t>(kCustomCollisionDataPosition),
3890 static_cast<uint32_t>(kCustomCollisionDataSoftEnd),
3891 "CustomCollisionData"));
3892 yaze::rom::ScopedWriteFence scope(rom, &fence);
3893
3894 const int room_limit = std::min(room_count, kNumberOfRooms);
3895 for (int room_id = 0; room_id < room_limit; ++room_id) {
3896 const Room* room = room_lookup(room_id);
3897 if (room == nullptr || !room->custom_collision_dirty()) {
3898 continue;
3899 }
3900
3901 const int actual_room_id = room->id();
3902 const int ptr_offset = kCustomCollisionRoomPointers + (actual_room_id * 3);
3903 if (ptr_offset + 2 >= static_cast<int>(rom_data.size())) {
3904 return absl::OutOfRangeError("Custom collision pointer out of range");
3905 }
3906
3907 if (!room->has_custom_collision()) {
3908 // Disable: clear the pointer entry.
3909 RETURN_IF_ERROR(rom->WriteByte(ptr_offset, 0));
3910 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 1, 0));
3911 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 2, 0));
3912 const_cast<Room*>(room)->ClearCustomCollisionDirty();
3913 continue;
3914 }
3915
3916 // A floor-only map is populated; only absent overrides disable the room.
3917 if (!room->custom_collision().HasOverrides()) {
3918 RETURN_IF_ERROR(rom->WriteByte(ptr_offset, 0));
3919 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 1, 0));
3920 RETURN_IF_ERROR(rom->WriteByte(ptr_offset + 2, 0));
3921 const_cast<Room*>(room)->ClearCustomCollisionDirty();
3922 continue;
3923 }
3924
3926 WriteTrackCollision(rom, actual_room_id, room->custom_collision()));
3927 const_cast<Room*>(room)->ClearCustomCollisionDirty();
3928 }
3929
3930 return absl::OkStatus();
3931}
3932
3933absl::Status SaveAllCollision(Rom* rom, absl::Span<Room> rooms) {
3934 return SaveAllCollisionImpl(
3935 rom, static_cast<int>(rooms.size()),
3936 [&rooms](int room_id) { return &rooms[room_id]; });
3937}
3938
3939absl::Status SaveAllCollision(Rom* rom, int room_count,
3940 const std::function<Room*(int)>& room_lookup) {
3941 return SaveAllCollisionImpl(rom, room_count, room_lookup);
3942}
3943
3944absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
3946 if (rom == nullptr || !rom->is_loaded()) {
3947 return absl::InvalidArgumentError("ROM not loaded");
3948 }
3949 const auto& rom_data = rom->vector();
3950 if (kChestsLengthPointer + 1 >= static_cast<int>(rom_data.size()) ||
3951 kChestsDataPointer1 + 2 >= static_cast<int>(rom_data.size())) {
3952 return absl::OutOfRangeError("Chest pointers out of range");
3953 }
3954
3955 const uint32_t data_pointer =
3956 (static_cast<uint32_t>(rom_data[kChestsDataPointer1 + 2]) << 16) |
3957 (static_cast<uint32_t>(rom_data[kChestsDataPointer1 + 1]) << 8) |
3958 rom_data[kChestsDataPointer1];
3959 const uint32_t data_pc = SnesToPc(data_pointer);
3960 if (data_pc > rom_data.size() ||
3961 static_cast<size_t>(kChestTableCapacityBytes) >
3962 rom_data.size() - static_cast<size_t>(data_pc)) {
3963 return absl::OutOfRangeError("Chest data region out of range");
3964 }
3965 const uint32_t data_end =
3966 data_pc + static_cast<uint32_t>(kChestTableCapacityBytes);
3967 const auto overlaps = [](uint32_t begin, uint32_t end, uint32_t other_begin,
3968 uint32_t other_end) {
3969 return begin < other_end && other_begin < end;
3970 };
3971 if (overlaps(data_pc, data_end, kChestsLengthPointer,
3972 kChestsLengthPointer + 2) ||
3973 overlaps(data_pc, data_end, kChestsDataPointer1,
3974 kChestsDataPointer1 + 3)) {
3975 return absl::FailedPreconditionError(
3976 "Chest data region overlaps chest metadata operands");
3977 }
3978
3979 return std::vector<std::pair<uint32_t, uint32_t>>{
3980 {static_cast<uint32_t>(kChestsLengthPointer),
3981 static_cast<uint32_t>(kChestsLengthPointer + 2)},
3982 {data_pc, data_end},
3983 };
3984}
3985
3986namespace {
3987
3989 uint16_t word = 0;
3990 uint8_t item = 0;
3991
3992 uint16_t room_id() const { return word & 0x7FFF; }
3993};
3994
3995// Parse current ROM chest data without grouping or normalizing records.
3996// `byte_length` is the runtime byte count at kChestsLengthPointer.
3997std::vector<PhysicalChestRecord> ParsePhysicalRomChests(
3998 const std::vector<uint8_t>& rom_data, int cpos, int byte_length) {
3999 std::vector<PhysicalChestRecord> records;
4000 const int record_count = byte_length / kChestTableRecordSize;
4001 records.reserve(record_count);
4002 for (int i = 0; i < record_count; ++i) {
4003 const int off = cpos + i * kChestTableRecordSize;
4004 if (off < 0 ||
4005 off + kChestTableRecordSize > static_cast<int>(rom_data.size())) {
4006 break;
4007 }
4008 const uint16_t word =
4009 (static_cast<uint16_t>(rom_data[off + 1]) << 8) | rom_data[off];
4010 records.push_back(PhysicalChestRecord{word, rom_data[off + 2]});
4011 }
4012 return records;
4013}
4014
4015void AppendChestRecord(std::vector<uint8_t>* bytes, uint16_t word,
4016 uint8_t item) {
4017 bytes->push_back(word & 0xFF);
4018 bytes->push_back((word >> 8) & 0xFF);
4019 bytes->push_back(item);
4020}
4021
4022void AppendEditedChestRecord(std::vector<uint8_t>* bytes, int room_id,
4023 const chest_data& chest) {
4024 const uint16_t word = static_cast<uint16_t>(room_id) |
4025 (chest.size ? static_cast<uint16_t>(0x8000) : 0);
4026 AppendChestRecord(bytes, word, chest.id);
4027}
4028
4029void AppendChangedChestRuns(uint32_t pc, absl::Span<const uint8_t> expected,
4030 absl::Span<const uint8_t> replacement,
4031 std::vector<ChestWriteRun>* writes) {
4032 size_t cursor = 0;
4033 while (cursor < replacement.size()) {
4034 if (replacement[cursor] == expected[cursor]) {
4035 ++cursor;
4036 continue;
4037 }
4038 const size_t begin = cursor;
4039 do {
4040 ++cursor;
4041 } while (cursor < replacement.size() &&
4042 replacement[cursor] != expected[cursor]);
4043
4044 ChestWriteRun run;
4045 run.pc = pc + static_cast<uint32_t>(begin);
4046 run.expected_bytes.assign(expected.begin() + begin,
4047 expected.begin() + cursor);
4048 run.replacement_bytes.assign(replacement.begin() + begin,
4049 replacement.begin() + cursor);
4050 if (!writes->empty() && writes->back().end() == run.pc) {
4051 writes->back().expected_bytes.insert(writes->back().expected_bytes.end(),
4052 run.expected_bytes.begin(),
4053 run.expected_bytes.end());
4054 writes->back().replacement_bytes.insert(
4055 writes->back().replacement_bytes.end(), run.replacement_bytes.begin(),
4056 run.replacement_bytes.end());
4057 } else {
4058 writes->push_back(std::move(run));
4059 }
4060 }
4061}
4062
4063void SortAndCoalesceChestRuns(std::vector<ChestWriteRun>* writes) {
4064 std::sort(writes->begin(), writes->end(),
4065 [](const ChestWriteRun& lhs, const ChestWriteRun& rhs) {
4066 return lhs.pc < rhs.pc;
4067 });
4068 std::vector<ChestWriteRun> merged;
4069 merged.reserve(writes->size());
4070 for (ChestWriteRun& write : *writes) {
4071 if (!merged.empty() && merged.back().end() == write.pc) {
4072 merged.back().expected_bytes.insert(merged.back().expected_bytes.end(),
4073 write.expected_bytes.begin(),
4074 write.expected_bytes.end());
4075 merged.back().replacement_bytes.insert(
4076 merged.back().replacement_bytes.end(),
4077 write.replacement_bytes.begin(), write.replacement_bytes.end());
4078 continue;
4079 }
4080 merged.push_back(std::move(write));
4081 }
4082 *writes = std::move(merged);
4083}
4084
4085std::vector<uint8_t> EncodeChestRoomState(int room_id, const Room& room) {
4086 std::vector<uint8_t> bytes;
4087 bytes.reserve(room.GetChests().size() * kChestTableRecordSize);
4088 for (const chest_data& chest : room.GetChests()) {
4089 AppendEditedChestRecord(&bytes, room_id, chest);
4090 }
4091 return bytes;
4092}
4093
4094absl::StatusOr<ChestSavePlan> BuildChestSavePlanImpl(
4095 const Rom* rom, int room_count,
4096 const std::function<const Room*(int)>& room_lookup) {
4097 if (rom == nullptr || !rom->is_loaded()) {
4098 return absl::InvalidArgumentError("ROM not loaded");
4099 }
4100 const auto& rom_data = rom->vector();
4101 if (kChestsLengthPointer + 1 >= static_cast<int>(rom_data.size()) ||
4102 kChestsDataPointer1 + 2 >= static_cast<int>(rom_data.size())) {
4103 return absl::OutOfRangeError("Chest pointers out of range");
4104 }
4105
4106 ChestSavePlan plan;
4107 plan.room_limit = std::min(room_count, kNumberOfRooms);
4108 std::vector<const Room*> dirty_rooms(kNumberOfRooms, nullptr);
4109 for (int room_id = 0; room_id < plan.room_limit; ++room_id) {
4110 const Room* room = room_lookup(room_id);
4111 if (room != nullptr && room->chests_dirty()) {
4112 dirty_rooms[room_id] = room;
4113 plan.any_dirty = true;
4114 plan.dirty_rooms.push_back(
4115 ChestDirtyRoomState{room_id, EncodeChestRoomState(room_id, *room)});
4116 }
4117 }
4118 if (!plan.any_dirty) {
4119 return plan;
4120 }
4121
4122 ASSIGN_OR_RETURN(auto potential_ranges, GetChestTableWriteRanges(rom));
4123 plan.data_pc = potential_ranges[1].first;
4124 std::copy_n(rom_data.begin() + kChestsDataPointer1,
4125 plan.pointer_operand.size(), plan.pointer_operand.begin());
4127 static_cast<uint16_t>((rom_data[kChestsLengthPointer + 1] << 8) |
4128 rom_data[kChestsLengthPointer]);
4129 plan.original_capacity_bytes.assign(
4130 rom_data.begin() + plan.data_pc,
4131 rom_data.begin() + plan.data_pc + kChestTableCapacityBytes);
4134 return absl::FailedPreconditionError(absl::StrFormat(
4135 "Chest table byte length %d is invalid (capacity %d)",
4136 static_cast<int>(plan.original_byte_length), kChestTableCapacityBytes));
4137 }
4138
4139 const auto physical_records = ParsePhysicalRomChests(
4140 rom_data, static_cast<int>(plan.data_pc), plan.original_byte_length);
4141 if (physical_records.size() !=
4142 static_cast<size_t>(plan.original_byte_length / kChestTableRecordSize)) {
4143 return absl::OutOfRangeError("Chest data region is truncated");
4144 }
4145
4146 std::vector<size_t> old_counts(kNumberOfRooms, 0);
4147 for (const PhysicalChestRecord& record : physical_records) {
4148 if (record.room_id() < kNumberOfRooms) {
4149 ++old_counts[record.room_id()];
4150 }
4151 }
4152
4153 size_t final_record_count = physical_records.size();
4154 for (int room_id = 0; room_id < plan.room_limit; ++room_id) {
4155 if (dirty_rooms[room_id] == nullptr) {
4156 continue;
4157 }
4158 final_record_count -= old_counts[room_id];
4159 final_record_count += dirty_rooms[room_id]->GetChests().size();
4160 }
4161 if (final_record_count > static_cast<size_t>(kChestTableCapacityRecords)) {
4162 return absl::ResourceExhaustedError(absl::StrFormat(
4163 "Chest table has %d records; capacity is %d",
4164 static_cast<int>(final_record_count), kChestTableCapacityRecords));
4165 }
4166
4167 std::vector<uint8_t> replacement_bytes;
4168 replacement_bytes.reserve(final_record_count * kChestTableRecordSize);
4169 std::vector<size_t> seen_counts(kNumberOfRooms, 0);
4170 for (const PhysicalChestRecord& record : physical_records) {
4171 const uint16_t room_id = record.room_id();
4172 const Room* dirty_room =
4173 room_id < kNumberOfRooms ? dirty_rooms[room_id] : nullptr;
4174 if (dirty_room == nullptr) {
4175 AppendChestRecord(&replacement_bytes, record.word, record.item);
4176 continue;
4177 }
4178
4179 const size_t occurrence = seen_counts[room_id]++;
4180 const auto& replacements = dirty_room->GetChests();
4181 if (occurrence < replacements.size()) {
4182 AppendEditedChestRecord(&replacement_bytes, room_id,
4183 replacements[occurrence]);
4184 }
4185 }
4186
4187 // Growth has no existing physical slot. Append extras in room-ID order so
4188 // repeated saves are deterministic while every pre-existing record keeps its
4189 // relative position.
4190 for (int room_id = 0; room_id < plan.room_limit; ++room_id) {
4191 const Room* dirty_room = dirty_rooms[room_id];
4192 if (dirty_room == nullptr) {
4193 continue;
4194 }
4195 const auto& replacements = dirty_room->GetChests();
4196 for (size_t i = seen_counts[room_id]; i < replacements.size(); ++i) {
4197 AppendEditedChestRecord(&replacement_bytes, room_id, replacements[i]);
4198 }
4199 }
4200
4201 if (replacement_bytes.size() != final_record_count * kChestTableRecordSize) {
4202 return absl::InternalError("Chest save plan size mismatch");
4203 }
4204
4205 const std::array<uint8_t, 2> expected_length = {
4206 static_cast<uint8_t>(plan.original_byte_length & 0xFF),
4207 static_cast<uint8_t>((plan.original_byte_length >> 8) & 0xFF)};
4208 const uint16_t replacement_length =
4209 static_cast<uint16_t>(replacement_bytes.size());
4210 const std::array<uint8_t, 2> encoded_replacement_length = {
4211 static_cast<uint8_t>(replacement_length & 0xFF),
4212 static_cast<uint8_t>((replacement_length >> 8) & 0xFF)};
4214 encoded_replacement_length, &plan.writes);
4216 absl::MakeConstSpan(plan.original_capacity_bytes)
4217 .subspan(0, replacement_bytes.size()),
4218 replacement_bytes, &plan.writes);
4220 return plan;
4221}
4222
4223int ReadRoomPotItemAddressPc(const std::vector<uint8_t>& rom_data,
4224 int room_id) {
4225 if (room_id < 0 || room_id >= kNumberOfRooms) {
4226 return -1;
4227 }
4228 const int ptr_off = kRoomItemsPointers + (room_id * 2);
4229 if (ptr_off < 0 || ptr_off + 1 >= static_cast<int>(rom_data.size())) {
4230 return -1;
4231 }
4232 const uint16_t item_ptr =
4233 (static_cast<uint16_t>(rom_data[ptr_off + 1]) << 8) | rom_data[ptr_off];
4234 if (item_ptr < 0x8000) {
4235 return -1;
4236 }
4237 const int item_addr = static_cast<int>(SnesToPc(0x010000 | item_ptr));
4238 return item_addr >= 0 && item_addr < static_cast<int>(rom_data.size())
4239 ? item_addr
4240 : -1;
4241}
4242
4243absl::StatusOr<PhysicalStreamInfo> GetPotItemStreamInfo(
4244 const std::vector<uint8_t>& rom_data, int room_id) {
4246 static_cast<int>(rom_data.size())) {
4247 return absl::OutOfRangeError("Room items pointer table out of range");
4248 }
4249 if (room_id < 0 || room_id >= kNumberOfRooms) {
4250 return absl::OutOfRangeError("Room ID out of range");
4251 }
4252
4253 std::vector<int> addresses(kNumberOfRooms, -1);
4254 for (int id = 0; id < kNumberOfRooms; ++id) {
4255 addresses[id] = ReadRoomPotItemAddressPc(rom_data, id);
4256 }
4257 const int hard_end =
4258 std::min(static_cast<int>(rom_data.size()), kRoomItemsDataEnd);
4259 PhysicalStreamInfo info = AnalyzePhysicalStream(addresses, room_id, hard_end);
4260 if (info.address < 0 || info.address >= hard_end) {
4261 return absl::FailedPreconditionError(
4262 "Room pot item pointer is null, invalid, or outside the item region");
4263 }
4264 return info;
4265}
4266
4267} // namespace
4268
4269absl::StatusOr<ChestSavePlan> BuildChestSavePlan(
4270 const Rom* rom, int room_count,
4271 const std::function<const Room*(int)>& room_lookup) {
4272 return BuildChestSavePlanImpl(rom, room_count, room_lookup);
4273}
4274
4275absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
4276GetDirtyChestWriteRanges(const Rom* rom, int room_count,
4277 const std::function<const Room*(int)>& room_lookup) {
4279 BuildChestSavePlan(rom, room_count, room_lookup));
4280 return plan.write_ranges();
4281}
4282
4284 Rom* rom, const ChestSavePlan& plan,
4285 const std::function<const Room*(int)>& room_lookup) {
4286 if (rom == nullptr || !rom->is_loaded()) {
4287 return absl::InvalidArgumentError("ROM not loaded");
4288 }
4289 if (!plan.any_dirty) {
4290 ASSIGN_OR_RETURN(ChestSavePlan canonical_plan,
4291 BuildChestSavePlan(rom, plan.room_limit, room_lookup));
4292 if (canonical_plan != plan) {
4293 return absl::FailedPreconditionError(
4294 "Chest save plan does not match canonical serialization");
4295 }
4296 return absl::OkStatus();
4297 }
4298
4299 const auto& rom_data = rom->vector();
4300 if (kChestsLengthPointer + 1 >= static_cast<int>(rom_data.size()) ||
4301 kChestsDataPointer1 + 2 >= static_cast<int>(rom_data.size()) ||
4302 plan.data_pc > rom_data.size() ||
4303 plan.original_capacity_bytes.size() > rom_data.size() - plan.data_pc) {
4304 return absl::FailedPreconditionError(
4305 "Chest save plan source is no longer addressable");
4306 }
4307 if (!std::equal(plan.pointer_operand.begin(), plan.pointer_operand.end(),
4308 rom_data.begin() + kChestsDataPointer1)) {
4309 return absl::FailedPreconditionError(
4310 "Chest save plan is stale: data pointer changed");
4311 }
4312 const uint16_t current_length =
4313 static_cast<uint16_t>((rom_data[kChestsLengthPointer + 1] << 8) |
4314 rom_data[kChestsLengthPointer]);
4315 if (current_length != plan.original_byte_length) {
4316 return absl::FailedPreconditionError(
4317 "Chest save plan is stale: runtime length changed");
4318 }
4319 if (!std::equal(plan.original_capacity_bytes.begin(),
4320 plan.original_capacity_bytes.end(),
4321 rom_data.begin() + plan.data_pc)) {
4322 return absl::FailedPreconditionError(
4323 "Chest save plan is stale: table bytes changed");
4324 }
4325 for (const ChestDirtyRoomState& state : plan.dirty_rooms) {
4326 const Room* room = room_lookup(state.room_id);
4327 if (room == nullptr || !room->chests_dirty() ||
4328 EncodeChestRoomState(state.room_id, *room) != state.encoded_chests) {
4329 return absl::FailedPreconditionError(absl::StrFormat(
4330 "Chest save plan is stale for room 0x%03X", state.room_id));
4331 }
4332 }
4333 ASSIGN_OR_RETURN(ChestSavePlan canonical_plan,
4334 BuildChestSavePlan(rom, plan.room_limit, room_lookup));
4335 if (canonical_plan != plan) {
4336 return absl::FailedPreconditionError(
4337 "Chest save plan does not match canonical serialization");
4338 }
4339
4340 yaze::ScopedRomTransaction transaction(*rom);
4342 for (const ChestWriteRun& write : plan.writes) {
4343 RETURN_IF_ERROR(fence.Allow(write.pc, write.end(), "ChestTableExactDelta"));
4344 }
4345 yaze::rom::ScopedWriteFence scope(rom, &fence);
4346 for (const ChestWriteRun& write : plan.writes) {
4347 RETURN_IF_ERROR(rom->WriteVector(write.pc, write.replacement_bytes));
4348 }
4349 for (const ChestDirtyRoomState& state : plan.dirty_rooms) {
4350 const_cast<Room*>(room_lookup(state.room_id))->ClearChestsDirty();
4351 }
4352 transaction.Commit();
4353 return absl::OkStatus();
4354}
4355
4356template <typename RoomLookup>
4357absl::Status SaveAllChestsImpl(Rom* rom, int room_count,
4358 RoomLookup&& room_lookup) {
4359 const std::function<const Room*(int)> lookup =
4360 std::forward<RoomLookup>(room_lookup);
4362 BuildChestSavePlan(rom, room_count, lookup));
4363 return ApplyChestSavePlan(rom, plan, lookup);
4364}
4365
4366absl::Status SaveAllChests(Rom* rom, absl::Span<const Room> rooms) {
4367 return SaveAllChestsImpl(rom, static_cast<int>(rooms.size()),
4368 [&rooms](int room_id) { return &rooms[room_id]; });
4369}
4370
4371absl::Status SaveAllChests(Rom* rom, int room_count,
4372 const std::function<const Room*(int)>& room_lookup) {
4373 return SaveAllChestsImpl(rom, room_count, room_lookup);
4374}
4375
4376template <typename RoomLookup>
4378 Rom* rom, int room_count, RoomLookup&& room_lookup,
4379 const DungeonStreamLayout* repack_layout = nullptr) {
4380 if (!rom || !rom->is_loaded()) {
4381 return absl::InvalidArgumentError("ROM not loaded");
4382 }
4383 const auto& rom_data = rom->vector();
4385 static_cast<int>(rom_data.size())) {
4386 return absl::OutOfRangeError("Room items pointer table out of range");
4387 }
4388
4389 const int room_limit = std::min(room_count, kNumberOfRooms);
4390 if (repack_layout != nullptr) {
4391 std::vector<DungeonStreamReplacement> replacements;
4392 for (int room_id = 0; room_id < room_limit; ++room_id) {
4393 const Room* room = room_lookup(room_id);
4394 if (room == nullptr || !room->pot_items_dirty()) {
4395 continue;
4396 }
4397
4398 DungeonStreamReplacement replacement;
4399 replacement.room_id = static_cast<uint32_t>(room_id);
4400 replacement.encoded_stream.reserve(room->GetPotItems().size() * 3 + 2);
4401 for (const PotItem& item : room->GetPotItems()) {
4402 replacement.encoded_stream.push_back(item.position & 0xFF);
4403 replacement.encoded_stream.push_back((item.position >> 8) & 0xFF);
4404 replacement.encoded_stream.push_back(item.item);
4405 }
4406 replacement.encoded_stream.push_back(0xFF);
4407 replacement.encoded_stream.push_back(0xFF);
4408 replacements.push_back(std::move(replacement));
4409 }
4410 if (replacements.empty()) {
4411 return absl::OkStatus();
4412 }
4413
4415 InventoryDungeonStreams(*rom, *repack_layout));
4417 PlanDungeonStreamRepack(inventory, replacements));
4419 for (const DungeonStreamReplacement& replacement : replacements) {
4420 if (const Room* room = room_lookup(replacement.room_id);
4421 room != nullptr) {
4422 const_cast<Room*>(room)->ClearPotItemsDirty();
4423 }
4424 }
4425 return absl::OkStatus();
4426 }
4427
4428 struct PendingPotItemWrite {
4429 int room_id = -1;
4430 int address = -1;
4431 std::vector<uint8_t> bytes;
4432 };
4433
4434 // Build and validate every dirty write before touching the ROM. A later
4435 // shared/overfull stream must not leave earlier rooms partially written.
4436 std::vector<PendingPotItemWrite> pending_writes;
4437 for (int room_id = 0; room_id < room_limit; ++room_id) {
4438 const Room* room = room_lookup(room_id);
4439 if (room == nullptr || !room->pot_items_dirty()) {
4440 continue;
4441 }
4442
4443 ASSIGN_OR_RETURN(const PhysicalStreamInfo stream_info,
4444 GetPotItemStreamInfo(rom_data, room_id));
4445 if (stream_info.shared) {
4446 return absl::FailedPreconditionError(absl::StrFormat(
4447 "Room %d pot item stream at PC 0x%06X is shared; repacking is "
4448 "required",
4449 room_id, stream_info.address));
4450 }
4451 if (stream_info.capacity() <= 0) {
4452 return absl::FailedPreconditionError(absl::StrFormat(
4453 "Room %d pot item stream has no safe physical boundary", room_id));
4454 }
4455
4456 PendingPotItemWrite pending;
4457 pending.room_id = room_id;
4458 pending.address = stream_info.address;
4459 for (const auto& pi : room->GetPotItems()) {
4460 pending.bytes.push_back(pi.position & 0xFF);
4461 pending.bytes.push_back((pi.position >> 8) & 0xFF);
4462 pending.bytes.push_back(pi.item);
4463 }
4464 pending.bytes.push_back(0xFF);
4465 pending.bytes.push_back(0xFF);
4466 if (static_cast<int>(pending.bytes.size()) > stream_info.capacity()) {
4467 return absl::ResourceExhaustedError(absl::StrFormat(
4468 "Room %d pot item data too large! Size: %d, Available: %d", room_id,
4469 static_cast<int>(pending.bytes.size()), stream_info.capacity()));
4470 }
4471 pending_writes.push_back(std::move(pending));
4472 }
4473
4474 for (const auto& pending : pending_writes) {
4475 const bool data_changed =
4476 !std::equal(pending.bytes.begin(), pending.bytes.end(),
4477 rom_data.begin() + pending.address);
4478 if (data_changed) {
4479 RETURN_IF_ERROR(rom->WriteVector(pending.address, pending.bytes));
4480 }
4481 }
4482
4483 for (const auto& pending : pending_writes) {
4484 if (const Room* room = room_lookup(pending.room_id); room != nullptr) {
4485 const_cast<Room*>(room)->ClearPotItemsDirty();
4486 }
4487 }
4488 return absl::OkStatus();
4489}
4490
4491absl::Status SaveAllPotItems(Rom* rom, absl::Span<const Room> rooms) {
4492 return SaveAllPotItemsImpl(rom, static_cast<int>(rooms.size()),
4493 [&rooms](int room_id) { return &rooms[room_id]; });
4494}
4495
4496absl::Status SaveAllPotItems(Rom* rom, absl::Span<const Room> rooms,
4497 const DungeonStreamLayout* repack_layout) {
4498 return SaveAllPotItemsImpl(
4499 rom, static_cast<int>(rooms.size()),
4500 [&rooms](int room_id) { return &rooms[room_id]; }, repack_layout);
4501}
4502
4503absl::Status SaveAllPotItems(
4504 Rom* rom, int room_count,
4505 const std::function<const Room*(int)>& room_lookup) {
4506 return SaveAllPotItemsImpl(rom, room_count, room_lookup);
4507}
4508
4509absl::Status SaveAllPotItems(Rom* rom, int room_count,
4510 const std::function<const Room*(int)>& room_lookup,
4511 const DungeonStreamLayout* repack_layout) {
4512 return SaveAllPotItemsImpl(rom, room_count, room_lookup, repack_layout);
4513}
4514
4516 const auto& rom_data = rom()->vector();
4517
4518 // Read blocks length
4519 int blocks_count =
4520 (rom_data[kBlocksLength + 1] << 8) | rom_data[kBlocksLength];
4521
4522 LOG_DEBUG("Room", "LoadBlocks: room_id=%d, blocks_count=%d", room_id_,
4523 blocks_count);
4524
4525 // Load block data from the four data regions.
4526 //
4527 // `kBlocksPointer1..4` are 3-byte SNES long-address operand slots
4528 // embedded in bank_02's LDA.l instructions (`$02:DAF9..$02:DB2E`),
4529 // not inline data offsets. Each operand encodes
4530 // `data_base + region_offset` where data_base is the SNES address
4531 // of `SpecialUnderworldObjects_pushable_block` ($04:F1DE in vanilla)
4532 // and region_offset is `r * 0x80`. The previous code read directly
4533 // from the operand slots, so it was decoding the LDA.l opcode
4534 // operand bytes as block data — silently corrupting every block on
4535 // load. `SaveAllBlocks` has always dereferenced these correctly;
4536 // load now matches.
4537 const int kRegionSize = 0x80;
4538 const int kPointerSlots[4] = {kBlocksPointer1, kBlocksPointer2,
4540 std::vector<uint8_t> blocks_data(blocks_count, 0);
4541 for (int r = 0; r < 4; ++r) {
4542 const int slot = kPointerSlots[r];
4543 if (slot + 2 >= static_cast<int>(rom_data.size())) {
4544 LOG_WARN("Room", "LoadBlocks: pointer slot %d out of range", r);
4545 return;
4546 }
4547 const absl::Status operand_status =
4548 ValidateBlocksLoaderPointerOperand(rom_data, slot);
4549 if (!operand_status.ok()) {
4550 LOG_WARN("Room", "LoadBlocks: %s",
4551 std::string(operand_status.message()).c_str());
4552 return;
4553 }
4554 const int snes =
4555 (rom_data[slot + 2] << 16) | (rom_data[slot + 1] << 8) | rom_data[slot];
4556 const int pc = SnesToPc(snes);
4557 const int off = r * kRegionSize;
4558 const int len = std::min(kRegionSize, blocks_count - off);
4559 if (len <= 0)
4560 break;
4561 if (pc < 0 || pc + len > static_cast<int>(rom_data.size())) {
4562 LOG_WARN("Room", "LoadBlocks: region %d data out of range", r);
4563 return;
4564 }
4565 std::copy_n(rom_data.begin() + pc, len, blocks_data.begin() + off);
4566 }
4567
4568 // Avoid duplication if LoadBlocks is called multiple times. Do this only
4569 // after the ROM pointer operands and data regions are known-good so a guard
4570 // failure cannot make existing in-memory block objects vanish.
4571 tile_objects_.erase(
4572 std::remove_if(tile_objects_.begin(), tile_objects_.end(),
4573 [](const RoomObject& obj) {
4574 return (obj.options() & ObjectOption::Block) !=
4575 ObjectOption::Nothing;
4576 }),
4577 tile_objects_.end());
4578
4579 // Parse blocks for this room (4 bytes per block entry).
4580 //
4581 // Vanilla scan (bank_01.asm:1162) walks the flat 396-byte table linearly
4582 // matching on room_id; there is no per-room 0xFFFF terminator. The
4583 // previous "break on b3==0xFF && b4==0xFF after room_id match" guard was
4584 // a phantom — it never fired in vanilla and would have prematurely
4585 // truncated a room's block list if a future tombstone happened to share
4586 // its room_id. Removed alongside the decoder fix.
4587 for (int i = 0; i + 3 < blocks_count; i += 4) {
4588 PushableBlockBytes bytes{blocks_data[i], blocks_data[i + 1],
4589 blocks_data[i + 2], blocks_data[i + 3]};
4590 const PushableBlockEntry entry = DecodePushableBlockEntry(bytes);
4591 if (entry.room_id != room_id_)
4592 continue;
4593
4594 RoomObject block_obj(0x0E00, entry.px, entry.py, 0, entry.draw_layer);
4595 block_obj.SetRom(rom_);
4598 // Capture the entry's slot index in the global buffer so
4599 // SaveAllBlocks can emit entries in vanilla authoring order
4600 // (interleaved across rooms; sorting by room_id would reshuffle
4601 // bytes and break byte equality on no-op saves).
4602 block_obj.set_block_load_order(i / 4);
4603 tile_objects_.push_back(block_obj);
4604
4605 LOG_DEBUG("Room", "Loaded block at (%d,%d) draw_layer=%d behavior_layer=%d",
4606 entry.px, entry.py, entry.draw_layer, entry.behavior_layer);
4607 }
4608 blocks_loaded_ = true;
4609}
4610
4612 if (!rom_ || !rom_->is_loaded())
4613 return;
4614 auto rom_data = rom()->vector();
4615 pot_items_.clear();
4616 pot_items_loaded_ = false;
4617
4618 // Load pot items
4619 // Format per ASM analysis (bank_01.asm):
4620 // - Pointer table at kRoomItemsPointers (0x01DB69)
4621 // - Each room has a pointer to item data
4622 // - Item data format: 3 bytes per item
4623 // - 2 bytes: position word (Y_hi, X_lo encoding)
4624 // - 1 byte: item type
4625 // - Terminated by 0xFFFF position word
4626
4627 int table_addr = kRoomItemsPointers; // 0x01DB69
4628
4629 // Read pointer for this room
4630 int ptr_addr = table_addr + (room_id_ * 2);
4631 if (ptr_addr + 1 >= static_cast<int>(rom_data.size()))
4632 return;
4633
4634 uint16_t item_ptr = (rom_data[ptr_addr + 1] << 8) | rom_data[ptr_addr];
4635
4636 // Convert to PC address (Bank 01 offset)
4637 int item_addr = SnesToPc(0x010000 | item_ptr);
4638
4639 // Read 3-byte entries until 0xFFFF terminator
4640 while (item_addr + 2 < static_cast<int>(rom_data.size())) {
4641 // Read position word (little endian)
4642 uint16_t position = (rom_data[item_addr + 1] << 8) | rom_data[item_addr];
4643
4644 // Check for terminator
4645 if (position == 0xFFFF)
4646 break;
4647
4648 // Read item type (3rd byte)
4649 uint8_t item_type = rom_data[item_addr + 2];
4650
4651 PotItem pot_item;
4652 pot_item.position = position;
4653 pot_item.item = item_type;
4654 pot_items_.push_back(pot_item);
4655
4656 item_addr += 3; // Move to next entry
4657 }
4658
4659 pot_items_loaded_ = true;
4660}
4661
4663 auto rom_data = rom()->vector();
4664
4665 // The legacy symbol `kPitCount` is the LDX.w immediate at PC 0x394A6
4666 // — the **maximum X offset** in the runtime CMP loop, not an entry
4667 // count. Total entries = `(max_offset / 2) + 1`. This function does
4668 // not actually consume the table contents (yaze has no editable
4669 // surface for pit-damage gating); it just resolves the dereferenced
4670 // address for diagnostic logging. See
4671 // `test/integration/zelda3/dungeon_save_region_test.cc` for the
4672 // format-pinning tests and `memory/project_dungeon_pit_audit.md`
4673 // for the audit conclusion.
4674 const int max_offset = rom_data[kPitCount];
4675 const int pit_entries = max_offset / 2 + 1;
4676
4677 const int pit_ptr = (rom_data[kPitPointer + 2] << 16) |
4678 (rom_data[kPitPointer + 1] << 8) | rom_data[kPitPointer];
4679
4680 LOG_DEBUG("Room",
4681 "LoadPits: room_id=%d, RoomsWithPitDamage entries=%d, "
4682 "table_snes=0x%06X",
4683 room_id_, pit_entries, pit_ptr);
4684
4685 // The per-room pit DESTINATION (where Link goes when falling through
4686 // a non-damaging pit) is unrelated to the global RoomsWithPitDamage
4687 // table read above. It lives in the room header and was loaded into
4688 // `pits_` (target room + target_layer) by `LoadRoomFromRom`. The
4689 // round-trip for that state goes through the room header save path,
4690 // not `SaveAllPits`.
4691 LOG_DEBUG("Room", "Per-room pit destination: target=%d, target_layer=%d",
4693}
4694
4695// ============================================================================
4696// Object Limit Counting (ZScream Feature Parity)
4697// ============================================================================
4698
4699std::map<DungeonLimit, int> Room::GetLimitedObjectCounts() const {
4700 auto counts = CreateLimitCounter();
4701
4702 // Count sprites
4703 counts[DungeonLimit::kSprites] = static_cast<int>(sprites_.size());
4704
4705 // Count overlords (sprites with ID > 0x40 are overlords in ALTTP)
4706 for (const auto& sprite : sprites_) {
4707 if (sprite.IsOverlord()) {
4708 counts[DungeonLimit::Overlords]++;
4709 }
4710 }
4711
4712 // Count chests
4713 counts[DungeonLimit::kChests] = static_cast<int>(chests_in_room_.size());
4714
4715 // Count doors (total and special)
4716 counts[DungeonLimit::kDoors] = static_cast<int>(doors_.size());
4717 for (const auto& door : doors_) {
4718 // Special doors: shutters and key-locked doors.
4719 const bool is_special = [&]() -> bool {
4720 switch (door.type) {
4735 return true;
4736 default:
4737 return false;
4738 }
4739 }();
4740 if (is_special) {
4742 }
4743 }
4744
4745 // Count stairs
4747 static_cast<int>(z3_staircases_.size());
4748
4749 // Count objects with specific options
4750 for (const auto& obj : tile_objects_) {
4751 auto options = obj.options();
4752
4753 // Count blocks
4754 if ((options & ObjectOption::Block) != ObjectOption::Nothing) {
4755 counts[DungeonLimit::Blocks]++;
4756 }
4757
4758 // Count torches
4759 if ((options & ObjectOption::Torch) != ObjectOption::Nothing) {
4760 counts[DungeonLimit::Torches]++;
4761 }
4762
4763 // Count star tiles (object IDs 0x11E and 0x11F)
4764 if (obj.id_ == 0x11E || obj.id_ == 0x11F) {
4765 counts[DungeonLimit::StarTiles]++;
4766 }
4767
4768 // Count somaria paths (object IDs in 0xF83-0xF8F range)
4769 if (obj.id_ >= 0xF83 && obj.id_ <= 0xF8F) {
4770 counts[DungeonLimit::SomariaLine]++;
4771 }
4772
4773 // Count staircase objects based on direction
4774 if ((options & ObjectOption::Stairs) != ObjectOption::Nothing) {
4775 // North-facing stairs: IDs 0x130-0x135
4776 if ((obj.id_ >= 0x130 && obj.id_ <= 0x135) || obj.id_ == 0x139 ||
4777 obj.id_ == 0x13A || obj.id_ == 0x13B) {
4778 counts[DungeonLimit::StairsNorth]++;
4779 }
4780 // South-facing stairs: IDs 0x13B-0x13D
4781 else if (obj.id_ >= 0x13C && obj.id_ <= 0x13F) {
4782 counts[DungeonLimit::StairsSouth]++;
4783 }
4784 }
4785
4786 // Count general manipulable objects
4787 if ((options & ObjectOption::Block) != ObjectOption::Nothing ||
4791 }
4792 }
4793
4794 return counts;
4795}
4796
4798 auto counts = GetLimitedObjectCounts();
4799 return yaze::zelda3::HasExceededLimits(counts);
4800}
4801
4802std::vector<DungeonLimitInfo> Room::GetExceededLimitDetails() const {
4803 auto counts = GetLimitedObjectCounts();
4804 return GetExceededLimits(counts);
4805}
4806
4807} // namespace zelda3
4808} // 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:49
absl::Status WriteByte(int addr, uint8_t value)
Definition rom.cc:853
absl::StatusOr< uint8_t > ReadByte(int offset) const
Definition rom.cc:785
const auto & vector() const
Definition rom.h:205
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
Definition rom.cc:928
absl::StatusOr< uint16_t > ReadWord(int offset) const
Definition rom.cc:793
auto data() const
Definition rom.h:201
auto size() const
Definition rom.h:200
bool is_loaded() const
Definition rom.h:187
absl::Status WriteWord(int addr, uint16_t value)
Definition rom.cc:873
absl::Status WriteLong(uint32_t addr, uint32_t value)
Definition rom.cc:901
absl::StatusOr< uint32_t > ReadLong(int offset) const
Definition rom.cc:800
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:2826
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:4797
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:2799
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:2946
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:2786
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:4515
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:4802
void ParseObjectsFromLocation(int objects_location)
Definition room.cc:1961
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > GetObjectSaveWriteRanges(const DungeonStreamLayout *layout=nullptr) const
Definition room.cc:2437
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:2318
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:3006
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:2557
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:2442
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:4699
void RenderRoomGraphics()
Definition room.cc:1254
PaletteAddressMap rendered_palette_addresses_
Definition room.h:1200
absl::Status SaveRoomHeader()
Definition room.cc:2681
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:2667
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:2067
void SetEffect(EffectKey effect)
Definition room.h:795
absl::Status SaveObjectStreamHeader(const DungeonStreamLayout *layout=nullptr)
Definition room.cc:2564
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:2332
absl::StatusOr< StreamSavePlan > PlanSpriteSave(const DungeonStreamLayout *layout) const
Definition room.cc:2574
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:2672
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:4611
int BackgroundPaletteOffset(int slot) const
Definition room.cc:1085
absl::StatusOr< StreamSavePlan > PlanObjectStreamHeaderSave(const DungeonStreamLayout *layout) const
Definition room.cc:2451
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:2155
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:2886
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:2848
void SetStair4TargetLayer(uint8_t layer)
Definition room.h:932
absl::Status AddObject(const RoomObject &object)
Definition room.cc:2772
absl::StatusOr< size_t > FindObjectAt(int x, int y, int layer) const
Definition room.cc:2816
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:4015
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:3507
absl::Status ValidateSpecialObjectDrawLayerSelector(const RoomObject &object, int room_id, const char *object_type)
Definition room.cc:3174
int ReadRoomPotItemAddressPc(const std::vector< uint8_t > &rom_data, int room_id)
Definition room.cc:4223
absl::StatusOr< TorchSavePlan > BuildTorchSavePlan(const Rom *rom, int room_count, RoomLookup &&room_lookup)
Definition room.cc:3213
void SortAndCoalesceChestRuns(std::vector< ChestWriteRun > *writes)
Definition room.cc:4063
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:4029
std::vector< TorchSegment > ParseRomTorchSegments(const std::vector< uint8_t > &rom_data, int bytes_count)
Definition room.cc:3101
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:3308
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:3434
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:4085
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:3590
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:3147
bool HalfOpenRangesOverlap(int first_begin, int first_end, int second_begin, int second_end)
Definition room.cc:3403
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:3408
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:4094
void AppendEditedChestRecord(std::vector< uint8_t > *bytes, int room_id, const chest_data &chest)
Definition room.cc:4022
constexpr std::array< int, 4 > kBlocksPointerSlots
Definition room.cc:3400
std::vector< PhysicalChestRecord > ParsePhysicalRomChests(const std::vector< uint8_t > &rom_data, int cpos, int byte_length)
Definition room.cc:3997
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:4243
absl::Status ValidateLightableTorchForSave(const RoomObject &object, int room_id)
Definition room.cc:3188
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:4366
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:4269
constexpr int kChestsLengthPointer
int FindMaxUsedSpriteAddress(Rom *rom)
Definition room.cc:2193
constexpr int kMessagesIdDungeon
absl::Status RelocateSpriteData(Rom *rom, int room_id, const std::vector< uint8_t > &encoded_bytes)
Definition room.cc:2233
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:4491
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:4377
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:3945
constexpr int kRoomItemsDataEnd
absl::Status SaveAllTorches(Rom *rom, absl::Span< const Room > rooms)
Definition room.cc:3331
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:3358
constexpr int kChestsDataPointer1
constexpr int kBlocksLength
absl::Status SaveAllBlocks(Rom *rom)
Definition room.cc:3543
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:4276
absl::Status SaveAllCollisionImpl(Rom *rom, int room_count, RoomLookup &&room_lookup)
Definition room.cc:3844
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:4357
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:4283
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:3343
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:3933
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:3830
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:2292
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:3586
std::vector< std::pair< const RoomObject *, int > > load_order_updates
Definition room.cc:3587
Public YAZE API umbrella header.