11#include "absl/strings/numbers.h"
12#include "absl/strings/str_format.h"
13#include "absl/strings/str_split.h"
43 if (!value.has_value()) {
44 return absl::InvalidArgumentError(
45 absl::StrFormat(
"Missing required argument '--%s'",
name));
54 return parsed.status();
56 return parsed.value();
60 const char*
name,
int default_value) {
67 return parsed.status();
69 return parsed.value();
74 return absl::InvalidArgumentError(
75 absl::StrFormat(
"Room ID out of range: 0x%X (expected 0x00-0x%02X)",
78 return absl::OkStatus();
82 if (value < 0 || value > 31) {
83 return absl::InvalidArgumentError(
84 absl::StrFormat(
"%c must be 0-31 (5-bit tile coord)", axis));
86 return absl::OkStatus();
93 auto disk_status = rom->
SaveToFile(save_settings);
94 if (!disk_status.ok()) {
95 formatter.
AddField(
"save_error", std::string(disk_status.message()));
99 formatter.
AddField(
"save_status",
"saved");
100 return absl::OkStatus();
103template <
typename Mutation>
106 Mutation&& mutation) {
109 auto write_status = std::forward<Mutation>(mutation)();
110 if (!write_status.ok()) {
111 formatter.
AddField(
"write_error", std::string(write_status.message()));
114 formatter.
AddField(
"write_status",
"success");
118 return absl::OkStatus();
130absl::StatusOr<std::optional<ObjectSaveManifestContext>>
132 const auto manifest_path = parser.
GetString(
"manifest");
133 if (!manifest_path.has_value()) {
142 if (manifest_layout ==
nullptr) {
143 return absl::FailedPreconditionError(
144 "Manifest does not define dungeon_stream_regions.objects");
147 return absl::FailedPreconditionError(
148 "dungeon_stream_regions.objects must use copy_on_write");
154 return std::optional<ObjectSaveManifestContext>(std::move(context));
160 std::vector<std::pair<uint32_t, uint32_t>> ranges;
165 uint32_t object_pointer_table_snes = 0;
168 const uint32_t object_pointer_table_pc =
SnesToPc(object_pointer_table_snes);
169 const uint64_t current_pointer_slot =
170 static_cast<uint64_t
>(object_pointer_table_pc) +
171 static_cast<uint64_t
>(room_id) * 3u;
172 if (current_pointer_slot + 3u > source_rom.
size()) {
173 return absl::OutOfRangeError(
174 "Selected object pointer slot is outside the ROM");
177 uint32_t current_stream_snes = 0;
179 source_rom.
ReadLong(
static_cast<int>(current_pointer_slot)));
180 const uint32_t current_stream_pc =
SnesToPc(current_stream_snes);
181 const uint64_t current_stream_end =
static_cast<uint64_t
>(current_stream_pc) +
183 if (current_stream_end > std::numeric_limits<uint32_t>::max()) {
184 return absl::OutOfRangeError(
"Selected object write range overflows");
186 ranges.emplace_back(current_stream_pc,
187 static_cast<uint32_t
>(current_stream_end));
189 const uint64_t door_pointer_slot =
191 static_cast<uint64_t
>(room_id) * 3u;
192 if (door_pointer_slot + 3u > std::numeric_limits<uint32_t>::max()) {
193 return absl::OutOfRangeError(
"Selected door pointer range overflows");
195 ranges.emplace_back(
static_cast<uint32_t
>(door_pointer_slot),
196 static_cast<uint32_t
>(door_pointer_slot + 3u));
199 ranges.emplace_back(range.begin, range.end);
205 const uint64_t cow_pointer_slot =
207 static_cast<uint64_t
>(room_id) * pointer_width;
208 if (cow_pointer_slot + pointer_width > std::numeric_limits<uint32_t>::max()) {
209 return absl::OutOfRangeError(
"Selected COW pointer range overflows");
211 ranges.emplace_back(
static_cast<uint32_t
>(cow_pointer_slot),
212 static_cast<uint32_t
>(cow_pointer_slot + pointer_width));
215 if (conflicts.empty() ||
217 return absl::OkStatus();
219 return absl::PermissionDeniedError(
"Write conflict with Hack Manifest");
226 if (manifest_context !=
nullptr) {
228 source_rom, room_id, pending_room, *manifest_context));
242 return scratch_room.
SaveObjects(manifest_context !=
nullptr
250 if (!value_or.ok()) {
251 return value_or.status();
255 if (!ParseHexString(*value_or, &value)) {
256 return absl::InvalidArgumentError(absl::StrFormat(
257 "Invalid --%s value '%s' (expected integer/hex)",
name, *value_or));
263 if (type < 0 || type > 0x66 || (type & 1) != 0) {
264 return absl::InvalidArgumentError(absl::StrFormat(
265 "--%s must be an even door type in range 0x00-0x66", argument_name));
267 return absl::OkStatus();
271 return std::find(room_ids.begin(), room_ids.end(),
272 static_cast<uint32_t
>(room_id)) != room_ids.end();
276 if ((snes_address & 0xFFFFu) < 0x8000u) {
279 const uint8_t normalized_bank =
280 static_cast<uint8_t
>((snes_address >> 16) & 0x7Fu);
281 if (normalized_bank >= 0x7Eu) {
284 const uint32_t pc_address =
SnesToPc(snes_address);
285 return (
PcToSnes(pc_address) & 0x7FFFFFu) == (snes_address & 0x7FFFFFu);
297 return start <= rom.
size() && length <= rom.
size() - start;
302 return absl::FailedPreconditionError(
303 "ROM is too small to prove US/OOS dungeon palette table layout");
305 uint8_t destination_code = 0;
308 if (destination_code != 0x01) {
309 return absl::FailedPreconditionError(absl::StrFormat(
310 "Dungeon palette commands currently support only the US/OOS table "
311 "layout (SNES destination code 0x01); got unsupported code 0x%02X",
314 return absl::OkStatus();
319 uint8_t palette_set_id = 0;
320 uint8_t palette_pointer_offset = 0;
321 uint16_t palette_pointer_word = 0;
322 int palette_index = -1;
323 uint32_t room_header_pc = 0;
324 uint32_t palette_set_pc = 0;
325 uint32_t palette_pointer_pc = 0;
326 uint32_t palette_pc = 0;
334 return absl::FailedPreconditionError(
335 "Dungeon room-header pointer metadata is outside the ROM");
338 uint32_t table_snes = 0;
341 return absl::FailedPreconditionError(absl::StrFormat(
342 "Dungeon room-header pointer table is not a valid LoROM pointer: "
346 const uint32_t table_pc =
SnesToPc(table_snes);
347 const uint64_t slot_pc =
348 static_cast<uint64_t
>(table_pc) +
static_cast<uint64_t
>(room_id) * 2u;
350 return absl::FailedPreconditionError(absl::StrFormat(
351 "Dungeon room 0x%03X header pointer slot is outside the ROM", room_id));
354 uint16_t header_offset = 0;
355 uint8_t header_bank = 0;
358 const uint32_t header_snes =
359 (
static_cast<uint32_t
>(header_bank) << 16) | header_offset;
361 return absl::FailedPreconditionError(absl::StrFormat(
362 "Dungeon room 0x%03X header is not a valid LoROM pointer: 0x%06X",
363 room_id, header_snes));
366 const uint32_t header_pc =
SnesToPc(header_snes);
368 return absl::FailedPreconditionError(absl::StrFormat(
369 "Dungeon room 0x%03X header is outside the ROM", room_id));
375 const Rom& rom,
int room_id) {
383 return absl::FailedPreconditionError(absl::StrFormat(
384 "Dungeon room 0x%03X has out-of-range 8-bit palette-set ID 0x%02X "
385 "(expected 0x00-0x%02X)",
392 return absl::FailedPreconditionError(absl::StrFormat(
393 "Palette-set entry 0x%02X is outside the ROM", mapping.
palette_set_id));
399 return absl::FailedPreconditionError(absl::StrFormat(
400 "Palette-set 0x%02X has malformed dungeon palette pointer offset "
401 "0x%02X (expected an even byte offset 0x00-0x%02X)",
409 return absl::FailedPreconditionError(
410 "Resolved dungeon palette pointer is outside the ROM");
415 return absl::FailedPreconditionError(absl::StrFormat(
416 "Palette-set 0x%02X resolves to malformed palette word 0x%04X "
417 "(not divisible by %d)",
426 return absl::FailedPreconditionError(absl::StrFormat(
427 "Palette-set 0x%02X resolves to out-of-range dungeon palette %d",
436 return absl::FailedPreconditionError(absl::StrFormat(
437 "Palette-set 0x%02X resolves outside the fixed dungeon palette "
438 "region [0x%06X,0x%06X)",
451 const Rom& rom,
int room_id) {
459 if (!mapping_or.ok()) {
461 mapping_or.status().code(),
463 "Cannot prove complete shared palette fanout because room "
464 "0x%03X mapping is invalid: %s",
465 candidate, mapping_or.status().message()));
468 selection.
affected_rooms.push_back(
static_cast<uint32_t
>(candidate));
476 return absl::InvalidArgumentError(absl::StrFormat(
479 return absl::OkStatus();
483 if (color < 0 || color > 0x7FFF) {
484 return absl::InvalidArgumentError(absl::StrFormat(
485 "--%s must be a 15-bit SNES color in range 0x0000-0x7FFF",
488 return absl::OkStatus();
493 return mapping.
palette_pc +
static_cast<uint32_t
>(color_index) * 2u;
503 return absl::FailedPreconditionError(
504 "Resolved dungeon palette color is outside the fixed palette region");
508 if (color > 0x7FFF) {
509 return absl::FailedPreconditionError(absl::StrFormat(
510 "Resolved dungeon palette color at PC 0x%06X is not a 15-bit SNES "
519 const auto& mapping = selection.
selected;
520 formatter.
AddHexField(
"room_id", mapping.room_id, 3);
521 formatter.
AddHexField(
"palette_set_id", mapping.palette_set_id, 2);
522 formatter.
AddField(
"resolved_palette_index", mapping.palette_index);
523 formatter.
AddHexField(
"room_header_pc", mapping.room_header_pc, 6);
524 formatter.
AddHexField(
"palette_set_pc", mapping.palette_set_pc, 6);
526 mapping.palette_pointer_offset, 2);
527 formatter.
AddHexField(
"palette_pointer_pc", mapping.palette_pointer_pc, 6);
528 formatter.
AddHexField(
"palette_pointer_word", mapping.palette_pointer_word,
530 formatter.
AddHexField(
"palette_pc", mapping.palette_pc, 6);
532 formatter.
AddField(
"scope",
"shared_global_dungeon_palette");
533 formatter.
AddField(
"affected_room_count",
537 formatter.
AddArrayItem(absl::StrFormat(
"0x%03X", affected_room));
544 const auto conflicts =
546 if (conflicts.empty()) {
547 return absl::OkStatus();
550 return absl::PermissionDeniedError(absl::StrFormat(
551 "Dungeon palette color range [0x%06X,0x%06X) conflicts with Hack "
552 "Manifest ownership at SNES 0x%06X (%s%s%s)",
553 color_pc, color_pc + 2u, conflict.
address,
555 conflict.module.empty() ?
"" :
", module=",
556 conflict.module.empty() ?
"" : conflict.module));
560 const std::vector<uint8_t>& before,
const Rom& after, uint32_t color_pc) {
561 if (after.
size() != before.size()) {
562 return absl::DataLossError(
563 "Dungeon palette write unexpectedly resized the ROM");
566 std::vector<uint32_t> changed;
567 for (uint32_t pc = 0; pc < before.size(); ++pc) {
568 if (before[pc] == after.
data()[pc]) {
571 if (pc < color_pc || pc >= color_pc + 2u) {
572 return absl::DataLossError(absl::StrFormat(
573 "Dungeon palette write changed unexpected ROM byte 0x%06X", pc));
575 changed.push_back(pc);
577 if (changed.empty() || changed.size() > 2u) {
578 return absl::DataLossError(absl::StrFormat(
579 "Dungeon palette write changed %zu bytes; expected one or two bytes "
580 "inside the fenced color word",
588 return absl::FailedPreconditionError(
589 "Dungeon palette write requires a clean file-backed ROM; save or "
590 "discard unrelated in-memory edits first");
593 return absl::FailedPreconditionError(
594 "Dungeon palette write requires a ROM filename");
603 return absl::FailedPreconditionError(
604 "Dungeon palette write requires in-memory ROM bytes to exactly match "
605 "the current file; refusing to persist unrelated or stale bytes");
607 return absl::OkStatus();
611 const Rom& saved_rom,
int room_id,
int expected_palette_set,
612 int expected_palette_index,
int color_index, uint16_t expected_color) {
614 return absl::FailedPreconditionError(
615 "Write mode requires a ROM filename for external readback");
624 return absl::DataLossError(
625 "External reopen bytes do not match the saved in-memory ROM");
634 return absl::DataLossError(
635 "External reopen changed the selected dungeon palette mapping");
637 uint16_t reopened_color = 0;
641 if (reopened_color != expected_color) {
642 return absl::DataLossError(absl::StrFormat(
643 "External reopen color mismatch: expected 0x%04X, got 0x%04X",
644 expected_color, reopened_color));
646 return absl::OkStatus();
652 uint32_t object_stream_pc = 0;
653 uint32_t object_stream_end_pc = 0;
654 uint32_t door_pointer_pc = 0;
655 uint32_t door_entry_pc = 0;
656 uint32_t type_pc = 0;
660 Rom* rom,
int room_id,
int x,
int y,
664 return absl::FailedPreconditionError(
665 "Dungeon object manifest pointer encoding must be long24");
669 return absl::FailedPreconditionError(absl::StrFormat(
670 "Dungeon object manifest pointer_count must equal %d rooms",
674 uint32_t runtime_pointer_table_snes = 0;
678 return absl::FailedPreconditionError(
679 "Runtime object pointer table is not a valid LoROM data pointer");
681 const uint32_t runtime_pointer_table_pc =
682 SnesToPc(runtime_pointer_table_snes);
683 if (runtime_pointer_table_pc !=
685 return absl::FailedPreconditionError(absl::StrFormat(
686 "Manifest object pointer table PC 0x%06X does not match runtime "
689 runtime_pointer_table_pc));
695 if (
static_cast<size_t>(room_id) >= inventory.
streams.size()) {
696 return absl::FailedPreconditionError(
697 "Selected room is absent from the object stream inventory");
699 for (
const auto& issue : inventory.
issues) {
700 if (issue.room_id ==
static_cast<uint32_t
>(room_id)) {
701 return absl::FailedPreconditionError(absl::StrFormat(
702 "Selected object stream is invalid: %s", issue.message));
706 const auto& stream = inventory.
streams[room_id];
708 return absl::FailedPreconditionError(
"Selected object stream is invalid");
710 for (
const auto& alias : inventory.
aliases) {
712 return absl::FailedPreconditionError(absl::StrFormat(
713 "Selected object stream at PC 0x%06X is shared by %d rooms",
714 alias.data_pc,
static_cast<int>(alias.room_ids.size())));
717 for (
const auto& overlap : inventory.
overlaps) {
720 return absl::FailedPreconditionError(absl::StrFormat(
721 "Selected object stream overlaps PC range [0x%06X, 0x%06X)",
722 overlap.intersection.begin, overlap.intersection.end));
725 for (
const auto& other_stream : inventory.
streams) {
726 if (other_stream.room_id ==
static_cast<uint32_t
>(room_id)) {
729 if (other_stream.data_pc >= stream.data_pc &&
730 other_stream.data_pc < stream.logical_end_pc) {
731 return absl::FailedPreconditionError(absl::StrFormat(
732 "Room 0x%03X object pointer PC 0x%06X starts inside selected "
733 "object stream [0x%06X, 0x%06X)",
734 other_stream.room_id, other_stream.data_pc, stream.data_pc,
735 stream.logical_end_pc));
740 if (other_room_id == room_id) {
743 const uint32_t other_door_pointer_slot =
745 uint32_t other_door_pointer_snes = 0;
747 rom->
ReadLong(
static_cast<int>(other_door_pointer_slot)));
751 const uint32_t other_door_pointer_pc =
SnesToPc(other_door_pointer_snes);
752 if (other_door_pointer_pc >= stream.data_pc &&
753 other_door_pointer_pc < stream.logical_end_pc) {
754 return absl::FailedPreconditionError(absl::StrFormat(
755 "Room 0x%03X door pointer PC 0x%06X points inside selected "
756 "object stream [0x%06X, 0x%06X)",
757 other_room_id, other_door_pointer_pc, stream.data_pc,
758 stream.logical_end_pc));
762 const uint64_t runtime_pointer_slot =
763 static_cast<uint64_t
>(runtime_pointer_table_pc) +
764 static_cast<uint64_t
>(room_id) * 3u;
765 if (runtime_pointer_slot + 3u > rom->
size() ||
766 runtime_pointer_slot != stream.pointer_slot_pc) {
767 return absl::FailedPreconditionError(
768 "Selected runtime object pointer slot does not match the manifest");
770 uint32_t runtime_stream_snes = 0;
772 rom->
ReadLong(
static_cast<int>(runtime_pointer_slot)));
774 return absl::FailedPreconditionError(
775 "Selected runtime object stream is not a valid LoROM data pointer");
777 const uint32_t runtime_stream_pc =
SnesToPc(runtime_stream_snes);
778 if (runtime_stream_pc != stream.data_pc) {
779 return absl::FailedPreconditionError(absl::StrFormat(
780 "Selected runtime object stream PC 0x%06X does not match inventory "
782 runtime_stream_pc, stream.data_pc));
786 const auto& doors = room.
GetDoors();
787 int selected_index = -1;
788 for (
int index = 0; index < static_cast<int>(doors.size()); ++index) {
789 const auto [door_x, door_y] = doors[index].GetTileCoords();
790 if (door_x != x || door_y != y) {
793 if (selected_index >= 0) {
794 return absl::FailedPreconditionError(absl::StrFormat(
795 "Multiple doors match tile coordinates (%d, %d)", x, y));
797 selected_index = index;
799 if (selected_index < 0) {
800 return absl::NotFoundError(absl::StrFormat(
801 "No door matches tile coordinates (%d, %d) in room 0x%03X", x, y,
805 const uint64_t door_pointer_slot =
807 static_cast<uint64_t
>(room_id) * 3u;
808 if (door_pointer_slot + 3u > rom->
size()) {
809 return absl::OutOfRangeError(
"Selected door pointer slot is outside ROM");
811 uint32_t door_pointer_snes = 0;
813 rom->
ReadLong(
static_cast<int>(door_pointer_slot)));
815 return absl::FailedPreconditionError(
816 "Selected door pointer is not a valid LoROM data pointer");
818 const uint32_t door_pointer_pc =
SnesToPc(door_pointer_snes);
819 const uint64_t door_entry_pc =
static_cast<uint64_t
>(door_pointer_pc) +
820 static_cast<uint64_t
>(selected_index) * 2u;
821 const uint64_t expected_stream_end =
822 static_cast<uint64_t
>(door_pointer_pc) +
823 static_cast<uint64_t
>(doors.size()) * 2u + 2u;
824 if (door_pointer_pc < stream.data_pc ||
825 door_entry_pc + 2u > stream.logical_end_pc ||
826 expected_stream_end != stream.logical_end_pc) {
827 return absl::FailedPreconditionError(
828 "Door pointer/count does not match the inventoried object stream end");
831 uint8_t raw_byte1 = 0;
832 uint8_t raw_byte2 = 0;
833 for (
int index = 0; index < static_cast<int>(doors.size()); ++index) {
834 const uint32_t entry_pc =
835 door_pointer_pc +
static_cast<uint32_t
>(index) * 2u;
836 uint8_t entry_byte1 = 0;
837 uint8_t entry_byte2 = 0;
840 rom->
ReadByte(
static_cast<int>(entry_pc + 1u)));
841 const auto [model_byte1, model_byte2] = doors[index].EncodeBytes();
842 if (model_byte1 != entry_byte1 || model_byte2 != entry_byte2 ||
843 doors[index].byte1 != entry_byte1 ||
844 doors[index].byte2 != entry_byte2) {
845 return absl::DataLossError(
846 "Door model does not match the raw door-pointer entries");
848 if (index == selected_index) {
849 raw_byte1 = entry_byte1;
850 raw_byte2 = entry_byte2;
853 uint16_t door_terminator = 0;
855 rom->
ReadWord(
static_cast<int>(expected_stream_end - 2u)));
856 if (door_terminator != 0xFFFFu) {
857 return absl::DataLossError(
858 "Door list does not end with the expected 0xFFFF terminator");
867 model_door.
type != raw_door.
type || raw_x != x || raw_y != y) {
868 return absl::DataLossError(
869 "Door model does not match the raw door-pointer entry");
872 const uint32_t type_pc =
static_cast<uint32_t
>(door_entry_pc + 1u);
873 const auto conflicts =
875 if (!conflicts.empty()) {
876 return absl::PermissionDeniedError(absl::StrFormat(
877 "Door type byte at PC 0x%06X conflicts with Hack Manifest (%s)",
883 target.
door = model_door;
899 std::string manifest_path;
907 if (manifest_layout ==
nullptr) {
908 return absl::FailedPreconditionError(
909 "Manifest does not define dungeon_stream_regions.pot_items");
920 uint32_t entry_pc = 0;
921 uint32_t item_pc = 0;
925 uint32_t pointer_slot_pc = 0;
926 uint32_t stream_pc = 0;
927 uint32_t stream_end_pc = 0;
933 if (rom ==
nullptr || !rom->
is_loaded()) {
934 return absl::InvalidArgumentError(
"ROM not loaded");
937 const uint64_t pointer_table_end =
940 if (pointer_table_end > rom->
size()) {
941 return absl::OutOfRangeError(
942 "Complete pot-item pointer table is outside ROM");
944 const uint32_t pointer_slot_pc =
946 uint16_t pointer_word = 0;
948 rom->
ReadWord(
static_cast<int>(pointer_slot_pc)));
949 const uint32_t pointer_snes = 0x010000u | pointer_word;
951 return absl::FailedPreconditionError(
952 "Selected pot-item pointer is not a valid bank-01 LoROM pointer");
954 const uint32_t stream_pc =
SnesToPc(pointer_snes);
955 const uint64_t bank_end =
956 (
static_cast<uint64_t
>(stream_pc) / 0x8000u + 1u) * 0x8000u;
957 const uint32_t parse_end =
958 static_cast<uint32_t
>(std::min<uint64_t>(rom->
size(), bank_end));
959 if (stream_pc >= parse_end) {
960 return absl::OutOfRangeError(
"Selected pot-item stream is outside ROM");
967 uint32_t cursor = stream_pc;
969 if (
static_cast<uint64_t
>(cursor) + 2u > parse_end) {
970 return absl::DataLossError(
"Pot-item stream has no 0xFFFF terminator");
972 uint16_t position = 0;
974 if (position == 0xFFFFu) {
978 if (
static_cast<uint64_t
>(cursor) + 3u > parse_end) {
979 return absl::DataLossError(
"Pot-item stream has a truncated record");
983 view.
entries.push_back({.index =
static_cast<int>(view.
entries.size()),
984 .item = {.position = position, .item = item},
986 .item_pc = cursor + 2u});
994 return absl::DataLossError(
995 "Pot-item room model count does not match the raw stream");
997 for (
size_t index = 0; index < view.
entries.size(); ++index) {
1001 return absl::DataLossError(
1002 "Pot-item room model does not match the raw stream");
1010 uint32_t pointer_slot_pc = 0;
1011 uint32_t stream_pc = 0;
1012 uint32_t stream_end_pc = 0;
1016 Rom* rom,
int room_id,
int index,
1019 return absl::FailedPreconditionError(absl::StrFormat(
1020 "Dungeon pot-item manifest pointer_count must equal %d rooms",
1026 *rom, manifest_context.
layout));
1027 if (
static_cast<size_t>(room_id) >= inventory.
streams.size()) {
1028 return absl::FailedPreconditionError(
1029 "Selected room is absent from the pot-item stream inventory");
1031 if (!inventory.
issues.empty()) {
1032 const auto& issue = inventory.
issues.front();
1033 return absl::FailedPreconditionError(absl::StrFormat(
1034 "Pot-item stream inventory is invalid at room 0x%03X: %s",
1035 issue.room_id, issue.message));
1038 const auto& stream = inventory.
streams[room_id];
1039 if (!stream.valid) {
1040 return absl::FailedPreconditionError(
"Selected pot-item stream is invalid");
1042 for (
const auto& alias : inventory.
aliases) {
1044 return absl::FailedPreconditionError(absl::StrFormat(
1045 "Selected pot-item stream at PC 0x%06X is shared by %d rooms",
1046 alias.data_pc,
static_cast<int>(alias.room_ids.size())));
1049 for (
const auto& overlap : inventory.
overlaps) {
1052 return absl::FailedPreconditionError(absl::StrFormat(
1053 "Selected pot-item stream overlaps PC range [0x%06X, 0x%06X)",
1054 overlap.intersection.begin, overlap.intersection.end));
1057 for (
const auto& other_stream : inventory.
streams) {
1058 if (other_stream.room_id ==
static_cast<uint32_t
>(room_id)) {
1061 if (other_stream.data_pc >= stream.data_pc &&
1062 other_stream.data_pc < stream.logical_end_pc) {
1063 return absl::FailedPreconditionError(absl::StrFormat(
1064 "Room 0x%03X pot-item pointer PC 0x%06X starts inside selected "
1065 "pot-item stream [0x%06X, 0x%06X)",
1066 other_stream.room_id, other_stream.data_pc, stream.data_pc,
1067 stream.logical_end_pc));
1076 runtime.
entries.size() * 3u + 2u != stream.encoded_stream.size()) {
1077 return absl::DataLossError(
1078 "Runtime pot-item stream does not match the manifest inventory");
1080 if (index < 0 || index >=
static_cast<int>(runtime.
entries.size())) {
1081 return absl::OutOfRangeError(
1082 absl::StrFormat(
"Pot-item index %d out of range (room has %d entries)",
1083 index,
static_cast<int>(runtime.
entries.size())));
1089 if (!conflicts.empty()) {
1090 return absl::PermissionDeniedError(absl::StrFormat(
1091 "Pot-item byte at PC 0x%06X conflicts with Hack Manifest (%s)",
1113 auto room_id_str_or = GetRequiredString(parser,
"room");
1114 if (!room_id_str_or.ok()) {
1115 return room_id_str_or.status();
1117 auto sprite_id_str_or = GetRequiredString(parser,
"id");
1118 if (!sprite_id_str_or.ok()) {
1119 return sprite_id_str_or.status();
1121 const std::string room_id_str = room_id_str_or.value();
1122 const std::string sprite_id_str = sprite_id_str_or.value();
1124 int room_id, sprite_id;
1125 if (!ParseHexString(room_id_str, &room_id)) {
1126 return absl::InvalidArgumentError(
"Invalid room ID. Must be hex.");
1128 if (!ParseHexString(sprite_id_str, &sprite_id)) {
1129 return absl::InvalidArgumentError(
"Invalid sprite ID. Must be hex.");
1131 auto room_status = ValidateRoomId(room_id);
1132 if (!room_status.ok()) {
1136 auto x_or = GetRequiredInt(parser,
"x");
1138 return x_or.status();
1140 auto y_or = GetRequiredInt(parser,
"y");
1142 return y_or.status();
1144 auto subtype_or = GetOptionalInt(parser,
"subtype", 0);
1145 if (!subtype_or.ok()) {
1146 return subtype_or.status();
1148 auto layer_or = GetOptionalInt(parser,
"layer", 0);
1149 if (!layer_or.ok()) {
1150 return layer_or.status();
1153 int x = x_or.value();
1154 int y = y_or.value();
1155 int subtype = subtype_or.value();
1156 int layer = layer_or.value();
1157 bool do_write = parser.
HasFlag(
"write");
1162 if (sprite_id < 0 || sprite_id > 0xFF) {
1163 return absl::InvalidArgumentError(
"Sprite ID must be 0x00-0xFF");
1165 if (subtype < 0 || subtype > 0x1F) {
1166 return absl::InvalidArgumentError(
"Subtype must be 0-31 (5-bit flags)");
1168 if (layer < 0 || layer > 1) {
1169 return absl::InvalidArgumentError(
"Layer must be 0 or 1");
1176 int count_before =
static_cast<int>(room.
GetSprites().size());
1180 static_cast<uint8_t
>(sprite_id),
static_cast<uint8_t
>(x),
1181 static_cast<uint8_t
>(y),
static_cast<uint8_t
>(subtype),
1182 static_cast<uint8_t
>(layer));
1191 formatter.
AddField(
"subtype", subtype);
1192 formatter.
AddField(
"layer", layer);
1193 formatter.
AddField(
"sprites_before", count_before);
1194 formatter.
AddField(
"sprites_after",
1196 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
1199 auto save_status = MutateAndSaveRomWithBackup(
1200 rom, formatter, [&room]() {
return room.
SaveSprites(); });
1201 if (!save_status.ok()) {
1208 return absl::OkStatus();
1218 auto room_id_str_or = GetRequiredString(parser,
"room");
1219 if (!room_id_str_or.ok()) {
1220 return room_id_str_or.status();
1222 const std::string room_id_str = room_id_str_or.value();
1225 if (!ParseHexString(room_id_str, &room_id)) {
1226 return absl::InvalidArgumentError(
"Invalid room ID. Must be hex.");
1228 auto room_status = ValidateRoomId(room_id);
1229 if (!room_status.ok()) {
1233 bool do_write = parser.
HasFlag(
"write");
1240 int count_before =
static_cast<int>(sprites.size());
1243 const bool has_index = parser.
GetString(
"index").has_value();
1244 const bool has_x = parser.
GetString(
"x").has_value();
1245 const bool has_y = parser.
GetString(
"y").has_value();
1246 if (has_index && (has_x || has_y)) {
1247 return absl::InvalidArgumentError(
1248 "Use either --index or --x/--y, not both");
1250 if (!has_index && has_x != has_y) {
1251 return absl::InvalidArgumentError(
1252 "Both --x and --y are required when removing by position");
1254 if (!has_index && !has_x) {
1255 return absl::InvalidArgumentError(
1256 "Either --index or both --x and --y are required");
1259 int remove_index = -1;
1261 auto index_or = GetRequiredInt(parser,
"index");
1262 if (!index_or.ok()) {
1263 return index_or.status();
1265 remove_index = index_or.value();
1267 auto x_or = GetRequiredInt(parser,
"x");
1269 return x_or.status();
1271 auto y_or = GetRequiredInt(parser,
"y");
1273 return y_or.status();
1276 const int x = x_or.value();
1277 const int y = y_or.value();
1281 for (
int i = 0; i < static_cast<int>(sprites.size()); ++i) {
1282 if (sprites[i].x() == x && sprites[i].y() == y) {
1287 if (remove_index < 0) {
1288 return absl::NotFoundError(absl::StrFormat(
1289 "No sprite at (%d, %d) in room 0x%02X", x, y, room_id));
1293 if (remove_index < 0 || remove_index >=
static_cast<int>(sprites.size())) {
1294 return absl::OutOfRangeError(
1295 absl::StrFormat(
"Sprite index %d out of range (room has %d sprites)",
1296 remove_index, count_before));
1300 const auto& target = sprites[remove_index];
1303 formatter.
AddField(
"removed_index", remove_index);
1304 formatter.
AddHexField(
"sprite_id", target.id(), 2);
1306 formatter.
AddField(
"x", target.x());
1307 formatter.
AddField(
"y", target.y());
1308 formatter.
AddField(
"sprites_before", count_before);
1311 sprites.erase(sprites.begin() + remove_index);
1313 formatter.
AddField(
"sprites_after",
static_cast<int>(sprites.size()));
1314 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
1317 auto save_status = MutateAndSaveRomWithBackup(
1318 rom, formatter, [&room]() {
return room.
SaveSprites(); });
1319 if (!save_status.ok()) {
1326 return absl::OkStatus();
1336 auto room_id_str_or = GetRequiredString(parser,
"room");
1337 if (!room_id_str_or.ok()) {
1338 return room_id_str_or.status();
1340 auto object_id_str_or = GetRequiredString(parser,
"id");
1341 if (!object_id_str_or.ok()) {
1342 return object_id_str_or.status();
1344 const std::string room_id_str = room_id_str_or.value();
1345 const std::string object_id_str = object_id_str_or.value();
1347 int room_id, object_id;
1348 if (!ParseHexString(room_id_str, &room_id)) {
1349 return absl::InvalidArgumentError(
"Invalid room ID. Must be hex.");
1351 if (!ParseHexString(object_id_str, &object_id)) {
1352 return absl::InvalidArgumentError(
"Invalid object ID. Must be hex.");
1354 auto room_status = ValidateRoomId(room_id);
1355 if (!room_status.ok()) {
1358 if (object_id < 0 || object_id > 0xFFFF) {
1359 return absl::InvalidArgumentError(
"Object ID must be 0x0000-0xFFFF");
1362 auto x_or = GetRequiredInt(parser,
"x");
1364 return x_or.status();
1366 auto y_or = GetRequiredInt(parser,
"y");
1368 return y_or.status();
1370 auto layer_or = GetOptionalInt(parser,
"layer", 0);
1371 if (!layer_or.ok()) {
1372 return layer_or.status();
1375 int x = x_or.value();
1376 int y = y_or.value();
1380 if (parser.
GetString(
"size").has_value()) {
1381 auto size_or = parser.
GetInt(
"size");
1382 if (!size_or.ok()) {
1383 return size_or.status();
1385 size = size_or.value();
1387 int layer = layer_or.value();
1388 bool do_write = parser.
HasFlag(
"write");
1391 if (x < 0 || x > 63) {
1392 return absl::InvalidArgumentError(
"X must be 0-63");
1394 if (y < 0 || y > 63) {
1395 return absl::InvalidArgumentError(
"Y must be 0-63");
1397 if (layer < 0 || layer > 2) {
1398 return absl::InvalidArgumentError(
"Layer must be 0, 1, or 2");
1400 if (size < 0 || size > 0xFF) {
1401 return absl::InvalidArgumentError(
"Size must be 0-255");
1403 const uint8_t canonical_size =
1405 if (size != canonical_size) {
1406 return absl::InvalidArgumentError(absl::StrFormat(
1407 "Size %d is not encodable for object 0x%03X; expected %d", size,
1408 object_id, canonical_size));
1411 std::optional<ObjectSaveManifestContext> manifest_context;
1414 manifest_context.has_value() ? &manifest_context->allocator_layout
1420 int count_before =
static_cast<int>(room.
GetTileObjects().size());
1424 static_cast<uint8_t
>(x),
static_cast<uint8_t
>(y),
1425 static_cast<uint8_t
>(size),
1426 static_cast<uint8_t
>(layer));
1436 formatter.
AddField(
"object_type", type);
1440 formatter.
AddField(
"layer", layer);
1441 formatter.
AddField(
"objects_before", count_before);
1442 if (
const auto manifest_path = parser.
GetString(
"manifest");
1443 manifest_path.has_value()) {
1444 formatter.
AddField(
"manifest", *manifest_path);
1445 formatter.
AddField(
"manifest_write_policy",
"block");
1447 formatter.
AddField(
"allocator_capability",
1448 layout !=
nullptr ?
"copy_on_write" :
"none");
1452 if (!add_status.ok()) {
1453 formatter.
AddField(
"error", std::string(add_status.message()));
1458 formatter.
AddField(
"objects_after",
1460 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
1462 const auto preflight_status = PreflightObjectSave(
1463 *rom, room_id, obj, room,
1464 manifest_context.has_value() ? &*manifest_context :
nullptr);
1465 if (!preflight_status.ok()) {
1466 formatter.
AddField(
"preflight_status",
"failed");
1467 formatter.
AddField(
"preflight_error",
1468 std::string(preflight_status.message()));
1470 return preflight_status;
1472 formatter.
AddField(
"preflight_status",
"success");
1475 auto save_status = MutateAndSaveRomWithBackup(
1476 rom, formatter, [&room, layout]() {
return room.
SaveObjects(layout); });
1477 if (!save_status.ok()) {
1484 return absl::OkStatus();
1498 std::optional<int> requested_index;
1499 if (parser.
GetString(
"index").has_value()) {
1500 int color_index = 0;
1503 requested_index = color_index;
1506 DungeonPaletteSelection selection;
1507 ASSIGN_OR_RETURN(selection, ResolveDungeonPaletteSelection(*rom, room_id));
1510 AddDungeonPaletteScope(formatter, selection);
1511 formatter.
AddField(
"color_count", kDungeonPaletteColorCount);
1513 if (requested_index.has_value()) {
1514 const uint32_t color_pc =
1515 DungeonPaletteColorPc(selection.selected, *requested_index);
1519 formatter.
AddField(
"color_index", *requested_index);
1525 for (
int color_index = 0; color_index < kDungeonPaletteColorCount;
1527 const uint32_t color_pc =
1528 DungeonPaletteColorPc(selection.selected, color_index);
1533 formatter.
AddField(
"index", color_index);
1542 formatter.
AddField(
"status",
"success");
1544 return absl::OkStatus();
1554 const bool do_write = parser.
HasFlag(
"write");
1555#if defined(__EMSCRIPTEN__)
1557 return absl::FailedPreconditionError(
1558 "dungeon-set-palette-color --write is unavailable in the browser "
1559 "terminal because durable filesystem sync cannot be guaranteed; use "
1560 "native z3ed on a disposable ROM copy");
1563 if (do_write && rom->
filename().empty()) {
1564 return absl::FailedPreconditionError(
1565 "dungeon-set-palette-color --write requires a ROM loaded from a "
1570 int color_index = 0;
1571 int expected_palette_set = 0;
1572 int expected_palette_index = 0;
1573 int expected_color = 0;
1574 int replacement_color = 0;
1578 GetRequiredHex(parser,
"expect-palette-set"));
1580 GetRequiredInt(parser,
"expect-palette-index"));
1586 if (expected_palette_set < 0 || expected_palette_set > 0xFF) {
1587 return absl::InvalidArgumentError(
1588 "--expect-palette-set must be an 8-bit value in range 0x00-0xFF");
1590 if (expected_palette_index < 0 ||
1591 expected_palette_index >= kDungeonPaletteCount) {
1592 return absl::InvalidArgumentError(
1593 absl::StrFormat(
"--expect-palette-index must be in range 0-%d",
1594 kDungeonPaletteCount - 1));
1596 RETURN_IF_ERROR(ValidateSnes15BitColor(expected_color,
"expect-color"));
1599 const std::string manifest_path = parser.
GetString(
"manifest").value();
1603 DungeonPaletteSelection selection;
1604 ASSIGN_OR_RETURN(selection, ResolveDungeonPaletteSelection(*rom, room_id));
1605 if (selection.selected.palette_set_id != expected_palette_set) {
1606 return absl::FailedPreconditionError(absl::StrFormat(
1607 "Palette-set CAS failed for room 0x%03X: expected full 8-bit ID "
1608 "0x%02X, got 0x%02X",
1609 room_id, expected_palette_set, selection.selected.palette_set_id));
1611 if (selection.selected.palette_index != expected_palette_index) {
1612 return absl::FailedPreconditionError(absl::StrFormat(
1613 "Resolved palette CAS failed for room 0x%03X: expected %d, got %d",
1614 room_id, expected_palette_index, selection.selected.palette_index));
1617 uint16_t actual_color = 0;
1619 *rom, selection.selected, color_index));
1620 if (actual_color != expected_color) {
1621 return absl::FailedPreconditionError(absl::StrFormat(
1622 "Color CAS failed at palette %d index %d: expected 0x%04X, got "
1624 selection.selected.palette_index, color_index, expected_color,
1627 if (replacement_color == actual_color) {
1628 return absl::InvalidArgumentError(absl::StrFormat(
1629 "Refusing no-op dungeon palette write: color is already 0x%04X",
1633 const uint32_t color_pc =
1634 DungeonPaletteColorPc(selection.selected, color_index);
1635 RETURN_IF_ERROR(ValidateDungeonPaletteManifestRange(manifest, color_pc));
1640 formatter.
BeginObject(
"Set Dungeon Palette Color");
1641 AddDungeonPaletteScope(formatter, selection);
1642 formatter.
AddField(
"manifest", manifest_path);
1643 formatter.
AddField(
"manifest_write_policy",
"block");
1644 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
1645 formatter.
AddField(
"color_index", color_index);
1648 formatter.
AddHexField(
"old_color", actual_color, 4);
1649 formatter.
AddHexField(
"new_color", replacement_color, 4);
1650 formatter.
AddField(
"preflight_status",
"success");
1653 formatter.
AddField(
"status",
"dry-run");
1655 return absl::OkStatus();
1660 write_fence.
Allow(color_pc, color_pc + 2u,
"DungeonPaletteColor"));
1661 const std::vector<uint8_t> before = rom->
vector();
1666 rom->
WriteShort(color_pc,
static_cast<uint16_t
>(replacement_color)));
1669 std::vector<uint32_t> changed_bytes;
1671 ValidateDungeonPaletteWholeRomDiff(before, *rom, color_pc));
1672 uint16_t memory_color = 0;
1674 *rom, selection.selected, color_index));
1675 if (memory_color != replacement_color) {
1676 return absl::DataLossError(
1677 "In-memory dungeon palette readback did not match the replacement");
1679 formatter.
AddField(
"write_status",
"success");
1680 formatter.
AddField(
"whole_rom_diff_status",
"verified");
1681 formatter.
AddField(
"changed_byte_count",
1682 static_cast<int>(changed_bytes.size()));
1684 for (uint32_t changed_pc : changed_bytes) {
1685 formatter.
AddArrayItem(absl::StrFormat(
"0x%06X", changed_pc));
1688 formatter.
AddField(
"memory_readback_status",
"verified");
1690 const absl::Status save_status = SaveRomWithBackup(rom, formatter);
1691 if (!save_status.ok()) {
1692 formatter.
AddField(
"status",
"error");
1701 const absl::Status reopen_status = VerifyDungeonPaletteExternalReadback(
1702 *rom, room_id, expected_palette_set, expected_palette_index, color_index,
1703 static_cast<uint16_t
>(replacement_color));
1704 if (!reopen_status.ok()) {
1705 formatter.
AddField(
"readback_status",
"external_reopen_failed");
1706 formatter.
AddField(
"readback_error", std::string(reopen_status.message()));
1707 formatter.
AddField(
"status",
"error");
1709 return reopen_status;
1712 formatter.
AddField(
"readback_status",
"external_reopen_verified");
1713 formatter.
AddField(
"status",
"success");
1715 return absl::OkStatus();
1733 if (x < 0 || x > 63 || y < 0 || y > 63) {
1734 return absl::InvalidArgumentError(
1735 "Door tile coordinates must be in range 0-63");
1739 int expected_type = 0;
1745 std::optional<ObjectSaveManifestContext> manifest_context;
1747 if (!manifest_context.has_value()) {
1748 return absl::FailedPreconditionError(
1749 "--manifest is required for dungeon-set-door-type");
1752 DoorTypeTarget target;
1754 target, ResolveDoorTypeTarget(rom, room_id, x, y, *manifest_context));
1755 const int old_type =
static_cast<int>(target.door.type);
1756 if (old_type != expected_type) {
1757 return absl::FailedPreconditionError(absl::StrFormat(
1758 "Door type compare-and-swap failed at (%d, %d): expected 0x%02X, "
1760 x, y, expected_type, old_type));
1762 if (new_type == old_type) {
1763 return absl::InvalidArgumentError(
1764 "--type must differ from the current door type");
1767 const bool do_write = parser.
HasFlag(
"write");
1768 if (do_write && rom->
filename().empty()) {
1769 return absl::FailedPreconditionError(
"Write mode requires a ROM filename");
1774 formatter.
AddField(
"door_index", target.door_index);
1777 formatter.
AddField(
"position", target.door.position);
1778 formatter.
AddField(
"direction", std::string(target.door.GetDirectionName()));
1780 formatter.
AddField(
"old_type_name", std::string(target.door.GetTypeName()));
1786 formatter.
AddHexField(
"object_stream_pc", target.object_stream_pc, 6);
1787 formatter.
AddHexField(
"object_stream_end_pc", target.object_stream_end_pc, 6);
1788 formatter.
AddHexField(
"door_pointer_pc", target.door_pointer_pc, 6);
1789 formatter.
AddHexField(
"door_entry_pc", target.door_entry_pc, 6);
1790 formatter.
AddHexField(
"type_pc", target.type_pc, 6);
1793 formatter.
AddField(
"manifest_write_policy",
"block");
1794 formatter.
AddField(
"stream_alias_status",
"unique");
1795 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
1796 formatter.
AddField(
"preflight_status",
"success");
1799 formatter.
AddField(
"write_status",
"not_requested");
1800 formatter.
AddField(
"save_status",
"not_requested");
1801 formatter.
AddField(
"readback_status",
"not_requested");
1802 formatter.
AddField(
"status",
"success");
1804 return absl::OkStatus();
1807 const auto finish_error = [&formatter](
const absl::Status& status,
1808 const char* field) -> absl::Status {
1809 formatter.
AddField(
"status",
"error");
1810 formatter.
AddField(field, std::string(status.message()));
1814 const auto verify_readback =
1815 [&target, new_type](
const DoorTypeTarget& readback) -> absl::Status {
1816 if (readback.door_index != target.door_index ||
1817 readback.type_pc != target.type_pc ||
1818 readback.door.position != target.door.position ||
1819 readback.door.direction != target.door.direction ||
1820 static_cast<int>(readback.door.type) != new_type) {
1821 return absl::DataLossError(
1822 "Door type readback does not match the requested change");
1824 return absl::OkStatus();
1828 const absl::Status allow_status = write_fence.
Allow(
1829 target.type_pc, target.type_pc + 1u,
"dungeon door type");
1830 if (!allow_status.ok()) {
1831 return finish_error(allow_status,
"write_error");
1834 const std::vector<uint8_t> before = rom->
vector();
1838 const absl::Status write_status = rom->
WriteByte(
1839 static_cast<int>(target.type_pc),
static_cast<uint8_t
>(new_type));
1840 if (!write_status.ok()) {
1841 return finish_error(write_status,
"write_error");
1845 if (rom->
size() != before.size()) {
1846 return finish_error(
1847 absl::DataLossError(
"Door type edit unexpectedly resized the ROM"),
1850 int changed_bytes = 0;
1851 uint32_t changed_pc = 0;
1852 for (uint32_t pc = 0; pc < before.size(); ++pc) {
1853 if (before[pc] != rom->
data()[pc]) {
1858 if (changed_bytes != 1 || changed_pc != target.type_pc) {
1859 return finish_error(
1860 absl::DataLossError(absl::StrFormat(
1861 "Door type edit changed %d bytes; expected only PC 0x%06X",
1862 changed_bytes, target.type_pc)),
1866 auto memory_readback =
1867 ResolveDoorTypeTarget(rom, room_id, x, y, *manifest_context);
1868 if (!memory_readback.ok()) {
1869 return finish_error(memory_readback.status(),
"readback_error");
1871 const absl::Status memory_verify = verify_readback(*memory_readback);
1872 if (!memory_verify.ok()) {
1873 return finish_error(memory_verify,
"readback_error");
1875 formatter.
AddField(
"readback_status",
"pre_save_verified");
1876 formatter.
AddField(
"write_status",
"success");
1878 const absl::Status save_status = SaveRomWithBackup(rom, formatter);
1879 if (!save_status.ok()) {
1880 formatter.
AddField(
"status",
"error");
1886 formatter.
AddField(
"status",
"success");
1888 return absl::OkStatus();
1902 PotItemStreamView stream;
1907 formatter.
AddHexField(
"pointer_slot_pc", stream.pointer_slot_pc, 6);
1908 formatter.
AddHexField(
"stream_pc", stream.stream_pc, 6);
1909 formatter.
AddHexField(
"stream_end_pc", stream.stream_end_pc, 6);
1910 formatter.
AddField(
"item_count",
static_cast<int>(stream.entries.size()));
1912 for (
const PotItemEntryView& entry : stream.entries) {
1914 formatter.
AddField(
"index", entry.index);
1915 formatter.
AddHexField(
"position", entry.item.position, 4);
1916 formatter.
AddField(
"tile_x", entry.item.GetTileX());
1917 formatter.
AddField(
"tile_y", entry.item.GetTileY());
1918 formatter.
AddHexField(
"item_id", entry.item.item, 2);
1919 formatter.
AddHexField(
"item_pc", entry.item_pc, 6);
1924 formatter.
AddField(
"status",
"success");
1926 return absl::OkStatus();
1938 int expected_position = 0;
1939 int expected_item = 0;
1945 GetRequiredHex(parser,
"expect-position"));
1948 if (expected_position < 0 || expected_position > 0xFFFF) {
1949 return absl::InvalidArgumentError(
1950 "--expect-position must be in range 0x0000-0xFFFF");
1952 if (expected_item < 0 || expected_item > 0xFF) {
1953 return absl::InvalidArgumentError(
1954 "--expect-item must be in range 0x00-0xFF");
1956 if (new_item < 0 || new_item > 0xFF) {
1957 return absl::InvalidArgumentError(
"--item must be in range 0x00-0xFF");
1960 PotItemManifestContext manifest_context;
1962 PotItemTarget target;
1964 ResolvePotItemTarget(rom, room_id, index, manifest_context));
1965 if (target.entry.item.position != expected_position ||
1966 target.entry.item.item != expected_item) {
1967 return absl::FailedPreconditionError(absl::StrFormat(
1968 "Pot-item compare-and-swap failed at index %d: expected "
1969 "position/item 0x%04X/0x%02X, found 0x%04X/0x%02X",
1970 index, expected_position, expected_item, target.entry.item.position,
1971 target.entry.item.item));
1973 if (new_item == expected_item) {
1974 return absl::InvalidArgumentError(
1975 "--item must differ from the current pot item");
1978 const bool do_write = parser.
HasFlag(
"write");
1979 if (do_write && rom->
filename().empty()) {
1980 return absl::FailedPreconditionError(
"Write mode requires a ROM filename");
1985 formatter.
AddField(
"index", index);
1986 formatter.
AddHexField(
"position", target.entry.item.position, 4);
1987 formatter.
AddField(
"tile_x", target.entry.item.GetTileX());
1988 formatter.
AddField(
"tile_y", target.entry.item.GetTileY());
1989 formatter.
AddHexField(
"old_item", expected_item, 2);
1991 formatter.
AddHexField(
"pointer_slot_pc", target.pointer_slot_pc, 6);
1992 formatter.
AddHexField(
"stream_pc", target.stream_pc, 6);
1993 formatter.
AddHexField(
"stream_end_pc", target.stream_end_pc, 6);
1994 formatter.
AddHexField(
"item_pc", target.entry.item_pc, 6);
1997 formatter.
AddField(
"manifest_write_policy",
"block");
1998 formatter.
AddField(
"stream_alias_status",
"unique");
1999 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
2000 formatter.
AddField(
"preflight_status",
"success");
2003 formatter.
AddField(
"write_status",
"not_requested");
2004 formatter.
AddField(
"save_status",
"not_requested");
2005 formatter.
AddField(
"readback_status",
"not_requested");
2006 formatter.
AddField(
"status",
"success");
2008 return absl::OkStatus();
2011 const auto finish_error = [&formatter](
const absl::Status& status,
2012 const char* field) -> absl::Status {
2013 formatter.
AddField(
"status",
"error");
2014 formatter.
AddField(field, std::string(status.message()));
2018 const auto verify_readback =
2019 [&target, new_item](
const PotItemTarget& readback) -> absl::Status {
2020 if (readback.entry.index != target.entry.index ||
2021 readback.entry.entry_pc != target.entry.entry_pc ||
2022 readback.entry.item_pc != target.entry.item_pc ||
2023 readback.entry.item.position != target.entry.item.position ||
2024 readback.entry.item.item != new_item ||
2025 readback.stream_pc != target.stream_pc ||
2026 readback.stream_end_pc != target.stream_end_pc) {
2027 return absl::DataLossError(
2028 "Pot-item readback does not match the requested change");
2030 return absl::OkStatus();
2034 const absl::Status allow_status = write_fence.
Allow(
2035 target.entry.item_pc, target.entry.item_pc + 1u,
"dungeon pot item");
2036 if (!allow_status.ok()) {
2037 return finish_error(allow_status,
"write_error");
2040 const std::vector<uint8_t> before = rom->
vector();
2044 const absl::Status write_status = rom->
WriteByte(
2045 static_cast<int>(target.entry.item_pc),
static_cast<uint8_t
>(new_item));
2046 if (!write_status.ok()) {
2047 return finish_error(write_status,
"write_error");
2051 if (rom->
size() != before.size()) {
2052 return finish_error(
2053 absl::DataLossError(
"Pot-item edit unexpectedly resized the ROM"),
2056 int changed_bytes = 0;
2057 uint32_t changed_pc = 0;
2058 for (uint32_t pc = 0; pc < before.size(); ++pc) {
2059 if (before[pc] != rom->
data()[pc]) {
2064 if (changed_bytes != 1 || changed_pc != target.entry.item_pc) {
2065 return finish_error(
2066 absl::DataLossError(absl::StrFormat(
2067 "Pot-item edit changed %d bytes; expected only PC 0x%06X",
2068 changed_bytes, target.entry.item_pc)),
2072 auto memory_readback =
2073 ResolvePotItemTarget(rom, room_id, index, manifest_context);
2074 if (!memory_readback.ok()) {
2075 return finish_error(memory_readback.status(),
"readback_error");
2077 const absl::Status memory_verify = verify_readback(*memory_readback);
2078 if (!memory_verify.ok()) {
2079 return finish_error(memory_verify,
"readback_error");
2081 formatter.
AddField(
"readback_status",
"pre_save_verified");
2082 formatter.
AddField(
"write_status",
"success");
2084 const absl::Status save_status = SaveRomWithBackup(rom, formatter);
2085 if (!save_status.ok()) {
2086 formatter.
AddField(
"status",
"error");
2092 formatter.
AddField(
"status",
"success");
2094 return absl::OkStatus();
2104 auto room_id_str_or = GetRequiredString(parser,
"room");
2105 if (!room_id_str_or.ok()) {
2106 return room_id_str_or.status();
2108 auto tiles_str_or = GetRequiredString(parser,
"tiles");
2109 if (!tiles_str_or.ok()) {
2110 return tiles_str_or.status();
2112 const std::string room_id_str = room_id_str_or.value();
2113 const std::string tiles_str = tiles_str_or.value();
2116 if (!ParseHexString(room_id_str, &room_id)) {
2117 return absl::InvalidArgumentError(
"Invalid room ID. Must be hex.");
2119 auto room_status = ValidateRoomId(room_id);
2120 if (!room_status.ok()) {
2124 bool do_write = parser.
HasFlag(
"write");
2130 std::vector<TileSpec> specs;
2132 for (absl::string_view entry :
2133 absl::StrSplit(tiles_str,
';', absl::SkipEmpty())) {
2134 std::vector<std::string> parts =
2135 absl::StrSplit(entry,
',', absl::SkipEmpty());
2136 if (parts.size() != 3) {
2137 return absl::InvalidArgumentError(absl::StrFormat(
2138 "Invalid tile spec '%s'. Expected x,y,tile (e.g. 10,5,0xB7)", entry));
2143 if (!absl::SimpleAtoi(parts[0], &spec.x)) {
2144 return absl::InvalidArgumentError(
2145 absl::StrFormat(
"Invalid X coord '%s'", parts[0]));
2147 if (!absl::SimpleAtoi(parts[1], &spec.y)) {
2148 return absl::InvalidArgumentError(
2149 absl::StrFormat(
"Invalid Y coord '%s'", parts[1]));
2151 if (!ParseHexString(parts[2], &spec.tile)) {
2152 return absl::InvalidArgumentError(
2153 absl::StrFormat(
"Invalid tile value '%s'. Must be hex.", parts[2]));
2157 if (spec.x < 0 || spec.x > 63 || spec.y < 0 || spec.y > 63) {
2158 return absl::InvalidArgumentError(absl::StrFormat(
2159 "Tile coords (%d,%d) out of range (0-63)", spec.x, spec.y));
2161 if (spec.tile < 0 || spec.tile > 0xFF) {
2162 return absl::InvalidArgumentError(
"Tile value must be 0x00-0xFF");
2165 specs.push_back(spec);
2168 if (specs.empty()) {
2169 return absl::InvalidArgumentError(
"No tile specs provided");
2178 formatter.
AddField(
"tile_count",
static_cast<int>(specs.size()));
2179 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
2183 for (
const auto& spec : specs) {
2198 MutateAndSaveRomWithBackup(rom, formatter, [&rom, &room]()
mutable {
2200 std::array<zelda3::Room, 1> rooms_arr = {std::move(room)};
2203 if (!save_status.ok()) {
2210 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.
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
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 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)
bool ParseHexString(absl::string_view str, int *out)
std::string AddressOwnershipToString(AddressOwnership ownership)
absl::StatusOr< zelda3::DungeonStreamLayout > ToDungeonStreamAllocatorLayout(DungeonStreamType stream_type, const DungeonStreamLayout &manifest_layout)
constexpr int DungeonsMainPalettesMax
constexpr int kDungeonMainPalettes
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
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.