178 return absl::FailedPreconditionError(
"ROM not loaded");
181 if (!util::ParseHexString(*parser.
GetString(
"screen"), &screen))
182 return absl::InvalidArgumentError(
"--screen must be hex (e.g. 0x40)");
184 if (!absl::SimpleAtoi(*parser.
GetString(
"phase"), &phase) || phase < 0 ||
186 return absl::InvalidArgumentError(
"--phase must be 0, 1, or 2");
188 const bool do_write = parser.
HasFlag(
"write");
193 std::filesystem::path(rom->
filename()));
194 if (do_write && !parser.
HasFlag(
"allow-project-rom") &&
196 return absl::PermissionDeniedError(absl::StrFormat(
197 "Refusing --write to %s: it is inside an Oracle of Secrets checkout "
198 "(Oracle_main.asm next to its Roms/ folder). Copy the ROM (with "
199 "Data/dungeons/custom_collision.json) and write the copy, or pass "
200 "--allow-project-rom deliberately.",
201 active_path.string()));
208 return absl::InvalidArgumentError(
209 absl::StrFormat(
"Screen 0x%02X is out of range", screen));
210 const int parent = map->parent();
211 int cols = 1, rows = 1;
229 if (parent >= layout.counts[phase]) {
230 return absl::InvalidArgumentError(absl::StrFormat(
231 "Phase %d has no sprite list for area 0x%02X in this ROM "
233 phase, parent, layout.counts[phase]));
235 const int pointer_pc = layout.tables[phase] + parent * 2;
238 const auto before = Decode(old_bytes);
241 auto check_tile = [&](
int x,
int y) -> absl::Status {
242 if (x < 0 || y < 0 || x >= cols * 32 || y >= rows * 32) {
243 return absl::OutOfRangeError(absl::StrFormat(
244 "Tile (%d,%d) is outside area 0x%02X (%dx%d tiles of 16px)", x, y,
245 parent, cols * 32, rows * 32));
247 return absl::OkStatus();
249 auto check_index = [&](
const std::string& flag) -> absl::StatusOr<int> {
251 if (index < 0 || index >=
static_cast<int>(before.size())) {
252 return absl::OutOfRangeError(
253 absl::StrFormat(
"--%s %d out of range (list has %d entries)", flag,
254 index,
static_cast<int>(before.size())));
256 if (
auto expect = parser.
GetString(
"expect-id"); expect.has_value()) {
258 if (!util::ParseHexString(*expect, &
id) || before[index].id != id) {
259 return absl::FailedPreconditionError(
260 absl::StrFormat(
"--expect-id %s does not match entry %d (0x%02X)",
261 *expect, index, before[index].
id));
267 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
269 formatter.
AddField(
"requested_screen", absl::StrFormat(
"0x%02X", screen));
270 formatter.
AddField(
"parent_area", absl::StrFormat(
"0x%02X", parent));
272 absl::StrFormat(
"%dx%d", cols * 32, rows * 32));
279 if (!util::ParseHexString(*parser.
GetString(
"id"), &
id) ||
id < 0 ||
281 return absl::InvalidArgumentError(
"--id must be hex 0x00-0xFF");
286 if (parser.
GetString(
"replace-index").has_value()) {
288 formatter.
AddField(
"replaced_index", replace);
289 after.erase(after.begin() + replace);
291 after.push_back({
static_cast<uint8_t
>(y),
static_cast<uint8_t
>(x),
292 static_cast<uint8_t
>(id)});
293 formatter.
AddField(
"sprite_id", absl::StrFormat(
"0x%02X",
id));
302 after[index].x =
static_cast<uint8_t
>((after[index].x & 0xC0) | x);
303 after[index].y =
static_cast<uint8_t
>((after[index].y & 0xC0) | y);
309 after.erase(after.begin() + index);
315 AddEntries(formatter,
"list_before", before);
316 AddEntries(formatter,
"list_after", after);
318 formatter.
BeginArray(
"duplicate_entries_before");
319 for (
size_t i = 0; i < before.size(); ++i) {
320 for (
size_t j = 0; j < i; ++j) {
321 if (before[i].x == before[j].x && before[i].y == before[j].y &&
322 before[i].
id == before[j].
id) {
323 formatter.
AddArrayItem(absl::StrFormat(
"index %d duplicates index %d",
325 static_cast<int>(j)));
332 const auto new_bytes = Encode(after);
335 absl::StrFormat(
"PC $%06X / SNES %s", pointer_pc, Snes(pointer_pc)));
339 formatter.
AddField(
"strategy",
"refused");
340 formatter.
AddField(
"error", std::string(plan_or.status().message()));
341 return plan_or.status();
343 const auto& plan = *plan_or;
347 absl::StrFormat(
"PC $%06X / SNES %s, %d bytes",
348 plan.old_list_pc, Snes(plan.old_list_pc),
349 static_cast<int>(plan.old_bytes.size())));
351 absl::StrFormat(
"PC $%06X / SNES %s, %d bytes",
352 plan.new_list_pc, Snes(plan.new_list_pc),
353 static_cast<int>(plan.new_bytes.size())));
354 formatter.
AddField(
"old_list_shared_with_slots",
355 static_cast<int>(plan.sharers.size()));
357 absl::StrFormat(
"PC $%06X-$%06X (%d unreferenced bytes)",
358 plan.region_start, plan.region_end,
359 plan.unreferenced_bytes));
361 const auto& rom_bytes = rom->
vector();
362 for (
const auto& write : plan.writes) {
363 const std::vector<uint8_t> old(
364 rom_bytes.begin() + write.address,
365 rom_bytes.begin() + write.address + write.bytes.size());
367 formatter.
AddField(
"pc", absl::StrFormat(
"$%06X", write.address));
368 formatter.
AddField(
"snes", Snes(write.address));
369 formatter.
AddField(
"length",
static_cast<int>(write.bytes.size()));
370 formatter.
AddField(
"old", Hex(old));
371 formatter.
AddField(
"new", Hex(write.bytes));
376 if (!do_write || plan.writes.empty()) {
377 formatter.
AddField(
"write_status", do_write ?
"not-needed" :
"dry-run");
378 return absl::OkStatus();
381 const auto before_rom = rom->
vector();
386 if (
auto status = rom->
SaveToFile(save_settings); !status.ok()) {
387 formatter.
AddField(
"save_error", std::string(status.message()));
391 formatter.
AddField(
"save_status",
"saved");
399 if (readback != new_bytes)
400 return absl::DataLossError(
"Saved sprite list does not read back");
401 const auto& saved = reopened.
vector();
402 if (saved.size() != before_rom.size())
403 return absl::DataLossError(
"Saved ROM size changed");
405 for (
size_t i = 0; i < saved.size(); ++i) {
406 if (saved[i] == before_rom[i])
409 bool planned =
false;
410 for (
const auto& write : plan.writes) {
411 planned |=
static_cast<int>(i) >= write.address &&
412 static_cast<int>(i) <
413 write.address +
static_cast<int>(write.bytes.size());
416 return absl::DataLossError(
417 absl::StrFormat(
"Unplanned byte changed at PC $%06X", i));
419 formatter.
AddField(
"verified_reload",
true);
420 formatter.
AddField(
"changed_bytes", changed);
421 return absl::OkStatus();
std::optional< std::filesystem::path > active_rom_path
std::optional< std::filesystem::path > source_rom_path