12#include <unordered_map>
13#include <unordered_set>
17#include "absl/strings/str_cat.h"
18#include "absl/strings/str_format.h"
46 return std::any_of(objects.begin(), objects.end(),
47 [](
const RoomObject& obj) { return obj.id_ == 0x31; });
51 return static_cast<uint8_t
>((byte0 >> 5) & 0x07);
55 return (byte0 & 0x01) != 0;
66 return background2::DarkRoom;
71template <
typename WriteColor>
74 WriteColor write_color) {
75 if (hud_palette !=
nullptr) {
76 const size_t hud_count = std::min<size_t>(hud_palette->
size(), 32);
77 for (
size_t i = 0; i < hud_count; ++i) {
78 write_color(
static_cast<int>(i), (*hud_palette)[i]);
82 constexpr int kColorsPerRomBank = 15;
83 constexpr int kIndicesPerSdlBank = 16;
84 constexpr int kNumRomBanks = 6;
85 constexpr int kDungeonBankStart = 2;
86 for (
int rom_bank = 0; rom_bank < kNumRomBanks; ++rom_bank) {
87 const int sdl_bank = rom_bank + kDungeonBankStart;
88 for (
int color = 0; color < kColorsPerRomBank; ++color) {
89 const size_t rom_index =
90 static_cast<size_t>(rom_bank * kColorsPerRomBank + color);
91 if (rom_index >= dungeon_palette.
size()) {
94 const int dst_index = sdl_bank * kIndicesPerSdlBank + color + 1;
95 write_color(dst_index, dungeon_palette[rom_index]);
105 std::vector<SDL_Color> colors(256, {0, 0, 0, 0});
106 PopulateDungeonRenderPaletteRows(
107 dungeon_palette, hud_palette,
109 if (dst_index < 0 || dst_index >=
static_cast<int>(colors.size())) {
112 const ImVec4 rgb = color.
rgb();
113 colors[dst_index] = {
static_cast<Uint8
>(rgb.x),
114 static_cast<Uint8
>(rgb.y),
115 static_cast<Uint8
>(rgb.z), 255};
117 colors[255] = {0, 0, 0, 0};
124 PopulateDungeonRenderPaletteRows(
125 dungeon_palette, hud_palette,
127 if (dst_index < 0 || dst_index >=
static_cast<int>(cgram.size())) {
130 cgram[dst_index] = color.
snes();
141 "Light Torch to See Floor",
146 "NW Kill Enemy to Open",
147 "NE Kill Enemy to Open",
148 "SW Kill Enemy to Open",
149 "SE Kill Enemy to Open",
150 "W Kill Enemy to Open",
151 "E Kill Enemy to Open",
152 "N Kill Enemy to Open",
153 "S Kill Enemy to Open",
154 "Clear Quadrant to Open",
155 "Clear Full Tile to Open",
156 "NW Push Block to Open",
157 "NE Push Block to Open",
158 "SW Push Block to Open",
159 "SE Push Block to Open",
160 "W Push Block to Open",
161 "E Push Block to Open",
162 "N Push Block to Open",
163 "S Push Block to Open",
164 "Push Block to Open",
165 "Pull Lever to Open",
166 "Collect Prize to Open",
167 "Hold Switch Open Door",
168 "Toggle Switch to Open Door",
177 "Push Switch Exploding Wall",
179 "Open Chest (Holes 0)",
182 "Defeat Boss for Dungeon Prize",
183 "SE Kill Enemy to Push Block",
184 "Trigger Switch Chest",
185 "Pull Lever Exploding Wall",
186 "NW Kill Enemy for Chest",
187 "NE Kill Enemy for Chest",
188 "SW Kill Enemy for Chest",
189 "SE Kill Enemy for Chest",
190 "W Kill Enemy for Chest",
191 "E Kill Enemy for Chest",
192 "N Kill Enemy for Chest",
193 "S Kill Enemy for Chest",
194 "Clear Quadrant for Chest",
195 "Clear Full Tile for Chest",
196 "Light Torches to Open",
204 "Open Chest for Holes 8",
205 "Push Block for Chest",
206 "Clear Room for Triforce Door",
207 "Light Torches for Chest",
214 int physical_end = -1;
218 return physical_end > address ? physical_end - address : 0;
224 int known_region_end = -1) {
226 if (room_id < 0 || room_id >=
static_cast<int>(room_addresses.size())) {
230 info.
address = room_addresses[room_id];
235 int next_address = std::numeric_limits<int>::max();
236 for (
int other_room_id = 0;
237 other_room_id < static_cast<int>(room_addresses.size());
239 if (other_room_id == room_id || room_addresses[other_room_id] < 0) {
242 const int other_address = room_addresses[other_room_id];
243 if (other_address == info.
address) {
245 }
else if (other_address > info.
address) {
246 next_address = std::min(next_address, other_address);
250 if (known_region_end > info.
address) {
251 next_address = std::min(next_address, known_region_end);
253 if (next_address == std::numeric_limits<int>::max()) {
261 constexpr int kLoRomBankSize = 0x8000;
262 const int bank_end = ((info.
address / kLoRomBankSize) + 1) * kLoRomBankSize;
263 if (next_address > bank_end) {
272 if (table_pc ==
nullptr) {
273 return absl::InvalidArgumentError(
"table_pc pointer is null");
276 return absl::OutOfRangeError(
277 "Object pointer table address is out of range");
280 const uint32_t table_snes =
284 const int pc =
static_cast<int>(
SnesToPc(table_snes));
285 if (pc < 0 || pc + (
kNumberOfRooms * 3) >
static_cast<int>(rom_data.size())) {
286 return absl::OutOfRangeError(
"Object pointer table is out of range");
290 return absl::OkStatus();
294 int table_pc,
int room_id) {
298 const int ptr_off = table_pc + (room_id * 3);
299 if (ptr_off < 0 || ptr_off + 2 >=
static_cast<int>(rom_data.size())) {
302 return (
static_cast<uint32_t
>(rom_data[ptr_off + 2]) << 16) |
303 (
static_cast<uint32_t
>(rom_data[ptr_off + 1]) << 8) |
310 if ((snes & 0xFFFF) < 0x8000) {
313 const int pc =
static_cast<int>(
SnesToPc(snes));
314 return pc >= 0 && pc < static_cast<int>(rom_data.size()) ? pc : -1;
318 const std::vector<uint8_t>& rom_data,
int room_id) {
320 return absl::OutOfRangeError(
"Room ID out of range");
332 return absl::OutOfRangeError(
"Object stream pointer is out of range");
339 if (table_pc ==
nullptr) {
340 return absl::InvalidArgumentError(
"table_pc pointer is null");
343 return absl::OutOfRangeError(
344 "Sprite pointer table address is out of range");
350 if (pc < 0 || pc + (
kNumberOfRooms * 2) >
static_cast<int>(rom_data.size())) {
351 return absl::OutOfRangeError(
"Sprite pointer table is out of range");
355 return absl::OkStatus();
363 const int ptr_off = table_pc + (room_id * 2);
364 if (ptr_off < 0 || ptr_off + 1 >=
static_cast<int>(rom_data.size())) {
368 const uint16_t pointer =
369 (
static_cast<uint16_t
>(rom_data[ptr_off + 1]) << 8) | rom_data[ptr_off];
370 if (pointer < 0x8000) {
373 const int sprite_address =
static_cast<int>(
SnesToPc((0x09 << 16) | pointer));
374 return sprite_address >= 0 &&
375 sprite_address < static_cast<int>(rom_data.size())
381 const std::vector<uint8_t>& rom_data,
int room_id) {
383 return absl::OutOfRangeError(
"Room ID out of range");
396 return absl::OutOfRangeError(
"Sprite stream pointer is out of range");
402 int sprite_address,
int hard_end) {
403 if (sprite_address < 0 || sprite_address >= hard_end ||
404 sprite_address >=
static_cast<int>(rom_data.size())) {
408 int cursor = sprite_address + 1;
409 while (cursor < hard_end) {
410 if (rom_data[cursor] == 0xFF) {
414 if (cursor + 2 >= hard_end) {
421 return std::max(0, cursor - sprite_address);
427 std::vector<uint8_t> encoded_stream) {
428 if (layout.
kind != expected_kind) {
429 return absl::InvalidArgumentError(absl::StrFormat(
430 "Room %d relocation layout has the wrong dungeon stream kind",
439 std::move(encoded_stream)}}));
446 if (layout.
kind != expected_kind) {
447 return absl::InvalidArgumentError(absl::StrFormat(
448 "Room %d save layout has the wrong dungeon stream kind", room_id));
450 if (room_id < 0 ||
static_cast<uint32_t
>(room_id) >= layout.
pointer_count) {
451 return absl::OutOfRangeError(
452 "Room ID is outside the dungeon stream layout");
457 if (!inventory.
ok()) {
458 return absl::FailedPreconditionError(absl::StrFormat(
459 "Dungeon stream inventory has %zu issue(s); refusing an in-place "
461 inventory.
issues.size()));
464 const auto contains_room = [room_id](
const std::vector<uint32_t>& owners) {
465 return std::find(owners.begin(), owners.end(),
466 static_cast<uint32_t
>(room_id)) != owners.end();
468 for (
const auto& alias : inventory.
aliases) {
469 if (contains_room(alias.room_ids)) {
473 for (
const auto& overlap : inventory.
overlaps) {
474 if (contains_room(overlap.first_room_ids) ||
475 contains_room(overlap.second_room_ids)) {
480 const auto& record = inventory.
streams[room_id];
481 const uint64_t replacement_end =
482 static_cast<uint64_t
>(record.data_pc) + replacement_size;
483 const uint32_t bank_end = ((record.data_pc / 0x8000u) + 1u) * 0x8000u;
484 const bool stays_in_declared_data =
485 replacement_end <= bank_end &&
488 return range.begin <= record.data_pc &&
489 replacement_end <= range.end;
491 return !stays_in_declared_data;
498 if (!rom || !rom->
is_loaded() || rom->
size() == 0 || room_id < 0 ||
503 const auto& rom_data = rom->
vector();
505 if (!GetObjectPointerTablePc(rom_data, &table_pc).ok()) {
508 room_size.room_size_pointer =
509 ReadRoomObjectAddressSnes(rom_data, table_pc, room_id);
511 auto stream_info = GetObjectStreamInfo(rom_data, room_id);
512 if (!stream_info.ok() || stream_info->shared) {
515 room_size.room_size = stream_info->capacity();
547 Room room(room_id, rom);
563 header_pointer =
SnesToPc(header_pointer);
572 int table_offset = (header_pointer) + (room_id * 2);
573 if (table_offset < 0 || table_offset + 1 >=
static_cast<int>(rom->
size())) {
578 (rom->
data()[table_offset + 1] << 8) +
579 rom->
data()[table_offset];
581 auto header_location =
SnesToPc(address);
584 if (header_location < 0 ||
585 header_location + 13 >=
static_cast<int>(rom->
size())) {
589 const uint8_t header_byte0 = rom->
data()[header_location];
592 room.
SetBg2(Background2FromHeaderByte(header_byte0));
594 room.
SetIsLight(IsDarkRoomHeaderByte(header_byte0));
595 room.
SetIsDark(IsDarkRoomHeaderByte(header_byte0));
630 header_pointer_2 =
SnesToPc(header_pointer_2);
639 int table_offset_2 = (header_pointer_2) + (room_id * 2);
640 if (table_offset_2 < 0 ||
641 table_offset_2 + 1 >=
static_cast<int>(rom->
size())) {
646 (rom->
data()[table_offset_2 + 1] << 8) +
647 rom->
data()[table_offset_2];
650 if (msg_addr >= 0 && msg_addr + 1 <
static_cast<int>(rom->
size())) {
651 uint16_t msg_val = (rom->
data()[msg_addr + 1] << 8) | rom->
data()[msg_addr];
659 if (hpos < 0 || hpos + 14 >=
static_cast<int>(rom->
size())) {
663 uint8_t b = rom->
data()[hpos];
689 b = rom->
data()[hpos];
719 game_data_(game_data),
731 const int num_palettes =
static_cast<int>(group.size());
732 if (num_palettes == 0)
736 if (palette_ < game_data_->paletteset_ids.size() &&
744 if (id < 0 || id >= num_palettes)
757 const uint8_t requested_main_blockset =
759 uint8_t main_blockset = 0;
760 if (requested_main_blockset != 0xFF &&
761 requested_main_blockset < game_data_->main_blockset_ids.size()) {
762 main_blockset = requested_main_blockset;
763 }
else if (blockset_ < game_data_->main_blockset_ids.size()) {
767 "Room %d: invalid main fallback blockset %d; using main group 0",
770 if (requested_main_blockset != 0xFF &&
773 "Room %d: entrance main blockset %d out of range; using %d",
774 room_id_, requested_main_blockset, main_blockset);
779 "Room %d: room_blockset=%d, main_blockset=%d, spriteset=%d, "
783 for (
int i = 0; i < 8; i++) {
785 if (i >= 3 && i <= 6 &&
blockset_ < room_gfx.size()) {
786 const uint8_t room_sheet = room_gfx[
blockset_][i - 3];
787 if (room_sheet != 0) {
793 LOG_WARN(
"Room",
"Room %d: room blockset %d out of range; skipped $0AA2",
801 const size_t sprite_gfx_index =
static_cast<size_t>(
spriteset_) + 64;
802 if (sprite_gfx_index < sprite_gfx.size()) {
803 for (
int i = 0; i < 4; i++) {
805 static_cast<uint8_t
>(sprite_gfx[sprite_gfx_index][i] + 115);
809 "Room %d: spriteset %d out of range; clearing sprite sheets",
811 for (
int i = 0; i < 4; i++) {
843 if (entrance_blockset.has_value()) {
854 if (entrance_blockset.has_value()) {
863 !bg2_bmp.is_active() || bg2_bmp.width() == 0) {
879 LOG_DEBUG(
"Room",
"CopyRoomGraphicsToBuffer: ROM not loaded");
884 LOG_DEBUG(
"Room",
"CopyRoomGraphicsToBuffer: GameData not set");
888 if (gfx_buffer_data->empty()) {
889 LOG_DEBUG(
"Room",
"CopyRoomGraphicsToBuffer: Graphics buffer is empty");
893 LOG_DEBUG(
"Room",
"Room %d: Copying 8BPP graphics (buffer size: %zu)",
911 const uint8_t active_main_blockset =
916 auto is_right_palette_background_slot = [&](
int block) ->
bool {
917 if (block < 0 || block >= 8) {
920 const int runtime_slot = 7 - block;
921 if (active_main_blockset < 0x20) {
922 return runtime_slot >= 4;
924 return (runtime_slot == 2 || runtime_slot == 3 || runtime_slot == 4 ||
929 for (
int block = 0; block < 16; block++) {
933 if (sheet_id >= 223) {
934 LOG_WARN(
"Room",
"Invalid sheet index %d for block %d", sheet_id, block);
940 int src_sheet_offset = sheet_id * 4096;
943 if (src_sheet_offset + 4096 > gfx_buffer_data->size()) {
944 LOG_ERROR(
"Room",
"Graphics offset out of bounds: %d (size: %zu)",
945 src_sheet_offset, gfx_buffer_data->size());
950 int dest_index_base = block * 4096;
952 const uint8_t* src = gfx_buffer_data->data() + src_sheet_offset;
957 const bool right_pal = is_right_palette_background_slot(block);
959 memcpy(dst, src, 4096);
962 for (
int i = 0; i < 4096; ++i) {
964 if (p != 0 && p < 8) {
992 bool properties_changed =
false;
1006 properties_changed =
true;
1016 properties_changed =
true;
1024 if (bg1_bmp.is_active() && bg1_bmp.width() > 0 && bg2_bmp.is_active() &&
1025 bg2_bmp.width() > 0) {
1027 "Room %d: No changes detected, skipping render",
room_id_);
1033 "Room %d: Rendering graphics (dirty_flags: g=%d o=%d l=%d t=%d)",
1052 "Room %d: floor1=%d, floor2=%d, blocks_size=%zu",
room_id_,
1057 bool need_floor_draw = was_graphics_dirty;
1062 if (!bg1_bmp.is_active() || bg1_bmp.width() == 0 || !bg2_bmp.is_active() ||
1063 bg2_bmp.width() == 0) {
1064 need_floor_draw =
true;
1066 "Room %d: Bitmaps not created yet, forcing floor draw",
room_id_);
1069 if (need_floor_draw) {
1078 bool need_bg_draw = was_graphics_dirty || need_floor_draw;
1090 if (was_layout_dirty || need_floor_draw) {
1099 if (dungeon_pal_group.empty())
1103 auto bg1_palette = dungeon_pal_group[palette_id];
1112 LOG_DEBUG(
"Room",
"RenderRoomGraphics: Palette ID=%d, Size=%zu", palette_id,
1113 bg1_palette.size());
1114 if (!bg1_palette.empty()) {
1115 LOG_DEBUG(
"Room",
"RenderRoomGraphics: First color: R=%d G=%d B=%d",
1116 bg1_palette[0].rom_color().red, bg1_palette[0].rom_color().green,
1117 bg1_palette[0].rom_color().blue);
1120 if (bg1_palette.size() > 0) {
1121 std::optional<gfx::SnesPalette> hud_palette_storage;
1125 hud_palette = &*hud_palette_storage;
1144 const auto render_palette =
1152 auto set_dungeon_palette = [&](
gfx::Bitmap& bmp) {
1153 bmp.SetPalette(render_palette);
1154 if (bmp.surface()) {
1156 SDL_SetColorKey(bmp.surface(), SDL_TRUE, 255);
1157 SDL_SetSurfaceBlendMode(bmp.surface(), SDL_BLENDMODE_BLEND);
1161 set_dungeon_palette(bg1_bmp);
1162 set_dungeon_palette(bg2_bmp);
1167 auto* surface = bg1_bmp.surface();
1172 "Room::RenderRoomGraphics (BG1)", palette_id,
true);
1176 "Room::RenderRoomGraphics (after SetPalette)", surface);
1179 "Room::RenderRoomGraphics", palette_id,
false,
1180 "SDL surface has no palette!");
1194 if (bg2_bmp.surface()) {
1195 SDL_SetSurfaceAlphaMod(bg2_bmp.surface(), 128);
1203 if (bg2_bmp.surface()) {
1204 SDL_SetSurfaceBlendMode(bg2_bmp.surface(), SDL_BLENDMODE_ADD);
1220 if (bitmap->texture()) {
1226 release_texture(&bg1_bmp);
1227 release_texture(&bg2_bmp);
1242 "Texture commands queued for batch processing");
1250 LOG_DEBUG(
"Room",
"ROM not loaded, aborting");
1261 if (!layout_status.ok()) {
1263 layout_status.message().data());
1269 layout_objects.size());
1270 if (layout_objects.empty()) {
1278 LOG_DEBUG(
"RenderRoomGraphics",
"GameData not set, cannot render layout");
1284 if (dungeon_pal_group.empty())
1288 auto room_palette = dungeon_pal_group[palette_id];
1302 "RenderRoomGraphics",
"Layout Draw failed: %s",
1303 std::string(status.message().data(), status.message().size()).c_str());
1305 LOG_DEBUG(
"RenderRoomGraphics",
"Layout rendered with %zu objects",
1306 layout_objects.size());
1311 LOG_DEBUG(
"[RenderObjectsToBackground]",
1312 "Starting object rendering for room %d",
room_id_);
1315 LOG_DEBUG(
"[RenderObjectsToBackground]",
"ROM not loaded, aborting");
1324 bool bitmaps_exist = bg1_bmp.is_active() && bg1_bmp.width() > 0 &&
1325 bg2_bmp.is_active() && bg2_bmp.width() > 0;
1328 LOG_DEBUG(
"[RenderObjectsToBackground]",
1329 "Room %d: Objects not dirty, skipping render",
room_id_);
1335 LOG_DEBUG(
"[RenderObjectsToBackground]",
1336 "Room %d: Emulator rendering objects",
room_id_);
1341 if (dungeon_pal_group.empty())
1345 auto room_palette = dungeon_pal_group[palette_id];
1389 int layer0_count = 0, layer1_count = 0, layer2_count = 0;
1391 switch (obj.GetLayerValue()) {
1405 "Room %03X Object Stream Summary: Main=%d, BG2Overlay=%d, BG1Overlay=%d",
1406 room_id_, layer0_count, layer1_count, layer2_count);
1423 std::vector<std::vector<RoomObject>> by_list(3);
1435 uint8_t list_index = obj.GetLayerValue();
1436 if (list_index > 2) {
1441 by_list[list_index].push_back(std::move(render_obj));
1444 absl::Status status = absl::OkStatus();
1445 bool reset_room_events_for_next_chunk =
true;
1446 for (
int pass = 0; pass < 3; ++pass) {
1447 if (by_list[pass].empty()) {
1453 reset_room_events_for_next_chunk =
false;
1454 if (!chunk_status.ok() && status.ok()) {
1455 status = chunk_status;
1462 for (
int i = 0; i < static_cast<int>(
doors_.size()); ++i) {
1463 const auto& door =
doors_[i];
1465 door_def.
type = door.type;
1482 if (pot_item.item != 0) {
1484 int tile_x = pot_item.GetTileX();
1485 int tile_y = pot_item.GetTileY();
1492 for (
const auto& sprite :
sprites_) {
1493 if (sprite.key_drop() > 0) {
1499 uint8_t key_item = (sprite.key_drop() == 1) ? 0xFD : 0xFE;
1509 constexpr uint16_t kRoomDrawObj_PushableBlock = 0x0E52;
1510 constexpr uint16_t kRoomDrawObj_TorchUnlit = 0x0EC2;
1511 constexpr uint16_t kRoomDrawObj_TorchLit = 0x0ECA;
1518 static_cast<uint16_t
>(obj.id_), obj.x_, obj.y_, obj.layer_,
1523 const uint16_t off =
1524 obj.lit_ ? kRoomDrawObj_TorchLit : kRoomDrawObj_TorchUnlit;
1529 static_cast<uint16_t
>(obj.id_), obj.x_, obj.y_, obj.layer_, off,
1537 "[RenderObjectsToBackground]",
1538 "Room %03X: ObjectDrawer failed: %s (objects left dirty for retry)",
1540 std::string(status.message().data(), status.message().size()).c_str());
1547 LOG_DEBUG(
"[RenderObjectsToBackground]",
1548 "Room %d: Objects rendered successfully",
room_id_);
1564 if (gfx_buffer_data->empty()) {
1568 auto rom_data =
rom()->vector();
1569 if (rom_data.empty()) {
1574 if (animated_frame_ < 0 || animated_frame_ > 10) {
1579 if (background_tileset_ < 0 || background_tileset_ > 255) {
1584 if (gfx_ptr < 0 || gfx_ptr >=
static_cast<int>(rom_data.size())) {
1589 while (data < 1024) {
1593 if (first_offset >= 0 &&
1594 first_offset <
static_cast<int>(gfx_buffer_data->size())) {
1595 uint8_t map_byte = (*gfx_buffer_data)[first_offset];
1598 int gfx_offset = data + (7 * 4096);
1599 if (gfx_offset >= 0 &&
1609 if (second_offset >= 0 &&
1610 second_offset <
static_cast<int>(gfx_buffer_data->size())) {
1611 uint8_t map_byte = (*gfx_buffer_data)[second_offset];
1614 int gfx_offset = data + (7 * 4096) - 1024;
1615 if (gfx_offset >= 0 &&
1627 auto rom_data =
rom()->vector();
1633 object_pointer =
SnesToPc(object_pointer);
1636 if (object_pointer < 0 || object_pointer >= (
int)
rom_->
size()) {
1640 int room_address = object_pointer + (
room_id_ * 3);
1643 if (room_address < 0 || room_address + 2 >= (
int)
rom_->
size()) {
1647 int tile_address = (rom_data[room_address + 2] << 16) +
1648 (rom_data[room_address + 1] << 8) + rom_data[room_address];
1650 int objects_location =
SnesToPc(tile_address);
1653 if (objects_location < 0 || objects_location >= (
int)
rom_->
size()) {
1658 if (objects_location + 1 < (
int)
rom_->
size()) {
1661 static_cast<uint8_t
>(rom_data[objects_location] & 0x0F);
1663 static_cast<uint8_t
>((rom_data[objects_location] >> 4) & 0x0F);
1665 "Room %d: Set floor1_graphics_=%d, floor2_graphics_=%d",
1670 static_cast<uint8_t
>((rom_data[objects_location + 1] >> 2) & 0x07);
1692 auto rom_data =
rom()->vector();
1698 int nbr_of_staircase = 0;
1700 int pos = objects_location;
1706 bool end_read =
false;
1710 while (!end_read && pos < (
int)
rom_->
size()) {
1712 if (pos + 1 >= (
int)
rom_->
size()) {
1717 b2 = rom_data[pos + 1];
1724 if (b1 == 0xFF && b2 == 0xFF) {
1728 "Room",
"Room %03X: Object list transition to index %d (%s)",
1730 layer == 1 ?
"BG2 overlay" : (layer == 2 ?
"BG1 overlay" :
"END"));
1740 if (b1 == 0xF0 && b2 == 0xFF) {
1747 if (pos + 2 >= (
int)
rom_->
size()) {
1751 b3 = rom_data[pos + 2];
1762 b1, b2, b3,
static_cast<uint8_t
>(layer));
1764 LOG_DEBUG(
"Room",
"Room %03X: Object 0x%03X at (%d,%d) stream=%d (%s)",
1766 layer == 0 ?
"Primary"
1767 : (layer == 1 ?
"BG2 overlay" :
"BG1 overlay"));
1771 if (r.
id_ >= 0 && r.
id_ <= 0xFFF) {
1785 "ParseDoor: room=%d b1=0x%02X b2=0x%02X pos=%d dir=%d type=%d",
1787 static_cast<int>(door.direction),
static_cast<int>(door.type));
1798 std::vector<uint8_t> bytes;
1802 std::vector<RoomObject> layer0_objects;
1803 std::vector<RoomObject> layer1_objects;
1804 std::vector<RoomObject> layer2_objects;
1817 switch (obj.GetLayerValue()) {
1819 layer0_objects.push_back(obj);
1822 layer1_objects.push_back(obj);
1825 layer2_objects.push_back(obj);
1841 for (
const auto& obj : layer0_objects) {
1842 auto encoded = obj.EncodeObjectToBytes();
1843 bytes.push_back(encoded.b1);
1844 bytes.push_back(encoded.b2);
1845 bytes.push_back(encoded.b3);
1847 bytes.push_back(0xFF);
1848 bytes.push_back(0xFF);
1851 for (
const auto& obj : layer1_objects) {
1852 auto encoded = obj.EncodeObjectToBytes();
1853 bytes.push_back(encoded.b1);
1854 bytes.push_back(encoded.b2);
1855 bytes.push_back(encoded.b3);
1857 bytes.push_back(0xFF);
1858 bytes.push_back(0xFF);
1861 for (
const auto& obj : layer2_objects) {
1862 auto encoded = obj.EncodeObjectToBytes();
1863 bytes.push_back(encoded.b1);
1864 bytes.push_back(encoded.b2);
1865 bytes.push_back(encoded.b3);
1870 bytes.push_back(0xF0);
1871 bytes.push_back(0xFF);
1872 for (
const auto& door :
doors_) {
1873 auto [b1, b2] = door.EncodeBytes();
1874 bytes.push_back(b1);
1875 bytes.push_back(b2);
1879 bytes.push_back(0xFF);
1880 bytes.push_back(0xFF);
1886 std::vector<uint8_t> bytes;
1888 for (
const auto& sprite :
sprites_) {
1896 b2 = (sprite.x() & 0x1F) | ((sprite.subtype() & 0x07) << 5);
1901 b1 = (sprite.y() & 0x1F) | ((sprite.subtype() & 0x18) << 2) |
1902 ((sprite.layer() & 0x01) << 7);
1904 bytes.push_back(b1);
1905 bytes.push_back(b2);
1906 bytes.push_back(b3);
1910 if (sprite.key_drop() == 1) {
1911 bytes.insert(bytes.end(), {0xFE, 0x00, 0xE4});
1912 }
else if (sprite.key_drop() == 2) {
1913 bytes.insert(bytes.end(), {0xFD, 0x00, 0xE4});
1918 bytes.push_back(0xFF);
1928 const auto& rom_data = rom->
vector();
1929 int sprite_pointer = 0;
1930 if (!GetSpritePointerTablePc(rom_data, &sprite_pointer).ok()) {
1934 const int hard_end =
1936 if (hard_end <= 0) {
1941 std::unordered_set<int> visited_addresses;
1943 int sprite_address =
1944 ReadRoomSpriteAddressPc(rom_data, sprite_pointer, room_id);
1945 if (sprite_address < kSpritesData || sprite_address >= hard_end) {
1948 if (!visited_addresses.insert(sprite_address).second) {
1953 MeasureSpriteStreamSize(rom_data, sprite_address, hard_end);
1954 int stream_end = sprite_address + stream_size;
1955 if (stream_end > max_used) {
1956 max_used = stream_end;
1964 const std::vector<uint8_t>& encoded_bytes) {
1966 return absl::InvalidArgumentError(
"ROM not loaded");
1969 return absl::OutOfRangeError(
"Room ID out of range");
1971 if (encoded_bytes.empty() || encoded_bytes.back() != 0xFF ||
1972 (encoded_bytes.size() % 3) != 1) {
1973 return absl::InvalidArgumentError(
1974 "Encoded sprite payload must be N*3 bytes plus 0xFF terminator");
1977 const auto& rom_data = rom->
vector();
1978 int sprite_pointer = 0;
1981 int old_sprite_address =
1982 ReadRoomSpriteAddressPc(rom_data, sprite_pointer, room_id);
1983 if (old_sprite_address < 0 ||
1984 old_sprite_address >=
static_cast<int>(rom_data.size())) {
1985 return absl::OutOfRangeError(
"Sprite address out of range");
1988 const uint8_t sort_mode = rom_data[old_sprite_address];
1991 const size_t required_size = 1u + encoded_bytes.size();
1993 static_cast<size_t>(write_pos) + required_size >
1995 return absl::ResourceExhaustedError(absl::StrFormat(
1996 "Not enough sprite data space. Need %d bytes at 0x%06X, "
1997 "region ends at 0x%06X",
2000 if (
static_cast<size_t>(write_pos) + required_size > rom_data.size()) {
2001 const int required_end = write_pos +
static_cast<int>(required_size);
2002 return absl::OutOfRangeError(
2003 absl::StrFormat(
"ROM too small for sprite relocation write (need "
2004 "end=0x%06X, size=0x%06X)",
2005 required_end,
static_cast<int>(rom_data.size())));
2008 std::vector<uint8_t> relocated;
2009 relocated.reserve(required_size);
2010 relocated.push_back(sort_mode);
2011 relocated.insert(relocated.end(), encoded_bytes.begin(), encoded_bytes.end());
2014 const uint32_t snes_addr =
PcToSnes(write_pos);
2015 const int ptr_off = sprite_pointer + (room_id * 2);
2019 return absl::OkStatus();
2023 if (
rom_ ==
nullptr) {
2024 return absl::InvalidArgumentError(
"ROM pointer is null");
2027 return absl::OkStatus();
2036 const auto& rom_data =
rom()->vector();
2038 GetObjectStreamInfo(rom_data,
room_id_));
2040 bool requires_copy_on_write =
false;
2041 if (layout !=
nullptr) {
2043 DungeonStreamRequiresCopyOnWrite(
2045 encoded_bytes.size() + 2u));
2047 const auto relocate = [&]() -> absl::Status {
2048 if (stream_info.address + 2 >
static_cast<int>(rom_data.size())) {
2049 return absl::OutOfRangeError(
"Object stream header is out of range");
2051 std::vector<uint8_t> replacement = {rom_data[stream_info.address],
2052 rom_data[stream_info.address + 1]};
2053 replacement.insert(replacement.end(), encoded_bytes.begin(),
2054 encoded_bytes.end());
2057 std::move(replacement)));
2059 return absl::OkStatus();
2061 if (stream_info.shared || requires_copy_on_write) {
2062 if (layout !=
nullptr) {
2065 return absl::FailedPreconditionError(absl::StrFormat(
2066 "Room %d object stream at PC 0x%06X is shared; repacking is required",
2069 if (stream_info.capacity() <= 2) {
2070 if (layout !=
nullptr) {
2073 return absl::FailedPreconditionError(absl::StrFormat(
2074 "Room %d object stream has no safe physical boundary",
room_id_));
2078 const int write_pos = stream_info.address + 2;
2081 const int available_payload_size = stream_info.capacity() - 2;
2085 if (encoded_bytes.size() >
static_cast<size_t>(available_payload_size)) {
2086 if (layout !=
nullptr) {
2089 return absl::ResourceExhaustedError(absl::StrFormat(
2090 "Room %d object data too large! Size: %d, Available: %d",
room_id_,
2091 static_cast<int>(encoded_bytes.size()), available_payload_size));
2094 const int door_list_offset =
static_cast<int>(encoded_bytes.size()) -
2095 static_cast<int>(
doors_.size()) * 2 - 2;
2096 if (door_list_offset < 0) {
2097 return absl::FailedPreconditionError(
"Invalid encoded door list offset");
2100 if (door_pointer_slot < 0 ||
2101 door_pointer_slot + 2 >=
static_cast<int>(rom_data.size())) {
2102 return absl::OutOfRangeError(
"Door pointer slot is out of range");
2104 const int door_pointer_pc = write_pos + door_list_offset;
2115 uint32_t encoded_door_pointer =
PcToSnes(door_pointer_pc);
2116 encoded_door_pointer |= source_door_pointer & 0x800000u;
2121 return absl::OkStatus();
2125 if (
rom_ ==
nullptr) {
2126 return absl::InvalidArgumentError(
"ROM pointer is null");
2129 return absl::OkStatus();
2132 return absl::InvalidArgumentError(
2133 "Dungeon floor graphics values must be in range 0..15");
2136 return absl::InvalidArgumentError(
2137 "Dungeon layout ID must be in range 0..7");
2142 GetObjectStreamInfo(rom_data,
room_id_));
2143 if (stream_info.address < 0 ||
2144 stream_info.address + 1 >=
static_cast<int>(rom_data.size())) {
2145 return absl::OutOfRangeError(
"Object stream header is out of range");
2149 auto patch_header = [&](std::vector<uint8_t>* stream) -> absl::Status {
2150 if (stream ==
nullptr || stream->size() < 2) {
2151 return absl::DataLossError(
2152 "Object stream is missing its two-byte header");
2155 (*stream)[0] =
static_cast<uint8_t
>(((*stream)[0] & 0xF0) |
2163 (*stream)[1] =
static_cast<uint8_t
>(((*stream)[1] & 0xE3) |
2166 return absl::OkStatus();
2169 bool requires_copy_on_write = stream_info.shared;
2170 std::vector<uint8_t> replacement;
2171 if (layout !=
nullptr) {
2173 return absl::InvalidArgumentError(
2174 "Object-stream header save requires an object stream layout");
2178 if (!inventory.
ok()) {
2179 return absl::FailedPreconditionError(absl::StrFormat(
2180 "Dungeon stream inventory has %zu issue(s); refusing object "
2182 inventory.
issues.size()));
2186 return absl::OutOfRangeError(
2187 "Room ID is outside the dungeon stream layout");
2190 bool layout_requires_copy_on_write =
false;
2192 DungeonStreamRequiresCopyOnWrite(
2194 replacement.size()));
2195 requires_copy_on_write =
2196 requires_copy_on_write || layout_requires_copy_on_write;
2199 if (requires_copy_on_write) {
2200 if (layout ==
nullptr) {
2201 return absl::FailedPreconditionError(absl::StrFormat(
2202 "Room %d object stream at PC 0x%06X is shared; a copy-on-write "
2203 "manifest is required to save its header",
2209 std::move(replacement)));
2211 return absl::OkStatus();
2214 std::vector<uint8_t> header = {rom_data[stream_info.address],
2215 rom_data[stream_info.address + 1]};
2219 return absl::OkStatus();
2223 if (
rom_ ==
nullptr) {
2224 return absl::InvalidArgumentError(
"ROM pointer is null");
2227 return absl::OkStatus();
2230 const auto& rom_data =
rom()->vector();
2232 return absl::OutOfRangeError(
"Room ID out of range");
2236 GetSpriteStreamInfo(rom_data,
room_id_));
2238 bool requires_copy_on_write =
false;
2239 if (layout !=
nullptr) {
2241 DungeonStreamRequiresCopyOnWrite(
2243 encoded_bytes.size() + 1u));
2245 const auto relocate = [&]() -> absl::Status {
2246 std::vector<uint8_t> replacement = {rom_data[stream_info.address]};
2247 replacement.insert(replacement.end(), encoded_bytes.begin(),
2248 encoded_bytes.end());
2251 std::move(replacement)));
2253 return absl::OkStatus();
2255 if (stream_info.shared || requires_copy_on_write) {
2256 if (layout !=
nullptr) {
2259 return absl::FailedPreconditionError(absl::StrFormat(
2260 "Room %d sprite stream at PC 0x%06X is shared; repacking is required",
2263 if (stream_info.capacity() <= 1) {
2264 if (layout !=
nullptr) {
2267 return absl::FailedPreconditionError(absl::StrFormat(
2268 "Room %d sprite stream has no safe physical boundary",
room_id_));
2271 const int available_payload_size = stream_info.capacity() - 1;
2272 const int payload_address = stream_info.address + 1;
2273 if (payload_address < 0 ||
2274 payload_address >=
static_cast<int>(
rom_->
size())) {
2275 return absl::OutOfRangeError(absl::StrFormat(
2276 "Room %d has invalid sprite payload address",
room_id_));
2279 if (
static_cast<int>(encoded_bytes.size()) > available_payload_size) {
2280 if (layout !=
nullptr) {
2283 return absl::ResourceExhaustedError(absl::StrFormat(
2284 "Room %d sprite data too large! Size: %d, Available: %d; repacking "
2286 room_id_,
static_cast<int>(encoded_bytes.size()),
2287 available_payload_size));
2292 return absl::OkStatus();
2296 if (
rom_ ==
nullptr) {
2297 return absl::InvalidArgumentError(
"ROM pointer is null");
2300 const auto& rom_data =
rom()->vector();
2303 return absl::OutOfRangeError(
"Room header pointer out of range");
2307 return absl::OutOfRangeError(
"Room header pointer bank out of range");
2313 header_pointer =
SnesToPc(header_pointer);
2315 int table_offset = header_pointer + (
room_id_ * 2);
2316 if (table_offset < 0 ||
2317 table_offset + 1 >=
static_cast<int>(rom_data.size())) {
2318 return absl::OutOfRangeError(
"Room header table offset out of range");
2322 (rom_data[table_offset + 1] << 8) + rom_data[table_offset];
2323 int header_location =
SnesToPc(address);
2325 if (header_location < 0 ||
2326 header_location + 13 >=
static_cast<int>(rom_data.size())) {
2327 return absl::OutOfRangeError(
"Room header location out of range");
2334 if (
bg2() != background2::DarkRoom) {
2335 layer2_mode_for_save =
static_cast<uint8_t
>(
bg2()) & 0x07;
2337 const bool dark_room =
2339 uint8_t byte0 =
static_cast<uint8_t
>(
2340 (layer2_mode_for_save << 5) |
2341 ((
static_cast<uint8_t
>(
collision()) & 0x07) << 2) |
2342 (rom_data[header_location] & 0x02) | (dark_room ? 1 : 0));
2350 const uint8_t byte8 =
static_cast<uint8_t
>(
2351 (rom_data[header_location + 8] & 0xFC) | (
staircase_plane(3) & 0x03));
2372 if (msg_addr < 0 || msg_addr + 1 >=
static_cast<int>(rom_data.size())) {
2373 return absl::OutOfRangeError(
"Message ID address out of range");
2379 return absl::OkStatus();
2389 return absl::InvalidArgumentError(
"Invalid object parameters");
2397 return absl::OkStatus();
2402 return absl::OutOfRangeError(
"Object index out of range");
2410 return absl::OkStatus();
2415 return absl::OutOfRangeError(
"Object index out of range");
2419 return absl::InvalidArgumentError(
"Invalid object parameters");
2427 return absl::OkStatus();
2433 if (obj.x() == x && obj.y() == y && obj.GetLayerValue() == layer) {
2437 return absl::NotFoundError(
"No object found at position");
2442 if (
object.x() < 0 ||
object.x() > 63)
2444 if (
object.y() < 0 ||
object.y() > 63)
2448 if (
object.GetLayerValue() < 0 ||
object.GetLayerValue() > 2)
2452 if (
object.id_ < 0 || object.id_ > 0xFFF)
2456 if (
object.id_ < 0x100 &&
object.size() > 15)
2463 int& nbr_of_staircase) {
2467 if (nbr_of_staircase < 4) {
2490 }
else if (oid == 0xFB1) {
2500 const auto& rom_data =
rom()->vector();
2508 int sprite_pointer = 0;
2509 if (!GetSpritePointerTablePc(rom_data, &sprite_pointer).ok()) {
2513 int sprite_address =
2514 ReadRoomSpriteAddressPc(rom_data, sprite_pointer,
room_id_);
2515 if (sprite_address < 0 ||
2516 sprite_address + 1 >=
static_cast<int>(rom_data.size())) {
2521 sprite_address += 1;
2523 while (sprite_address + 2 <
static_cast<int>(rom_data.size())) {
2524 uint8_t b1 = rom_data[sprite_address];
2525 uint8_t b2 = rom_data[sprite_address + 1];
2526 uint8_t b3 = rom_data[sprite_address + 2];
2532 sprites_.emplace_back(b3, (b2 & 0x1F), (b1 & 0x1F),
2533 ((b2 & 0xE0) >> 5) + ((b1 & 0x60) >> 2),
2540 if (spr.
id() == 0xE4 && spr.
x() == 0x00 && spr.
y() == 0x1E &&
2546 if (spr.
id() == 0xE4 && spr.
x() == 0x00 && spr.
y() == 0x1D &&
2553 sprite_address += 3;
2565 const auto& rom_data =
rom()->vector();
2571 const int cpos =
static_cast<int>(
SnesToPc(
2575 const size_t byte_length =
2578 const size_t bounded_byte_length = std::min<size_t>(
2579 byte_length, cpos >= 0 && cpos <
static_cast<int>(rom_data.size())
2580 ? rom_data.size() -
static_cast<size_t>(cpos)
2582 const size_t record_count = std::min<size_t>(
2585 for (
size_t i = 0; i < record_count; ++i) {
2586 const size_t offset =
2588 if ((((rom_data[offset + 1] << 8) + rom_data[offset]) & 0x7FFF) ==
2592 if ((((rom_data[offset + 1] << 8) + rom_data[offset]) & 0x8000) ==
2604 auto rom_data =
rom()->vector();
2615 "LoadDoors for room %d - doors are loaded via object stream",
2620 auto rom_data =
rom()->vector();
2633 return (obj.options() & ObjectOption::Torch) !=
2634 ObjectOption::Nothing;
2639 for (
int i = 0; i < bytes_count; i += 2) {
2640 if (i + 1 >= bytes_count)
2647 if (b1 == 0xFF && b2 == 0xFF) {
2652 uint16_t torch_room_id = (b2 << 8) | b1;
2656 while (i < bytes_count) {
2657 if (i + 1 >= bytes_count)
2664 if (b1 == 0xFF && b2 == 0xFF) {
2672 torch_obj.SetRom(
rom_);
2674 torch_obj.set_torch_reserved_bit(entry.
reserved);
2675 torch_obj.lit_ = entry.
lit;
2680 "Room",
"Loaded torch at (%d,%d) draw_layer=%d reserved=%d lit=%d",
2689 while (i < bytes_count) {
2690 if (i + 1 >= bytes_count)
2694 if (b1 == 0xFF && b2 == 0xFF) {
2709 uint16_t room_id = 0;
2715 const std::vector<uint8_t>& rom_data,
int bytes_count) {
2716 std::vector<TorchSegment> segments;
2721 if (b1 == 0xFF && b2 == 0xFF) {
2727 padding.
bytes = {0xFF, 0xFF};
2728 segments.push_back(std::move(padding));
2732 uint16_t room_id = (b2 << 8) | b1;
2739 seg.
bytes.push_back(b1);
2740 seg.
bytes.push_back(b2);
2745 if (b1 == 0xFF && b2 == 0xFF) {
2746 seg.
bytes.push_back(0xFF);
2747 seg.
bytes.push_back(0xFF);
2751 seg.
bytes.push_back(b1);
2752 seg.
bytes.push_back(b2);
2755 segments.push_back(std::move(seg));
2761 std::vector<uint8_t> bytes;
2766 if (bytes.empty()) {
2767 bytes.push_back(room_id & 0xFF);
2768 bytes.push_back((room_id >> 8) & 0xFF);
2771 .px =
static_cast<uint8_t
>(obj.x()),
2772 .py =
static_cast<uint8_t
>(obj.y()),
2773 .draw_layer =
static_cast<uint8_t
>(obj.GetLayerValue() & 1),
2774 .reserved = obj.torch_reserved_bit(),
2777 bytes.push_back(encoded.
low);
2778 bytes.push_back(encoded.
high);
2780 if (!bytes.empty()) {
2781 bytes.push_back(0xFF);
2782 bytes.push_back(0xFF);
2789 const char* object_type) {
2790 const uint8_t selector =
object.GetLayerValue();
2791 if (selector <= 1) {
2792 return absl::OkStatus();
2794 return absl::InvalidArgumentError(absl::StrFormat(
2795 "%s in room 0x%03X has invalid special draw-layer selector %d; "
2797 "(upper/BG1) or 1 (lower/BG2)",
2798 object_type, room_id, selector));
2805 if (
object.x() <= 0x3E &&
object.y() <= 0x3E) {
2806 return absl::OkStatus();
2808 return absl::InvalidArgumentError(absl::StrFormat(
2809 "Torch in room 0x%03X has invalid position (%d,%d); expected x/y in "
2811 room_id,
object.x(),
object.y()));
2816template <
typename RoomLookup>
2818 RoomLookup&& room_lookup) {
2820 return absl::InvalidArgumentError(
"ROM not loaded");
2823 const auto& rom_data = rom->
vector();
2826 if (existing_count > kTorchesMaxSize) {
2827 existing_count = kTorchesMaxSize;
2829 auto rom_segments = ParseRomTorchSegments(rom_data, existing_count);
2831 std::vector<uint8_t> bytes;
2833 std::vector<bool> owned_rooms(room_limit,
false);
2834 std::vector<bool> seen_original_room(room_limit,
false);
2835 std::vector<bool> emitted_owned_room(room_limit,
false);
2836 std::vector<std::vector<uint8_t>> replacements(room_limit);
2837 bool any_owned_room =
false;
2838 for (
int room_id = 0; room_id < room_limit; ++room_id) {
2839 const Room* room = room_lookup(room_id);
2840 const bool room_owned =
2850 owned_rooms[room_id] =
true;
2851 any_owned_room =
true;
2852 replacements[room_id] = EncodeTorchSegmentForRoom(room_id, *room);
2855 if (!any_owned_room) {
2856 return absl::OkStatus();
2859 for (
const auto& segment : rom_segments) {
2860 if (segment.room_id < room_limit) {
2861 seen_original_room[segment.room_id] =
true;
2862 if (owned_rooms[segment.room_id]) {
2863 if (!emitted_owned_room[segment.room_id]) {
2864 bytes.insert(bytes.end(), replacements[segment.room_id].begin(),
2865 replacements[segment.room_id].end());
2866 emitted_owned_room[segment.room_id] =
true;
2871 bytes.insert(bytes.end(), segment.bytes.begin(), segment.bytes.end());
2874 for (
int room_id = 0; room_id < room_limit; ++room_id) {
2875 if (owned_rooms[room_id] && !seen_original_room[room_id] &&
2876 !replacements[room_id].empty()) {
2877 bytes.insert(bytes.end(), replacements[room_id].begin(),
2878 replacements[room_id].end());
2882 if (bytes.size() > kTorchesMaxSize) {
2883 return absl::ResourceExhaustedError(
2884 absl::StrFormat(
"Torch data too large: %d bytes (max %d)", bytes.size(),
2888 const uint16_t current_len =
2891 if (current_len == bytes.size() &&
2892 kTorchData +
static_cast<int>(bytes.size()) <=
2893 static_cast<int>(rom_data.size()) &&
2894 std::equal(bytes.begin(), bytes.end(), rom_data.begin() +
kTorchData)) {
2895 for (
int room_id = 0; room_id < room_limit; ++room_id) {
2896 if (
const Room* room = room_lookup(room_id);
2898 const_cast<Room*
>(room)->ClearTorchesDirty();
2901 return absl::OkStatus();
2905 static_cast<uint16_t
>(bytes.size())));
2907 for (
int room_id = 0; room_id < room_limit; ++room_id) {
2908 if (
const Room* room = room_lookup(room_id);
2910 const_cast<Room*
>(room)->ClearTorchesDirty();
2913 return absl::OkStatus();
2918 [&rooms](
int room_id) { return &rooms[room_id]; });
2922 Rom* rom,
int room_count,
2923 const std::function<
const Room*(
int)>& room_lookup) {
2935 if (pit_damage_table !=
nullptr && pit_damage_table->
dirty()) {
2938 return absl::OkStatus();
2941 return absl::InvalidArgumentError(
"ROM not loaded");
2943 const auto& rom_data = rom->
vector();
2944 if (kPitCount < 0 || kPitCount >=
static_cast<int>(rom_data.size()) ||
2945 kPitPointer + 2 >=
static_cast<int>(rom_data.size())) {
2946 return absl::OutOfRangeError(
"Pit count/pointer out of range");
2952 int data_len = max_offset + 2;
2953 int pit_ptr_snes = (rom_data[
kPitPointer + 2] << 16) |
2955 int pit_data_pc =
SnesToPc(pit_ptr_snes);
2956 if (pit_data_pc < 0 ||
2957 pit_data_pc + data_len >
static_cast<int>(rom_data.size())) {
2958 return absl::OutOfRangeError(
"Pit data region out of range");
2960 std::vector<uint8_t> data(rom_data.begin() + pit_data_pc,
2961 rom_data.begin() + pit_data_pc + data_len);
2977 return first_begin < second_end && second_begin < first_end;
2981 const std::vector<uint8_t>& rom_data,
int operand_pc) {
2982 if (operand_pc <= 0 || operand_pc + 5 >=
static_cast<int>(rom_data.size())) {
2983 return absl::OutOfRangeError(
"Blocks pointer operand out of range");
2997 if (rom_data[operand_pc - 1] != 0xBF || rom_data[operand_pc + 3] != 0x9D) {
2998 return absl::FailedPreconditionError(absl::StrFormat(
2999 "Blocks pointer operand at PC 0x%05X is not in the expected "
3000 "LDA.l ...,X / STA.w loader sequence",
3003 return absl::OkStatus();
3007 const std::vector<uint8_t>& rom_data, std::array<int, 4>* destination_pcs) {
3010 return absl::OutOfRangeError(
"Blocks length out of range");
3016 const int snes = (rom_data[operand_pc + 2] << 16) |
3017 (rom_data[operand_pc + 1] << 8) | rom_data[operand_pc];
3018 const int data_pc =
SnesToPc(snes);
3021 return absl::OutOfRangeError(absl::StrFormat(
3022 "Blocks data region out of range for loader page %d", page + 1));
3024 (*destination_pcs)[page] = data_pc;
3028 for (
size_t page = 0; page < destination_pcs->size(); ++page) {
3029 const int page_begin = (*destination_pcs)[page];
3032 kLengthMetadataEnd)) {
3033 return absl::FailedPreconditionError(absl::StrFormat(
3034 "Blocks data page %d at PC [0x%05X, 0x%05X) overlaps block-table "
3035 "length metadata [0x%05X, 0x%05X)",
3036 page + 1, page_begin, page_end,
kBlocksLength, kLengthMetadataEnd));
3047 return absl::FailedPreconditionError(absl::StrFormat(
3048 "Blocks data page %d at PC [0x%05X, 0x%05X) overlaps loader %d "
3049 "opcode/operand metadata [0x%05X, 0x%05X)",
3050 page + 1, page_begin, page_end, loader + 1, loader_begin,
3055 for (
size_t previous = 0; previous < page; ++previous) {
3056 const int previous_begin = (*destination_pcs)[previous];
3060 return absl::FailedPreconditionError(absl::StrFormat(
3061 "Blocks data pages %d and %d overlap at PC ranges [0x%05X, "
3062 "0x%05X) and [0x%05X, 0x%05X)",
3063 previous + 1, page + 1, previous_begin, previous_end, page_begin,
3069 return absl::OkStatus();
3076 return absl::InvalidArgumentError(
"ROM not loaded");
3078 const auto& rom_data = rom->
vector();
3079 if (
kBlocksLength + 1 >=
static_cast<int>(rom_data.size())) {
3080 return absl::OutOfRangeError(
"Blocks length out of range");
3084 std::array<int, 4> destination_pcs{};
3086 PreflightBlocksLoaderDestinations(rom_data, &destination_pcs));
3087 if (blocks_count <= 0) {
3088 return absl::OkStatus();
3090 for (
int r = 0; r < 4; ++r) {
3091 const int pc = destination_pcs[r];
3092 int off = r * kBlocksRegionSize;
3093 int len = std::min(kBlocksRegionSize, blocks_count - off);
3096 std::vector<uint8_t> chunk(rom_data.begin() + pc,
3097 rom_data.begin() + pc + len);
3102 return absl::OkStatus();
3106 const std::function<
const Room*(
int)>& room_lookup) {
3108 return absl::InvalidArgumentError(
"ROM not loaded");
3110 const auto& rom_data = rom->
vector();
3111 if (
kBlocksLength + 1 >=
static_cast<int>(rom_data.size())) {
3112 return absl::OutOfRangeError(
"Blocks length out of range");
3115 std::array<int, 4> destination_pcs{};
3117 PreflightBlocksLoaderDestinations(rom_data, &destination_pcs));
3125 const int original_count_word =
3127 const int original_byte_len = std::max(0, original_count_word);
3128 std::vector<uint8_t> original_buffer(original_byte_len, 0);
3129 for (
int r = 0; r < 4; ++r) {
3130 const int pc = destination_pcs[r];
3131 const int off = r * kBlocksRegionSize;
3132 const int len = std::min(kBlocksRegionSize, original_byte_len - off);
3135 std::copy_n(rom_data.begin() + pc, len, original_buffer.begin() + off);
3137 const int original_slot_count = original_byte_len / 4;
3148 struct EncodedBlock {
3152 std::unordered_set<uint16_t> owned_room_ids;
3153 std::unordered_set<int> claimed_load_orders;
3154 std::unordered_map<int, EncodedBlock> slot_replacements;
3155 std::vector<EncodedBlock> appended;
3156 for (
int rid = 0; rid < room_count; ++rid) {
3157 const Room* room = room_lookup(rid);
3158 if (room ==
nullptr)
3162 return absl::FailedPreconditionError(absl::StrFormat(
3163 "Room 0x%03X has unsaved pushable-block edits, but its block "
3164 "table is not loaded. Load the room's blocks before saving.",
3169 owned_room_ids.insert(
static_cast<uint16_t
>(rid));
3174 ValidateSpecialObjectDrawLayerSelector(obj, rid,
"Pushable block"));
3176 encoded_entry.
room_id =
static_cast<uint16_t
>(rid);
3177 encoded_entry.
px = obj.x();
3178 encoded_entry.
py = obj.y();
3179 encoded_entry.
draw_layer = obj.GetLayerValue();
3183 const EncodedBlock encoded_block{encoded, &obj};
3184 const int load_order = obj.block_load_order();
3185 if (load_order >= 0 && !claimed_load_orders.insert(load_order).second) {
3186 return absl::FailedPreconditionError(absl::StrFormat(
3187 "Room 0x%03X has multiple pushable blocks claiming non-new "
3188 "load-order slot %d",
3192 appended.push_back(encoded_block);
3193 }
else if (load_order >= 0 && load_order < original_slot_count) {
3194 const int original_offset = load_order * 4;
3195 const uint16_t original_room_id =
3196 static_cast<uint16_t
>(original_buffer[original_offset] |
3197 (original_buffer[original_offset + 1] << 8));
3198 if (original_room_id !=
static_cast<uint16_t
>(rid)) {
3203 appended.push_back(encoded_block);
3206 slot_replacements.emplace(load_order, encoded_block);
3210 appended.push_back(encoded_block);
3217 std::vector<uint8_t> output;
3218 output.reserve(original_byte_len + appended.size() * 4);
3219 std::vector<std::pair<const RoomObject*, int>> load_order_updates;
3220 load_order_updates.reserve(slot_replacements.size() + appended.size());
3221 const auto append_encoded_block =
3222 [&output, &load_order_updates](
const EncodedBlock& block) {
3223 const int output_slot =
static_cast<int>(output.size() / 4);
3224 output.push_back(block.bytes.b1);
3225 output.push_back(block.bytes.b2);
3226 output.push_back(block.bytes.b3);
3227 output.push_back(block.bytes.b4);
3228 load_order_updates.emplace_back(block.source_object, output_slot);
3230 for (
int slot = 0; slot < original_slot_count; ++slot) {
3231 const uint8_t b1 = original_buffer[slot * 4 + 0];
3232 const uint8_t b2 = original_buffer[slot * 4 + 1];
3233 const uint16_t slot_room_id =
static_cast<uint16_t
>(b1 | (b2 << 8));
3234 if (owned_room_ids.contains(slot_room_id)) {
3235 const auto it = slot_replacements.find(slot);
3236 if (it == slot_replacements.end()) {
3241 append_encoded_block(it->second);
3244 output.push_back(b1);
3245 output.push_back(b2);
3246 output.push_back(original_buffer[slot * 4 + 2]);
3247 output.push_back(original_buffer[slot * 4 + 3]);
3253 for (
const auto& block : appended) {
3254 append_encoded_block(block);
3263 if (output.empty()) {
3264 return absl::FailedPreconditionError(
3265 "Pushable-block table cannot be empty: ALTTP's runtime scan reads one "
3266 "entry before comparing the byte-length limit. Keep at least one "
3267 "pushable block, or patch the runtime loop before removing the last "
3272 const int kMaxEntries = (4 * kBlocksRegionSize) / 4;
3273 if (
static_cast<int>(output.size() / 4) > kMaxEntries) {
3274 return absl::FailedPreconditionError(absl::StrCat(
3275 "Pushable-block table overflow: ", output.size() / 4,
3276 " entries exceeds the vanilla cap of ", kMaxEntries,
3277 " (expand layout requires repointing all 4 LDA.l operand slots; "
3278 "out of scope for this encoder)."));
3285 const int total_bytes =
static_cast<int>(output.size());
3286 struct BlockWriteDestination {
3291 std::vector<BlockWriteDestination> write_destinations;
3292 write_destinations.reserve(4);
3293 for (
int r = 0; r < 4; ++r) {
3294 const int off = r * kBlocksRegionSize;
3295 const int len = std::min(kBlocksRegionSize, total_bytes - off);
3298 write_destinations.push_back({destination_pcs[r], off, len});
3303 for (
const auto&
destination : write_destinations) {
3304 std::vector<uint8_t> chunk(
3318 for (
const auto& [
object, load_order] : load_order_updates) {
3319 const_cast<RoomObject*
>(object)->set_block_load_order(load_order);
3321 for (
int room_id = 0; room_id < room_count; ++room_id) {
3322 if (
const Room* room = room_lookup(room_id);
3324 const_cast<Room*
>(room)->ClearBlocksDirty();
3327 return absl::OkStatus();
3330template <
typename RoomLookup>
3332 RoomLookup&& room_lookup) {
3334 return absl::InvalidArgumentError(
"ROM not loaded");
3341 const auto& rom_data = rom->
vector();
3346 if (!has_ptr_table) {
3347 for (
int room_id = 0; room_id < room_count; ++room_id) {
3348 const Room* room = room_lookup(room_id);
3350 return absl::FailedPreconditionError(
3351 "Custom collision region not present in this ROM");
3354 return absl::OkStatus();
3357 if (!has_data_region) {
3358 for (
int room_id = 0; room_id < room_count; ++room_id) {
3359 const Room* room = room_lookup(room_id);
3361 return absl::FailedPreconditionError(
3362 "Custom collision data region not present in this ROM");
3365 return absl::OkStatus();
3374 "CustomCollisionPointers"));
3378 "CustomCollisionData"));
3382 for (
int room_id = 0; room_id < room_limit; ++room_id) {
3383 const Room* room = room_lookup(room_id);
3388 const int actual_room_id = room->
id();
3390 if (ptr_offset + 2 >=
static_cast<int>(rom_data.size())) {
3391 return absl::OutOfRangeError(
"Custom collision pointer out of range");
3399 const_cast<Room*
>(room)->ClearCustomCollisionDirty();
3415 const_cast<Room*
>(room)->ClearCustomCollisionDirty();
3421 const_cast<Room*
>(room)->ClearCustomCollisionDirty();
3424 return absl::OkStatus();
3429 rom,
static_cast<int>(rooms.size()),
3430 [&rooms](
int room_id) { return &rooms[room_id]; });
3434 const std::function<
Room*(
int)>& room_lookup) {
3438absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
3440 if (rom ==
nullptr || !rom->
is_loaded()) {
3441 return absl::InvalidArgumentError(
"ROM not loaded");
3443 const auto& rom_data = rom->
vector();
3446 return absl::OutOfRangeError(
"Chest pointers out of range");
3449 const uint32_t data_pointer =
3453 const uint32_t data_pc =
SnesToPc(data_pointer);
3454 if (data_pc > rom_data.size() ||
3456 rom_data.size() -
static_cast<size_t>(data_pc)) {
3457 return absl::OutOfRangeError(
"Chest data region out of range");
3459 const uint32_t data_end =
3461 const auto overlaps = [](uint32_t begin, uint32_t end, uint32_t other_begin,
3462 uint32_t other_end) {
3463 return begin < other_end && other_begin < end;
3469 return absl::FailedPreconditionError(
3470 "Chest data region overlaps chest metadata operands");
3473 return std::vector<std::pair<uint32_t, uint32_t>>{
3476 {data_pc, data_end},
3486 uint16_t
room_id()
const {
return word & 0x7FFF; }
3492 const std::vector<uint8_t>& rom_data,
int cpos,
int byte_length) {
3493 std::vector<PhysicalChestRecord> records;
3495 records.reserve(record_count);
3496 for (
int i = 0; i < record_count; ++i) {
3502 const uint16_t word =
3503 (
static_cast<uint16_t
>(rom_data[off + 1]) << 8) | rom_data[off];
3511 bytes->push_back(word & 0xFF);
3512 bytes->push_back((word >> 8) & 0xFF);
3513 bytes->push_back(item);
3518 const uint16_t word =
static_cast<uint16_t
>(room_id) |
3519 (
chest.size ?
static_cast<uint16_t
>(0x8000) : 0);
3524 absl::Span<const uint8_t> replacement,
3525 std::vector<ChestWriteRun>* writes) {
3527 while (cursor < replacement.size()) {
3528 if (replacement[cursor] == expected[cursor]) {
3532 const size_t begin = cursor;
3535 }
while (cursor < replacement.size() &&
3536 replacement[cursor] != expected[cursor]);
3539 run.
pc = pc +
static_cast<uint32_t
>(begin);
3541 expected.begin() + cursor);
3543 replacement.begin() + cursor);
3544 if (!writes->empty() && writes->back().end() == run.
pc) {
3545 writes->back().expected_bytes.insert(writes->back().expected_bytes.end(),
3548 writes->back().replacement_bytes.insert(
3552 writes->push_back(std::move(run));
3558 std::sort(writes->begin(), writes->end(),
3560 return lhs.pc < rhs.pc;
3562 std::vector<ChestWriteRun> merged;
3563 merged.reserve(writes->size());
3565 if (!merged.empty() && merged.back().end() == write.pc) {
3566 merged.back().expected_bytes.insert(merged.back().expected_bytes.end(),
3567 write.expected_bytes.begin(),
3568 write.expected_bytes.end());
3569 merged.back().replacement_bytes.insert(
3570 merged.back().replacement_bytes.end(),
3571 write.replacement_bytes.begin(), write.replacement_bytes.end());
3574 merged.push_back(std::move(write));
3576 *writes = std::move(merged);
3580 std::vector<uint8_t> bytes;
3589 const Rom* rom,
int room_count,
3590 const std::function<
const Room*(
int)>& room_lookup) {
3591 if (rom ==
nullptr || !rom->
is_loaded()) {
3592 return absl::InvalidArgumentError(
"ROM not loaded");
3594 const auto& rom_data = rom->
vector();
3597 return absl::OutOfRangeError(
"Chest pointers out of range");
3603 for (
int room_id = 0; room_id < plan.
room_limit; ++room_id) {
3604 const Room* room = room_lookup(room_id);
3606 dirty_rooms[room_id] = room;
3617 plan.
data_pc = potential_ranges[1].first;
3624 rom_data.begin() + plan.
data_pc,
3628 return absl::FailedPreconditionError(absl::StrFormat(
3629 "Chest table byte length %d is invalid (capacity %d)",
3635 if (physical_records.size() !=
3637 return absl::OutOfRangeError(
"Chest data region is truncated");
3643 ++old_counts[record.room_id()];
3647 size_t final_record_count = physical_records.size();
3648 for (
int room_id = 0; room_id < plan.
room_limit; ++room_id) {
3649 if (dirty_rooms[room_id] ==
nullptr) {
3652 final_record_count -= old_counts[room_id];
3653 final_record_count += dirty_rooms[room_id]->GetChests().size();
3656 return absl::ResourceExhaustedError(absl::StrFormat(
3657 "Chest table has %d records; capacity is %d",
3661 std::vector<uint8_t> replacement_bytes;
3665 const uint16_t room_id = record.room_id();
3666 const Room* dirty_room =
3668 if (dirty_room ==
nullptr) {
3673 const size_t occurrence = seen_counts[room_id]++;
3674 const auto& replacements = dirty_room->
GetChests();
3675 if (occurrence < replacements.size()) {
3677 replacements[occurrence]);
3684 for (
int room_id = 0; room_id < plan.
room_limit; ++room_id) {
3685 const Room* dirty_room = dirty_rooms[room_id];
3686 if (dirty_room ==
nullptr) {
3689 const auto& replacements = dirty_room->
GetChests();
3690 for (
size_t i = seen_counts[room_id]; i < replacements.size(); ++i) {
3696 return absl::InternalError(
"Chest save plan size mismatch");
3699 const std::array<uint8_t, 2> expected_length = {
3702 const uint16_t replacement_length =
3703 static_cast<uint16_t
>(replacement_bytes.size());
3704 const std::array<uint8_t, 2> encoded_replacement_length = {
3705 static_cast<uint8_t
>(replacement_length & 0xFF),
3706 static_cast<uint8_t
>((replacement_length >> 8) & 0xFF)};
3708 encoded_replacement_length, &plan.
writes);
3711 .subspan(0, replacement_bytes.size()),
3712 replacement_bytes, &plan.
writes);
3723 if (ptr_off < 0 || ptr_off + 1 >=
static_cast<int>(rom_data.size())) {
3726 const uint16_t item_ptr =
3727 (
static_cast<uint16_t
>(rom_data[ptr_off + 1]) << 8) | rom_data[ptr_off];
3728 if (item_ptr < 0x8000) {
3731 const int item_addr =
static_cast<int>(
SnesToPc(0x010000 | item_ptr));
3732 return item_addr >= 0 && item_addr < static_cast<int>(rom_data.size())
3738 const std::vector<uint8_t>& rom_data,
int room_id) {
3740 static_cast<int>(rom_data.size())) {
3741 return absl::OutOfRangeError(
"Room items pointer table out of range");
3744 return absl::OutOfRangeError(
"Room ID out of range");
3751 const int hard_end =
3755 return absl::FailedPreconditionError(
3756 "Room pot item pointer is null, invalid, or outside the item region");
3764 const Rom* rom,
int room_count,
3765 const std::function<
const Room*(
int)>& room_lookup) {
3766 return BuildChestSavePlanImpl(rom, room_count, room_lookup);
3769absl::StatusOr<std::vector<std::pair<uint32_t, uint32_t>>>
3771 const std::function<
const Room*(
int)>& room_lookup) {
3779 const std::function<
const Room*(
int)>& room_lookup) {
3780 if (rom ==
nullptr || !rom->
is_loaded()) {
3781 return absl::InvalidArgumentError(
"ROM not loaded");
3786 if (canonical_plan != plan) {
3787 return absl::FailedPreconditionError(
3788 "Chest save plan does not match canonical serialization");
3790 return absl::OkStatus();
3793 const auto& rom_data = rom->
vector();
3796 plan.
data_pc > rom_data.size() ||
3798 return absl::FailedPreconditionError(
3799 "Chest save plan source is no longer addressable");
3803 return absl::FailedPreconditionError(
3804 "Chest save plan is stale: data pointer changed");
3806 const uint16_t current_length =
3810 return absl::FailedPreconditionError(
3811 "Chest save plan is stale: runtime length changed");
3815 rom_data.begin() + plan.
data_pc)) {
3816 return absl::FailedPreconditionError(
3817 "Chest save plan is stale: table bytes changed");
3820 const Room* room = room_lookup(state.room_id);
3822 EncodeChestRoomState(state.room_id, *room) != state.encoded_chests) {
3823 return absl::FailedPreconditionError(absl::StrFormat(
3824 "Chest save plan is stale for room 0x%03X", state.room_id));
3829 if (canonical_plan != plan) {
3830 return absl::FailedPreconditionError(
3831 "Chest save plan does not match canonical serialization");
3844 const_cast<Room*
>(room_lookup(state.room_id))->ClearChestsDirty();
3847 return absl::OkStatus();
3850template <
typename RoomLookup>
3852 RoomLookup&& room_lookup) {
3853 const std::function<
const Room*(int)> lookup =
3854 std::forward<RoomLookup>(room_lookup);
3862 [&rooms](
int room_id) { return &rooms[room_id]; });
3866 const std::function<
const Room*(
int)>& room_lookup) {
3870template <
typename RoomLookup>
3872 Rom* rom,
int room_count, RoomLookup&& room_lookup,
3875 return absl::InvalidArgumentError(
"ROM not loaded");
3877 const auto& rom_data = rom->
vector();
3879 static_cast<int>(rom_data.size())) {
3880 return absl::OutOfRangeError(
"Room items pointer table out of range");
3884 if (repack_layout !=
nullptr) {
3885 std::vector<DungeonStreamReplacement> replacements;
3886 for (
int room_id = 0; room_id < room_limit; ++room_id) {
3887 const Room* room = room_lookup(room_id);
3893 replacement.
room_id =
static_cast<uint32_t
>(room_id);
3897 replacement.
encoded_stream.push_back((item.position >> 8) & 0xFF);
3902 replacements.push_back(std::move(replacement));
3904 if (replacements.empty()) {
3905 return absl::OkStatus();
3914 if (
const Room* room = room_lookup(replacement.room_id);
3916 const_cast<Room*
>(room)->ClearPotItemsDirty();
3919 return absl::OkStatus();
3922 struct PendingPotItemWrite {
3925 std::vector<uint8_t> bytes;
3930 std::vector<PendingPotItemWrite> pending_writes;
3931 for (
int room_id = 0; room_id < room_limit; ++room_id) {
3932 const Room* room = room_lookup(room_id);
3938 GetPotItemStreamInfo(rom_data, room_id));
3939 if (stream_info.shared) {
3940 return absl::FailedPreconditionError(absl::StrFormat(
3941 "Room %d pot item stream at PC 0x%06X is shared; repacking is "
3943 room_id, stream_info.address));
3945 if (stream_info.capacity() <= 0) {
3946 return absl::FailedPreconditionError(absl::StrFormat(
3947 "Room %d pot item stream has no safe physical boundary", room_id));
3950 PendingPotItemWrite pending;
3951 pending.room_id = room_id;
3952 pending.address = stream_info.address;
3954 pending.bytes.push_back(pi.position & 0xFF);
3955 pending.bytes.push_back((pi.position >> 8) & 0xFF);
3956 pending.bytes.push_back(pi.item);
3958 pending.bytes.push_back(0xFF);
3959 pending.bytes.push_back(0xFF);
3960 if (
static_cast<int>(pending.bytes.size()) > stream_info.capacity()) {
3961 return absl::ResourceExhaustedError(absl::StrFormat(
3962 "Room %d pot item data too large! Size: %d, Available: %d", room_id,
3963 static_cast<int>(pending.bytes.size()), stream_info.capacity()));
3965 pending_writes.push_back(std::move(pending));
3968 for (
const auto& pending : pending_writes) {
3969 const bool data_changed =
3970 !std::equal(pending.bytes.begin(), pending.bytes.end(),
3971 rom_data.begin() + pending.address);
3977 for (
const auto& pending : pending_writes) {
3978 if (
const Room* room = room_lookup(pending.room_id); room !=
nullptr) {
3979 const_cast<Room*
>(room)->ClearPotItemsDirty();
3982 return absl::OkStatus();
3987 [&rooms](
int room_id) { return &rooms[room_id]; });
3993 rom,
static_cast<int>(rooms.size()),
3994 [&rooms](
int room_id) { return &rooms[room_id]; }, repack_layout);
3998 Rom* rom,
int room_count,
3999 const std::function<
const Room*(
int)>& room_lookup) {
4004 const std::function<
const Room*(
int)>& room_lookup,
4010 auto rom_data =
rom()->vector();
4031 const int kRegionSize = 0x80;
4034 std::vector<uint8_t> blocks_data(blocks_count, 0);
4035 for (
int r = 0; r < 4; ++r) {
4036 const int slot = kPointerSlots[r];
4037 if (slot + 2 >=
static_cast<int>(rom_data.size())) {
4038 LOG_WARN(
"Room",
"LoadBlocks: pointer slot %d out of range", r);
4041 const absl::Status operand_status =
4042 ValidateBlocksLoaderPointerOperand(rom_data, slot);
4043 if (!operand_status.ok()) {
4045 std::string(operand_status.message()).c_str());
4049 (rom_data[slot + 2] << 16) | (rom_data[slot + 1] << 8) | rom_data[slot];
4051 const int off = r * kRegionSize;
4052 const int len = std::min(kRegionSize, blocks_count - off);
4055 if (pc < 0 || pc + len >
static_cast<int>(rom_data.size())) {
4056 LOG_WARN(
"Room",
"LoadBlocks: region %d data out of range", r);
4059 std::copy_n(rom_data.begin() + pc, len, blocks_data.begin() + off);
4068 return (obj.options() & ObjectOption::Block) !=
4069 ObjectOption::Nothing;
4081 for (
int i = 0; i + 3 < blocks_count; i += 4) {
4083 blocks_data[i + 2], blocks_data[i + 3]};
4099 LOG_DEBUG(
"Room",
"Loaded block at (%d,%d) draw_layer=%d behavior_layer=%d",
4108 auto rom_data =
rom()->vector();
4124 int ptr_addr = table_addr + (
room_id_ * 2);
4125 if (ptr_addr + 1 >=
static_cast<int>(rom_data.size()))
4128 uint16_t item_ptr = (rom_data[ptr_addr + 1] << 8) | rom_data[ptr_addr];
4131 int item_addr =
SnesToPc(0x010000 | item_ptr);
4134 while (item_addr + 2 <
static_cast<int>(rom_data.size())) {
4136 uint16_t position = (rom_data[item_addr + 1] << 8) | rom_data[item_addr];
4139 if (position == 0xFFFF)
4143 uint8_t item_type = rom_data[item_addr + 2];
4147 pot_item.
item = item_type;
4157 auto rom_data =
rom()->vector();
4168 const int max_offset = rom_data[
kPitCount];
4169 const int pit_entries = max_offset / 2 + 1;
4171 const int pit_ptr = (rom_data[
kPitPointer + 2] << 16) |
4175 "LoadPits: room_id=%d, RoomsWithPitDamage entries=%d, "
4176 "table_snes=0x%06X",
4185 LOG_DEBUG(
"Room",
"Per-room pit destination: target=%d, target_layer=%d",
4200 for (
const auto& sprite :
sprites_) {
4201 if (sprite.IsOverlord()) {
4211 for (
const auto& door :
doors_) {
4213 const bool is_special = [&]() ->
bool {
4214 switch (door.type) {
4245 auto options = obj.options();
4258 if (obj.id_ == 0x11E || obj.id_ == 0x11F) {
4263 if (obj.id_ >= 0xF83 && obj.id_ <= 0xF8F) {
4270 if ((obj.id_ >= 0x130 && obj.id_ <= 0x135) || obj.id_ == 0x139 ||
4271 obj.id_ == 0x13A || obj.id_ == 0x13B) {
4275 else if (obj.id_ >= 0x13C && obj.id_ <= 0x13F) {
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
absl::Status WriteByte(int addr, uint8_t value)
const auto & vector() const
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
absl::StatusOr< uint16_t > ReadWord(int offset) const
absl::Status WriteWord(int addr, uint16_t value)
absl::Status WriteLong(uint32_t addr, uint32_t value)
absl::StatusOr< uint32_t > ReadLong(int offset) const
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
void ClearBG1RevealMask()
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)
void EnsureBitmapInitialized()
void ClearCoverageBuffer()
void ClearPriorityBuffer()
Represents a bitmap image optimized for SNES ROM hacking.
constexpr ImVec4 rgb() const
Get RGB values (WARNING: stored as 0-255 in ImVec4)
constexpr uint16_t snes() const
Get SNES 15-bit color.
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
absl::Status Allow(uint32_t start, uint32_t end, std::string_view label)
Editor implementation of DungeonState.
Draws dungeon objects to background buffers using game patterns.
void SetBG1RevealMaskSource(gfx::BG1RevealMaskSource source)
void DrawDoor(const DoorDef &door, int door_index, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2, const DungeonState *state=nullptr)
Draw a door to background buffers.
void DrawPotItem(uint8_t item_id, int x, int y, gfx::BackgroundBuffer &bg)
Draw a pot item visualization.
absl::Status DrawObjectList(const std::vector< RoomObject > &objects, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2, const gfx::PaletteGroup &palette_group, const DungeonState *state=nullptr, gfx::BackgroundBuffer *layout_bg1=nullptr, bool reset_room_event_indices=true)
Draw all objects in a room.
absl::Status DrawRoomDrawObjectData2x2(uint16_t object_id, int tile_x, int tile_y, RoomObject::LayerType layer, uint16_t room_draw_object_data_offset, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2)
Draw a fixed 2x2 (16x16) tile pattern from RoomDrawObjectData.
void SetAllowTrackCornerAliases(bool allow)
void SetCurrentBitmap(gfx::Bitmap *bitmap)
void LogPaletteLoad(const std::string &location, int palette_id, const gfx::SnesPalette &palette)
static PaletteDebugger & Get()
void LogPaletteApplication(const std::string &location, int palette_id, bool success, const std::string &reason="")
void SetCurrentPalette(const gfx::SnesPalette &palette)
void LogSurfaceState(const std::string &location, SDL_Surface *surface)
void SetCurrentRenderPalette(const std::vector< SDL_Color > &palette)
absl::Status SaveToRom(Rom *rom) const
RoomLayerManager - Manages layer visibility and compositing.
void CompositeToOutput(Room &room, gfx::Bitmap &output) const
Composite all visible layers into a single output bitmap.
uint64_t CompositeStateSignature() const
const std::vector< RoomObject > & GetObjects() const
absl::Status Draw(int room_id, const uint8_t *gfx_data, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2, const gfx::PaletteGroup &palette_group, DungeonState *state) const
absl::Status LoadLayout(int layout_id)
static RoomObject DecodeObjectFromBytes(uint8_t b1, uint8_t b2, uint8_t b3, uint8_t layer)
void set_block_behavior_layer(uint8_t layer)
static constexpr int kBlockLoadOrderNew
void set_block_load_order(int order)
void set_options(ObjectOption options)
bool ValidateObject(const RoomObject &object) const
SaveDirtyState save_dirty_state_
std::vector< RoomObject > tile_objects_
gfx::BackgroundBuffer object_bg1_buffer_
void SetTag2Direct(TagKey tag2)
bool HasExceededLimits() const
Check if any object limits are exceeded.
void ClearObjectStreamHeaderDirty()
uint8_t render_entrance_blockset_
const CustomCollisionMap & custom_collision() const
absl::Status UpdateObject(size_t index, const RoomObject &object)
void ClearSaveDirtyState()
void SetStair4Target(uint8_t target)
void SetPitsTarget(uint8_t target)
void SetIsLight(bool is_light)
void EnsureSpritesLoaded()
gfx::BackgroundBuffer bg2_buffer_
uint8_t resolved_main_blockset_
const std::vector< chest_data > & GetChests() const
uint8_t cached_floor2_graphics_
std::vector< zelda3::Sprite > sprites_
CustomCollisionMap custom_collision_
void MarkSaveDirtyForTileObject(const RoomObject &object)
void SetLoaded(bool loaded)
void ClearObjectStreamDirty()
void ClearCustomCollisionDirty()
void CopyRoomGraphicsToBuffer()
bool custom_collision_dirty() const
zelda3_version_pointers version_constants() const
std::vector< Door > doors_
void SetStaircaseRoom(int index, uint8_t room)
static constexpr uint8_t kObjectHeaderFloor1Dirty
absl::Status RemoveObject(size_t index)
void SetStair1TargetLayer(uint8_t layer)
void SetLayer2Mode(uint8_t mode)
void LoadLayoutTilesToBuffer()
uint8_t staircase_room(int index) const
void SetTag2(TagKey tag2)
std::vector< DungeonLimitInfo > GetExceededLimitDetails() const
Get list of exceeded limits with details.
void ParseObjectsFromLocation(int objects_location)
uint8_t cached_floor1_graphics_
bool custom_collision_dirty_
static constexpr uint8_t kObjectHeaderFloor2Dirty
void ReloadGraphics(std::optional< uint8_t > entrance_blockset=std::nullopt)
bool pot_items_dirty() const
void SetTag1Direct(TagKey tag1)
void SetHolewarp(uint8_t hw)
void SetStair2Target(uint8_t target)
bool object_stream_dirty() const
bool sprites_dirty() const
void SetCollision(CollisionKey collision)
bool object_stream_header_dirty() const
gfx::BackgroundBuffer bg1_buffer_
void SetStaircasePlane(int index, uint8_t plane)
absl::Status SaveObjects(const DungeonStreamLayout *layout=nullptr)
void SetIsDark(bool is_dark)
std::map< DungeonLimit, int > GetLimitedObjectCounts() const
Count limited objects in this room.
void RenderRoomGraphics()
absl::Status SaveRoomHeader()
gfx::Bitmap composite_bitmap_
Room & operator=(Room &&)
bool chests_dirty() const
uint64_t composite_signature_
CollisionKey collision() const
void LoadRoomGraphics(std::optional< uint8_t > entrance_blockset=std::nullopt)
uint8_t staircase_rooms_[4]
gfx::Bitmap & GetCompositeBitmap(RoomLayerManager &layer_mgr)
Get a composite bitmap of all layers merged.
std::vector< uint8_t > EncodeObjects() const
void SetEffect(EffectKey effect)
absl::Status SaveObjectStreamHeader(const DungeonStreamLayout *layout=nullptr)
gfx::BackgroundBuffer object_bg2_buffer_
void ClearWaterFillDirty()
std::array< uint8_t, 16 > blocks_
void SetTag1(TagKey tag1)
void SetBackgroundTileset(uint8_t tileset)
void SetStair3TargetLayer(uint8_t layer)
void SetRenderEntranceBlockset(uint8_t entrance_blockset)
absl::Status SaveSprites(const DungeonStreamLayout *layout=nullptr)
void PrepareForRender(std::optional< uint8_t > entrance_blockset=std::nullopt)
const std::vector< RoomObject > & GetTileObjects() const
void SetLayerMerging(LayerMergeType merging)
void SetPitsTargetLayer(uint8_t layer)
bool blocks_dirty() const
void SetSpriteTileset(uint8_t tileset)
void SetStair1Target(uint8_t target)
void SetBg2(background2 bg2)
static constexpr uint8_t kObjectHeaderLayoutDirty
bool torches_dirty() const
void EnsureObjectsLoaded()
void SetSpriteset(uint8_t ss)
int ResolveDungeonPaletteId() const
std::unique_ptr< DungeonState > dungeon_state_
void LoadAnimatedGraphics()
std::vector< uint8_t > EncodeSprites() const
std::vector< chest_data > chests_in_room_
void SetBlockset(uint8_t bs)
LayerMergeType layer_merging_
void EnsurePotItemsLoaded()
uint8_t staircase_plane(int index) const
bool has_composite_signature_
bool AreTorchesLoaded() const
uint8_t cached_spriteset_
std::array< uint8_t, 0x10000 > current_gfx16_
std::vector< staircase > z3_staircases_
std::vector< PotItem > pot_items_
bool AreBlocksLoaded() const
uint8_t background_tileset_
void SetStair3Target(uint8_t target)
void HandleSpecialObjects(short oid, uint8_t posX, uint8_t posY, int &nbr_of_staircase)
void SetStair4TargetLayer(uint8_t layer)
absl::Status AddObject(const RoomObject &object)
absl::StatusOr< size_t > FindObjectAt(int x, int y, int layer) const
void SetPalette(uint8_t pal)
bool has_custom_collision() const
const std::vector< PotItem > & GetPotItems() const
void SetStair2TargetLayer(uint8_t layer)
void RenderObjectsToBackground()
void SetLayer2Behavior(uint8_t behavior)
void SetMessageId(uint16_t mid)
A class for managing sprites in the overworld and underworld.
auto set_key_drop(int key)
zelda3_bg2_effect
Background layer 2 effects.
#define LOG_DEBUG(category, format,...)
#define LOG_ERROR(category, format,...)
#define LOG_WARN(category, format,...)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
absl::Status GetObjectPointerTablePc(const std::vector< uint8_t > &rom_data, int *table_pc)
void AppendChestRecord(std::vector< uint8_t > *bytes, uint16_t word, uint8_t item)
constexpr int kTorchesMaxSize
absl::Status ValidateSpecialObjectDrawLayerSelector(const RoomObject &object, int room_id, const char *object_type)
int ReadRoomPotItemAddressPc(const std::vector< uint8_t > &rom_data, int room_id)
absl::Status RelocateDungeonStream(Rom *rom, int room_id, DungeonStreamKind expected_kind, const DungeonStreamLayout &layout, std::vector< uint8_t > encoded_stream)
void SortAndCoalesceChestRuns(std::vector< ChestWriteRun > *writes)
bool RoomUsesTrackCornerAliases(const std::vector< RoomObject > &objects)
const LayerMergeType & LayerMergeFromHeaderByte(uint8_t byte0)
void AppendChangedChestRuns(uint32_t pc, absl::Span< const uint8_t > expected, absl::Span< const uint8_t > replacement, std::vector< ChestWriteRun > *writes)
std::vector< TorchSegment > ParseRomTorchSegments(const std::vector< uint8_t > &rom_data, int bytes_count)
absl::Status GetSpritePointerTablePc(const std::vector< uint8_t > &rom_data, int *table_pc)
absl::StatusOr< bool > DungeonStreamRequiresCopyOnWrite(const Rom &rom, int room_id, DungeonStreamKind expected_kind, const DungeonStreamLayout &layout, size_t replacement_size)
int ReadRoomObjectAddressPc(const std::vector< uint8_t > &rom_data, int table_pc, int room_id)
int ReadRoomSpriteAddressPc(const std::vector< uint8_t > &rom_data, int table_pc, int room_id)
absl::Status PreflightBlocksLoaderDestinations(const std::vector< uint8_t > &rom_data, std::array< int, 4 > *destination_pcs)
absl::StatusOr< PhysicalStreamInfo > GetObjectStreamInfo(const std::vector< uint8_t > &rom_data, int room_id)
std::vector< uint8_t > EncodeChestRoomState(int room_id, const Room &room)
absl::StatusOr< PhysicalStreamInfo > GetSpriteStreamInfo(const std::vector< uint8_t > &rom_data, int room_id)
PhysicalStreamInfo AnalyzePhysicalStream(const std::vector< int > &room_addresses, int room_id, int known_region_end=-1)
constexpr int kBlocksRegionSize
std::vector< uint8_t > EncodeTorchSegmentForRoom(int room_id, const Room &room)
bool HalfOpenRangesOverlap(int first_begin, int first_end, int second_begin, int second_end)
int MeasureSpriteStreamSize(const std::vector< uint8_t > &rom_data, int sprite_address, int hard_end)
void PopulateDungeonRenderPaletteRows(const gfx::SnesPalette &dungeon_palette, const gfx::SnesPalette *hud_palette, WriteColor write_color)
absl::Status ValidateBlocksLoaderPointerOperand(const std::vector< uint8_t > &rom_data, int operand_pc)
bool IsDarkRoomHeaderByte(uint8_t byte0)
absl::StatusOr< ChestSavePlan > BuildChestSavePlanImpl(const Rom *rom, int room_count, const std::function< const Room *(int)> &room_lookup)
void AppendEditedChestRecord(std::vector< uint8_t > *bytes, int room_id, const chest_data &chest)
constexpr std::array< int, 4 > kBlocksPointerSlots
std::vector< PhysicalChestRecord > ParsePhysicalRomChests(const std::vector< uint8_t > &rom_data, int cpos, int byte_length)
background2 Background2FromHeaderByte(uint8_t byte0)
uint32_t ReadRoomObjectAddressSnes(const std::vector< uint8_t > &rom_data, int table_pc, int room_id)
uint8_t Layer2ModeFromHeaderByte(uint8_t byte0)
absl::StatusOr< PhysicalStreamInfo > GetPotItemStreamInfo(const std::vector< uint8_t > &rom_data, int room_id)
absl::Status ValidateLightableTorchForSave(const RoomObject &object, int room_id)
constexpr int kBlocksPointer4
constexpr int kSpritesDataEndExclusive
absl::Status SaveAllChests(Rom *rom, absl::Span< const Room > rooms)
constexpr int kDoorPointers
constexpr int kGfxBufferAnimatedFrameStride
const std::string RoomTag[65]
absl::Status WriteTrackCollision(Rom *rom, int room_id, const CustomCollisionMap &map)
constexpr int kGfxBufferAnimatedFrameOffset
@ 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)
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)
constexpr int kChestsLengthPointer
int FindMaxUsedSpriteAddress(Rom *rom)
constexpr int kMessagesIdDungeon
absl::Status RelocateSpriteData(Rom *rom, int room_id, const std::vector< uint8_t > &encoded_bytes)
constexpr int kGfxBufferRoomOffset
RoomObject::LayerType MapRoomObjectListIndexToDrawLayer(uint8_t list_index)
absl::Status SaveAllPotItems(Rom *rom, absl::Span< const Room > rooms)
constexpr int kGfxBufferRoomSpriteOffset
RoomSize CalculateRoomSize(Rom *rom, int room_id)
absl::Status SaveAllPotItemsImpl(Rom *rom, int room_count, RoomLookup &&room_lookup, const DungeonStreamLayout *repack_layout=nullptr)
constexpr int kPitPointer
constexpr int kDungeonPaletteBytes
constexpr int kSpritesData
void LoadDungeonRenderPaletteToCgram(std::span< uint16_t > cgram, const gfx::SnesPalette &dungeon_palette, const gfx::SnesPalette *hud_palette)
LightableTorchEntry DecodeLightableTorchEntry(const LightableTorchBytes &bytes)
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > GetChestTableWriteRanges(const Rom *rom)
constexpr int kRoomItemsDataEnd
absl::Status SaveAllTorches(Rom *rom, absl::Span< const Room > rooms)
constexpr int kTileAddress
LightableTorchBytes EncodeLightableTorchEntry(const LightableTorchEntry &entry)
std::vector< SDL_Color > BuildDungeonRenderPalette(const gfx::SnesPalette &dungeon_palette, const gfx::SnesPalette *hud_palette)
absl::StatusOr< DungeonStreamWritePlan > PlanDungeonStreamRepack(const DungeonStreamInventory &inventory, const std::vector< DungeonStreamReplacement > &requested_replacements)
constexpr int kRoomsSpritePointer
constexpr int kTileAddressFloor
constexpr int GetDungeonObjectDataRegionEnd(int pc_address)
absl::Status SaveAllPits(Rom *rom)
constexpr int kChestsDataPointer1
constexpr int kBlocksLength
absl::Status SaveAllBlocks(Rom *rom)
constexpr int kBlocksPointer1
constexpr int kChestTableCapacityRecords
constexpr int kRoomItemsPointers
constexpr int kGfxBufferRoomSpriteStride
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)
absl::Status SaveAllCollisionImpl(Rom *rom, int room_count, RoomLookup &&room_lookup)
Room LoadRoomFromRom(Rom *rom, int room_id)
constexpr int kCustomCollisionDataPosition
absl::Status ApplyDungeonStreamWritePlan(Rom *rom, const DungeonStreamWritePlan &plan)
bool HasExceededLimits(const std::map< DungeonLimit, int > &counts)
constexpr uint32_t kDungeonPalettePointerTable
bool UsesRoomObjectStream(const RoomObject &object)
constexpr uint16_t kStairsObjects[]
constexpr int kChestTableCapacityBytes
absl::Status SaveAllChestsImpl(Rom *rom, int room_count, RoomLookup &&room_lookup)
absl::StatusOr< DungeonStreamWritePlan > PlanDungeonStreamWrites(const DungeonStreamInventory &inventory, const std::vector< DungeonStreamReplacement > &requested_replacements)
absl::StatusOr< DungeonStreamInventory > InventoryDungeonStreams(const Rom &rom, const DungeonStreamLayout &requested_layout)
absl::Status SaveAllTorchesImpl(Rom *rom, int room_count, RoomLookup &&room_lookup)
constexpr int kNumberOfRooms
absl::Status ApplyChestSavePlan(Rom *rom, const ChestSavePlan &plan, const std::function< const Room *(int)> &room_lookup)
const std::string RoomEffect[8]
constexpr int kRoomHeaderPointer
constexpr bool HasCustomCollisionDataRegion(std::size_t rom_size)
constexpr int kBlocksPointer3
constexpr int kRoomHeaderPointerBank
absl::Status ValidateRoomObjectStreamEntryForSave(const RoomObject &object)
constexpr int kCustomCollisionRoomPointers
constexpr int kGfxBufferStride
std::map< DungeonLimit, int > CreateLimitCounter()
absl::Status SaveAllCollision(Rom *rom, absl::Span< Room > rooms)
constexpr int kGfxBufferRoomSpriteLastLineOffset
PushableBlockEntry DecodePushableBlockEntry(const PushableBlockBytes &bytes)
constexpr int kBlocksPointer2
constexpr int kRoomObjectPointer
constexpr int kGfxBufferOffset
uint32_t PcToSnes(uint32_t addr)
uint32_t SnesToPc(uint32_t addr) noexcept
SDL2/SDL3 compatibility layer.
#define RETURN_IF_ERROR(expr)
Legacy chest data structure.
Room transition destination.
PaletteGroup dungeon_main
Represents a group of palettes.
const SnesPalette & palette_ref(int i) const
void AddPalette(SnesPalette pal)
std::vector< uint8_t > original_capacity_bytes
std::vector< std::pair< uint32_t, uint32_t > > write_ranges() const
uint16_t original_byte_length
std::vector< ChestWriteRun > writes
std::array< uint8_t, 3 > pointer_operand
std::vector< ChestDirtyRoomState > dirty_rooms
std::vector< uint8_t > expected_bytes
std::vector< uint8_t > replacement_bytes
std::array< uint8_t, 64 *64 > tiles
std::vector< DungeonStreamAliasGroup > aliases
DungeonStreamLayout layout
std::vector< DungeonStreamIssue > issues
std::vector< DungeonStreamOverlap > overlaps
std::vector< DungeonStreamRecord > streams
std::vector< DungeonStreamPcRange > data_ranges
std::vector< uint8_t > encoded_stream
std::array< std::array< uint8_t, 4 >, kNumSpritesets > spriteset_ids
std::array< std::array< uint8_t, 4 >, kNumRoomBlocksets > room_blockset_ids
std::array< std::array< uint8_t, 4 >, kNumPalettesets > paletteset_ids
gfx::PaletteGroupMap palette_groups
std::array< std::array< uint8_t, 8 >, kNumMainBlocksets > main_blockset_ids
std::vector< uint8_t > graphics_buffer
static Door FromRomBytes(uint8_t b1, uint8_t b2)
uint8_t object_stream_header
std::vector< uint8_t > bytes
Public YAZE API umbrella header.