11#include "absl/strings/numbers.h"
12#include "absl/strings/str_format.h"
13#include "absl/strings/str_split.h"
44 if (!value.has_value()) {
45 return absl::InvalidArgumentError(
46 absl::StrFormat(
"Missing required argument '--%s'",
name));
55 return parsed.status();
57 return parsed.value();
61 const char*
name,
int default_value) {
68 return parsed.status();
70 return parsed.value();
75 return absl::InvalidArgumentError(
76 absl::StrFormat(
"Room ID out of range: 0x%X (expected 0x00-0x%02X)",
79 return absl::OkStatus();
83 if (value < 0 || value > 31) {
84 return absl::InvalidArgumentError(
85 absl::StrFormat(
"%c must be 0-31 (5-bit tile coord)", axis));
87 return absl::OkStatus();
94 auto disk_status = rom->
SaveToFile(save_settings);
95 if (!disk_status.ok()) {
96 formatter.
AddField(
"save_error", std::string(disk_status.message()));
100 formatter.
AddField(
"save_status",
"saved");
101 return absl::OkStatus();
104template <
typename Mutation>
107 Mutation&& mutation) {
110 auto write_status = std::forward<Mutation>(mutation)();
111 if (!write_status.ok()) {
112 formatter.
AddField(
"write_error", std::string(write_status.message()));
115 formatter.
AddField(
"write_status",
"success");
119 return absl::OkStatus();
131absl::StatusOr<std::optional<StreamSaveManifestContext>>
134 const auto manifest_path = parser.
GetString(
"manifest");
135 if (!manifest_path.has_value()) {
143 const char* stream_name =
145 if (manifest_layout ==
nullptr) {
146 return absl::FailedPreconditionError(absl::StrFormat(
147 "Manifest does not define dungeon_stream_regions.%s", stream_name));
150 return absl::FailedPreconditionError(absl::StrFormat(
151 "dungeon_stream_regions.%s must use copy_on_write", stream_name));
157 return std::optional<StreamSaveManifestContext>(std::move(context));
161 uint32_t source_data_pc = 0;
162 uint32_t planned_data_pc = 0;
170 if ((subtype & 0x07) == 0x07 && (
id == 0 ||
id > 0x1A)) {
171 return absl::InvalidArgumentError(
172 "Subtype low bits 7 encode an overlord; only IDs 0x01-0x1A are valid");
174 if ((y | ((subtype & 0x18) << 2) | (layer << 7)) == 0xFF) {
175 return absl::InvalidArgumentError(
176 "Sprite encoding is the end of the sprite list (0xFF)");
178 if (
id == 0xE4 && x == 0 && (y == 29 || y == 30) && subtype == 24 &&
180 return absl::InvalidArgumentError(
181 "This E4 position/subtype/layer encodes a hidden key marker; use "
182 "the owning sprite's Key drop property");
184 return absl::OkStatus();
190 const auto* layout = manifest_context !=
nullptr
201 if (manifest_context !=
nullptr &&
204 return absl::PermissionDeniedError(
"Write conflict with Hack Manifest");
224 const auto& expected = pending_room.
GetSprites();
225 const auto& reopened = scratch_room.
GetSprites();
226 if (reopened.size() != expected.size()) {
227 return absl::FailedPreconditionError(
228 "Sprite placement does not survive save/reopen");
230 for (
size_t i = 0; i < expected.size(); ++i) {
231 if (reopened[i].
id() != expected[i].
id() ||
232 reopened[i].x() != expected[i].x() ||
233 reopened[i].y() != expected[i].y() ||
234 reopened[i].subtype() != expected[i].subtype() ||
235 reopened[i].layer() != expected[i].layer() ||
236 reopened[i].key_drop() != expected[i].key_drop()) {
237 return absl::FailedPreconditionError(
238 "Sprite placement does not survive save/reopen");
247 std::vector<std::pair<uint32_t, uint32_t>> ranges;
252 uint32_t object_pointer_table_snes = 0;
255 const uint32_t object_pointer_table_pc =
SnesToPc(object_pointer_table_snes);
256 const uint64_t current_pointer_slot =
257 static_cast<uint64_t
>(object_pointer_table_pc) +
258 static_cast<uint64_t
>(room_id) * 3u;
259 if (current_pointer_slot + 3u > source_rom.
size()) {
260 return absl::OutOfRangeError(
261 "Selected object pointer slot is outside the ROM");
264 uint32_t current_stream_snes = 0;
266 source_rom.
ReadLong(
static_cast<int>(current_pointer_slot)));
267 const uint32_t current_stream_pc =
SnesToPc(current_stream_snes);
268 const uint64_t current_stream_end =
static_cast<uint64_t
>(current_stream_pc) +
270 if (current_stream_end > std::numeric_limits<uint32_t>::max()) {
271 return absl::OutOfRangeError(
"Selected object write range overflows");
273 ranges.emplace_back(current_stream_pc,
274 static_cast<uint32_t
>(current_stream_end));
276 const uint64_t door_pointer_slot =
278 static_cast<uint64_t
>(room_id) * 3u;
279 if (door_pointer_slot + 3u > std::numeric_limits<uint32_t>::max()) {
280 return absl::OutOfRangeError(
"Selected door pointer range overflows");
282 ranges.emplace_back(
static_cast<uint32_t
>(door_pointer_slot),
283 static_cast<uint32_t
>(door_pointer_slot + 3u));
286 ranges.emplace_back(range.begin, range.end);
292 const uint64_t cow_pointer_slot =
294 static_cast<uint64_t
>(room_id) * pointer_width;
295 if (cow_pointer_slot + pointer_width > std::numeric_limits<uint32_t>::max()) {
296 return absl::OutOfRangeError(
"Selected COW pointer range overflows");
298 ranges.emplace_back(
static_cast<uint32_t
>(cow_pointer_slot),
299 static_cast<uint32_t
>(cow_pointer_slot + pointer_width));
302 if (conflicts.empty() ||
304 return absl::OkStatus();
306 return absl::PermissionDeniedError(
"Write conflict with Hack Manifest");
313 if (manifest_context !=
nullptr) {
315 source_rom, room_id, pending_room, *manifest_context));
329 return scratch_room.
SaveObjects(manifest_context !=
nullptr
335 const Rom& source_rom,
int room_id,
int object_vector_index,
338 if (manifest_context !=
nullptr) {
340 source_rom, room_id, pending_room, *manifest_context));
351 return scratch_room.
SaveObjects(manifest_context !=
nullptr
359 if (!value_or.ok()) {
360 return value_or.status();
364 if (!ParseHexString(*value_or, &value)) {
365 return absl::InvalidArgumentError(absl::StrFormat(
366 "Invalid --%s value '%s' (expected integer/hex)",
name, *value_or));
372 if (type < 0 || type > 0x66 || (type & 1) != 0) {
373 return absl::InvalidArgumentError(absl::StrFormat(
374 "--%s must be an even door type in range 0x00-0x66", argument_name));
376 return absl::OkStatus();
380 return std::find(room_ids.begin(), room_ids.end(),
381 static_cast<uint32_t
>(room_id)) != room_ids.end();
385 if ((snes_address & 0xFFFFu) < 0x8000u) {
388 const uint8_t normalized_bank =
389 static_cast<uint8_t
>((snes_address >> 16) & 0x7Fu);
390 if (normalized_bank >= 0x7Eu) {
393 const uint32_t pc_address =
SnesToPc(snes_address);
394 return (
PcToSnes(pc_address) & 0x7FFFFFu) == (snes_address & 0x7FFFFFu);
406 return start <= rom.
size() && length <= rom.
size() - start;
411 return absl::FailedPreconditionError(
412 "ROM is too small to prove US/OOS dungeon palette table layout");
414 uint8_t destination_code = 0;
417 if (destination_code != 0x01) {
418 return absl::FailedPreconditionError(absl::StrFormat(
419 "Dungeon palette commands currently support only the US/OOS table "
420 "layout (SNES destination code 0x01); got unsupported code 0x%02X",
423 return absl::OkStatus();
428 uint8_t palette_set_id = 0;
429 uint8_t palette_pointer_offset = 0;
430 uint16_t palette_pointer_word = 0;
431 int palette_index = -1;
432 uint32_t room_header_pc = 0;
433 uint32_t palette_set_pc = 0;
434 uint32_t palette_pointer_pc = 0;
435 uint32_t palette_pc = 0;
443 return absl::FailedPreconditionError(
444 "Dungeon room-header pointer metadata is outside the ROM");
447 uint32_t table_snes = 0;
450 return absl::FailedPreconditionError(absl::StrFormat(
451 "Dungeon room-header pointer table is not a valid LoROM pointer: "
455 const uint32_t table_pc =
SnesToPc(table_snes);
456 const uint64_t slot_pc =
457 static_cast<uint64_t
>(table_pc) +
static_cast<uint64_t
>(room_id) * 2u;
459 return absl::FailedPreconditionError(absl::StrFormat(
460 "Dungeon room 0x%03X header pointer slot is outside the ROM", room_id));
463 uint16_t header_offset = 0;
464 uint8_t header_bank = 0;
467 const uint32_t header_snes =
468 (
static_cast<uint32_t
>(header_bank) << 16) | header_offset;
470 return absl::FailedPreconditionError(absl::StrFormat(
471 "Dungeon room 0x%03X header is not a valid LoROM pointer: 0x%06X",
472 room_id, header_snes));
475 const uint32_t header_pc =
SnesToPc(header_snes);
477 return absl::FailedPreconditionError(absl::StrFormat(
478 "Dungeon room 0x%03X header is outside the ROM", room_id));
484 const Rom& rom,
int room_id) {
492 return absl::FailedPreconditionError(absl::StrFormat(
493 "Dungeon room 0x%03X has out-of-range 8-bit palette-set ID 0x%02X "
494 "(expected 0x00-0x%02X)",
501 return absl::FailedPreconditionError(absl::StrFormat(
502 "Palette-set entry 0x%02X is outside the ROM", mapping.
palette_set_id));
508 return absl::FailedPreconditionError(absl::StrFormat(
509 "Palette-set 0x%02X has malformed dungeon palette pointer offset "
510 "0x%02X (expected an even byte offset 0x00-0x%02X)",
518 return absl::FailedPreconditionError(
519 "Resolved dungeon palette pointer is outside the ROM");
524 return absl::FailedPreconditionError(absl::StrFormat(
525 "Palette-set 0x%02X resolves to malformed palette word 0x%04X "
526 "(not divisible by %d)",
535 return absl::FailedPreconditionError(absl::StrFormat(
536 "Palette-set 0x%02X resolves to out-of-range dungeon palette %d",
545 return absl::FailedPreconditionError(absl::StrFormat(
546 "Palette-set 0x%02X resolves outside the fixed dungeon palette "
547 "region [0x%06X,0x%06X)",
560 const Rom& rom,
int room_id) {
568 if (!mapping_or.ok()) {
570 mapping_or.status().code(),
572 "Cannot prove complete shared palette fanout because room "
573 "0x%03X mapping is invalid: %s",
574 candidate, mapping_or.status().message()));
577 selection.
affected_rooms.push_back(
static_cast<uint32_t
>(candidate));
585 return absl::InvalidArgumentError(absl::StrFormat(
588 return absl::OkStatus();
592 if (color < 0 || color > 0x7FFF) {
593 return absl::InvalidArgumentError(absl::StrFormat(
594 "--%s must be a 15-bit SNES color in range 0x0000-0x7FFF",
597 return absl::OkStatus();
602 return mapping.
palette_pc +
static_cast<uint32_t
>(color_index) * 2u;
612 return absl::FailedPreconditionError(
613 "Resolved dungeon palette color is outside the fixed palette region");
617 if (color > 0x7FFF) {
618 return absl::FailedPreconditionError(absl::StrFormat(
619 "Resolved dungeon palette color at PC 0x%06X is not a 15-bit SNES "
628 const auto& mapping = selection.
selected;
629 formatter.
AddHexField(
"room_id", mapping.room_id, 3);
630 formatter.
AddHexField(
"palette_set_id", mapping.palette_set_id, 2);
631 formatter.
AddField(
"resolved_palette_index", mapping.palette_index);
632 formatter.
AddHexField(
"room_header_pc", mapping.room_header_pc, 6);
633 formatter.
AddHexField(
"palette_set_pc", mapping.palette_set_pc, 6);
635 mapping.palette_pointer_offset, 2);
636 formatter.
AddHexField(
"palette_pointer_pc", mapping.palette_pointer_pc, 6);
637 formatter.
AddHexField(
"palette_pointer_word", mapping.palette_pointer_word,
639 formatter.
AddHexField(
"palette_pc", mapping.palette_pc, 6);
641 formatter.
AddField(
"scope",
"shared_global_dungeon_palette");
642 formatter.
AddField(
"affected_room_count",
646 formatter.
AddArrayItem(absl::StrFormat(
"0x%03X", affected_room));
653 const auto conflicts =
655 if (conflicts.empty()) {
656 return absl::OkStatus();
659 return absl::PermissionDeniedError(absl::StrFormat(
660 "Dungeon palette color range [0x%06X,0x%06X) conflicts with Hack "
661 "Manifest ownership at SNES 0x%06X (%s%s%s)",
662 color_pc, color_pc + 2u, conflict.
address,
664 conflict.module.empty() ?
"" :
", module=",
665 conflict.module.empty() ?
"" : conflict.module));
669 const std::vector<uint8_t>& before,
const Rom& after, uint32_t color_pc) {
670 if (after.
size() != before.size()) {
671 return absl::DataLossError(
672 "Dungeon palette write unexpectedly resized the ROM");
675 std::vector<uint32_t> changed;
676 for (uint32_t pc = 0; pc < before.size(); ++pc) {
677 if (before[pc] == after.
data()[pc]) {
680 if (pc < color_pc || pc >= color_pc + 2u) {
681 return absl::DataLossError(absl::StrFormat(
682 "Dungeon palette write changed unexpected ROM byte 0x%06X", pc));
684 changed.push_back(pc);
686 if (changed.empty() || changed.size() > 2u) {
687 return absl::DataLossError(absl::StrFormat(
688 "Dungeon palette write changed %zu bytes; expected one or two bytes "
689 "inside the fenced color word",
697 return absl::FailedPreconditionError(
698 "Dungeon palette write requires a clean file-backed ROM; save or "
699 "discard unrelated in-memory edits first");
702 return absl::FailedPreconditionError(
703 "Dungeon palette write requires a ROM filename");
712 return absl::FailedPreconditionError(
713 "Dungeon palette write requires in-memory ROM bytes to exactly match "
714 "the current file; refusing to persist unrelated or stale bytes");
716 return absl::OkStatus();
720 const Rom& saved_rom,
int room_id,
int expected_palette_set,
721 int expected_palette_index,
int color_index, uint16_t expected_color) {
723 return absl::FailedPreconditionError(
724 "Write mode requires a ROM filename for external readback");
733 return absl::DataLossError(
734 "External reopen bytes do not match the saved in-memory ROM");
743 return absl::DataLossError(
744 "External reopen changed the selected dungeon palette mapping");
746 uint16_t reopened_color = 0;
750 if (reopened_color != expected_color) {
751 return absl::DataLossError(absl::StrFormat(
752 "External reopen color mismatch: expected 0x%04X, got 0x%04X",
753 expected_color, reopened_color));
755 return absl::OkStatus();
761 uint32_t object_stream_pc = 0;
762 uint32_t object_stream_end_pc = 0;
763 uint32_t door_pointer_pc = 0;
764 uint32_t door_entry_pc = 0;
765 uint32_t type_pc = 0;
769 Rom* rom,
int room_id,
int x,
int y,
773 return absl::FailedPreconditionError(
774 "Dungeon object manifest pointer encoding must be long24");
778 return absl::FailedPreconditionError(absl::StrFormat(
779 "Dungeon object manifest pointer_count must equal %d rooms",
783 uint32_t runtime_pointer_table_snes = 0;
787 return absl::FailedPreconditionError(
788 "Runtime object pointer table is not a valid LoROM data pointer");
790 const uint32_t runtime_pointer_table_pc =
791 SnesToPc(runtime_pointer_table_snes);
792 if (runtime_pointer_table_pc !=
794 return absl::FailedPreconditionError(absl::StrFormat(
795 "Manifest object pointer table PC 0x%06X does not match runtime "
798 runtime_pointer_table_pc));
804 if (
static_cast<size_t>(room_id) >= inventory.
streams.size()) {
805 return absl::FailedPreconditionError(
806 "Selected room is absent from the object stream inventory");
808 for (
const auto& issue : inventory.
issues) {
809 if (issue.room_id ==
static_cast<uint32_t
>(room_id)) {
810 return absl::FailedPreconditionError(absl::StrFormat(
811 "Selected object stream is invalid: %s", issue.message));
815 const auto& stream = inventory.
streams[room_id];
817 return absl::FailedPreconditionError(
"Selected object stream is invalid");
819 for (
const auto& alias : inventory.
aliases) {
821 return absl::FailedPreconditionError(absl::StrFormat(
822 "Selected object stream at PC 0x%06X is shared by %d rooms",
823 alias.data_pc,
static_cast<int>(alias.room_ids.size())));
826 for (
const auto& overlap : inventory.
overlaps) {
829 return absl::FailedPreconditionError(absl::StrFormat(
830 "Selected object stream overlaps PC range [0x%06X, 0x%06X)",
831 overlap.intersection.begin, overlap.intersection.end));
834 for (
const auto& other_stream : inventory.
streams) {
835 if (other_stream.room_id ==
static_cast<uint32_t
>(room_id)) {
838 if (other_stream.data_pc >= stream.data_pc &&
839 other_stream.data_pc < stream.logical_end_pc) {
840 return absl::FailedPreconditionError(absl::StrFormat(
841 "Room 0x%03X object pointer PC 0x%06X starts inside selected "
842 "object stream [0x%06X, 0x%06X)",
843 other_stream.room_id, other_stream.data_pc, stream.data_pc,
844 stream.logical_end_pc));
849 if (other_room_id == room_id) {
852 const uint32_t other_door_pointer_slot =
854 uint32_t other_door_pointer_snes = 0;
856 rom->
ReadLong(
static_cast<int>(other_door_pointer_slot)));
860 const uint32_t other_door_pointer_pc =
SnesToPc(other_door_pointer_snes);
861 if (other_door_pointer_pc >= stream.data_pc &&
862 other_door_pointer_pc < stream.logical_end_pc) {
863 return absl::FailedPreconditionError(absl::StrFormat(
864 "Room 0x%03X door pointer PC 0x%06X points inside selected "
865 "object stream [0x%06X, 0x%06X)",
866 other_room_id, other_door_pointer_pc, stream.data_pc,
867 stream.logical_end_pc));
871 const uint64_t runtime_pointer_slot =
872 static_cast<uint64_t
>(runtime_pointer_table_pc) +
873 static_cast<uint64_t
>(room_id) * 3u;
874 if (runtime_pointer_slot + 3u > rom->
size() ||
875 runtime_pointer_slot != stream.pointer_slot_pc) {
876 return absl::FailedPreconditionError(
877 "Selected runtime object pointer slot does not match the manifest");
879 uint32_t runtime_stream_snes = 0;
881 rom->
ReadLong(
static_cast<int>(runtime_pointer_slot)));
883 return absl::FailedPreconditionError(
884 "Selected runtime object stream is not a valid LoROM data pointer");
886 const uint32_t runtime_stream_pc =
SnesToPc(runtime_stream_snes);
887 if (runtime_stream_pc != stream.data_pc) {
888 return absl::FailedPreconditionError(absl::StrFormat(
889 "Selected runtime object stream PC 0x%06X does not match inventory "
891 runtime_stream_pc, stream.data_pc));
895 const auto& doors = room.
GetDoors();
896 int selected_index = -1;
897 for (
int index = 0; index < static_cast<int>(doors.size()); ++index) {
898 const auto [door_x, door_y] = doors[index].GetTileCoords();
899 if (door_x != x || door_y != y) {
902 if (selected_index >= 0) {
903 return absl::FailedPreconditionError(absl::StrFormat(
904 "Multiple doors match tile coordinates (%d, %d)", x, y));
906 selected_index = index;
908 if (selected_index < 0) {
909 return absl::NotFoundError(absl::StrFormat(
910 "No door matches tile coordinates (%d, %d) in room 0x%03X", x, y,
914 const uint64_t door_pointer_slot =
916 static_cast<uint64_t
>(room_id) * 3u;
917 if (door_pointer_slot + 3u > rom->
size()) {
918 return absl::OutOfRangeError(
"Selected door pointer slot is outside ROM");
920 uint32_t door_pointer_snes = 0;
922 rom->
ReadLong(
static_cast<int>(door_pointer_slot)));
924 return absl::FailedPreconditionError(
925 "Selected door pointer is not a valid LoROM data pointer");
927 const uint32_t door_pointer_pc =
SnesToPc(door_pointer_snes);
928 const uint64_t door_entry_pc =
static_cast<uint64_t
>(door_pointer_pc) +
929 static_cast<uint64_t
>(selected_index) * 2u;
930 const uint64_t expected_stream_end =
931 static_cast<uint64_t
>(door_pointer_pc) +
932 static_cast<uint64_t
>(doors.size()) * 2u + 2u;
933 if (door_pointer_pc < stream.data_pc ||
934 door_entry_pc + 2u > stream.logical_end_pc ||
935 expected_stream_end != stream.logical_end_pc) {
936 return absl::FailedPreconditionError(
937 "Door pointer/count does not match the inventoried object stream end");
940 uint8_t raw_byte1 = 0;
941 uint8_t raw_byte2 = 0;
942 for (
int index = 0; index < static_cast<int>(doors.size()); ++index) {
943 const uint32_t entry_pc =
944 door_pointer_pc +
static_cast<uint32_t
>(index) * 2u;
945 uint8_t entry_byte1 = 0;
946 uint8_t entry_byte2 = 0;
949 rom->
ReadByte(
static_cast<int>(entry_pc + 1u)));
950 const auto [model_byte1, model_byte2] = doors[index].EncodeBytes();
951 if (model_byte1 != entry_byte1 || model_byte2 != entry_byte2 ||
952 doors[index].byte1 != entry_byte1 ||
953 doors[index].byte2 != entry_byte2) {
954 return absl::DataLossError(
955 "Door model does not match the raw door-pointer entries");
957 if (index == selected_index) {
958 raw_byte1 = entry_byte1;
959 raw_byte2 = entry_byte2;
962 uint16_t door_terminator = 0;
964 rom->
ReadWord(
static_cast<int>(expected_stream_end - 2u)));
965 if (door_terminator != 0xFFFFu) {
966 return absl::DataLossError(
967 "Door list does not end with the expected 0xFFFF terminator");
976 model_door.
type != raw_door.
type || raw_x != x || raw_y != y) {
977 return absl::DataLossError(
978 "Door model does not match the raw door-pointer entry");
981 const uint32_t type_pc =
static_cast<uint32_t
>(door_entry_pc + 1u);
982 const auto conflicts =
984 if (!conflicts.empty()) {
985 return absl::PermissionDeniedError(absl::StrFormat(
986 "Door type byte at PC 0x%06X conflicts with Hack Manifest (%s)",
992 target.
door = model_door;
1008 std::string manifest_path;
1016 if (manifest_layout ==
nullptr) {
1017 return absl::FailedPreconditionError(
1018 "Manifest does not define dungeon_stream_regions.pot_items");
1029 uint32_t entry_pc = 0;
1030 uint32_t item_pc = 0;
1034 uint32_t pointer_slot_pc = 0;
1035 uint32_t stream_pc = 0;
1036 uint32_t stream_end_pc = 0;
1042 if (rom ==
nullptr || !rom->
is_loaded()) {
1043 return absl::InvalidArgumentError(
"ROM not loaded");
1046 const uint64_t pointer_table_end =
1049 if (pointer_table_end > rom->
size()) {
1050 return absl::OutOfRangeError(
1051 "Complete pot-item pointer table is outside ROM");
1053 const uint32_t pointer_slot_pc =
1055 uint16_t pointer_word = 0;
1057 rom->
ReadWord(
static_cast<int>(pointer_slot_pc)));
1058 const uint32_t pointer_snes = 0x010000u | pointer_word;
1060 return absl::FailedPreconditionError(
1061 "Selected pot-item pointer is not a valid bank-01 LoROM pointer");
1063 const uint32_t stream_pc =
SnesToPc(pointer_snes);
1064 const uint64_t bank_end =
1065 (
static_cast<uint64_t
>(stream_pc) / 0x8000u + 1u) * 0x8000u;
1066 const uint32_t parse_end =
1067 static_cast<uint32_t
>(std::min<uint64_t>(rom->
size(), bank_end));
1068 if (stream_pc >= parse_end) {
1069 return absl::OutOfRangeError(
"Selected pot-item stream is outside ROM");
1076 uint32_t cursor = stream_pc;
1078 if (
static_cast<uint64_t
>(cursor) + 2u > parse_end) {
1079 return absl::DataLossError(
"Pot-item stream has no 0xFFFF terminator");
1081 uint16_t position = 0;
1083 if (position == 0xFFFFu) {
1087 if (
static_cast<uint64_t
>(cursor) + 3u > parse_end) {
1088 return absl::DataLossError(
"Pot-item stream has a truncated record");
1092 view.
entries.push_back({.index =
static_cast<int>(view.
entries.size()),
1093 .item = {.position = position, .item = item},
1095 .item_pc = cursor + 2u});
1103 return absl::DataLossError(
1104 "Pot-item room model count does not match the raw stream");
1106 for (
size_t index = 0; index < view.
entries.size(); ++index) {
1110 return absl::DataLossError(
1111 "Pot-item room model does not match the raw stream");
1119 uint32_t pointer_slot_pc = 0;
1120 uint32_t stream_pc = 0;
1121 uint32_t stream_end_pc = 0;
1125 Rom* rom,
int room_id,
int index,
1128 return absl::FailedPreconditionError(absl::StrFormat(
1129 "Dungeon pot-item manifest pointer_count must equal %d rooms",
1135 *rom, manifest_context.
layout));
1136 if (
static_cast<size_t>(room_id) >= inventory.
streams.size()) {
1137 return absl::FailedPreconditionError(
1138 "Selected room is absent from the pot-item stream inventory");
1140 if (!inventory.
issues.empty()) {
1141 const auto& issue = inventory.
issues.front();
1142 return absl::FailedPreconditionError(absl::StrFormat(
1143 "Pot-item stream inventory is invalid at room 0x%03X: %s",
1144 issue.room_id, issue.message));
1147 const auto& stream = inventory.
streams[room_id];
1148 if (!stream.valid) {
1149 return absl::FailedPreconditionError(
"Selected pot-item stream is invalid");
1151 for (
const auto& alias : inventory.
aliases) {
1153 return absl::FailedPreconditionError(absl::StrFormat(
1154 "Selected pot-item stream at PC 0x%06X is shared by %d rooms",
1155 alias.data_pc,
static_cast<int>(alias.room_ids.size())));
1158 for (
const auto& overlap : inventory.
overlaps) {
1161 return absl::FailedPreconditionError(absl::StrFormat(
1162 "Selected pot-item stream overlaps PC range [0x%06X, 0x%06X)",
1163 overlap.intersection.begin, overlap.intersection.end));
1166 for (
const auto& other_stream : inventory.
streams) {
1167 if (other_stream.room_id ==
static_cast<uint32_t
>(room_id)) {
1170 if (other_stream.data_pc >= stream.data_pc &&
1171 other_stream.data_pc < stream.logical_end_pc) {
1172 return absl::FailedPreconditionError(absl::StrFormat(
1173 "Room 0x%03X pot-item pointer PC 0x%06X starts inside selected "
1174 "pot-item stream [0x%06X, 0x%06X)",
1175 other_stream.room_id, other_stream.data_pc, stream.data_pc,
1176 stream.logical_end_pc));
1185 runtime.
entries.size() * 3u + 2u != stream.encoded_stream.size()) {
1186 return absl::DataLossError(
1187 "Runtime pot-item stream does not match the manifest inventory");
1189 if (index < 0 || index >=
static_cast<int>(runtime.
entries.size())) {
1190 return absl::OutOfRangeError(
1191 absl::StrFormat(
"Pot-item index %d out of range (room has %d entries)",
1192 index,
static_cast<int>(runtime.
entries.size())));
1198 if (!conflicts.empty()) {
1199 return absl::PermissionDeniedError(absl::StrFormat(
1200 "Pot-item byte at PC 0x%06X conflicts with Hack Manifest (%s)",
1222 auto room_id_str_or = GetRequiredString(parser,
"room");
1223 if (!room_id_str_or.ok()) {
1224 return room_id_str_or.status();
1226 auto sprite_id_str_or = GetRequiredString(parser,
"id");
1227 if (!sprite_id_str_or.ok()) {
1228 return sprite_id_str_or.status();
1230 const std::string room_id_str = room_id_str_or.value();
1231 const std::string sprite_id_str = sprite_id_str_or.value();
1233 int room_id, sprite_id;
1234 if (!ParseHexString(room_id_str, &room_id)) {
1235 return absl::InvalidArgumentError(
"Invalid room ID. Must be hex.");
1237 if (!ParseHexString(sprite_id_str, &sprite_id)) {
1238 return absl::InvalidArgumentError(
"Invalid sprite ID. Must be hex.");
1240 auto room_status = ValidateRoomId(room_id);
1241 if (!room_status.ok()) {
1245 auto x_or = GetRequiredInt(parser,
"x");
1247 return x_or.status();
1249 auto y_or = GetRequiredInt(parser,
"y");
1251 return y_or.status();
1253 auto subtype_or = GetOptionalInt(parser,
"subtype", 0);
1254 if (!subtype_or.ok()) {
1255 return subtype_or.status();
1257 auto layer_or = GetOptionalInt(parser,
"layer", 0);
1258 if (!layer_or.ok()) {
1259 return layer_or.status();
1262 int x = x_or.value();
1263 int y = y_or.value();
1264 int subtype = subtype_or.value();
1265 int layer = layer_or.value();
1266 bool do_write = parser.
HasFlag(
"write");
1271 if (sprite_id < 0 || sprite_id > 0xFF) {
1272 return absl::InvalidArgumentError(
"Sprite ID must be 0x00-0xFF");
1274 if (subtype < 0 || subtype > 0x1F) {
1275 return absl::InvalidArgumentError(
"Subtype must be 0-31 (5-bit flags)");
1277 if (layer < 0 || layer > 1) {
1278 return absl::InvalidArgumentError(
"Layer must be 0 or 1");
1281 RETURN_IF_ERROR(ValidateSpriteEncoding(sprite_id, x, y, subtype, layer));
1282 std::optional<StreamSaveManifestContext> manifest_context;
1286 const auto* layout = manifest_context.has_value()
1287 ? &manifest_context->allocator_layout
1294 int count_before =
static_cast<int>(room.
GetSprites().size());
1298 static_cast<uint8_t
>(sprite_id),
static_cast<uint8_t
>(x),
1299 static_cast<uint8_t
>(y),
static_cast<uint8_t
>(subtype),
1300 static_cast<uint8_t
>(layer));
1309 formatter.
AddField(
"subtype", subtype);
1310 formatter.
AddField(
"layer", layer);
1311 formatter.
AddField(
"sprites_before", count_before);
1312 formatter.
AddField(
"sprites_after",
1314 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
1316 if (
const auto manifest_path = parser.
GetString(
"manifest");
1317 manifest_path.has_value()) {
1318 formatter.
AddField(
"manifest", *manifest_path);
1319 formatter.
AddField(
"manifest_write_policy",
"block");
1321 formatter.
AddField(
"allocator_capability",
1322 layout !=
nullptr ?
"copy_on_write" :
"none");
1323 const auto preview = PreflightSpriteSave(
1324 *rom, room_id, room,
1325 manifest_context.has_value() ? &*manifest_context :
nullptr);
1326 if (!preview.ok()) {
1327 formatter.
AddField(
"preflight_status",
"failed");
1328 formatter.
AddField(
"preflight_error",
1329 std::string(preview.status().message()));
1331 return preview.status();
1333 formatter.
AddField(
"preflight_status",
"passed");
1334 formatter.
AddField(
"storage_strategy",
1335 preview->source_data_pc == preview->planned_data_pc
1338 formatter.
AddHexField(
"source_data_pc", preview->source_data_pc, 6);
1339 formatter.
AddHexField(
"planned_data_pc", preview->planned_data_pc, 6);
1341 for (
const auto& [begin, end] : preview->write_ranges) {
1350 auto save_status = MutateAndSaveRomWithBackup(
1351 rom, formatter, [&room, layout]() {
return room.
SaveSprites(layout); });
1352 if (!save_status.ok()) {
1359 return absl::OkStatus();
1369 auto room_id_str_or = GetRequiredString(parser,
"room");
1370 if (!room_id_str_or.ok()) {
1371 return room_id_str_or.status();
1373 const std::string room_id_str = room_id_str_or.value();
1376 if (!ParseHexString(room_id_str, &room_id)) {
1377 return absl::InvalidArgumentError(
"Invalid room ID. Must be hex.");
1379 auto room_status = ValidateRoomId(room_id);
1380 if (!room_status.ok()) {
1384 bool do_write = parser.
HasFlag(
"write");
1391 int count_before =
static_cast<int>(sprites.size());
1394 const bool has_index = parser.
GetString(
"index").has_value();
1395 const bool has_x = parser.
GetString(
"x").has_value();
1396 const bool has_y = parser.
GetString(
"y").has_value();
1397 if (has_index && (has_x || has_y)) {
1398 return absl::InvalidArgumentError(
1399 "Use either --index or --x/--y, not both");
1401 if (!has_index && has_x != has_y) {
1402 return absl::InvalidArgumentError(
1403 "Both --x and --y are required when removing by position");
1405 if (!has_index && !has_x) {
1406 return absl::InvalidArgumentError(
1407 "Either --index or both --x and --y are required");
1410 int remove_index = -1;
1412 auto index_or = GetRequiredInt(parser,
"index");
1413 if (!index_or.ok()) {
1414 return index_or.status();
1416 remove_index = index_or.value();
1418 auto x_or = GetRequiredInt(parser,
"x");
1420 return x_or.status();
1422 auto y_or = GetRequiredInt(parser,
"y");
1424 return y_or.status();
1427 const int x = x_or.value();
1428 const int y = y_or.value();
1432 for (
int i = 0; i < static_cast<int>(sprites.size()); ++i) {
1433 if (sprites[i].x() == x && sprites[i].y() == y) {
1438 if (remove_index < 0) {
1439 return absl::NotFoundError(absl::StrFormat(
1440 "No sprite at (%d, %d) in room 0x%02X", x, y, room_id));
1444 if (remove_index < 0 || remove_index >=
static_cast<int>(sprites.size())) {
1445 return absl::OutOfRangeError(
1446 absl::StrFormat(
"Sprite index %d out of range (room has %d sprites)",
1447 remove_index, count_before));
1451 const auto& target = sprites[remove_index];
1454 formatter.
AddField(
"removed_index", remove_index);
1455 formatter.
AddHexField(
"sprite_id", target.id(), 2);
1457 formatter.
AddField(
"x", target.x());
1458 formatter.
AddField(
"y", target.y());
1459 formatter.
AddField(
"sprites_before", count_before);
1462 sprites.erase(sprites.begin() + remove_index);
1464 formatter.
AddField(
"sprites_after",
static_cast<int>(sprites.size()));
1465 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
1468 auto save_status = MutateAndSaveRomWithBackup(
1469 rom, formatter, [&room]() {
return room.
SaveSprites(); });
1470 if (!save_status.ok()) {
1477 return absl::OkStatus();
1487 auto room_id_str_or = GetRequiredString(parser,
"room");
1488 if (!room_id_str_or.ok()) {
1489 return room_id_str_or.status();
1491 auto object_id_str_or = GetRequiredString(parser,
"id");
1492 if (!object_id_str_or.ok()) {
1493 return object_id_str_or.status();
1495 const std::string room_id_str = room_id_str_or.value();
1496 const std::string object_id_str = object_id_str_or.value();
1498 int room_id, object_id;
1499 if (!ParseHexString(room_id_str, &room_id)) {
1500 return absl::InvalidArgumentError(
"Invalid room ID. Must be hex.");
1502 if (!ParseHexString(object_id_str, &object_id)) {
1503 return absl::InvalidArgumentError(
"Invalid object ID. Must be hex.");
1505 auto room_status = ValidateRoomId(room_id);
1506 if (!room_status.ok()) {
1509 if (object_id < 0 || object_id > 0xFFFF) {
1510 return absl::InvalidArgumentError(
"Object ID must be 0x0000-0xFFFF");
1513 auto x_or = GetRequiredInt(parser,
"x");
1515 return x_or.status();
1517 auto y_or = GetRequiredInt(parser,
"y");
1519 return y_or.status();
1521 auto layer_or = GetOptionalInt(parser,
"layer", 0);
1522 if (!layer_or.ok()) {
1523 return layer_or.status();
1526 int x = x_or.value();
1527 int y = y_or.value();
1531 if (parser.
GetString(
"size").has_value()) {
1532 auto size_or = parser.
GetInt(
"size");
1533 if (!size_or.ok()) {
1534 return size_or.status();
1536 size = size_or.value();
1538 int layer = layer_or.value();
1539 bool do_write = parser.
HasFlag(
"write");
1542 if (x < 0 || x > 63) {
1543 return absl::InvalidArgumentError(
"X must be 0-63");
1545 if (y < 0 || y > 63) {
1546 return absl::InvalidArgumentError(
"Y must be 0-63");
1548 if (layer < 0 || layer > 2) {
1549 return absl::InvalidArgumentError(
"Layer must be 0, 1, or 2");
1551 if (size < 0 || size > 0xFF) {
1552 return absl::InvalidArgumentError(
"Size must be 0-255");
1554 const uint8_t canonical_size =
1556 if (size != canonical_size) {
1557 return absl::InvalidArgumentError(absl::StrFormat(
1558 "Size %d is not encodable for object 0x%03X; expected %d", size,
1559 object_id, canonical_size));
1562 std::optional<StreamSaveManifestContext> manifest_context;
1567 manifest_context.has_value() ? &manifest_context->allocator_layout
1573 int count_before =
static_cast<int>(room.
GetTileObjects().size());
1577 static_cast<uint8_t
>(x),
static_cast<uint8_t
>(y),
1578 static_cast<uint8_t
>(size),
1579 static_cast<uint8_t
>(layer));
1589 formatter.
AddField(
"object_type", type);
1593 formatter.
AddField(
"layer", layer);
1594 formatter.
AddField(
"objects_before", count_before);
1595 if (
const auto manifest_path = parser.
GetString(
"manifest");
1596 manifest_path.has_value()) {
1597 formatter.
AddField(
"manifest", *manifest_path);
1598 formatter.
AddField(
"manifest_write_policy",
"block");
1600 formatter.
AddField(
"allocator_capability",
1601 layout !=
nullptr ?
"copy_on_write" :
"none");
1605 if (!add_status.ok()) {
1606 formatter.
AddField(
"error", std::string(add_status.message()));
1611 formatter.
AddField(
"objects_after",
1613 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
1615 const auto preflight_status = PreflightObjectSave(
1616 *rom, room_id, obj, room,
1617 manifest_context.has_value() ? &*manifest_context :
nullptr);
1618 if (!preflight_status.ok()) {
1619 formatter.
AddField(
"preflight_status",
"failed");
1620 formatter.
AddField(
"preflight_error",
1621 std::string(preflight_status.message()));
1623 return preflight_status;
1625 formatter.
AddField(
"preflight_status",
"success");
1628 auto save_status = MutateAndSaveRomWithBackup(
1629 rom, formatter, [&room, layout]() {
return room.
SaveObjects(layout); });
1630 if (!save_status.ok()) {
1637 return absl::OkStatus();
1651 int remove_index = 0;
1653 if (remove_index < 0) {
1654 return absl::InvalidArgumentError(
"Object index must be non-negative");
1657 int expected_id = 0;
1663 int expected_size = 0;
1665 int expected_layer = 0;
1668 if (expected_id < 0 || expected_id > 0xFFF) {
1669 return absl::InvalidArgumentError(
"Expected object ID must be 0x000-0xFFF");
1671 if (expected_x < 0 || expected_x > 63) {
1672 return absl::InvalidArgumentError(
"Expected X must be 0-63");
1674 if (expected_y < 0 || expected_y > 63) {
1675 return absl::InvalidArgumentError(
"Expected Y must be 0-63");
1677 if (expected_size < 0 || expected_size > 0xFF) {
1678 return absl::InvalidArgumentError(
"Expected size must be 0-255");
1680 if (expected_layer < 0 || expected_layer > 2) {
1681 return absl::InvalidArgumentError(
"Expected layer must be 0, 1, or 2");
1684 std::optional<StreamSaveManifestContext> manifest_context;
1689 manifest_context.has_value() ? &manifest_context->allocator_layout
1694 int object_vector_index = -1;
1695 int stream_object_count = 0;
1696 for (
int i = 0; i < static_cast<int>(objects.size()); ++i) {
1700 if (stream_object_count == remove_index) {
1701 object_vector_index = i;
1703 ++stream_object_count;
1705 if (object_vector_index < 0) {
1706 return absl::OutOfRangeError(
1707 absl::StrFormat(
"Object index %d out of range (room has %d objects)",
1708 remove_index, stream_object_count));
1711 const auto& target = objects[object_vector_index];
1714 return absl::FailedPreconditionError(
1715 "Removing chest objects requires coordinated chest-table editing");
1718 if (target.id_ != expected_id || target.x() != expected_x ||
1719 target.y() != expected_y || target.size_ != expected_size ||
1720 target.GetLayerValue() != expected_layer) {
1721 return absl::FailedPreconditionError(
1722 absl::StrFormat(
"Object guard mismatch at index %d: expected 0x%03X at "
1723 "(%d,%d) size=%d layer=%d; found 0x%03X at (%d,%d) "
1725 remove_index, expected_id, expected_x, expected_y,
1726 expected_size, expected_layer, target.id_, target.x(),
1727 target.y(), target.size_, target.GetLayerValue()));
1730 const int object_id = target.id_;
1731 const int object_x = target.x();
1732 const int object_y = target.y();
1733 const int object_size = target.size_;
1734 const int object_layer = target.GetLayerValue();
1735 const auto encoded = target.EncodeObjectToBytes();
1736 const int object_type =
1741 formatter.
AddField(
"removed_index", remove_index);
1744 formatter.
AddField(
"object_type", object_type);
1747 formatter.
AddField(
"size", object_size);
1748 formatter.
AddField(
"layer", object_layer);
1749 formatter.
AddField(
"objects_before", stream_object_count);
1750 if (
const auto manifest_path = parser.
GetString(
"manifest");
1751 manifest_path.has_value()) {
1752 formatter.
AddField(
"manifest", *manifest_path);
1753 formatter.
AddField(
"manifest_write_policy",
"block");
1755 formatter.
AddField(
"allocator_capability",
1756 layout !=
nullptr ?
"copy_on_write" :
"none");
1758 const absl::Status remove_status = room.
RemoveObject(object_vector_index);
1759 if (!remove_status.ok()) {
1760 formatter.
AddField(
"remove_error", std::string(remove_status.message()));
1762 return remove_status;
1764 formatter.
AddField(
"objects_after", stream_object_count - 1);
1765 const bool do_write = parser.
HasFlag(
"write");
1766 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
1768 const absl::Status preflight_status = PreflightObjectRemoval(
1769 *rom, room_id, object_vector_index, room,
1770 manifest_context.has_value() ? &*manifest_context :
nullptr);
1771 if (!preflight_status.ok()) {
1772 formatter.
AddField(
"preflight_status",
"failed");
1773 formatter.
AddField(
"preflight_error",
1774 std::string(preflight_status.message()));
1776 return preflight_status;
1778 formatter.
AddField(
"preflight_status",
"success");
1781 const absl::Status save_status = MutateAndSaveRomWithBackup(
1782 rom, formatter, [&room, layout]() {
return room.
SaveObjects(layout); });
1783 if (!save_status.ok()) {
1790 return absl::OkStatus();
1804 std::optional<int> requested_index;
1805 if (parser.
GetString(
"index").has_value()) {
1806 int color_index = 0;
1809 requested_index = color_index;
1812 DungeonPaletteSelection selection;
1813 ASSIGN_OR_RETURN(selection, ResolveDungeonPaletteSelection(*rom, room_id));
1816 AddDungeonPaletteScope(formatter, selection);
1817 formatter.
AddField(
"color_count", kDungeonPaletteColorCount);
1819 if (requested_index.has_value()) {
1820 const uint32_t color_pc =
1821 DungeonPaletteColorPc(selection.selected, *requested_index);
1825 formatter.
AddField(
"color_index", *requested_index);
1831 for (
int color_index = 0; color_index < kDungeonPaletteColorCount;
1833 const uint32_t color_pc =
1834 DungeonPaletteColorPc(selection.selected, color_index);
1839 formatter.
AddField(
"index", color_index);
1848 formatter.
AddField(
"status",
"success");
1850 return absl::OkStatus();
1860 const bool do_write = parser.
HasFlag(
"write");
1861#if defined(__EMSCRIPTEN__)
1863 return absl::FailedPreconditionError(
1864 "dungeon-set-palette-color --write is unavailable in the browser "
1865 "terminal because durable filesystem sync cannot be guaranteed; use "
1866 "native z3ed on a disposable ROM copy");
1869 if (do_write && rom->
filename().empty()) {
1870 return absl::FailedPreconditionError(
1871 "dungeon-set-palette-color --write requires a ROM loaded from a "
1876 int color_index = 0;
1877 int expected_palette_set = 0;
1878 int expected_palette_index = 0;
1879 int expected_color = 0;
1880 int replacement_color = 0;
1884 GetRequiredHex(parser,
"expect-palette-set"));
1886 GetRequiredInt(parser,
"expect-palette-index"));
1892 if (expected_palette_set < 0 || expected_palette_set > 0xFF) {
1893 return absl::InvalidArgumentError(
1894 "--expect-palette-set must be an 8-bit value in range 0x00-0xFF");
1896 if (expected_palette_index < 0 ||
1897 expected_palette_index >= kDungeonPaletteCount) {
1898 return absl::InvalidArgumentError(
1899 absl::StrFormat(
"--expect-palette-index must be in range 0-%d",
1900 kDungeonPaletteCount - 1));
1902 RETURN_IF_ERROR(ValidateSnes15BitColor(expected_color,
"expect-color"));
1905 const std::string manifest_path = parser.
GetString(
"manifest").value();
1909 DungeonPaletteSelection selection;
1910 ASSIGN_OR_RETURN(selection, ResolveDungeonPaletteSelection(*rom, room_id));
1911 if (selection.selected.palette_set_id != expected_palette_set) {
1912 return absl::FailedPreconditionError(absl::StrFormat(
1913 "Palette-set CAS failed for room 0x%03X: expected full 8-bit ID "
1914 "0x%02X, got 0x%02X",
1915 room_id, expected_palette_set, selection.selected.palette_set_id));
1917 if (selection.selected.palette_index != expected_palette_index) {
1918 return absl::FailedPreconditionError(absl::StrFormat(
1919 "Resolved palette CAS failed for room 0x%03X: expected %d, got %d",
1920 room_id, expected_palette_index, selection.selected.palette_index));
1923 uint16_t actual_color = 0;
1925 *rom, selection.selected, color_index));
1926 if (actual_color != expected_color) {
1927 return absl::FailedPreconditionError(absl::StrFormat(
1928 "Color CAS failed at palette %d index %d: expected 0x%04X, got "
1930 selection.selected.palette_index, color_index, expected_color,
1933 if (replacement_color == actual_color) {
1934 return absl::InvalidArgumentError(absl::StrFormat(
1935 "Refusing no-op dungeon palette write: color is already 0x%04X",
1939 const uint32_t color_pc =
1940 DungeonPaletteColorPc(selection.selected, color_index);
1941 RETURN_IF_ERROR(ValidateDungeonPaletteManifestRange(manifest, color_pc));
1946 formatter.
BeginObject(
"Set Dungeon Palette Color");
1947 AddDungeonPaletteScope(formatter, selection);
1948 formatter.
AddField(
"manifest", manifest_path);
1949 formatter.
AddField(
"manifest_write_policy",
"block");
1950 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
1951 formatter.
AddField(
"color_index", color_index);
1954 formatter.
AddHexField(
"old_color", actual_color, 4);
1955 formatter.
AddHexField(
"new_color", replacement_color, 4);
1956 formatter.
AddField(
"preflight_status",
"success");
1959 formatter.
AddField(
"status",
"dry-run");
1961 return absl::OkStatus();
1966 write_fence.
Allow(color_pc, color_pc + 2u,
"DungeonPaletteColor"));
1967 const std::vector<uint8_t> before = rom->
vector();
1972 rom->
WriteShort(color_pc,
static_cast<uint16_t
>(replacement_color)));
1975 std::vector<uint32_t> changed_bytes;
1977 ValidateDungeonPaletteWholeRomDiff(before, *rom, color_pc));
1978 uint16_t memory_color = 0;
1980 *rom, selection.selected, color_index));
1981 if (memory_color != replacement_color) {
1982 return absl::DataLossError(
1983 "In-memory dungeon palette readback did not match the replacement");
1985 formatter.
AddField(
"write_status",
"success");
1986 formatter.
AddField(
"whole_rom_diff_status",
"verified");
1987 formatter.
AddField(
"changed_byte_count",
1988 static_cast<int>(changed_bytes.size()));
1990 for (uint32_t changed_pc : changed_bytes) {
1991 formatter.
AddArrayItem(absl::StrFormat(
"0x%06X", changed_pc));
1994 formatter.
AddField(
"memory_readback_status",
"verified");
1996 const absl::Status save_status = SaveRomWithBackup(rom, formatter);
1997 if (!save_status.ok()) {
1998 formatter.
AddField(
"status",
"error");
2007 const absl::Status reopen_status = VerifyDungeonPaletteExternalReadback(
2008 *rom, room_id, expected_palette_set, expected_palette_index, color_index,
2009 static_cast<uint16_t
>(replacement_color));
2010 if (!reopen_status.ok()) {
2011 formatter.
AddField(
"readback_status",
"external_reopen_failed");
2012 formatter.
AddField(
"readback_error", std::string(reopen_status.message()));
2013 formatter.
AddField(
"status",
"error");
2015 return reopen_status;
2018 formatter.
AddField(
"readback_status",
"external_reopen_verified");
2019 formatter.
AddField(
"status",
"success");
2021 return absl::OkStatus();
2039 if (x < 0 || x > 63 || y < 0 || y > 63) {
2040 return absl::InvalidArgumentError(
2041 "Door tile coordinates must be in range 0-63");
2045 int expected_type = 0;
2051 std::optional<StreamSaveManifestContext> manifest_context;
2055 if (!manifest_context.has_value()) {
2056 return absl::FailedPreconditionError(
2057 "--manifest is required for dungeon-set-door-type");
2060 DoorTypeTarget target;
2062 target, ResolveDoorTypeTarget(rom, room_id, x, y, *manifest_context));
2063 const int old_type =
static_cast<int>(target.door.type);
2064 if (old_type != expected_type) {
2065 return absl::FailedPreconditionError(absl::StrFormat(
2066 "Door type compare-and-swap failed at (%d, %d): expected 0x%02X, "
2068 x, y, expected_type, old_type));
2070 if (new_type == old_type) {
2071 return absl::InvalidArgumentError(
2072 "--type must differ from the current door type");
2075 const bool do_write = parser.
HasFlag(
"write");
2076 if (do_write && rom->
filename().empty()) {
2077 return absl::FailedPreconditionError(
"Write mode requires a ROM filename");
2082 formatter.
AddField(
"door_index", target.door_index);
2085 formatter.
AddField(
"position", target.door.position);
2086 formatter.
AddField(
"direction", std::string(target.door.GetDirectionName()));
2088 formatter.
AddField(
"old_type_name", std::string(target.door.GetTypeName()));
2094 formatter.
AddHexField(
"object_stream_pc", target.object_stream_pc, 6);
2095 formatter.
AddHexField(
"object_stream_end_pc", target.object_stream_end_pc, 6);
2096 formatter.
AddHexField(
"door_pointer_pc", target.door_pointer_pc, 6);
2097 formatter.
AddHexField(
"door_entry_pc", target.door_entry_pc, 6);
2098 formatter.
AddHexField(
"type_pc", target.type_pc, 6);
2101 formatter.
AddField(
"manifest_write_policy",
"block");
2102 formatter.
AddField(
"stream_alias_status",
"unique");
2103 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
2104 formatter.
AddField(
"preflight_status",
"success");
2107 formatter.
AddField(
"write_status",
"not_requested");
2108 formatter.
AddField(
"save_status",
"not_requested");
2109 formatter.
AddField(
"readback_status",
"not_requested");
2110 formatter.
AddField(
"status",
"success");
2112 return absl::OkStatus();
2115 const auto finish_error = [&formatter](
const absl::Status& status,
2116 const char* field) -> absl::Status {
2117 formatter.
AddField(
"status",
"error");
2118 formatter.
AddField(field, std::string(status.message()));
2122 const auto verify_readback =
2123 [&target, new_type](
const DoorTypeTarget& readback) -> absl::Status {
2124 if (readback.door_index != target.door_index ||
2125 readback.type_pc != target.type_pc ||
2126 readback.door.position != target.door.position ||
2127 readback.door.direction != target.door.direction ||
2128 static_cast<int>(readback.door.type) != new_type) {
2129 return absl::DataLossError(
2130 "Door type readback does not match the requested change");
2132 return absl::OkStatus();
2136 const absl::Status allow_status = write_fence.
Allow(
2137 target.type_pc, target.type_pc + 1u,
"dungeon door type");
2138 if (!allow_status.ok()) {
2139 return finish_error(allow_status,
"write_error");
2142 const std::vector<uint8_t> before = rom->
vector();
2146 const absl::Status write_status = rom->
WriteByte(
2147 static_cast<int>(target.type_pc),
static_cast<uint8_t
>(new_type));
2148 if (!write_status.ok()) {
2149 return finish_error(write_status,
"write_error");
2153 if (rom->
size() != before.size()) {
2154 return finish_error(
2155 absl::DataLossError(
"Door type edit unexpectedly resized the ROM"),
2158 int changed_bytes = 0;
2159 uint32_t changed_pc = 0;
2160 for (uint32_t pc = 0; pc < before.size(); ++pc) {
2161 if (before[pc] != rom->
data()[pc]) {
2166 if (changed_bytes != 1 || changed_pc != target.type_pc) {
2167 return finish_error(
2168 absl::DataLossError(absl::StrFormat(
2169 "Door type edit changed %d bytes; expected only PC 0x%06X",
2170 changed_bytes, target.type_pc)),
2174 auto memory_readback =
2175 ResolveDoorTypeTarget(rom, room_id, x, y, *manifest_context);
2176 if (!memory_readback.ok()) {
2177 return finish_error(memory_readback.status(),
"readback_error");
2179 const absl::Status memory_verify = verify_readback(*memory_readback);
2180 if (!memory_verify.ok()) {
2181 return finish_error(memory_verify,
"readback_error");
2183 formatter.
AddField(
"readback_status",
"pre_save_verified");
2184 formatter.
AddField(
"write_status",
"success");
2186 const absl::Status save_status = SaveRomWithBackup(rom, formatter);
2187 if (!save_status.ok()) {
2188 formatter.
AddField(
"status",
"error");
2194 formatter.
AddField(
"status",
"success");
2196 return absl::OkStatus();
2210 PotItemStreamView stream;
2215 formatter.
AddHexField(
"pointer_slot_pc", stream.pointer_slot_pc, 6);
2216 formatter.
AddHexField(
"stream_pc", stream.stream_pc, 6);
2217 formatter.
AddHexField(
"stream_end_pc", stream.stream_end_pc, 6);
2218 formatter.
AddField(
"item_count",
static_cast<int>(stream.entries.size()));
2220 for (
const PotItemEntryView& entry : stream.entries) {
2222 formatter.
AddField(
"index", entry.index);
2223 formatter.
AddHexField(
"position", entry.item.position, 4);
2224 formatter.
AddField(
"tile_x", entry.item.GetTileX());
2225 formatter.
AddField(
"tile_y", entry.item.GetTileY());
2226 formatter.
AddHexField(
"item_id", entry.item.item, 2);
2227 formatter.
AddHexField(
"item_pc", entry.item_pc, 6);
2232 formatter.
AddField(
"status",
"success");
2234 return absl::OkStatus();
2246 int expected_position = 0;
2247 int expected_item = 0;
2253 GetRequiredHex(parser,
"expect-position"));
2256 if (expected_position < 0 || expected_position > 0xFFFF) {
2257 return absl::InvalidArgumentError(
2258 "--expect-position must be in range 0x0000-0xFFFF");
2260 if (expected_item < 0 || expected_item > 0xFF) {
2261 return absl::InvalidArgumentError(
2262 "--expect-item must be in range 0x00-0xFF");
2264 if (new_item < 0 || new_item > 0xFF) {
2265 return absl::InvalidArgumentError(
"--item must be in range 0x00-0xFF");
2268 PotItemManifestContext manifest_context;
2270 PotItemTarget target;
2272 ResolvePotItemTarget(rom, room_id, index, manifest_context));
2273 if (target.entry.item.position != expected_position ||
2274 target.entry.item.item != expected_item) {
2275 return absl::FailedPreconditionError(absl::StrFormat(
2276 "Pot-item compare-and-swap failed at index %d: expected "
2277 "position/item 0x%04X/0x%02X, found 0x%04X/0x%02X",
2278 index, expected_position, expected_item, target.entry.item.position,
2279 target.entry.item.item));
2281 if (new_item == expected_item) {
2282 return absl::InvalidArgumentError(
2283 "--item must differ from the current pot item");
2286 const bool do_write = parser.
HasFlag(
"write");
2287 if (do_write && rom->
filename().empty()) {
2288 return absl::FailedPreconditionError(
"Write mode requires a ROM filename");
2293 formatter.
AddField(
"index", index);
2294 formatter.
AddHexField(
"position", target.entry.item.position, 4);
2295 formatter.
AddField(
"tile_x", target.entry.item.GetTileX());
2296 formatter.
AddField(
"tile_y", target.entry.item.GetTileY());
2297 formatter.
AddHexField(
"old_item", expected_item, 2);
2299 formatter.
AddHexField(
"pointer_slot_pc", target.pointer_slot_pc, 6);
2300 formatter.
AddHexField(
"stream_pc", target.stream_pc, 6);
2301 formatter.
AddHexField(
"stream_end_pc", target.stream_end_pc, 6);
2302 formatter.
AddHexField(
"item_pc", target.entry.item_pc, 6);
2305 formatter.
AddField(
"manifest_write_policy",
"block");
2306 formatter.
AddField(
"stream_alias_status",
"unique");
2307 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
2308 formatter.
AddField(
"preflight_status",
"success");
2311 formatter.
AddField(
"write_status",
"not_requested");
2312 formatter.
AddField(
"save_status",
"not_requested");
2313 formatter.
AddField(
"readback_status",
"not_requested");
2314 formatter.
AddField(
"status",
"success");
2316 return absl::OkStatus();
2319 const auto finish_error = [&formatter](
const absl::Status& status,
2320 const char* field) -> absl::Status {
2321 formatter.
AddField(
"status",
"error");
2322 formatter.
AddField(field, std::string(status.message()));
2326 const auto verify_readback =
2327 [&target, new_item](
const PotItemTarget& readback) -> absl::Status {
2328 if (readback.entry.index != target.entry.index ||
2329 readback.entry.entry_pc != target.entry.entry_pc ||
2330 readback.entry.item_pc != target.entry.item_pc ||
2331 readback.entry.item.position != target.entry.item.position ||
2332 readback.entry.item.item != new_item ||
2333 readback.stream_pc != target.stream_pc ||
2334 readback.stream_end_pc != target.stream_end_pc) {
2335 return absl::DataLossError(
2336 "Pot-item readback does not match the requested change");
2338 return absl::OkStatus();
2342 const absl::Status allow_status = write_fence.
Allow(
2343 target.entry.item_pc, target.entry.item_pc + 1u,
"dungeon pot item");
2344 if (!allow_status.ok()) {
2345 return finish_error(allow_status,
"write_error");
2348 const std::vector<uint8_t> before = rom->
vector();
2352 const absl::Status write_status = rom->
WriteByte(
2353 static_cast<int>(target.entry.item_pc),
static_cast<uint8_t
>(new_item));
2354 if (!write_status.ok()) {
2355 return finish_error(write_status,
"write_error");
2359 if (rom->
size() != before.size()) {
2360 return finish_error(
2361 absl::DataLossError(
"Pot-item edit unexpectedly resized the ROM"),
2364 int changed_bytes = 0;
2365 uint32_t changed_pc = 0;
2366 for (uint32_t pc = 0; pc < before.size(); ++pc) {
2367 if (before[pc] != rom->
data()[pc]) {
2372 if (changed_bytes != 1 || changed_pc != target.entry.item_pc) {
2373 return finish_error(
2374 absl::DataLossError(absl::StrFormat(
2375 "Pot-item edit changed %d bytes; expected only PC 0x%06X",
2376 changed_bytes, target.entry.item_pc)),
2380 auto memory_readback =
2381 ResolvePotItemTarget(rom, room_id, index, manifest_context);
2382 if (!memory_readback.ok()) {
2383 return finish_error(memory_readback.status(),
"readback_error");
2385 const absl::Status memory_verify = verify_readback(*memory_readback);
2386 if (!memory_verify.ok()) {
2387 return finish_error(memory_verify,
"readback_error");
2389 formatter.
AddField(
"readback_status",
"pre_save_verified");
2390 formatter.
AddField(
"write_status",
"success");
2392 const absl::Status save_status = SaveRomWithBackup(rom, formatter);
2393 if (!save_status.ok()) {
2394 formatter.
AddField(
"status",
"error");
2400 formatter.
AddField(
"status",
"success");
2402 return absl::OkStatus();
2412 auto room_id_str_or = GetRequiredString(parser,
"room");
2413 if (!room_id_str_or.ok()) {
2414 return room_id_str_or.status();
2416 auto tiles_str_or = GetRequiredString(parser,
"tiles");
2417 if (!tiles_str_or.ok()) {
2418 return tiles_str_or.status();
2420 const std::string room_id_str = room_id_str_or.value();
2421 const std::string tiles_str = tiles_str_or.value();
2424 if (!ParseHexString(room_id_str, &room_id)) {
2425 return absl::InvalidArgumentError(
"Invalid room ID. Must be hex.");
2427 auto room_status = ValidateRoomId(room_id);
2428 if (!room_status.ok()) {
2432 bool do_write = parser.
HasFlag(
"write");
2438 std::vector<TileSpec> specs;
2440 for (absl::string_view entry :
2441 absl::StrSplit(tiles_str,
';', absl::SkipEmpty())) {
2442 std::vector<std::string> parts =
2443 absl::StrSplit(entry,
',', absl::SkipEmpty());
2444 if (parts.size() != 3) {
2445 return absl::InvalidArgumentError(absl::StrFormat(
2446 "Invalid tile spec '%s'. Expected x,y,tile (e.g. 10,5,0xB7)", entry));
2451 if (!absl::SimpleAtoi(parts[0], &spec.x)) {
2452 return absl::InvalidArgumentError(
2453 absl::StrFormat(
"Invalid X coord '%s'", parts[0]));
2455 if (!absl::SimpleAtoi(parts[1], &spec.y)) {
2456 return absl::InvalidArgumentError(
2457 absl::StrFormat(
"Invalid Y coord '%s'", parts[1]));
2459 if (!ParseHexString(parts[2], &spec.tile)) {
2460 return absl::InvalidArgumentError(
2461 absl::StrFormat(
"Invalid tile value '%s'. Must be hex.", parts[2]));
2465 if (spec.x < 0 || spec.x > 63 || spec.y < 0 || spec.y > 63) {
2466 return absl::InvalidArgumentError(absl::StrFormat(
2467 "Tile coords (%d,%d) out of range (0-63)", spec.x, spec.y));
2469 if (spec.tile < 0 || spec.tile > 0xFF) {
2470 return absl::InvalidArgumentError(
"Tile value must be 0x00-0xFF");
2473 specs.push_back(spec);
2476 if (specs.empty()) {
2477 return absl::InvalidArgumentError(
"No tile specs provided");
2486 formatter.
AddField(
"tile_count",
static_cast<int>(specs.size()));
2487 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
2491 for (
const auto& spec : specs) {
2501 formatter.
AddField(
"old_override_present", old_override_present);
2502 formatter.
AddField(
"new_override_present",
2510 MutateAndSaveRomWithBackup(rom, formatter, [&rom, &room]()
mutable {
2512 std::array<zelda3::Room, 1> rooms_arr = {std::move(room)};
2515 if (!save_status.ok()) {
2522 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
absl::Status WriteByte(int addr, uint8_t value)
absl::StatusOr< uint8_t > ReadByte(int offset) const
const auto & vector() const
absl::Status SaveToFile(const SaveSettings &settings)
absl::StatusOr< uint16_t > ReadWord(int offset) const
absl::Status LoadFromData(const std::vector< uint8_t > &data, const LoadOptions &options=LoadOptions::Defaults())
absl::Status WriteShort(int addr, uint16_t value)
absl::StatusOr< uint32_t > ReadLong(int offset) const
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
Utility for parsing common CLI argument patterns.
std::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
bool HasFlag(const std::string &name) const
Check if a flag is present.
absl::StatusOr< int > GetInt(const std::string &name) const
Parse an integer argument (supports hex with 0x prefix)
Loads and queries the hack manifest JSON for yaze-ASM integration.
const DungeonStreamLayout * GetDungeonStreamLayout(DungeonStreamType stream) const
Get an explicitly declared dungeon stream layout.
absl::Status LoadFromFile(const std::string &filepath)
Load manifest from a JSON file path.
std::vector< WriteConflict > AnalyzePcWriteRanges(const std::vector< std::pair< uint32_t, uint32_t > > &pc_ranges) const
Analyze a set of PC-offset ranges for write conflicts.
absl::Status Allow(uint32_t start, uint32_t end, std::string_view label)
static int DetermineObjectType(uint8_t b1, uint8_t b3)
bool ArePotItemsLoaded() const
uint8_t GetCollisionTile(int x, int y) const
absl::Status RemoveObject(size_t index)
const std::vector< Door > & GetDoors() const
absl::Status SaveObjects(const DungeonStreamLayout *layout=nullptr)
bool HasCollisionOverride(int x, int y) const
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > GetSpriteSaveWriteRanges(const DungeonStreamLayout *layout=nullptr) const
std::vector< uint8_t > EncodeObjects() const
const std::vector< zelda3::Sprite > & GetSprites() const
absl::Status SaveSprites(const DungeonStreamLayout *layout=nullptr)
const std::vector< RoomObject > & GetTileObjects() const
void SetCollisionTile(int x, int y, uint8_t tile)
absl::Status AddObject(const RoomObject &object)
bool has_custom_collision() const
const std::vector< PotItem > & GetPotItems() const
#define ASSIGN_OR_RETURN(type_variable_name, expression)
absl::StatusOr< std::optional< StreamSaveManifestContext > > LoadStreamSaveManifestContext(const resources::ArgumentParser &parser, core::DungeonStreamType stream_type)
absl::StatusOr< std::vector< uint32_t > > ValidateDungeonPaletteWholeRomDiff(const std::vector< uint8_t > &before, const Rom &after, uint32_t color_pc)
absl::StatusOr< uint32_t > ResolveRoomHeaderPcStrict(const Rom &rom, int room_id)
absl::Status ValidateRoomId(int room_id)
absl::StatusOr< DungeonPaletteSelection > ResolveDungeonPaletteSelection(const Rom &rom, int room_id)
absl::Status ValidateDungeonPaletteColorIndex(int color_index)
constexpr uint32_t kDungeonPaletteDataStart
absl::StatusOr< SpriteSavePreview > PreflightSpriteSave(const Rom &source_rom, int room_id, const zelda3::Room &pending_room, const StreamSaveManifestContext *manifest_context)
absl::StatusOr< int > GetOptionalInt(const resources::ArgumentParser &parser, const char *name, int default_value)
absl::Status PreflightObjectRemoval(const Rom &source_rom, int room_id, int object_vector_index, const zelda3::Room &pending_room, const StreamSaveManifestContext *manifest_context)
absl::StatusOr< std::string > GetRequiredString(const resources::ArgumentParser &parser, const char *name)
bool IsLoRomDataPointer(uint32_t snes_address)
absl::Status ValidateSupportedDungeonPaletteRom(const Rom &rom)
constexpr int kDungeonPaletteCount
absl::StatusOr< int > GetRequiredHex(const resources::ArgumentParser &parser, const char *name)
constexpr uint32_t kDungeonPaletteDataEnd
uint32_t DungeonPaletteColorPc(const DungeonPaletteRoomMapping &mapping, int color_index)
constexpr uint32_t kSnesDestinationCodePc
absl::Status ValidateSpriteCoord(int value, char axis)
absl::Status ValidateSpriteEncoding(int id, int x, int y, int subtype, int layer)
bool ContainsRoomId(const std::vector< uint32_t > &room_ids, int room_id)
absl::Status ValidateSnes15BitColor(int color, const char *argument_name)
absl::StatusOr< DungeonPaletteRoomMapping > ResolveDungeonPaletteRoomMapping(const Rom &rom, int room_id)
absl::Status ValidateDungeonPaletteManifestRange(const core::HackManifest &manifest, uint32_t color_pc)
void AddDungeonPaletteScope(resources::OutputFormatter &formatter, const DungeonPaletteSelection &selection)
absl::StatusOr< int > GetRequiredInt(const resources::ArgumentParser &parser, const char *name)
absl::Status ValidateObjectSaveManifestConflicts(const Rom &source_rom, int room_id, const zelda3::Room &pending_room, const StreamSaveManifestContext &context)
absl::StatusOr< PotItemStreamView > ResolveRuntimePotItemStream(Rom *rom, int room_id)
absl::Status PreflightObjectSave(const Rom &source_rom, int room_id, const zelda3::RoomObject &object, const zelda3::Room &pending_room, const StreamSaveManifestContext *manifest_context)
absl::StatusOr< uint16_t > ReadDungeonPaletteColor(const Rom &rom, const DungeonPaletteRoomMapping &mapping, int color_index)
absl::Status VerifyDungeonPaletteExternalReadback(const Rom &saved_rom, int room_id, int expected_palette_set, int expected_palette_index, int color_index, uint16_t expected_color)
absl::Status ValidateDungeonPaletteWriteBaseline(const Rom &rom)
absl::StatusOr< PotItemManifestContext > LoadPotItemManifestContext(const resources::ArgumentParser &parser)
constexpr int kDungeonPaletteColorCount
absl::Status MutateAndSaveRomWithBackup(Rom *rom, resources::OutputFormatter &formatter, Mutation &&mutation)
absl::StatusOr< DoorTypeTarget > ResolveDoorTypeTarget(Rom *rom, int room_id, int x, int y, const StreamSaveManifestContext &manifest_context)
absl::Status SaveRomWithBackup(Rom *rom, resources::OutputFormatter &formatter)
absl::Status ValidateDoorType(int type, const char *argument_name)
bool IsValidRomRange(const Rom &rom, uint64_t start, uint64_t length)
absl::StatusOr< PotItemTarget > ResolvePotItemTarget(Rom *rom, int room_id, int index, const PotItemManifestContext &manifest_context)
std::string AddressOwnershipToString(AddressOwnership ownership)
DungeonStreamType
Dungeon stream kinds with allocator layouts in the hack manifest.
absl::StatusOr< zelda3::DungeonStreamLayout > ToDungeonStreamAllocatorLayout(DungeonStreamType stream_type, const DungeonStreamLayout &manifest_layout)
constexpr int DungeonsMainPalettesMax
constexpr int kDungeonMainPalettes
bool ParseHexString(absl::string_view str, uint64_t *out)
constexpr uint32_t kPalettesetIdsAddress
constexpr int kDoorPointers
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
constexpr uint32_t kNumPalettesets
constexpr int kDungeonPaletteBytes
uint8_t CanonicalRoomObjectSize(int object_id, uint8_t requested_size)
constexpr int kRoomItemsPointers
Room LoadRoomFromRom(Rom *rom, int room_id)
constexpr DoorType DoorTypeFromRaw(uint8_t raw_type)
Convert raw type byte to DoorType enum.
constexpr uint32_t kDungeonPalettePointerTable
bool UsesRoomObjectStream(const RoomObject &object)
absl::StatusOr< DungeonStreamInventory > InventoryDungeonStreams(const Rom &rom, const DungeonStreamLayout &requested_layout)
std::string GetObjectName(int object_id)
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
constexpr int kNumberOfRooms
constexpr int kRoomHeaderPointer
constexpr int kRoomHeaderPointerBank
absl::Status SaveAllCollision(Rom *rom, absl::Span< Room > rooms)
const char * ResolveSpriteName(uint16_t id)
constexpr int kRoomObjectPointer
absl::StatusOr< DungeonStreamRecord > ReadDungeonSpriteStream(const Rom &rom, int room_id)
uint32_t PcToSnes(uint32_t addr)
uint32_t SnesToPc(uint32_t addr) noexcept
#define RETURN_IF_ERROR(expr)
bool load_resource_labels
uint32_t object_stream_end_pc
uint32_t object_stream_pc
uint16_t palette_pointer_word
uint32_t palette_pointer_pc
uint8_t palette_pointer_offset
DungeonPaletteRoomMapping selected
std::vector< uint32_t > affected_rooms
core::HackManifest manifest
zelda3::DungeonStreamLayout layout
std::vector< PotItemEntryView > entries
std::vector< std::pair< uint32_t, uint32_t > > write_ranges
core::HackManifest manifest
project::RomWritePolicy write_policy
zelda3::DungeonStreamLayout allocator_layout
Explicit pointer and storage layout for one dungeon stream.
DungeonWriteStrategy strategy
A conflict detected when yaze wants to write to an ASM-owned address.
AddressOwnership ownership
std::vector< DungeonStreamAliasGroup > aliases
std::vector< DungeonStreamIssue > issues
std::vector< DungeonStreamOverlap > overlaps
std::vector< DungeonStreamRecord > streams
std::vector< DungeonStreamPcRange > allocation_ranges
DungeonPointerEncoding pointer_encoding
uint32_t pointer_table_pc
Represents a door in a dungeon room.
DoorType type
Door type (determines appearance/behavior)
static Door FromRomBytes(uint8_t b1, uint8_t b2)
DoorDirection direction
Which wall the door is on.
uint8_t position
Encoded position (5-bit, 0-31)
std::pair< int, int > GetTileCoords() const
Get tile coordinates for this door.