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<ObjectSaveManifestContext>>
133 const auto manifest_path = parser.
GetString(
"manifest");
134 if (!manifest_path.has_value()) {
143 if (manifest_layout ==
nullptr) {
144 return absl::FailedPreconditionError(
145 "Manifest does not define dungeon_stream_regions.objects");
148 return absl::FailedPreconditionError(
149 "dungeon_stream_regions.objects must use copy_on_write");
155 return std::optional<ObjectSaveManifestContext>(std::move(context));
161 std::vector<std::pair<uint32_t, uint32_t>> ranges;
166 uint32_t object_pointer_table_snes = 0;
169 const uint32_t object_pointer_table_pc =
SnesToPc(object_pointer_table_snes);
170 const uint64_t current_pointer_slot =
171 static_cast<uint64_t
>(object_pointer_table_pc) +
172 static_cast<uint64_t
>(room_id) * 3u;
173 if (current_pointer_slot + 3u > source_rom.
size()) {
174 return absl::OutOfRangeError(
175 "Selected object pointer slot is outside the ROM");
178 uint32_t current_stream_snes = 0;
180 source_rom.
ReadLong(
static_cast<int>(current_pointer_slot)));
181 const uint32_t current_stream_pc =
SnesToPc(current_stream_snes);
182 const uint64_t current_stream_end =
static_cast<uint64_t
>(current_stream_pc) +
184 if (current_stream_end > std::numeric_limits<uint32_t>::max()) {
185 return absl::OutOfRangeError(
"Selected object write range overflows");
187 ranges.emplace_back(current_stream_pc,
188 static_cast<uint32_t
>(current_stream_end));
190 const uint64_t door_pointer_slot =
192 static_cast<uint64_t
>(room_id) * 3u;
193 if (door_pointer_slot + 3u > std::numeric_limits<uint32_t>::max()) {
194 return absl::OutOfRangeError(
"Selected door pointer range overflows");
196 ranges.emplace_back(
static_cast<uint32_t
>(door_pointer_slot),
197 static_cast<uint32_t
>(door_pointer_slot + 3u));
200 ranges.emplace_back(range.begin, range.end);
206 const uint64_t cow_pointer_slot =
208 static_cast<uint64_t
>(room_id) * pointer_width;
209 if (cow_pointer_slot + pointer_width > std::numeric_limits<uint32_t>::max()) {
210 return absl::OutOfRangeError(
"Selected COW pointer range overflows");
212 ranges.emplace_back(
static_cast<uint32_t
>(cow_pointer_slot),
213 static_cast<uint32_t
>(cow_pointer_slot + pointer_width));
216 if (conflicts.empty() ||
218 return absl::OkStatus();
220 return absl::PermissionDeniedError(
"Write conflict with Hack Manifest");
227 if (manifest_context !=
nullptr) {
229 source_rom, room_id, pending_room, *manifest_context));
243 return scratch_room.
SaveObjects(manifest_context !=
nullptr
249 const Rom& source_rom,
int room_id,
int object_vector_index,
252 if (manifest_context !=
nullptr) {
254 source_rom, room_id, pending_room, *manifest_context));
265 return scratch_room.
SaveObjects(manifest_context !=
nullptr
273 if (!value_or.ok()) {
274 return value_or.status();
278 if (!ParseHexString(*value_or, &value)) {
279 return absl::InvalidArgumentError(absl::StrFormat(
280 "Invalid --%s value '%s' (expected integer/hex)",
name, *value_or));
286 if (type < 0 || type > 0x66 || (type & 1) != 0) {
287 return absl::InvalidArgumentError(absl::StrFormat(
288 "--%s must be an even door type in range 0x00-0x66", argument_name));
290 return absl::OkStatus();
294 return std::find(room_ids.begin(), room_ids.end(),
295 static_cast<uint32_t
>(room_id)) != room_ids.end();
299 if ((snes_address & 0xFFFFu) < 0x8000u) {
302 const uint8_t normalized_bank =
303 static_cast<uint8_t
>((snes_address >> 16) & 0x7Fu);
304 if (normalized_bank >= 0x7Eu) {
307 const uint32_t pc_address =
SnesToPc(snes_address);
308 return (
PcToSnes(pc_address) & 0x7FFFFFu) == (snes_address & 0x7FFFFFu);
320 return start <= rom.
size() && length <= rom.
size() - start;
325 return absl::FailedPreconditionError(
326 "ROM is too small to prove US/OOS dungeon palette table layout");
328 uint8_t destination_code = 0;
331 if (destination_code != 0x01) {
332 return absl::FailedPreconditionError(absl::StrFormat(
333 "Dungeon palette commands currently support only the US/OOS table "
334 "layout (SNES destination code 0x01); got unsupported code 0x%02X",
337 return absl::OkStatus();
342 uint8_t palette_set_id = 0;
343 uint8_t palette_pointer_offset = 0;
344 uint16_t palette_pointer_word = 0;
345 int palette_index = -1;
346 uint32_t room_header_pc = 0;
347 uint32_t palette_set_pc = 0;
348 uint32_t palette_pointer_pc = 0;
349 uint32_t palette_pc = 0;
357 return absl::FailedPreconditionError(
358 "Dungeon room-header pointer metadata is outside the ROM");
361 uint32_t table_snes = 0;
364 return absl::FailedPreconditionError(absl::StrFormat(
365 "Dungeon room-header pointer table is not a valid LoROM pointer: "
369 const uint32_t table_pc =
SnesToPc(table_snes);
370 const uint64_t slot_pc =
371 static_cast<uint64_t
>(table_pc) +
static_cast<uint64_t
>(room_id) * 2u;
373 return absl::FailedPreconditionError(absl::StrFormat(
374 "Dungeon room 0x%03X header pointer slot is outside the ROM", room_id));
377 uint16_t header_offset = 0;
378 uint8_t header_bank = 0;
381 const uint32_t header_snes =
382 (
static_cast<uint32_t
>(header_bank) << 16) | header_offset;
384 return absl::FailedPreconditionError(absl::StrFormat(
385 "Dungeon room 0x%03X header is not a valid LoROM pointer: 0x%06X",
386 room_id, header_snes));
389 const uint32_t header_pc =
SnesToPc(header_snes);
391 return absl::FailedPreconditionError(absl::StrFormat(
392 "Dungeon room 0x%03X header is outside the ROM", room_id));
398 const Rom& rom,
int room_id) {
406 return absl::FailedPreconditionError(absl::StrFormat(
407 "Dungeon room 0x%03X has out-of-range 8-bit palette-set ID 0x%02X "
408 "(expected 0x00-0x%02X)",
415 return absl::FailedPreconditionError(absl::StrFormat(
416 "Palette-set entry 0x%02X is outside the ROM", mapping.
palette_set_id));
422 return absl::FailedPreconditionError(absl::StrFormat(
423 "Palette-set 0x%02X has malformed dungeon palette pointer offset "
424 "0x%02X (expected an even byte offset 0x00-0x%02X)",
432 return absl::FailedPreconditionError(
433 "Resolved dungeon palette pointer is outside the ROM");
438 return absl::FailedPreconditionError(absl::StrFormat(
439 "Palette-set 0x%02X resolves to malformed palette word 0x%04X "
440 "(not divisible by %d)",
449 return absl::FailedPreconditionError(absl::StrFormat(
450 "Palette-set 0x%02X resolves to out-of-range dungeon palette %d",
459 return absl::FailedPreconditionError(absl::StrFormat(
460 "Palette-set 0x%02X resolves outside the fixed dungeon palette "
461 "region [0x%06X,0x%06X)",
474 const Rom& rom,
int room_id) {
482 if (!mapping_or.ok()) {
484 mapping_or.status().code(),
486 "Cannot prove complete shared palette fanout because room "
487 "0x%03X mapping is invalid: %s",
488 candidate, mapping_or.status().message()));
491 selection.
affected_rooms.push_back(
static_cast<uint32_t
>(candidate));
499 return absl::InvalidArgumentError(absl::StrFormat(
502 return absl::OkStatus();
506 if (color < 0 || color > 0x7FFF) {
507 return absl::InvalidArgumentError(absl::StrFormat(
508 "--%s must be a 15-bit SNES color in range 0x0000-0x7FFF",
511 return absl::OkStatus();
516 return mapping.
palette_pc +
static_cast<uint32_t
>(color_index) * 2u;
526 return absl::FailedPreconditionError(
527 "Resolved dungeon palette color is outside the fixed palette region");
531 if (color > 0x7FFF) {
532 return absl::FailedPreconditionError(absl::StrFormat(
533 "Resolved dungeon palette color at PC 0x%06X is not a 15-bit SNES "
542 const auto& mapping = selection.
selected;
543 formatter.
AddHexField(
"room_id", mapping.room_id, 3);
544 formatter.
AddHexField(
"palette_set_id", mapping.palette_set_id, 2);
545 formatter.
AddField(
"resolved_palette_index", mapping.palette_index);
546 formatter.
AddHexField(
"room_header_pc", mapping.room_header_pc, 6);
547 formatter.
AddHexField(
"palette_set_pc", mapping.palette_set_pc, 6);
549 mapping.palette_pointer_offset, 2);
550 formatter.
AddHexField(
"palette_pointer_pc", mapping.palette_pointer_pc, 6);
551 formatter.
AddHexField(
"palette_pointer_word", mapping.palette_pointer_word,
553 formatter.
AddHexField(
"palette_pc", mapping.palette_pc, 6);
555 formatter.
AddField(
"scope",
"shared_global_dungeon_palette");
556 formatter.
AddField(
"affected_room_count",
560 formatter.
AddArrayItem(absl::StrFormat(
"0x%03X", affected_room));
567 const auto conflicts =
569 if (conflicts.empty()) {
570 return absl::OkStatus();
573 return absl::PermissionDeniedError(absl::StrFormat(
574 "Dungeon palette color range [0x%06X,0x%06X) conflicts with Hack "
575 "Manifest ownership at SNES 0x%06X (%s%s%s)",
576 color_pc, color_pc + 2u, conflict.
address,
578 conflict.module.empty() ?
"" :
", module=",
579 conflict.module.empty() ?
"" : conflict.module));
583 const std::vector<uint8_t>& before,
const Rom& after, uint32_t color_pc) {
584 if (after.
size() != before.size()) {
585 return absl::DataLossError(
586 "Dungeon palette write unexpectedly resized the ROM");
589 std::vector<uint32_t> changed;
590 for (uint32_t pc = 0; pc < before.size(); ++pc) {
591 if (before[pc] == after.
data()[pc]) {
594 if (pc < color_pc || pc >= color_pc + 2u) {
595 return absl::DataLossError(absl::StrFormat(
596 "Dungeon palette write changed unexpected ROM byte 0x%06X", pc));
598 changed.push_back(pc);
600 if (changed.empty() || changed.size() > 2u) {
601 return absl::DataLossError(absl::StrFormat(
602 "Dungeon palette write changed %zu bytes; expected one or two bytes "
603 "inside the fenced color word",
611 return absl::FailedPreconditionError(
612 "Dungeon palette write requires a clean file-backed ROM; save or "
613 "discard unrelated in-memory edits first");
616 return absl::FailedPreconditionError(
617 "Dungeon palette write requires a ROM filename");
626 return absl::FailedPreconditionError(
627 "Dungeon palette write requires in-memory ROM bytes to exactly match "
628 "the current file; refusing to persist unrelated or stale bytes");
630 return absl::OkStatus();
634 const Rom& saved_rom,
int room_id,
int expected_palette_set,
635 int expected_palette_index,
int color_index, uint16_t expected_color) {
637 return absl::FailedPreconditionError(
638 "Write mode requires a ROM filename for external readback");
647 return absl::DataLossError(
648 "External reopen bytes do not match the saved in-memory ROM");
657 return absl::DataLossError(
658 "External reopen changed the selected dungeon palette mapping");
660 uint16_t reopened_color = 0;
664 if (reopened_color != expected_color) {
665 return absl::DataLossError(absl::StrFormat(
666 "External reopen color mismatch: expected 0x%04X, got 0x%04X",
667 expected_color, reopened_color));
669 return absl::OkStatus();
675 uint32_t object_stream_pc = 0;
676 uint32_t object_stream_end_pc = 0;
677 uint32_t door_pointer_pc = 0;
678 uint32_t door_entry_pc = 0;
679 uint32_t type_pc = 0;
683 Rom* rom,
int room_id,
int x,
int y,
687 return absl::FailedPreconditionError(
688 "Dungeon object manifest pointer encoding must be long24");
692 return absl::FailedPreconditionError(absl::StrFormat(
693 "Dungeon object manifest pointer_count must equal %d rooms",
697 uint32_t runtime_pointer_table_snes = 0;
701 return absl::FailedPreconditionError(
702 "Runtime object pointer table is not a valid LoROM data pointer");
704 const uint32_t runtime_pointer_table_pc =
705 SnesToPc(runtime_pointer_table_snes);
706 if (runtime_pointer_table_pc !=
708 return absl::FailedPreconditionError(absl::StrFormat(
709 "Manifest object pointer table PC 0x%06X does not match runtime "
712 runtime_pointer_table_pc));
718 if (
static_cast<size_t>(room_id) >= inventory.
streams.size()) {
719 return absl::FailedPreconditionError(
720 "Selected room is absent from the object stream inventory");
722 for (
const auto& issue : inventory.
issues) {
723 if (issue.room_id ==
static_cast<uint32_t
>(room_id)) {
724 return absl::FailedPreconditionError(absl::StrFormat(
725 "Selected object stream is invalid: %s", issue.message));
729 const auto& stream = inventory.
streams[room_id];
731 return absl::FailedPreconditionError(
"Selected object stream is invalid");
733 for (
const auto& alias : inventory.
aliases) {
735 return absl::FailedPreconditionError(absl::StrFormat(
736 "Selected object stream at PC 0x%06X is shared by %d rooms",
737 alias.data_pc,
static_cast<int>(alias.room_ids.size())));
740 for (
const auto& overlap : inventory.
overlaps) {
743 return absl::FailedPreconditionError(absl::StrFormat(
744 "Selected object stream overlaps PC range [0x%06X, 0x%06X)",
745 overlap.intersection.begin, overlap.intersection.end));
748 for (
const auto& other_stream : inventory.
streams) {
749 if (other_stream.room_id ==
static_cast<uint32_t
>(room_id)) {
752 if (other_stream.data_pc >= stream.data_pc &&
753 other_stream.data_pc < stream.logical_end_pc) {
754 return absl::FailedPreconditionError(absl::StrFormat(
755 "Room 0x%03X object pointer PC 0x%06X starts inside selected "
756 "object stream [0x%06X, 0x%06X)",
757 other_stream.room_id, other_stream.data_pc, stream.data_pc,
758 stream.logical_end_pc));
763 if (other_room_id == room_id) {
766 const uint32_t other_door_pointer_slot =
768 uint32_t other_door_pointer_snes = 0;
770 rom->
ReadLong(
static_cast<int>(other_door_pointer_slot)));
774 const uint32_t other_door_pointer_pc =
SnesToPc(other_door_pointer_snes);
775 if (other_door_pointer_pc >= stream.data_pc &&
776 other_door_pointer_pc < stream.logical_end_pc) {
777 return absl::FailedPreconditionError(absl::StrFormat(
778 "Room 0x%03X door pointer PC 0x%06X points inside selected "
779 "object stream [0x%06X, 0x%06X)",
780 other_room_id, other_door_pointer_pc, stream.data_pc,
781 stream.logical_end_pc));
785 const uint64_t runtime_pointer_slot =
786 static_cast<uint64_t
>(runtime_pointer_table_pc) +
787 static_cast<uint64_t
>(room_id) * 3u;
788 if (runtime_pointer_slot + 3u > rom->
size() ||
789 runtime_pointer_slot != stream.pointer_slot_pc) {
790 return absl::FailedPreconditionError(
791 "Selected runtime object pointer slot does not match the manifest");
793 uint32_t runtime_stream_snes = 0;
795 rom->
ReadLong(
static_cast<int>(runtime_pointer_slot)));
797 return absl::FailedPreconditionError(
798 "Selected runtime object stream is not a valid LoROM data pointer");
800 const uint32_t runtime_stream_pc =
SnesToPc(runtime_stream_snes);
801 if (runtime_stream_pc != stream.data_pc) {
802 return absl::FailedPreconditionError(absl::StrFormat(
803 "Selected runtime object stream PC 0x%06X does not match inventory "
805 runtime_stream_pc, stream.data_pc));
809 const auto& doors = room.
GetDoors();
810 int selected_index = -1;
811 for (
int index = 0; index < static_cast<int>(doors.size()); ++index) {
812 const auto [door_x, door_y] = doors[index].GetTileCoords();
813 if (door_x != x || door_y != y) {
816 if (selected_index >= 0) {
817 return absl::FailedPreconditionError(absl::StrFormat(
818 "Multiple doors match tile coordinates (%d, %d)", x, y));
820 selected_index = index;
822 if (selected_index < 0) {
823 return absl::NotFoundError(absl::StrFormat(
824 "No door matches tile coordinates (%d, %d) in room 0x%03X", x, y,
828 const uint64_t door_pointer_slot =
830 static_cast<uint64_t
>(room_id) * 3u;
831 if (door_pointer_slot + 3u > rom->
size()) {
832 return absl::OutOfRangeError(
"Selected door pointer slot is outside ROM");
834 uint32_t door_pointer_snes = 0;
836 rom->
ReadLong(
static_cast<int>(door_pointer_slot)));
838 return absl::FailedPreconditionError(
839 "Selected door pointer is not a valid LoROM data pointer");
841 const uint32_t door_pointer_pc =
SnesToPc(door_pointer_snes);
842 const uint64_t door_entry_pc =
static_cast<uint64_t
>(door_pointer_pc) +
843 static_cast<uint64_t
>(selected_index) * 2u;
844 const uint64_t expected_stream_end =
845 static_cast<uint64_t
>(door_pointer_pc) +
846 static_cast<uint64_t
>(doors.size()) * 2u + 2u;
847 if (door_pointer_pc < stream.data_pc ||
848 door_entry_pc + 2u > stream.logical_end_pc ||
849 expected_stream_end != stream.logical_end_pc) {
850 return absl::FailedPreconditionError(
851 "Door pointer/count does not match the inventoried object stream end");
854 uint8_t raw_byte1 = 0;
855 uint8_t raw_byte2 = 0;
856 for (
int index = 0; index < static_cast<int>(doors.size()); ++index) {
857 const uint32_t entry_pc =
858 door_pointer_pc +
static_cast<uint32_t
>(index) * 2u;
859 uint8_t entry_byte1 = 0;
860 uint8_t entry_byte2 = 0;
863 rom->
ReadByte(
static_cast<int>(entry_pc + 1u)));
864 const auto [model_byte1, model_byte2] = doors[index].EncodeBytes();
865 if (model_byte1 != entry_byte1 || model_byte2 != entry_byte2 ||
866 doors[index].byte1 != entry_byte1 ||
867 doors[index].byte2 != entry_byte2) {
868 return absl::DataLossError(
869 "Door model does not match the raw door-pointer entries");
871 if (index == selected_index) {
872 raw_byte1 = entry_byte1;
873 raw_byte2 = entry_byte2;
876 uint16_t door_terminator = 0;
878 rom->
ReadWord(
static_cast<int>(expected_stream_end - 2u)));
879 if (door_terminator != 0xFFFFu) {
880 return absl::DataLossError(
881 "Door list does not end with the expected 0xFFFF terminator");
890 model_door.
type != raw_door.
type || raw_x != x || raw_y != y) {
891 return absl::DataLossError(
892 "Door model does not match the raw door-pointer entry");
895 const uint32_t type_pc =
static_cast<uint32_t
>(door_entry_pc + 1u);
896 const auto conflicts =
898 if (!conflicts.empty()) {
899 return absl::PermissionDeniedError(absl::StrFormat(
900 "Door type byte at PC 0x%06X conflicts with Hack Manifest (%s)",
906 target.
door = model_door;
922 std::string manifest_path;
930 if (manifest_layout ==
nullptr) {
931 return absl::FailedPreconditionError(
932 "Manifest does not define dungeon_stream_regions.pot_items");
943 uint32_t entry_pc = 0;
944 uint32_t item_pc = 0;
948 uint32_t pointer_slot_pc = 0;
949 uint32_t stream_pc = 0;
950 uint32_t stream_end_pc = 0;
956 if (rom ==
nullptr || !rom->
is_loaded()) {
957 return absl::InvalidArgumentError(
"ROM not loaded");
960 const uint64_t pointer_table_end =
963 if (pointer_table_end > rom->
size()) {
964 return absl::OutOfRangeError(
965 "Complete pot-item pointer table is outside ROM");
967 const uint32_t pointer_slot_pc =
969 uint16_t pointer_word = 0;
971 rom->
ReadWord(
static_cast<int>(pointer_slot_pc)));
972 const uint32_t pointer_snes = 0x010000u | pointer_word;
974 return absl::FailedPreconditionError(
975 "Selected pot-item pointer is not a valid bank-01 LoROM pointer");
977 const uint32_t stream_pc =
SnesToPc(pointer_snes);
978 const uint64_t bank_end =
979 (
static_cast<uint64_t
>(stream_pc) / 0x8000u + 1u) * 0x8000u;
980 const uint32_t parse_end =
981 static_cast<uint32_t
>(std::min<uint64_t>(rom->
size(), bank_end));
982 if (stream_pc >= parse_end) {
983 return absl::OutOfRangeError(
"Selected pot-item stream is outside ROM");
990 uint32_t cursor = stream_pc;
992 if (
static_cast<uint64_t
>(cursor) + 2u > parse_end) {
993 return absl::DataLossError(
"Pot-item stream has no 0xFFFF terminator");
995 uint16_t position = 0;
997 if (position == 0xFFFFu) {
1001 if (
static_cast<uint64_t
>(cursor) + 3u > parse_end) {
1002 return absl::DataLossError(
"Pot-item stream has a truncated record");
1006 view.
entries.push_back({.index =
static_cast<int>(view.
entries.size()),
1007 .item = {.position = position, .item = item},
1009 .item_pc = cursor + 2u});
1017 return absl::DataLossError(
1018 "Pot-item room model count does not match the raw stream");
1020 for (
size_t index = 0; index < view.
entries.size(); ++index) {
1024 return absl::DataLossError(
1025 "Pot-item room model does not match the raw stream");
1033 uint32_t pointer_slot_pc = 0;
1034 uint32_t stream_pc = 0;
1035 uint32_t stream_end_pc = 0;
1039 Rom* rom,
int room_id,
int index,
1042 return absl::FailedPreconditionError(absl::StrFormat(
1043 "Dungeon pot-item manifest pointer_count must equal %d rooms",
1049 *rom, manifest_context.
layout));
1050 if (
static_cast<size_t>(room_id) >= inventory.
streams.size()) {
1051 return absl::FailedPreconditionError(
1052 "Selected room is absent from the pot-item stream inventory");
1054 if (!inventory.
issues.empty()) {
1055 const auto& issue = inventory.
issues.front();
1056 return absl::FailedPreconditionError(absl::StrFormat(
1057 "Pot-item stream inventory is invalid at room 0x%03X: %s",
1058 issue.room_id, issue.message));
1061 const auto& stream = inventory.
streams[room_id];
1062 if (!stream.valid) {
1063 return absl::FailedPreconditionError(
"Selected pot-item stream is invalid");
1065 for (
const auto& alias : inventory.
aliases) {
1067 return absl::FailedPreconditionError(absl::StrFormat(
1068 "Selected pot-item stream at PC 0x%06X is shared by %d rooms",
1069 alias.data_pc,
static_cast<int>(alias.room_ids.size())));
1072 for (
const auto& overlap : inventory.
overlaps) {
1075 return absl::FailedPreconditionError(absl::StrFormat(
1076 "Selected pot-item stream overlaps PC range [0x%06X, 0x%06X)",
1077 overlap.intersection.begin, overlap.intersection.end));
1080 for (
const auto& other_stream : inventory.
streams) {
1081 if (other_stream.room_id ==
static_cast<uint32_t
>(room_id)) {
1084 if (other_stream.data_pc >= stream.data_pc &&
1085 other_stream.data_pc < stream.logical_end_pc) {
1086 return absl::FailedPreconditionError(absl::StrFormat(
1087 "Room 0x%03X pot-item pointer PC 0x%06X starts inside selected "
1088 "pot-item stream [0x%06X, 0x%06X)",
1089 other_stream.room_id, other_stream.data_pc, stream.data_pc,
1090 stream.logical_end_pc));
1099 runtime.
entries.size() * 3u + 2u != stream.encoded_stream.size()) {
1100 return absl::DataLossError(
1101 "Runtime pot-item stream does not match the manifest inventory");
1103 if (index < 0 || index >=
static_cast<int>(runtime.
entries.size())) {
1104 return absl::OutOfRangeError(
1105 absl::StrFormat(
"Pot-item index %d out of range (room has %d entries)",
1106 index,
static_cast<int>(runtime.
entries.size())));
1112 if (!conflicts.empty()) {
1113 return absl::PermissionDeniedError(absl::StrFormat(
1114 "Pot-item byte at PC 0x%06X conflicts with Hack Manifest (%s)",
1136 auto room_id_str_or = GetRequiredString(parser,
"room");
1137 if (!room_id_str_or.ok()) {
1138 return room_id_str_or.status();
1140 auto sprite_id_str_or = GetRequiredString(parser,
"id");
1141 if (!sprite_id_str_or.ok()) {
1142 return sprite_id_str_or.status();
1144 const std::string room_id_str = room_id_str_or.value();
1145 const std::string sprite_id_str = sprite_id_str_or.value();
1147 int room_id, sprite_id;
1148 if (!ParseHexString(room_id_str, &room_id)) {
1149 return absl::InvalidArgumentError(
"Invalid room ID. Must be hex.");
1151 if (!ParseHexString(sprite_id_str, &sprite_id)) {
1152 return absl::InvalidArgumentError(
"Invalid sprite ID. Must be hex.");
1154 auto room_status = ValidateRoomId(room_id);
1155 if (!room_status.ok()) {
1159 auto x_or = GetRequiredInt(parser,
"x");
1161 return x_or.status();
1163 auto y_or = GetRequiredInt(parser,
"y");
1165 return y_or.status();
1167 auto subtype_or = GetOptionalInt(parser,
"subtype", 0);
1168 if (!subtype_or.ok()) {
1169 return subtype_or.status();
1171 auto layer_or = GetOptionalInt(parser,
"layer", 0);
1172 if (!layer_or.ok()) {
1173 return layer_or.status();
1176 int x = x_or.value();
1177 int y = y_or.value();
1178 int subtype = subtype_or.value();
1179 int layer = layer_or.value();
1180 bool do_write = parser.
HasFlag(
"write");
1185 if (sprite_id < 0 || sprite_id > 0xFF) {
1186 return absl::InvalidArgumentError(
"Sprite ID must be 0x00-0xFF");
1188 if (subtype < 0 || subtype > 0x1F) {
1189 return absl::InvalidArgumentError(
"Subtype must be 0-31 (5-bit flags)");
1191 if (layer < 0 || layer > 1) {
1192 return absl::InvalidArgumentError(
"Layer must be 0 or 1");
1199 int count_before =
static_cast<int>(room.
GetSprites().size());
1203 static_cast<uint8_t
>(sprite_id),
static_cast<uint8_t
>(x),
1204 static_cast<uint8_t
>(y),
static_cast<uint8_t
>(subtype),
1205 static_cast<uint8_t
>(layer));
1214 formatter.
AddField(
"subtype", subtype);
1215 formatter.
AddField(
"layer", layer);
1216 formatter.
AddField(
"sprites_before", count_before);
1217 formatter.
AddField(
"sprites_after",
1219 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
1222 auto save_status = MutateAndSaveRomWithBackup(
1223 rom, formatter, [&room]() {
return room.
SaveSprites(); });
1224 if (!save_status.ok()) {
1231 return absl::OkStatus();
1241 auto room_id_str_or = GetRequiredString(parser,
"room");
1242 if (!room_id_str_or.ok()) {
1243 return room_id_str_or.status();
1245 const std::string room_id_str = room_id_str_or.value();
1248 if (!ParseHexString(room_id_str, &room_id)) {
1249 return absl::InvalidArgumentError(
"Invalid room ID. Must be hex.");
1251 auto room_status = ValidateRoomId(room_id);
1252 if (!room_status.ok()) {
1256 bool do_write = parser.
HasFlag(
"write");
1263 int count_before =
static_cast<int>(sprites.size());
1266 const bool has_index = parser.
GetString(
"index").has_value();
1267 const bool has_x = parser.
GetString(
"x").has_value();
1268 const bool has_y = parser.
GetString(
"y").has_value();
1269 if (has_index && (has_x || has_y)) {
1270 return absl::InvalidArgumentError(
1271 "Use either --index or --x/--y, not both");
1273 if (!has_index && has_x != has_y) {
1274 return absl::InvalidArgumentError(
1275 "Both --x and --y are required when removing by position");
1277 if (!has_index && !has_x) {
1278 return absl::InvalidArgumentError(
1279 "Either --index or both --x and --y are required");
1282 int remove_index = -1;
1284 auto index_or = GetRequiredInt(parser,
"index");
1285 if (!index_or.ok()) {
1286 return index_or.status();
1288 remove_index = index_or.value();
1290 auto x_or = GetRequiredInt(parser,
"x");
1292 return x_or.status();
1294 auto y_or = GetRequiredInt(parser,
"y");
1296 return y_or.status();
1299 const int x = x_or.value();
1300 const int y = y_or.value();
1304 for (
int i = 0; i < static_cast<int>(sprites.size()); ++i) {
1305 if (sprites[i].x() == x && sprites[i].y() == y) {
1310 if (remove_index < 0) {
1311 return absl::NotFoundError(absl::StrFormat(
1312 "No sprite at (%d, %d) in room 0x%02X", x, y, room_id));
1316 if (remove_index < 0 || remove_index >=
static_cast<int>(sprites.size())) {
1317 return absl::OutOfRangeError(
1318 absl::StrFormat(
"Sprite index %d out of range (room has %d sprites)",
1319 remove_index, count_before));
1323 const auto& target = sprites[remove_index];
1326 formatter.
AddField(
"removed_index", remove_index);
1327 formatter.
AddHexField(
"sprite_id", target.id(), 2);
1329 formatter.
AddField(
"x", target.x());
1330 formatter.
AddField(
"y", target.y());
1331 formatter.
AddField(
"sprites_before", count_before);
1334 sprites.erase(sprites.begin() + remove_index);
1336 formatter.
AddField(
"sprites_after",
static_cast<int>(sprites.size()));
1337 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
1340 auto save_status = MutateAndSaveRomWithBackup(
1341 rom, formatter, [&room]() {
return room.
SaveSprites(); });
1342 if (!save_status.ok()) {
1349 return absl::OkStatus();
1359 auto room_id_str_or = GetRequiredString(parser,
"room");
1360 if (!room_id_str_or.ok()) {
1361 return room_id_str_or.status();
1363 auto object_id_str_or = GetRequiredString(parser,
"id");
1364 if (!object_id_str_or.ok()) {
1365 return object_id_str_or.status();
1367 const std::string room_id_str = room_id_str_or.value();
1368 const std::string object_id_str = object_id_str_or.value();
1370 int room_id, object_id;
1371 if (!ParseHexString(room_id_str, &room_id)) {
1372 return absl::InvalidArgumentError(
"Invalid room ID. Must be hex.");
1374 if (!ParseHexString(object_id_str, &object_id)) {
1375 return absl::InvalidArgumentError(
"Invalid object ID. Must be hex.");
1377 auto room_status = ValidateRoomId(room_id);
1378 if (!room_status.ok()) {
1381 if (object_id < 0 || object_id > 0xFFFF) {
1382 return absl::InvalidArgumentError(
"Object ID must be 0x0000-0xFFFF");
1385 auto x_or = GetRequiredInt(parser,
"x");
1387 return x_or.status();
1389 auto y_or = GetRequiredInt(parser,
"y");
1391 return y_or.status();
1393 auto layer_or = GetOptionalInt(parser,
"layer", 0);
1394 if (!layer_or.ok()) {
1395 return layer_or.status();
1398 int x = x_or.value();
1399 int y = y_or.value();
1403 if (parser.
GetString(
"size").has_value()) {
1404 auto size_or = parser.
GetInt(
"size");
1405 if (!size_or.ok()) {
1406 return size_or.status();
1408 size = size_or.value();
1410 int layer = layer_or.value();
1411 bool do_write = parser.
HasFlag(
"write");
1414 if (x < 0 || x > 63) {
1415 return absl::InvalidArgumentError(
"X must be 0-63");
1417 if (y < 0 || y > 63) {
1418 return absl::InvalidArgumentError(
"Y must be 0-63");
1420 if (layer < 0 || layer > 2) {
1421 return absl::InvalidArgumentError(
"Layer must be 0, 1, or 2");
1423 if (size < 0 || size > 0xFF) {
1424 return absl::InvalidArgumentError(
"Size must be 0-255");
1426 const uint8_t canonical_size =
1428 if (size != canonical_size) {
1429 return absl::InvalidArgumentError(absl::StrFormat(
1430 "Size %d is not encodable for object 0x%03X; expected %d", size,
1431 object_id, canonical_size));
1434 std::optional<ObjectSaveManifestContext> manifest_context;
1437 manifest_context.has_value() ? &manifest_context->allocator_layout
1443 int count_before =
static_cast<int>(room.
GetTileObjects().size());
1447 static_cast<uint8_t
>(x),
static_cast<uint8_t
>(y),
1448 static_cast<uint8_t
>(size),
1449 static_cast<uint8_t
>(layer));
1459 formatter.
AddField(
"object_type", type);
1463 formatter.
AddField(
"layer", layer);
1464 formatter.
AddField(
"objects_before", count_before);
1465 if (
const auto manifest_path = parser.
GetString(
"manifest");
1466 manifest_path.has_value()) {
1467 formatter.
AddField(
"manifest", *manifest_path);
1468 formatter.
AddField(
"manifest_write_policy",
"block");
1470 formatter.
AddField(
"allocator_capability",
1471 layout !=
nullptr ?
"copy_on_write" :
"none");
1475 if (!add_status.ok()) {
1476 formatter.
AddField(
"error", std::string(add_status.message()));
1481 formatter.
AddField(
"objects_after",
1483 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
1485 const auto preflight_status = PreflightObjectSave(
1486 *rom, room_id, obj, room,
1487 manifest_context.has_value() ? &*manifest_context :
nullptr);
1488 if (!preflight_status.ok()) {
1489 formatter.
AddField(
"preflight_status",
"failed");
1490 formatter.
AddField(
"preflight_error",
1491 std::string(preflight_status.message()));
1493 return preflight_status;
1495 formatter.
AddField(
"preflight_status",
"success");
1498 auto save_status = MutateAndSaveRomWithBackup(
1499 rom, formatter, [&room, layout]() {
return room.
SaveObjects(layout); });
1500 if (!save_status.ok()) {
1507 return absl::OkStatus();
1521 int remove_index = 0;
1523 if (remove_index < 0) {
1524 return absl::InvalidArgumentError(
"Object index must be non-negative");
1527 int expected_id = 0;
1533 int expected_size = 0;
1535 int expected_layer = 0;
1538 if (expected_id < 0 || expected_id > 0xFFF) {
1539 return absl::InvalidArgumentError(
"Expected object ID must be 0x000-0xFFF");
1541 if (expected_x < 0 || expected_x > 63) {
1542 return absl::InvalidArgumentError(
"Expected X must be 0-63");
1544 if (expected_y < 0 || expected_y > 63) {
1545 return absl::InvalidArgumentError(
"Expected Y must be 0-63");
1547 if (expected_size < 0 || expected_size > 0xFF) {
1548 return absl::InvalidArgumentError(
"Expected size must be 0-255");
1550 if (expected_layer < 0 || expected_layer > 2) {
1551 return absl::InvalidArgumentError(
"Expected layer must be 0, 1, or 2");
1554 std::optional<ObjectSaveManifestContext> manifest_context;
1557 manifest_context.has_value() ? &manifest_context->allocator_layout
1562 int object_vector_index = -1;
1563 int stream_object_count = 0;
1564 for (
int i = 0; i < static_cast<int>(objects.size()); ++i) {
1568 if (stream_object_count == remove_index) {
1569 object_vector_index = i;
1571 ++stream_object_count;
1573 if (object_vector_index < 0) {
1574 return absl::OutOfRangeError(
1575 absl::StrFormat(
"Object index %d out of range (room has %d objects)",
1576 remove_index, stream_object_count));
1579 const auto& target = objects[object_vector_index];
1582 return absl::FailedPreconditionError(
1583 "Removing chest objects requires coordinated chest-table editing");
1586 if (target.id_ != expected_id || target.x() != expected_x ||
1587 target.y() != expected_y || target.size_ != expected_size ||
1588 target.GetLayerValue() != expected_layer) {
1589 return absl::FailedPreconditionError(
1590 absl::StrFormat(
"Object guard mismatch at index %d: expected 0x%03X at "
1591 "(%d,%d) size=%d layer=%d; found 0x%03X at (%d,%d) "
1593 remove_index, expected_id, expected_x, expected_y,
1594 expected_size, expected_layer, target.id_, target.x(),
1595 target.y(), target.size_, target.GetLayerValue()));
1598 const int object_id = target.id_;
1599 const int object_x = target.x();
1600 const int object_y = target.y();
1601 const int object_size = target.size_;
1602 const int object_layer = target.GetLayerValue();
1603 const auto encoded = target.EncodeObjectToBytes();
1604 const int object_type =
1609 formatter.
AddField(
"removed_index", remove_index);
1612 formatter.
AddField(
"object_type", object_type);
1615 formatter.
AddField(
"size", object_size);
1616 formatter.
AddField(
"layer", object_layer);
1617 formatter.
AddField(
"objects_before", stream_object_count);
1618 if (
const auto manifest_path = parser.
GetString(
"manifest");
1619 manifest_path.has_value()) {
1620 formatter.
AddField(
"manifest", *manifest_path);
1621 formatter.
AddField(
"manifest_write_policy",
"block");
1623 formatter.
AddField(
"allocator_capability",
1624 layout !=
nullptr ?
"copy_on_write" :
"none");
1626 const absl::Status remove_status = room.
RemoveObject(object_vector_index);
1627 if (!remove_status.ok()) {
1628 formatter.
AddField(
"remove_error", std::string(remove_status.message()));
1630 return remove_status;
1632 formatter.
AddField(
"objects_after", stream_object_count - 1);
1633 const bool do_write = parser.
HasFlag(
"write");
1634 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
1636 const absl::Status preflight_status = PreflightObjectRemoval(
1637 *rom, room_id, object_vector_index, room,
1638 manifest_context.has_value() ? &*manifest_context :
nullptr);
1639 if (!preflight_status.ok()) {
1640 formatter.
AddField(
"preflight_status",
"failed");
1641 formatter.
AddField(
"preflight_error",
1642 std::string(preflight_status.message()));
1644 return preflight_status;
1646 formatter.
AddField(
"preflight_status",
"success");
1649 const absl::Status save_status = MutateAndSaveRomWithBackup(
1650 rom, formatter, [&room, layout]() {
return room.
SaveObjects(layout); });
1651 if (!save_status.ok()) {
1658 return absl::OkStatus();
1672 std::optional<int> requested_index;
1673 if (parser.
GetString(
"index").has_value()) {
1674 int color_index = 0;
1677 requested_index = color_index;
1680 DungeonPaletteSelection selection;
1681 ASSIGN_OR_RETURN(selection, ResolveDungeonPaletteSelection(*rom, room_id));
1684 AddDungeonPaletteScope(formatter, selection);
1685 formatter.
AddField(
"color_count", kDungeonPaletteColorCount);
1687 if (requested_index.has_value()) {
1688 const uint32_t color_pc =
1689 DungeonPaletteColorPc(selection.selected, *requested_index);
1693 formatter.
AddField(
"color_index", *requested_index);
1699 for (
int color_index = 0; color_index < kDungeonPaletteColorCount;
1701 const uint32_t color_pc =
1702 DungeonPaletteColorPc(selection.selected, color_index);
1707 formatter.
AddField(
"index", color_index);
1716 formatter.
AddField(
"status",
"success");
1718 return absl::OkStatus();
1728 const bool do_write = parser.
HasFlag(
"write");
1729#if defined(__EMSCRIPTEN__)
1731 return absl::FailedPreconditionError(
1732 "dungeon-set-palette-color --write is unavailable in the browser "
1733 "terminal because durable filesystem sync cannot be guaranteed; use "
1734 "native z3ed on a disposable ROM copy");
1737 if (do_write && rom->
filename().empty()) {
1738 return absl::FailedPreconditionError(
1739 "dungeon-set-palette-color --write requires a ROM loaded from a "
1744 int color_index = 0;
1745 int expected_palette_set = 0;
1746 int expected_palette_index = 0;
1747 int expected_color = 0;
1748 int replacement_color = 0;
1752 GetRequiredHex(parser,
"expect-palette-set"));
1754 GetRequiredInt(parser,
"expect-palette-index"));
1760 if (expected_palette_set < 0 || expected_palette_set > 0xFF) {
1761 return absl::InvalidArgumentError(
1762 "--expect-palette-set must be an 8-bit value in range 0x00-0xFF");
1764 if (expected_palette_index < 0 ||
1765 expected_palette_index >= kDungeonPaletteCount) {
1766 return absl::InvalidArgumentError(
1767 absl::StrFormat(
"--expect-palette-index must be in range 0-%d",
1768 kDungeonPaletteCount - 1));
1770 RETURN_IF_ERROR(ValidateSnes15BitColor(expected_color,
"expect-color"));
1773 const std::string manifest_path = parser.
GetString(
"manifest").value();
1777 DungeonPaletteSelection selection;
1778 ASSIGN_OR_RETURN(selection, ResolveDungeonPaletteSelection(*rom, room_id));
1779 if (selection.selected.palette_set_id != expected_palette_set) {
1780 return absl::FailedPreconditionError(absl::StrFormat(
1781 "Palette-set CAS failed for room 0x%03X: expected full 8-bit ID "
1782 "0x%02X, got 0x%02X",
1783 room_id, expected_palette_set, selection.selected.palette_set_id));
1785 if (selection.selected.palette_index != expected_palette_index) {
1786 return absl::FailedPreconditionError(absl::StrFormat(
1787 "Resolved palette CAS failed for room 0x%03X: expected %d, got %d",
1788 room_id, expected_palette_index, selection.selected.palette_index));
1791 uint16_t actual_color = 0;
1793 *rom, selection.selected, color_index));
1794 if (actual_color != expected_color) {
1795 return absl::FailedPreconditionError(absl::StrFormat(
1796 "Color CAS failed at palette %d index %d: expected 0x%04X, got "
1798 selection.selected.palette_index, color_index, expected_color,
1801 if (replacement_color == actual_color) {
1802 return absl::InvalidArgumentError(absl::StrFormat(
1803 "Refusing no-op dungeon palette write: color is already 0x%04X",
1807 const uint32_t color_pc =
1808 DungeonPaletteColorPc(selection.selected, color_index);
1809 RETURN_IF_ERROR(ValidateDungeonPaletteManifestRange(manifest, color_pc));
1814 formatter.
BeginObject(
"Set Dungeon Palette Color");
1815 AddDungeonPaletteScope(formatter, selection);
1816 formatter.
AddField(
"manifest", manifest_path);
1817 formatter.
AddField(
"manifest_write_policy",
"block");
1818 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
1819 formatter.
AddField(
"color_index", color_index);
1822 formatter.
AddHexField(
"old_color", actual_color, 4);
1823 formatter.
AddHexField(
"new_color", replacement_color, 4);
1824 formatter.
AddField(
"preflight_status",
"success");
1827 formatter.
AddField(
"status",
"dry-run");
1829 return absl::OkStatus();
1834 write_fence.
Allow(color_pc, color_pc + 2u,
"DungeonPaletteColor"));
1835 const std::vector<uint8_t> before = rom->
vector();
1840 rom->
WriteShort(color_pc,
static_cast<uint16_t
>(replacement_color)));
1843 std::vector<uint32_t> changed_bytes;
1845 ValidateDungeonPaletteWholeRomDiff(before, *rom, color_pc));
1846 uint16_t memory_color = 0;
1848 *rom, selection.selected, color_index));
1849 if (memory_color != replacement_color) {
1850 return absl::DataLossError(
1851 "In-memory dungeon palette readback did not match the replacement");
1853 formatter.
AddField(
"write_status",
"success");
1854 formatter.
AddField(
"whole_rom_diff_status",
"verified");
1855 formatter.
AddField(
"changed_byte_count",
1856 static_cast<int>(changed_bytes.size()));
1858 for (uint32_t changed_pc : changed_bytes) {
1859 formatter.
AddArrayItem(absl::StrFormat(
"0x%06X", changed_pc));
1862 formatter.
AddField(
"memory_readback_status",
"verified");
1864 const absl::Status save_status = SaveRomWithBackup(rom, formatter);
1865 if (!save_status.ok()) {
1866 formatter.
AddField(
"status",
"error");
1875 const absl::Status reopen_status = VerifyDungeonPaletteExternalReadback(
1876 *rom, room_id, expected_palette_set, expected_palette_index, color_index,
1877 static_cast<uint16_t
>(replacement_color));
1878 if (!reopen_status.ok()) {
1879 formatter.
AddField(
"readback_status",
"external_reopen_failed");
1880 formatter.
AddField(
"readback_error", std::string(reopen_status.message()));
1881 formatter.
AddField(
"status",
"error");
1883 return reopen_status;
1886 formatter.
AddField(
"readback_status",
"external_reopen_verified");
1887 formatter.
AddField(
"status",
"success");
1889 return absl::OkStatus();
1907 if (x < 0 || x > 63 || y < 0 || y > 63) {
1908 return absl::InvalidArgumentError(
1909 "Door tile coordinates must be in range 0-63");
1913 int expected_type = 0;
1919 std::optional<ObjectSaveManifestContext> manifest_context;
1921 if (!manifest_context.has_value()) {
1922 return absl::FailedPreconditionError(
1923 "--manifest is required for dungeon-set-door-type");
1926 DoorTypeTarget target;
1928 target, ResolveDoorTypeTarget(rom, room_id, x, y, *manifest_context));
1929 const int old_type =
static_cast<int>(target.door.type);
1930 if (old_type != expected_type) {
1931 return absl::FailedPreconditionError(absl::StrFormat(
1932 "Door type compare-and-swap failed at (%d, %d): expected 0x%02X, "
1934 x, y, expected_type, old_type));
1936 if (new_type == old_type) {
1937 return absl::InvalidArgumentError(
1938 "--type must differ from the current door type");
1941 const bool do_write = parser.
HasFlag(
"write");
1942 if (do_write && rom->
filename().empty()) {
1943 return absl::FailedPreconditionError(
"Write mode requires a ROM filename");
1948 formatter.
AddField(
"door_index", target.door_index);
1951 formatter.
AddField(
"position", target.door.position);
1952 formatter.
AddField(
"direction", std::string(target.door.GetDirectionName()));
1954 formatter.
AddField(
"old_type_name", std::string(target.door.GetTypeName()));
1960 formatter.
AddHexField(
"object_stream_pc", target.object_stream_pc, 6);
1961 formatter.
AddHexField(
"object_stream_end_pc", target.object_stream_end_pc, 6);
1962 formatter.
AddHexField(
"door_pointer_pc", target.door_pointer_pc, 6);
1963 formatter.
AddHexField(
"door_entry_pc", target.door_entry_pc, 6);
1964 formatter.
AddHexField(
"type_pc", target.type_pc, 6);
1967 formatter.
AddField(
"manifest_write_policy",
"block");
1968 formatter.
AddField(
"stream_alias_status",
"unique");
1969 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
1970 formatter.
AddField(
"preflight_status",
"success");
1973 formatter.
AddField(
"write_status",
"not_requested");
1974 formatter.
AddField(
"save_status",
"not_requested");
1975 formatter.
AddField(
"readback_status",
"not_requested");
1976 formatter.
AddField(
"status",
"success");
1978 return absl::OkStatus();
1981 const auto finish_error = [&formatter](
const absl::Status& status,
1982 const char* field) -> absl::Status {
1983 formatter.
AddField(
"status",
"error");
1984 formatter.
AddField(field, std::string(status.message()));
1988 const auto verify_readback =
1989 [&target, new_type](
const DoorTypeTarget& readback) -> absl::Status {
1990 if (readback.door_index != target.door_index ||
1991 readback.type_pc != target.type_pc ||
1992 readback.door.position != target.door.position ||
1993 readback.door.direction != target.door.direction ||
1994 static_cast<int>(readback.door.type) != new_type) {
1995 return absl::DataLossError(
1996 "Door type readback does not match the requested change");
1998 return absl::OkStatus();
2002 const absl::Status allow_status = write_fence.
Allow(
2003 target.type_pc, target.type_pc + 1u,
"dungeon door type");
2004 if (!allow_status.ok()) {
2005 return finish_error(allow_status,
"write_error");
2008 const std::vector<uint8_t> before = rom->
vector();
2012 const absl::Status write_status = rom->
WriteByte(
2013 static_cast<int>(target.type_pc),
static_cast<uint8_t
>(new_type));
2014 if (!write_status.ok()) {
2015 return finish_error(write_status,
"write_error");
2019 if (rom->
size() != before.size()) {
2020 return finish_error(
2021 absl::DataLossError(
"Door type edit unexpectedly resized the ROM"),
2024 int changed_bytes = 0;
2025 uint32_t changed_pc = 0;
2026 for (uint32_t pc = 0; pc < before.size(); ++pc) {
2027 if (before[pc] != rom->
data()[pc]) {
2032 if (changed_bytes != 1 || changed_pc != target.type_pc) {
2033 return finish_error(
2034 absl::DataLossError(absl::StrFormat(
2035 "Door type edit changed %d bytes; expected only PC 0x%06X",
2036 changed_bytes, target.type_pc)),
2040 auto memory_readback =
2041 ResolveDoorTypeTarget(rom, room_id, x, y, *manifest_context);
2042 if (!memory_readback.ok()) {
2043 return finish_error(memory_readback.status(),
"readback_error");
2045 const absl::Status memory_verify = verify_readback(*memory_readback);
2046 if (!memory_verify.ok()) {
2047 return finish_error(memory_verify,
"readback_error");
2049 formatter.
AddField(
"readback_status",
"pre_save_verified");
2050 formatter.
AddField(
"write_status",
"success");
2052 const absl::Status save_status = SaveRomWithBackup(rom, formatter);
2053 if (!save_status.ok()) {
2054 formatter.
AddField(
"status",
"error");
2060 formatter.
AddField(
"status",
"success");
2062 return absl::OkStatus();
2076 PotItemStreamView stream;
2081 formatter.
AddHexField(
"pointer_slot_pc", stream.pointer_slot_pc, 6);
2082 formatter.
AddHexField(
"stream_pc", stream.stream_pc, 6);
2083 formatter.
AddHexField(
"stream_end_pc", stream.stream_end_pc, 6);
2084 formatter.
AddField(
"item_count",
static_cast<int>(stream.entries.size()));
2086 for (
const PotItemEntryView& entry : stream.entries) {
2088 formatter.
AddField(
"index", entry.index);
2089 formatter.
AddHexField(
"position", entry.item.position, 4);
2090 formatter.
AddField(
"tile_x", entry.item.GetTileX());
2091 formatter.
AddField(
"tile_y", entry.item.GetTileY());
2092 formatter.
AddHexField(
"item_id", entry.item.item, 2);
2093 formatter.
AddHexField(
"item_pc", entry.item_pc, 6);
2098 formatter.
AddField(
"status",
"success");
2100 return absl::OkStatus();
2112 int expected_position = 0;
2113 int expected_item = 0;
2119 GetRequiredHex(parser,
"expect-position"));
2122 if (expected_position < 0 || expected_position > 0xFFFF) {
2123 return absl::InvalidArgumentError(
2124 "--expect-position must be in range 0x0000-0xFFFF");
2126 if (expected_item < 0 || expected_item > 0xFF) {
2127 return absl::InvalidArgumentError(
2128 "--expect-item must be in range 0x00-0xFF");
2130 if (new_item < 0 || new_item > 0xFF) {
2131 return absl::InvalidArgumentError(
"--item must be in range 0x00-0xFF");
2134 PotItemManifestContext manifest_context;
2136 PotItemTarget target;
2138 ResolvePotItemTarget(rom, room_id, index, manifest_context));
2139 if (target.entry.item.position != expected_position ||
2140 target.entry.item.item != expected_item) {
2141 return absl::FailedPreconditionError(absl::StrFormat(
2142 "Pot-item compare-and-swap failed at index %d: expected "
2143 "position/item 0x%04X/0x%02X, found 0x%04X/0x%02X",
2144 index, expected_position, expected_item, target.entry.item.position,
2145 target.entry.item.item));
2147 if (new_item == expected_item) {
2148 return absl::InvalidArgumentError(
2149 "--item must differ from the current pot item");
2152 const bool do_write = parser.
HasFlag(
"write");
2153 if (do_write && rom->
filename().empty()) {
2154 return absl::FailedPreconditionError(
"Write mode requires a ROM filename");
2159 formatter.
AddField(
"index", index);
2160 formatter.
AddHexField(
"position", target.entry.item.position, 4);
2161 formatter.
AddField(
"tile_x", target.entry.item.GetTileX());
2162 formatter.
AddField(
"tile_y", target.entry.item.GetTileY());
2163 formatter.
AddHexField(
"old_item", expected_item, 2);
2165 formatter.
AddHexField(
"pointer_slot_pc", target.pointer_slot_pc, 6);
2166 formatter.
AddHexField(
"stream_pc", target.stream_pc, 6);
2167 formatter.
AddHexField(
"stream_end_pc", target.stream_end_pc, 6);
2168 formatter.
AddHexField(
"item_pc", target.entry.item_pc, 6);
2171 formatter.
AddField(
"manifest_write_policy",
"block");
2172 formatter.
AddField(
"stream_alias_status",
"unique");
2173 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
2174 formatter.
AddField(
"preflight_status",
"success");
2177 formatter.
AddField(
"write_status",
"not_requested");
2178 formatter.
AddField(
"save_status",
"not_requested");
2179 formatter.
AddField(
"readback_status",
"not_requested");
2180 formatter.
AddField(
"status",
"success");
2182 return absl::OkStatus();
2185 const auto finish_error = [&formatter](
const absl::Status& status,
2186 const char* field) -> absl::Status {
2187 formatter.
AddField(
"status",
"error");
2188 formatter.
AddField(field, std::string(status.message()));
2192 const auto verify_readback =
2193 [&target, new_item](
const PotItemTarget& readback) -> absl::Status {
2194 if (readback.entry.index != target.entry.index ||
2195 readback.entry.entry_pc != target.entry.entry_pc ||
2196 readback.entry.item_pc != target.entry.item_pc ||
2197 readback.entry.item.position != target.entry.item.position ||
2198 readback.entry.item.item != new_item ||
2199 readback.stream_pc != target.stream_pc ||
2200 readback.stream_end_pc != target.stream_end_pc) {
2201 return absl::DataLossError(
2202 "Pot-item readback does not match the requested change");
2204 return absl::OkStatus();
2208 const absl::Status allow_status = write_fence.
Allow(
2209 target.entry.item_pc, target.entry.item_pc + 1u,
"dungeon pot item");
2210 if (!allow_status.ok()) {
2211 return finish_error(allow_status,
"write_error");
2214 const std::vector<uint8_t> before = rom->
vector();
2218 const absl::Status write_status = rom->
WriteByte(
2219 static_cast<int>(target.entry.item_pc),
static_cast<uint8_t
>(new_item));
2220 if (!write_status.ok()) {
2221 return finish_error(write_status,
"write_error");
2225 if (rom->
size() != before.size()) {
2226 return finish_error(
2227 absl::DataLossError(
"Pot-item edit unexpectedly resized the ROM"),
2230 int changed_bytes = 0;
2231 uint32_t changed_pc = 0;
2232 for (uint32_t pc = 0; pc < before.size(); ++pc) {
2233 if (before[pc] != rom->
data()[pc]) {
2238 if (changed_bytes != 1 || changed_pc != target.entry.item_pc) {
2239 return finish_error(
2240 absl::DataLossError(absl::StrFormat(
2241 "Pot-item edit changed %d bytes; expected only PC 0x%06X",
2242 changed_bytes, target.entry.item_pc)),
2246 auto memory_readback =
2247 ResolvePotItemTarget(rom, room_id, index, manifest_context);
2248 if (!memory_readback.ok()) {
2249 return finish_error(memory_readback.status(),
"readback_error");
2251 const absl::Status memory_verify = verify_readback(*memory_readback);
2252 if (!memory_verify.ok()) {
2253 return finish_error(memory_verify,
"readback_error");
2255 formatter.
AddField(
"readback_status",
"pre_save_verified");
2256 formatter.
AddField(
"write_status",
"success");
2258 const absl::Status save_status = SaveRomWithBackup(rom, formatter);
2259 if (!save_status.ok()) {
2260 formatter.
AddField(
"status",
"error");
2266 formatter.
AddField(
"status",
"success");
2268 return absl::OkStatus();
2278 auto room_id_str_or = GetRequiredString(parser,
"room");
2279 if (!room_id_str_or.ok()) {
2280 return room_id_str_or.status();
2282 auto tiles_str_or = GetRequiredString(parser,
"tiles");
2283 if (!tiles_str_or.ok()) {
2284 return tiles_str_or.status();
2286 const std::string room_id_str = room_id_str_or.value();
2287 const std::string tiles_str = tiles_str_or.value();
2290 if (!ParseHexString(room_id_str, &room_id)) {
2291 return absl::InvalidArgumentError(
"Invalid room ID. Must be hex.");
2293 auto room_status = ValidateRoomId(room_id);
2294 if (!room_status.ok()) {
2298 bool do_write = parser.
HasFlag(
"write");
2304 std::vector<TileSpec> specs;
2306 for (absl::string_view entry :
2307 absl::StrSplit(tiles_str,
';', absl::SkipEmpty())) {
2308 std::vector<std::string> parts =
2309 absl::StrSplit(entry,
',', absl::SkipEmpty());
2310 if (parts.size() != 3) {
2311 return absl::InvalidArgumentError(absl::StrFormat(
2312 "Invalid tile spec '%s'. Expected x,y,tile (e.g. 10,5,0xB7)", entry));
2317 if (!absl::SimpleAtoi(parts[0], &spec.x)) {
2318 return absl::InvalidArgumentError(
2319 absl::StrFormat(
"Invalid X coord '%s'", parts[0]));
2321 if (!absl::SimpleAtoi(parts[1], &spec.y)) {
2322 return absl::InvalidArgumentError(
2323 absl::StrFormat(
"Invalid Y coord '%s'", parts[1]));
2325 if (!ParseHexString(parts[2], &spec.tile)) {
2326 return absl::InvalidArgumentError(
2327 absl::StrFormat(
"Invalid tile value '%s'. Must be hex.", parts[2]));
2331 if (spec.x < 0 || spec.x > 63 || spec.y < 0 || spec.y > 63) {
2332 return absl::InvalidArgumentError(absl::StrFormat(
2333 "Tile coords (%d,%d) out of range (0-63)", spec.x, spec.y));
2335 if (spec.tile < 0 || spec.tile > 0xFF) {
2336 return absl::InvalidArgumentError(
"Tile value must be 0x00-0xFF");
2339 specs.push_back(spec);
2342 if (specs.empty()) {
2343 return absl::InvalidArgumentError(
"No tile specs provided");
2352 formatter.
AddField(
"tile_count",
static_cast<int>(specs.size()));
2353 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
2357 for (
const auto& spec : specs) {
2372 MutateAndSaveRomWithBackup(rom, formatter, [&rom, &room]()
mutable {
2374 std::array<zelda3::Room, 1> rooms_arr = {std::move(room)};
2377 if (!save_status.ok()) {
2384 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)
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::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 PreflightObjectRemoval(const Rom &source_rom, int room_id, int object_vector_index, const zelda3::Room &pending_room, const ObjectSaveManifestContext *manifest_context)
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::Status ValidateObjectSaveManifestConflicts(const Rom &source_rom, int room_id, const zelda3::Room &pending_room, const ObjectSaveManifestContext &context)
absl::StatusOr< int > GetOptionalInt(const resources::ArgumentParser &parser, const char *name, int default_value)
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)
absl::StatusOr< std::optional< ObjectSaveManifestContext > > LoadObjectSaveManifestContext(const resources::ArgumentParser &parser)
constexpr uint32_t kDungeonPaletteDataEnd
uint32_t DungeonPaletteColorPc(const DungeonPaletteRoomMapping &mapping, int color_index)
constexpr uint32_t kSnesDestinationCodePc
absl::Status ValidateSpriteCoord(int value, char axis)
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::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 ObjectSaveManifestContext *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 ObjectSaveManifestContext &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)
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
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 allocator_layout
project::RomWritePolicy write_policy
core::HackManifest manifest
zelda3::DungeonStreamLayout layout
std::vector< PotItemEntryView > entries
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.