yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_edit_commands.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstdint>
5#include <limits>
6#include <optional>
7#include <string>
8#include <utility>
9#include <vector>
10
11#include "absl/strings/numbers.h"
12#include "absl/strings/str_format.h"
13#include "absl/strings/str_split.h"
15#include "cli/util/hex_util.h"
17#include "core/hack_manifest.h"
18#include "core/project.h"
19#include "rom/rom.h"
20#include "rom/snes.h"
21#include "rom/transaction.h"
22#include "rom/write_fence.h"
23#include "util/macro.h"
25#include "zelda3/dungeon/room.h"
28#include "zelda3/game_data.h"
31
32namespace yaze {
33namespace cli {
34namespace handlers {
35
37
38namespace {
39
40absl::StatusOr<std::string> GetRequiredString(
41 const resources::ArgumentParser& parser, const char* name) {
42 auto value = parser.GetString(name);
43 if (!value.has_value()) {
44 return absl::InvalidArgumentError(
45 absl::StrFormat("Missing required argument '--%s'", name));
46 }
47 return *value;
48}
49
50absl::StatusOr<int> GetRequiredInt(const resources::ArgumentParser& parser,
51 const char* name) {
52 auto parsed = parser.GetInt(name);
53 if (!parsed.ok()) {
54 return parsed.status();
55 }
56 return parsed.value();
57}
58
59absl::StatusOr<int> GetOptionalInt(const resources::ArgumentParser& parser,
60 const char* name, int default_value) {
61 if (!parser.GetString(name).has_value()) {
62 return default_value;
63 }
64
65 auto parsed = parser.GetInt(name);
66 if (!parsed.ok()) {
67 return parsed.status();
68 }
69 return parsed.value();
70}
71
72absl::Status ValidateRoomId(int room_id) {
73 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms) {
74 return absl::InvalidArgumentError(
75 absl::StrFormat("Room ID out of range: 0x%X (expected 0x00-0x%02X)",
76 room_id, zelda3::kNumberOfRooms - 1));
77 }
78 return absl::OkStatus();
79}
80
81absl::Status ValidateSpriteCoord(int value, char axis) {
82 if (value < 0 || value > 31) {
83 return absl::InvalidArgumentError(
84 absl::StrFormat("%c must be 0-31 (5-bit tile coord)", axis));
85 }
86 return absl::OkStatus();
87}
88
89absl::Status SaveRomWithBackup(Rom* rom,
90 resources::OutputFormatter& formatter) {
91 Rom::SaveSettings save_settings;
92 save_settings.require_backup = true;
93 auto disk_status = rom->SaveToFile(save_settings);
94 if (!disk_status.ok()) {
95 formatter.AddField("save_error", std::string(disk_status.message()));
96 return disk_status;
97 }
98
99 formatter.AddField("save_status", "saved");
100 return absl::OkStatus();
101}
102
103template <typename Mutation>
106 Mutation&& mutation) {
107 ScopedRomTransaction transaction(*rom);
108
109 auto write_status = std::forward<Mutation>(mutation)();
110 if (!write_status.ok()) {
111 formatter.AddField("write_error", std::string(write_status.message()));
112 return write_status;
113 }
114 formatter.AddField("write_status", "success");
115
116 RETURN_IF_ERROR(SaveRomWithBackup(rom, formatter));
117 transaction.Commit();
118 return absl::OkStatus();
119}
120
124 // A standalone --manifest is a safety capability, not permission to ignore
125 // ownership. Unlike a project, the CLI has no separate policy setting, so
126 // protected writes are always blocked.
127 project::RomWritePolicy write_policy = project::RomWritePolicy::kBlock;
128};
129
130absl::StatusOr<std::optional<ObjectSaveManifestContext>>
132 const auto manifest_path = parser.GetString("manifest");
133 if (!manifest_path.has_value()) {
134 return std::nullopt;
135 }
136
138 RETURN_IF_ERROR(context.manifest.LoadFromFile(*manifest_path));
139 const core::DungeonStreamLayout* manifest_layout =
142 if (manifest_layout == nullptr) {
143 return absl::FailedPreconditionError(
144 "Manifest does not define dungeon_stream_regions.objects");
145 }
146 if (manifest_layout->strategy != core::DungeonWriteStrategy::kCopyOnWrite) {
147 return absl::FailedPreconditionError(
148 "dungeon_stream_regions.objects must use copy_on_write");
149 }
150
153 core::DungeonStreamType::kObjects, *manifest_layout));
154 return std::optional<ObjectSaveManifestContext>(std::move(context));
155}
156
158 const Rom& source_rom, int room_id, const zelda3::Room& pending_room,
159 const ObjectSaveManifestContext& context) {
160 std::vector<std::pair<uint32_t, uint32_t>> ranges;
161
162 // Match DungeonEditorV2's conservative object-save prediction: validate the
163 // selected in-place stream, the door pointer, every possible COW allocation,
164 // and the selected object-pointer slot before exercising either save path.
165 uint32_t object_pointer_table_snes = 0;
166 ASSIGN_OR_RETURN(object_pointer_table_snes,
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");
175 }
176
177 uint32_t current_stream_snes = 0;
178 ASSIGN_OR_RETURN(current_stream_snes,
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) +
182 pending_room.EncodeObjects().size() + 2u;
183 if (current_stream_end > std::numeric_limits<uint32_t>::max()) {
184 return absl::OutOfRangeError("Selected object write range overflows");
185 }
186 ranges.emplace_back(current_stream_pc,
187 static_cast<uint32_t>(current_stream_end));
188
189 const uint64_t door_pointer_slot =
190 static_cast<uint64_t>(zelda3::kDoorPointers) +
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");
194 }
195 ranges.emplace_back(static_cast<uint32_t>(door_pointer_slot),
196 static_cast<uint32_t>(door_pointer_slot + 3u));
197
198 for (const auto& range : context.allocator_layout.allocation_ranges) {
199 ranges.emplace_back(range.begin, range.end);
200 }
201 const uint32_t pointer_width = context.allocator_layout.pointer_encoding ==
203 ? 3u
204 : 2u;
205 const uint64_t cow_pointer_slot =
206 static_cast<uint64_t>(context.allocator_layout.pointer_table_pc) +
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");
210 }
211 ranges.emplace_back(static_cast<uint32_t>(cow_pointer_slot),
212 static_cast<uint32_t>(cow_pointer_slot + pointer_width));
213
214 const auto conflicts = context.manifest.AnalyzePcWriteRanges(ranges);
215 if (conflicts.empty() ||
217 return absl::OkStatus();
218 }
219 return absl::PermissionDeniedError("Write conflict with Hack Manifest");
220}
221
223 const Rom& source_rom, int room_id, const zelda3::RoomObject& object,
224 const zelda3::Room& pending_room,
225 const ObjectSaveManifestContext* manifest_context) {
226 if (manifest_context != nullptr) {
228 source_rom, room_id, pending_room, *manifest_context));
229 }
230
231 // Exercise the exact Room::SaveObjects path against an isolated snapshot so
232 // dry-run capacity and allocator outcomes match --write without mutating the
233 // caller's ROM.
234 Rom scratch_rom;
235 Rom::LoadOptions options;
236 options.strip_header = false;
237 options.load_resource_labels = false;
238 RETURN_IF_ERROR(scratch_rom.LoadFromData(source_rom.vector(), options));
239
240 zelda3::Room scratch_room = zelda3::LoadRoomFromRom(&scratch_rom, room_id);
241 RETURN_IF_ERROR(scratch_room.AddObject(object));
242 return scratch_room.SaveObjects(manifest_context != nullptr
243 ? &manifest_context->allocator_layout
244 : nullptr);
245}
246
247absl::StatusOr<int> GetRequiredHex(const resources::ArgumentParser& parser,
248 const char* name) {
249 auto value_or = GetRequiredString(parser, name);
250 if (!value_or.ok()) {
251 return value_or.status();
252 }
253
254 int value = 0;
255 if (!ParseHexString(*value_or, &value)) {
256 return absl::InvalidArgumentError(absl::StrFormat(
257 "Invalid --%s value '%s' (expected integer/hex)", name, *value_or));
258 }
259 return value;
260}
261
262absl::Status ValidateDoorType(int type, const char* argument_name) {
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));
266 }
267 return absl::OkStatus();
268}
269
270bool ContainsRoomId(const std::vector<uint32_t>& room_ids, int room_id) {
271 return std::find(room_ids.begin(), room_ids.end(),
272 static_cast<uint32_t>(room_id)) != room_ids.end();
273}
274
275bool IsLoRomDataPointer(uint32_t snes_address) {
276 if ((snes_address & 0xFFFFu) < 0x8000u) {
277 return false;
278 }
279 const uint8_t normalized_bank =
280 static_cast<uint8_t>((snes_address >> 16) & 0x7Fu);
281 if (normalized_bank >= 0x7Eu) {
282 return false;
283 }
284 const uint32_t pc_address = SnesToPc(snes_address);
285 return (PcToSnes(pc_address) & 0x7FFFFFu) == (snes_address & 0x7FFFFFu);
286}
287
288constexpr int kDungeonPaletteColorCount = 90;
291constexpr uint32_t kSnesDestinationCodePc = 0x7FD9;
292constexpr uint32_t kDungeonPaletteDataEnd =
295
296bool IsValidRomRange(const Rom& rom, uint64_t start, uint64_t length) {
297 return start <= rom.size() && length <= rom.size() - start;
298}
299
300absl::Status ValidateSupportedDungeonPaletteRom(const Rom& rom) {
302 return absl::FailedPreconditionError(
303 "ROM is too small to prove US/OOS dungeon palette table layout");
304 }
305 uint8_t destination_code = 0;
306 ASSIGN_OR_RETURN(destination_code,
307 rom.ReadByte(static_cast<int>(kSnesDestinationCodePc)));
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",
312 destination_code));
313 }
314 return absl::OkStatus();
315}
316
318 int room_id = -1;
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;
327};
328
329absl::StatusOr<uint32_t> ResolveRoomHeaderPcStrict(const Rom& rom,
330 int room_id) {
334 return absl::FailedPreconditionError(
335 "Dungeon room-header pointer metadata is outside the ROM");
336 }
337
338 uint32_t table_snes = 0;
340 if (!IsLoRomDataPointer(table_snes)) {
341 return absl::FailedPreconditionError(absl::StrFormat(
342 "Dungeon room-header pointer table is not a valid LoROM pointer: "
343 "0x%06X",
344 table_snes));
345 }
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;
349 if (!IsValidRomRange(rom, slot_pc, 2)) {
350 return absl::FailedPreconditionError(absl::StrFormat(
351 "Dungeon room 0x%03X header pointer slot is outside the ROM", room_id));
352 }
353
354 uint16_t header_offset = 0;
355 uint8_t header_bank = 0;
356 ASSIGN_OR_RETURN(header_offset, rom.ReadWord(static_cast<int>(slot_pc)));
358 const uint32_t header_snes =
359 (static_cast<uint32_t>(header_bank) << 16) | header_offset;
360 if (!IsLoRomDataPointer(header_snes)) {
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));
364 }
365
366 const uint32_t header_pc = SnesToPc(header_snes);
367 if (!IsValidRomRange(rom, header_pc, 14)) {
368 return absl::FailedPreconditionError(absl::StrFormat(
369 "Dungeon room 0x%03X header is outside the ROM", room_id));
370 }
371 return header_pc;
372}
373
374absl::StatusOr<DungeonPaletteRoomMapping> ResolveDungeonPaletteRoomMapping(
375 const Rom& rom, int room_id) {
377 mapping.room_id = room_id;
379 ResolveRoomHeaderPcStrict(rom, room_id));
381 rom.ReadByte(static_cast<int>(mapping.room_header_pc + 1u)));
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)",
386 room_id, mapping.palette_set_id, zelda3::kNumPalettesets - 1));
387 }
388
390 static_cast<uint32_t>(mapping.palette_set_id) * 4u;
391 if (!IsValidRomRange(rom, mapping.palette_set_pc, 4)) {
392 return absl::FailedPreconditionError(absl::StrFormat(
393 "Palette-set entry 0x%02X is outside the ROM", mapping.palette_set_id));
394 }
396 rom.ReadByte(static_cast<int>(mapping.palette_set_pc)));
397 if ((mapping.palette_pointer_offset & 1u) != 0u ||
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)",
402 mapping.palette_set_id, mapping.palette_pointer_offset,
403 kDungeonPaletteCount * 2 - 2));
404 }
405
406 mapping.palette_pointer_pc =
408 if (!IsValidRomRange(rom, mapping.palette_pointer_pc, 2)) {
409 return absl::FailedPreconditionError(
410 "Resolved dungeon palette pointer is outside the ROM");
411 }
413 rom.ReadWord(static_cast<int>(mapping.palette_pointer_pc)));
415 return absl::FailedPreconditionError(absl::StrFormat(
416 "Palette-set 0x%02X resolves to malformed palette word 0x%04X "
417 "(not divisible by %d)",
418 mapping.palette_set_id, mapping.palette_pointer_word,
420 }
421
422 mapping.palette_index =
424 if (mapping.palette_index < 0 ||
426 return absl::FailedPreconditionError(absl::StrFormat(
427 "Palette-set 0x%02X resolves to out-of-range dungeon palette %d",
428 mapping.palette_set_id, mapping.palette_index));
429 }
430
432 if (mapping.palette_pc < kDungeonPaletteDataStart ||
433 static_cast<uint64_t>(mapping.palette_pc) + zelda3::kDungeonPaletteBytes >
436 return absl::FailedPreconditionError(absl::StrFormat(
437 "Palette-set 0x%02X resolves outside the fixed dungeon palette "
438 "region [0x%06X,0x%06X)",
441 }
442 return mapping;
443}
444
449
450absl::StatusOr<DungeonPaletteSelection> ResolveDungeonPaletteSelection(
451 const Rom& rom, int room_id) {
453 DungeonPaletteSelection selection;
454 ASSIGN_OR_RETURN(selection.selected,
456 selection.affected_rooms.reserve(zelda3::kNumberOfRooms);
457 for (int candidate = 0; candidate < zelda3::kNumberOfRooms; ++candidate) {
458 auto mapping_or = ResolveDungeonPaletteRoomMapping(rom, candidate);
459 if (!mapping_or.ok()) {
460 return absl::Status(
461 mapping_or.status().code(),
462 absl::StrFormat(
463 "Cannot prove complete shared palette fanout because room "
464 "0x%03X mapping is invalid: %s",
465 candidate, mapping_or.status().message()));
466 }
467 if (mapping_or->palette_index == selection.selected.palette_index) {
468 selection.affected_rooms.push_back(static_cast<uint32_t>(candidate));
469 }
470 }
471 return selection;
472}
473
474absl::Status ValidateDungeonPaletteColorIndex(int color_index) {
475 if (color_index < 0 || color_index >= kDungeonPaletteColorCount) {
476 return absl::InvalidArgumentError(absl::StrFormat(
477 "--index must be in range 0-%d", kDungeonPaletteColorCount - 1));
478 }
479 return absl::OkStatus();
480}
481
482absl::Status ValidateSnes15BitColor(int color, const char* argument_name) {
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",
486 argument_name));
487 }
488 return absl::OkStatus();
489}
490
492 int color_index) {
493 return mapping.palette_pc + static_cast<uint32_t>(color_index) * 2u;
494}
495
496absl::StatusOr<uint16_t> ReadDungeonPaletteColor(
497 const Rom& rom, const DungeonPaletteRoomMapping& mapping, int color_index) {
499 const uint32_t color_pc = DungeonPaletteColorPc(mapping, color_index);
500 if (!IsValidRomRange(rom, color_pc, 2) ||
501 color_pc < kDungeonPaletteDataStart ||
502 static_cast<uint64_t>(color_pc) + 2u > kDungeonPaletteDataEnd) {
503 return absl::FailedPreconditionError(
504 "Resolved dungeon palette color is outside the fixed palette region");
505 }
506 uint16_t color = 0;
507 ASSIGN_OR_RETURN(color, rom.ReadWord(static_cast<int>(color_pc)));
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 "
511 "color: 0x%04X",
512 color_pc, color));
513 }
514 return color;
515}
516
518 const DungeonPaletteSelection& selection) {
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);
525 formatter.AddHexField("palette_pointer_offset",
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,
529 4);
530 formatter.AddHexField("palette_pc", mapping.palette_pc, 6);
531 formatter.AddHexField("palette_snes", PcToSnes(mapping.palette_pc), 6);
532 formatter.AddField("scope", "shared_global_dungeon_palette");
533 formatter.AddField("affected_room_count",
534 static_cast<int>(selection.affected_rooms.size()));
535 formatter.BeginArray("affected_rooms");
536 for (uint32_t affected_room : selection.affected_rooms) {
537 formatter.AddArrayItem(absl::StrFormat("0x%03X", affected_room));
538 }
539 formatter.EndArray();
540}
541
543 const core::HackManifest& manifest, uint32_t color_pc) {
544 const auto conflicts =
545 manifest.AnalyzePcWriteRanges({{color_pc, color_pc + 2u}});
546 if (conflicts.empty()) {
547 return absl::OkStatus();
548 }
549 const core::WriteConflict& conflict = conflicts.front();
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));
557}
558
559absl::StatusOr<std::vector<uint32_t>> ValidateDungeonPaletteWholeRomDiff(
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");
564 }
565
566 std::vector<uint32_t> changed;
567 for (uint32_t pc = 0; pc < before.size(); ++pc) {
568 if (before[pc] == after.data()[pc]) {
569 continue;
570 }
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));
574 }
575 changed.push_back(pc);
576 }
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",
581 changed.size()));
582 }
583 return changed;
584}
585
586absl::Status ValidateDungeonPaletteWriteBaseline(const Rom& rom) {
587 if (rom.dirty()) {
588 return absl::FailedPreconditionError(
589 "Dungeon palette write requires a clean file-backed ROM; save or "
590 "discard unrelated in-memory edits first");
591 }
592 if (rom.filename().empty()) {
593 return absl::FailedPreconditionError(
594 "Dungeon palette write requires a ROM filename");
595 }
596
597 Rom disk_rom;
598 Rom::LoadOptions options;
599 options.strip_header = false;
600 options.load_resource_labels = false;
601 RETURN_IF_ERROR(disk_rom.LoadFromFile(rom.filename(), options));
602 if (disk_rom.vector() != rom.vector()) {
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");
606 }
607 return absl::OkStatus();
608}
609
611 const Rom& saved_rom, int room_id, int expected_palette_set,
612 int expected_palette_index, int color_index, uint16_t expected_color) {
613 if (saved_rom.filename().empty()) {
614 return absl::FailedPreconditionError(
615 "Write mode requires a ROM filename for external readback");
616 }
617
618 Rom reopened;
619 Rom::LoadOptions options;
620 options.strip_header = false;
621 options.load_resource_labels = false;
622 RETURN_IF_ERROR(reopened.LoadFromFile(saved_rom.filename(), options));
623 if (reopened.vector() != saved_rom.vector()) {
624 return absl::DataLossError(
625 "External reopen bytes do not match the saved in-memory ROM");
626 }
627
629 DungeonPaletteRoomMapping reopened_mapping;
630 ASSIGN_OR_RETURN(reopened_mapping,
631 ResolveDungeonPaletteRoomMapping(reopened, room_id));
632 if (reopened_mapping.palette_set_id != expected_palette_set ||
633 reopened_mapping.palette_index != expected_palette_index) {
634 return absl::DataLossError(
635 "External reopen changed the selected dungeon palette mapping");
636 }
637 uint16_t reopened_color = 0;
639 reopened_color,
640 ReadDungeonPaletteColor(reopened, reopened_mapping, color_index));
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));
645 }
646 return absl::OkStatus();
647}
648
650 int door_index = -1;
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;
657};
658
659absl::StatusOr<DoorTypeTarget> ResolveDoorTypeTarget(
660 Rom* rom, int room_id, int x, int y,
661 const ObjectSaveManifestContext& manifest_context) {
662 if (manifest_context.allocator_layout.pointer_encoding !=
664 return absl::FailedPreconditionError(
665 "Dungeon object manifest pointer encoding must be long24");
666 }
667 if (manifest_context.allocator_layout.pointer_count !=
669 return absl::FailedPreconditionError(absl::StrFormat(
670 "Dungeon object manifest pointer_count must equal %d rooms",
672 }
673
674 uint32_t runtime_pointer_table_snes = 0;
675 ASSIGN_OR_RETURN(runtime_pointer_table_snes,
677 if (!IsLoRomDataPointer(runtime_pointer_table_snes)) {
678 return absl::FailedPreconditionError(
679 "Runtime object pointer table is not a valid LoROM data pointer");
680 }
681 const uint32_t runtime_pointer_table_pc =
682 SnesToPc(runtime_pointer_table_snes);
683 if (runtime_pointer_table_pc !=
684 manifest_context.allocator_layout.pointer_table_pc) {
685 return absl::FailedPreconditionError(absl::StrFormat(
686 "Manifest object pointer table PC 0x%06X does not match runtime "
687 "table PC 0x%06X",
688 manifest_context.allocator_layout.pointer_table_pc,
689 runtime_pointer_table_pc));
690 }
691
694 *rom, manifest_context.allocator_layout));
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");
698 }
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));
703 }
704 }
705
706 const auto& stream = inventory.streams[room_id];
707 if (!stream.valid) {
708 return absl::FailedPreconditionError("Selected object stream is invalid");
709 }
710 for (const auto& alias : inventory.aliases) {
711 if (ContainsRoomId(alias.room_ids, room_id)) {
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())));
715 }
716 }
717 for (const auto& overlap : inventory.overlaps) {
718 if (ContainsRoomId(overlap.first_room_ids, room_id) ||
719 ContainsRoomId(overlap.second_room_ids, room_id)) {
720 return absl::FailedPreconditionError(absl::StrFormat(
721 "Selected object stream overlaps PC range [0x%06X, 0x%06X)",
722 overlap.intersection.begin, overlap.intersection.end));
723 }
724 }
725 for (const auto& other_stream : inventory.streams) {
726 if (other_stream.room_id == static_cast<uint32_t>(room_id)) {
727 continue;
728 }
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));
736 }
737 }
738 for (int other_room_id = 0; other_room_id < zelda3::kNumberOfRooms;
739 ++other_room_id) {
740 if (other_room_id == room_id) {
741 continue;
742 }
743 const uint32_t other_door_pointer_slot =
744 zelda3::kDoorPointers + static_cast<uint32_t>(other_room_id) * 3u;
745 uint32_t other_door_pointer_snes = 0;
746 ASSIGN_OR_RETURN(other_door_pointer_snes,
747 rom->ReadLong(static_cast<int>(other_door_pointer_slot)));
748 if (!IsLoRomDataPointer(other_door_pointer_snes)) {
749 continue;
750 }
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));
759 }
760 }
761
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");
769 }
770 uint32_t runtime_stream_snes = 0;
771 ASSIGN_OR_RETURN(runtime_stream_snes,
772 rom->ReadLong(static_cast<int>(runtime_pointer_slot)));
773 if (!IsLoRomDataPointer(runtime_stream_snes)) {
774 return absl::FailedPreconditionError(
775 "Selected runtime object stream is not a valid LoROM data pointer");
776 }
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 "
781 "PC 0x%06X",
782 runtime_stream_pc, stream.data_pc));
783 }
784
785 zelda3::Room room = zelda3::LoadRoomFromRom(rom, room_id);
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) {
791 continue;
792 }
793 if (selected_index >= 0) {
794 return absl::FailedPreconditionError(absl::StrFormat(
795 "Multiple doors match tile coordinates (%d, %d)", x, y));
796 }
797 selected_index = index;
798 }
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,
802 room_id));
803 }
804
805 const uint64_t door_pointer_slot =
806 static_cast<uint64_t>(zelda3::kDoorPointers) +
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");
810 }
811 uint32_t door_pointer_snes = 0;
812 ASSIGN_OR_RETURN(door_pointer_snes,
813 rom->ReadLong(static_cast<int>(door_pointer_slot)));
814 if (!IsLoRomDataPointer(door_pointer_snes)) {
815 return absl::FailedPreconditionError(
816 "Selected door pointer is not a valid LoROM data pointer");
817 }
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");
829 }
830
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;
838 ASSIGN_OR_RETURN(entry_byte1, rom->ReadByte(static_cast<int>(entry_pc)));
839 ASSIGN_OR_RETURN(entry_byte2,
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");
847 }
848 if (index == selected_index) {
849 raw_byte1 = entry_byte1;
850 raw_byte2 = entry_byte2;
851 }
852 }
853 uint16_t door_terminator = 0;
854 ASSIGN_OR_RETURN(door_terminator,
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");
859 }
860
861 const zelda3::Room::Door raw_door =
862 zelda3::Room::Door::FromRomBytes(raw_byte1, raw_byte2);
863 const zelda3::Room::Door& model_door = doors[selected_index];
864 const auto [raw_x, raw_y] = raw_door.GetTileCoords();
865 if (model_door.position != raw_door.position ||
866 model_door.direction != raw_door.direction ||
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");
870 }
871
872 const uint32_t type_pc = static_cast<uint32_t>(door_entry_pc + 1u);
873 const auto conflicts =
874 manifest_context.manifest.AnalyzePcWriteRanges({{type_pc, type_pc + 1u}});
875 if (!conflicts.empty()) {
876 return absl::PermissionDeniedError(absl::StrFormat(
877 "Door type byte at PC 0x%06X conflicts with Hack Manifest (%s)",
878 type_pc, core::AddressOwnershipToString(conflicts.front().ownership)));
879 }
880
881 DoorTypeTarget target;
882 target.door_index = selected_index;
883 target.door = model_door;
884 target.object_stream_pc = stream.data_pc;
885 target.object_stream_end_pc = stream.logical_end_pc;
886 target.door_pointer_pc = door_pointer_pc;
887 target.door_entry_pc = static_cast<uint32_t>(door_entry_pc);
888 target.type_pc = type_pc;
889 return target;
890}
891
896
897absl::StatusOr<PotItemManifestContext> LoadPotItemManifestContext(
898 const resources::ArgumentParser& parser) {
899 std::string manifest_path;
900 ASSIGN_OR_RETURN(manifest_path, GetRequiredString(parser, "manifest"));
901
903 RETURN_IF_ERROR(context.manifest.LoadFromFile(manifest_path));
904 const core::DungeonStreamLayout* manifest_layout =
907 if (manifest_layout == nullptr) {
908 return absl::FailedPreconditionError(
909 "Manifest does not define dungeon_stream_regions.pot_items");
910 }
911 ASSIGN_OR_RETURN(context.layout,
913 core::DungeonStreamType::kPotItems, *manifest_layout));
914 return context;
915}
916
918 int index = -1;
920 uint32_t entry_pc = 0;
921 uint32_t item_pc = 0;
922};
923
925 uint32_t pointer_slot_pc = 0;
926 uint32_t stream_pc = 0;
927 uint32_t stream_end_pc = 0;
928 std::vector<PotItemEntryView> entries;
929};
930
931absl::StatusOr<PotItemStreamView> ResolveRuntimePotItemStream(Rom* rom,
932 int room_id) {
933 if (rom == nullptr || !rom->is_loaded()) {
934 return absl::InvalidArgumentError("ROM not loaded");
935 }
936
937 const uint64_t pointer_table_end =
938 static_cast<uint64_t>(zelda3::kRoomItemsPointers) +
939 static_cast<uint64_t>(zelda3::kNumberOfRooms) * 2u;
940 if (pointer_table_end > rom->size()) {
941 return absl::OutOfRangeError(
942 "Complete pot-item pointer table is outside ROM");
943 }
944 const uint32_t pointer_slot_pc =
945 zelda3::kRoomItemsPointers + static_cast<uint32_t>(room_id) * 2u;
946 uint16_t pointer_word = 0;
947 ASSIGN_OR_RETURN(pointer_word,
948 rom->ReadWord(static_cast<int>(pointer_slot_pc)));
949 const uint32_t pointer_snes = 0x010000u | pointer_word;
950 if (!IsLoRomDataPointer(pointer_snes)) {
951 return absl::FailedPreconditionError(
952 "Selected pot-item pointer is not a valid bank-01 LoROM pointer");
953 }
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");
961 }
962
964 view.pointer_slot_pc = pointer_slot_pc;
965 view.stream_pc = stream_pc;
966
967 uint32_t cursor = stream_pc;
968 while (true) {
969 if (static_cast<uint64_t>(cursor) + 2u > parse_end) {
970 return absl::DataLossError("Pot-item stream has no 0xFFFF terminator");
971 }
972 uint16_t position = 0;
973 ASSIGN_OR_RETURN(position, rom->ReadWord(static_cast<int>(cursor)));
974 if (position == 0xFFFFu) {
975 view.stream_end_pc = cursor + 2u;
976 break;
977 }
978 if (static_cast<uint64_t>(cursor) + 3u > parse_end) {
979 return absl::DataLossError("Pot-item stream has a truncated record");
980 }
981 uint8_t item = 0;
982 ASSIGN_OR_RETURN(item, rom->ReadByte(static_cast<int>(cursor + 2u)));
983 view.entries.push_back({.index = static_cast<int>(view.entries.size()),
984 .item = {.position = position, .item = item},
985 .entry_pc = cursor,
986 .item_pc = cursor + 2u});
987 cursor += 3u;
988 }
989
990 zelda3::Room room(room_id, rom);
991 room.LoadPotItems();
992 if (!room.ArePotItemsLoaded() ||
993 room.GetPotItems().size() != view.entries.size()) {
994 return absl::DataLossError(
995 "Pot-item room model count does not match the raw stream");
996 }
997 for (size_t index = 0; index < view.entries.size(); ++index) {
998 const zelda3::PotItem& model = room.GetPotItems()[index];
999 if (model.position != view.entries[index].item.position ||
1000 model.item != view.entries[index].item.item) {
1001 return absl::DataLossError(
1002 "Pot-item room model does not match the raw stream");
1003 }
1004 }
1005 return view;
1006}
1007
1010 uint32_t pointer_slot_pc = 0;
1011 uint32_t stream_pc = 0;
1012 uint32_t stream_end_pc = 0;
1013};
1014
1015absl::StatusOr<PotItemTarget> ResolvePotItemTarget(
1016 Rom* rom, int room_id, int index,
1017 const PotItemManifestContext& manifest_context) {
1018 if (manifest_context.layout.pointer_count != zelda3::kNumberOfRooms) {
1019 return absl::FailedPreconditionError(absl::StrFormat(
1020 "Dungeon pot-item manifest pointer_count must equal %d rooms",
1022 }
1023
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");
1030 }
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));
1036 }
1037
1038 const auto& stream = inventory.streams[room_id];
1039 if (!stream.valid) {
1040 return absl::FailedPreconditionError("Selected pot-item stream is invalid");
1041 }
1042 for (const auto& alias : inventory.aliases) {
1043 if (ContainsRoomId(alias.room_ids, room_id)) {
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())));
1047 }
1048 }
1049 for (const auto& overlap : inventory.overlaps) {
1050 if (ContainsRoomId(overlap.first_room_ids, room_id) ||
1051 ContainsRoomId(overlap.second_room_ids, room_id)) {
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));
1055 }
1056 }
1057 for (const auto& other_stream : inventory.streams) {
1058 if (other_stream.room_id == static_cast<uint32_t>(room_id)) {
1059 continue;
1060 }
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));
1068 }
1069 }
1070
1071 PotItemStreamView runtime;
1072 ASSIGN_OR_RETURN(runtime, ResolveRuntimePotItemStream(rom, room_id));
1073 if (runtime.pointer_slot_pc != stream.pointer_slot_pc ||
1074 runtime.stream_pc != stream.data_pc ||
1075 runtime.stream_end_pc != 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");
1079 }
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())));
1084 }
1085
1086 const PotItemEntryView& entry = runtime.entries[index];
1087 const auto conflicts = manifest_context.manifest.AnalyzePcWriteRanges(
1088 {{entry.item_pc, entry.item_pc + 1u}});
1089 if (!conflicts.empty()) {
1090 return absl::PermissionDeniedError(absl::StrFormat(
1091 "Pot-item byte at PC 0x%06X conflicts with Hack Manifest (%s)",
1092 entry.item_pc,
1093 core::AddressOwnershipToString(conflicts.front().ownership)));
1094 }
1095
1096 return PotItemTarget{
1097 .entry = entry,
1098 .pointer_slot_pc = runtime.pointer_slot_pc,
1099 .stream_pc = runtime.stream_pc,
1100 .stream_end_pc = runtime.stream_end_pc,
1101 };
1102}
1103
1104} // namespace
1105
1106// ---------------------------------------------------------------------------
1107// dungeon-place-sprite
1108// ---------------------------------------------------------------------------
1109
1111 Rom* rom, const resources::ArgumentParser& parser,
1112 resources::OutputFormatter& formatter) {
1113 auto room_id_str_or = GetRequiredString(parser, "room");
1114 if (!room_id_str_or.ok()) {
1115 return room_id_str_or.status();
1116 }
1117 auto sprite_id_str_or = GetRequiredString(parser, "id");
1118 if (!sprite_id_str_or.ok()) {
1119 return sprite_id_str_or.status();
1120 }
1121 const std::string room_id_str = room_id_str_or.value();
1122 const std::string sprite_id_str = sprite_id_str_or.value();
1123
1124 int room_id, sprite_id;
1125 if (!ParseHexString(room_id_str, &room_id)) {
1126 return absl::InvalidArgumentError("Invalid room ID. Must be hex.");
1127 }
1128 if (!ParseHexString(sprite_id_str, &sprite_id)) {
1129 return absl::InvalidArgumentError("Invalid sprite ID. Must be hex.");
1130 }
1131 auto room_status = ValidateRoomId(room_id);
1132 if (!room_status.ok()) {
1133 return room_status;
1134 }
1135
1136 auto x_or = GetRequiredInt(parser, "x");
1137 if (!x_or.ok()) {
1138 return x_or.status();
1139 }
1140 auto y_or = GetRequiredInt(parser, "y");
1141 if (!y_or.ok()) {
1142 return y_or.status();
1143 }
1144 auto subtype_or = GetOptionalInt(parser, "subtype", 0);
1145 if (!subtype_or.ok()) {
1146 return subtype_or.status();
1147 }
1148 auto layer_or = GetOptionalInt(parser, "layer", 0);
1149 if (!layer_or.ok()) {
1150 return layer_or.status();
1151 }
1152
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");
1158
1159 // Validate ranges
1160 RETURN_IF_ERROR(ValidateSpriteCoord(x, 'X'));
1161 RETURN_IF_ERROR(ValidateSpriteCoord(y, 'Y'));
1162 if (sprite_id < 0 || sprite_id > 0xFF) {
1163 return absl::InvalidArgumentError("Sprite ID must be 0x00-0xFF");
1164 }
1165 if (subtype < 0 || subtype > 0x1F) {
1166 return absl::InvalidArgumentError("Subtype must be 0-31 (5-bit flags)");
1167 }
1168 if (layer < 0 || layer > 1) {
1169 return absl::InvalidArgumentError("Layer must be 0 or 1");
1170 }
1171
1172 // Load room and its sprites
1173 zelda3::Room room = zelda3::LoadRoomHeaderFromRom(rom, room_id);
1174 room.LoadSprites();
1175
1176 int count_before = static_cast<int>(room.GetSprites().size());
1177
1178 // Add the new sprite
1179 room.GetSprites().emplace_back(
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));
1183 room.MarkSpritesDirty();
1184
1185 formatter.BeginObject("Place Sprite");
1186 formatter.AddHexField("room_id", room_id, 2);
1187 formatter.AddHexField("sprite_id", sprite_id, 2);
1188 formatter.AddField("sprite_name", zelda3::ResolveSpriteName(sprite_id));
1189 formatter.AddField("x", x);
1190 formatter.AddField("y", y);
1191 formatter.AddField("subtype", subtype);
1192 formatter.AddField("layer", layer);
1193 formatter.AddField("sprites_before", count_before);
1194 formatter.AddField("sprites_after",
1195 static_cast<int>(room.GetSprites().size()));
1196 formatter.AddField("mode", do_write ? "write" : "dry-run");
1197
1198 if (do_write) {
1199 auto save_status = MutateAndSaveRomWithBackup(
1200 rom, formatter, [&room]() { return room.SaveSprites(); });
1201 if (!save_status.ok()) {
1202 formatter.EndObject();
1203 return save_status;
1204 }
1205 }
1206
1207 formatter.EndObject();
1208 return absl::OkStatus();
1209}
1210
1211// ---------------------------------------------------------------------------
1212// dungeon-remove-sprite
1213// ---------------------------------------------------------------------------
1214
1216 Rom* rom, const resources::ArgumentParser& parser,
1217 resources::OutputFormatter& formatter) {
1218 auto room_id_str_or = GetRequiredString(parser, "room");
1219 if (!room_id_str_or.ok()) {
1220 return room_id_str_or.status();
1221 }
1222 const std::string room_id_str = room_id_str_or.value();
1223
1224 int room_id;
1225 if (!ParseHexString(room_id_str, &room_id)) {
1226 return absl::InvalidArgumentError("Invalid room ID. Must be hex.");
1227 }
1228 auto room_status = ValidateRoomId(room_id);
1229 if (!room_status.ok()) {
1230 return room_status;
1231 }
1232
1233 bool do_write = parser.HasFlag("write");
1234
1235 // Load room and its sprites
1236 zelda3::Room room = zelda3::LoadRoomHeaderFromRom(rom, room_id);
1237 room.LoadSprites();
1238
1239 auto& sprites = room.GetSprites();
1240 int count_before = static_cast<int>(sprites.size());
1241
1242 // Find sprite to remove: by --index or by --x/--y position.
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");
1249 }
1250 if (!has_index && has_x != has_y) {
1251 return absl::InvalidArgumentError(
1252 "Both --x and --y are required when removing by position");
1253 }
1254 if (!has_index && !has_x) {
1255 return absl::InvalidArgumentError(
1256 "Either --index or both --x and --y are required");
1257 }
1258
1259 int remove_index = -1;
1260 if (has_index) {
1261 auto index_or = GetRequiredInt(parser, "index");
1262 if (!index_or.ok()) {
1263 return index_or.status();
1264 }
1265 remove_index = index_or.value();
1266 } else {
1267 auto x_or = GetRequiredInt(parser, "x");
1268 if (!x_or.ok()) {
1269 return x_or.status();
1270 }
1271 auto y_or = GetRequiredInt(parser, "y");
1272 if (!y_or.ok()) {
1273 return y_or.status();
1274 }
1275
1276 const int x = x_or.value();
1277 const int y = y_or.value();
1278 RETURN_IF_ERROR(ValidateSpriteCoord(x, 'X'));
1279 RETURN_IF_ERROR(ValidateSpriteCoord(y, 'Y'));
1280
1281 for (int i = 0; i < static_cast<int>(sprites.size()); ++i) {
1282 if (sprites[i].x() == x && sprites[i].y() == y) {
1283 remove_index = i;
1284 break;
1285 }
1286 }
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));
1290 }
1291 }
1292
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));
1297 }
1298
1299 // Report which sprite we're removing
1300 const auto& target = sprites[remove_index];
1301 formatter.BeginObject("Remove Sprite");
1302 formatter.AddHexField("room_id", room_id, 2);
1303 formatter.AddField("removed_index", remove_index);
1304 formatter.AddHexField("sprite_id", target.id(), 2);
1305 formatter.AddField("sprite_name", zelda3::ResolveSpriteName(target.id()));
1306 formatter.AddField("x", target.x());
1307 formatter.AddField("y", target.y());
1308 formatter.AddField("sprites_before", count_before);
1309
1310 // Remove
1311 sprites.erase(sprites.begin() + remove_index);
1312 room.MarkSpritesDirty();
1313 formatter.AddField("sprites_after", static_cast<int>(sprites.size()));
1314 formatter.AddField("mode", do_write ? "write" : "dry-run");
1315
1316 if (do_write) {
1317 auto save_status = MutateAndSaveRomWithBackup(
1318 rom, formatter, [&room]() { return room.SaveSprites(); });
1319 if (!save_status.ok()) {
1320 formatter.EndObject();
1321 return save_status;
1322 }
1323 }
1324
1325 formatter.EndObject();
1326 return absl::OkStatus();
1327}
1328
1329// ---------------------------------------------------------------------------
1330// dungeon-place-object
1331// ---------------------------------------------------------------------------
1332
1334 Rom* rom, const resources::ArgumentParser& parser,
1335 resources::OutputFormatter& formatter) {
1336 auto room_id_str_or = GetRequiredString(parser, "room");
1337 if (!room_id_str_or.ok()) {
1338 return room_id_str_or.status();
1339 }
1340 auto object_id_str_or = GetRequiredString(parser, "id");
1341 if (!object_id_str_or.ok()) {
1342 return object_id_str_or.status();
1343 }
1344 const std::string room_id_str = room_id_str_or.value();
1345 const std::string object_id_str = object_id_str_or.value();
1346
1347 int room_id, object_id;
1348 if (!ParseHexString(room_id_str, &room_id)) {
1349 return absl::InvalidArgumentError("Invalid room ID. Must be hex.");
1350 }
1351 if (!ParseHexString(object_id_str, &object_id)) {
1352 return absl::InvalidArgumentError("Invalid object ID. Must be hex.");
1353 }
1354 auto room_status = ValidateRoomId(room_id);
1355 if (!room_status.ok()) {
1356 return room_status;
1357 }
1358 if (object_id < 0 || object_id > 0xFFFF) {
1359 return absl::InvalidArgumentError("Object ID must be 0x0000-0xFFFF");
1360 }
1361
1362 auto x_or = GetRequiredInt(parser, "x");
1363 if (!x_or.ok()) {
1364 return x_or.status();
1365 }
1366 auto y_or = GetRequiredInt(parser, "y");
1367 if (!y_or.ok()) {
1368 return y_or.status();
1369 }
1370 auto layer_or = GetOptionalInt(parser, "layer", 0);
1371 if (!layer_or.ok()) {
1372 return layer_or.status();
1373 }
1374
1375 int x = x_or.value();
1376 int y = y_or.value();
1377 // Preserve the command's legacy omitted Type 1 size of zero while deriving
1378 // the fixed, ID-backed size required by Type 2/3 stream entries.
1379 int size = zelda3::CanonicalRoomObjectSize(object_id, 0);
1380 if (parser.GetString("size").has_value()) {
1381 auto size_or = parser.GetInt("size");
1382 if (!size_or.ok()) {
1383 return size_or.status();
1384 }
1385 size = size_or.value();
1386 }
1387 int layer = layer_or.value();
1388 bool do_write = parser.HasFlag("write");
1389
1390 // Validate ranges
1391 if (x < 0 || x > 63) {
1392 return absl::InvalidArgumentError("X must be 0-63");
1393 }
1394 if (y < 0 || y > 63) {
1395 return absl::InvalidArgumentError("Y must be 0-63");
1396 }
1397 if (layer < 0 || layer > 2) {
1398 return absl::InvalidArgumentError("Layer must be 0, 1, or 2");
1399 }
1400 if (size < 0 || size > 0xFF) {
1401 return absl::InvalidArgumentError("Size must be 0-255");
1402 }
1403 const uint8_t canonical_size =
1404 zelda3::CanonicalRoomObjectSize(object_id, static_cast<uint8_t>(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));
1409 }
1410
1411 std::optional<ObjectSaveManifestContext> manifest_context;
1412 ASSIGN_OR_RETURN(manifest_context, LoadObjectSaveManifestContext(parser));
1413 const zelda3::DungeonStreamLayout* layout =
1414 manifest_context.has_value() ? &manifest_context->allocator_layout
1415 : nullptr;
1416
1417 // Load room with full objects
1418 zelda3::Room room = zelda3::LoadRoomFromRom(rom, room_id);
1419
1420 int count_before = static_cast<int>(room.GetTileObjects().size());
1421
1422 // Create the new object
1423 zelda3::RoomObject obj(static_cast<int16_t>(object_id),
1424 static_cast<uint8_t>(x), static_cast<uint8_t>(y),
1425 static_cast<uint8_t>(size),
1426 static_cast<uint8_t>(layer));
1427
1428 // Determine type for reporting
1429 int type = zelda3::RoomObject::DetermineObjectType((object_id & 0xFF),
1430 (object_id >> 8));
1431
1432 formatter.BeginObject("Place Object");
1433 formatter.AddHexField("room_id", room_id, 2);
1434 formatter.AddHexField("object_id", object_id, 4);
1435 formatter.AddField("object_name", zelda3::GetObjectName(object_id));
1436 formatter.AddField("object_type", type);
1437 formatter.AddField("x", x);
1438 formatter.AddField("y", y);
1439 formatter.AddField("size", size);
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");
1446 }
1447 formatter.AddField("allocator_capability",
1448 layout != nullptr ? "copy_on_write" : "none");
1449
1450 // Add the object
1451 auto add_status = room.AddObject(obj);
1452 if (!add_status.ok()) {
1453 formatter.AddField("error", std::string(add_status.message()));
1454 formatter.EndObject();
1455 return add_status;
1456 }
1457
1458 formatter.AddField("objects_after",
1459 static_cast<int>(room.GetTileObjects().size()));
1460 formatter.AddField("mode", do_write ? "write" : "dry-run");
1461
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()));
1469 formatter.EndObject();
1470 return preflight_status;
1471 }
1472 formatter.AddField("preflight_status", "success");
1473
1474 if (do_write) {
1475 auto save_status = MutateAndSaveRomWithBackup(
1476 rom, formatter, [&room, layout]() { return room.SaveObjects(layout); });
1477 if (!save_status.ok()) {
1478 formatter.EndObject();
1479 return save_status;
1480 }
1481 }
1482
1483 formatter.EndObject();
1484 return absl::OkStatus();
1485}
1486
1487// ---------------------------------------------------------------------------
1488// dungeon-get-palette
1489// ---------------------------------------------------------------------------
1490
1492 Rom* rom, const resources::ArgumentParser& parser,
1493 resources::OutputFormatter& formatter) {
1494 int room_id = 0;
1495 ASSIGN_OR_RETURN(room_id, GetRequiredHex(parser, "room"));
1496 RETURN_IF_ERROR(ValidateRoomId(room_id));
1497
1498 std::optional<int> requested_index;
1499 if (parser.GetString("index").has_value()) {
1500 int color_index = 0;
1501 ASSIGN_OR_RETURN(color_index, GetRequiredInt(parser, "index"));
1502 RETURN_IF_ERROR(ValidateDungeonPaletteColorIndex(color_index));
1503 requested_index = color_index;
1504 }
1505
1506 DungeonPaletteSelection selection;
1507 ASSIGN_OR_RETURN(selection, ResolveDungeonPaletteSelection(*rom, room_id));
1508
1509 formatter.BeginObject("Dungeon Palette");
1510 AddDungeonPaletteScope(formatter, selection);
1511 formatter.AddField("color_count", kDungeonPaletteColorCount);
1512
1513 if (requested_index.has_value()) {
1514 const uint32_t color_pc =
1515 DungeonPaletteColorPc(selection.selected, *requested_index);
1516 uint16_t color = 0;
1517 ASSIGN_OR_RETURN(color, ReadDungeonPaletteColor(*rom, selection.selected,
1518 *requested_index));
1519 formatter.AddField("color_index", *requested_index);
1520 formatter.AddHexField("color_pc", color_pc, 6);
1521 formatter.AddHexField("color_snes_address", PcToSnes(color_pc), 6);
1522 formatter.AddHexField("color_snes", color, 4);
1523 } else {
1524 formatter.BeginArray("colors");
1525 for (int color_index = 0; color_index < kDungeonPaletteColorCount;
1526 ++color_index) {
1527 const uint32_t color_pc =
1528 DungeonPaletteColorPc(selection.selected, color_index);
1529 uint16_t color = 0;
1530 ASSIGN_OR_RETURN(color, ReadDungeonPaletteColor(*rom, selection.selected,
1531 color_index));
1532 formatter.BeginObject();
1533 formatter.AddField("index", color_index);
1534 formatter.AddHexField("pc", color_pc, 6);
1535 formatter.AddHexField("snes_address", PcToSnes(color_pc), 6);
1536 formatter.AddHexField("snes_color", color, 4);
1537 formatter.EndObject();
1538 }
1539 formatter.EndArray();
1540 }
1541
1542 formatter.AddField("status", "success");
1543 formatter.EndObject();
1544 return absl::OkStatus();
1545}
1546
1547// ---------------------------------------------------------------------------
1548// dungeon-set-palette-color
1549// ---------------------------------------------------------------------------
1550
1552 Rom* rom, const resources::ArgumentParser& parser,
1553 resources::OutputFormatter& formatter) {
1554 const bool do_write = parser.HasFlag("write");
1555#if defined(__EMSCRIPTEN__)
1556 if (do_write) {
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");
1561 }
1562#endif
1563 if (do_write && rom->filename().empty()) {
1564 return absl::FailedPreconditionError(
1565 "dungeon-set-palette-color --write requires a ROM loaded from a "
1566 "filesystem path");
1567 }
1568
1569 int room_id = 0;
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;
1575 ASSIGN_OR_RETURN(room_id, GetRequiredHex(parser, "room"));
1576 ASSIGN_OR_RETURN(color_index, GetRequiredInt(parser, "index"));
1577 ASSIGN_OR_RETURN(expected_palette_set,
1578 GetRequiredHex(parser, "expect-palette-set"));
1579 ASSIGN_OR_RETURN(expected_palette_index,
1580 GetRequiredInt(parser, "expect-palette-index"));
1581 ASSIGN_OR_RETURN(expected_color, GetRequiredHex(parser, "expect-color"));
1582 ASSIGN_OR_RETURN(replacement_color, GetRequiredHex(parser, "color"));
1583
1584 RETURN_IF_ERROR(ValidateRoomId(room_id));
1585 RETURN_IF_ERROR(ValidateDungeonPaletteColorIndex(color_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");
1589 }
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));
1595 }
1596 RETURN_IF_ERROR(ValidateSnes15BitColor(expected_color, "expect-color"));
1597 RETURN_IF_ERROR(ValidateSnes15BitColor(replacement_color, "color"));
1598
1599 const std::string manifest_path = parser.GetString("manifest").value();
1600 core::HackManifest manifest;
1601 RETURN_IF_ERROR(manifest.LoadFromFile(manifest_path));
1602
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));
1610 }
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));
1615 }
1616
1617 uint16_t actual_color = 0;
1618 ASSIGN_OR_RETURN(actual_color, ReadDungeonPaletteColor(
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 "
1623 "0x%04X",
1624 selection.selected.palette_index, color_index, expected_color,
1625 actual_color));
1626 }
1627 if (replacement_color == actual_color) {
1628 return absl::InvalidArgumentError(absl::StrFormat(
1629 "Refusing no-op dungeon palette write: color is already 0x%04X",
1630 actual_color));
1631 }
1632
1633 const uint32_t color_pc =
1634 DungeonPaletteColorPc(selection.selected, color_index);
1635 RETURN_IF_ERROR(ValidateDungeonPaletteManifestRange(manifest, color_pc));
1636 if (do_write) {
1637 RETURN_IF_ERROR(ValidateDungeonPaletteWriteBaseline(*rom));
1638 }
1639
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);
1646 formatter.AddHexField("color_pc", color_pc, 6);
1647 formatter.AddHexField("color_snes_address", PcToSnes(color_pc), 6);
1648 formatter.AddHexField("old_color", actual_color, 4);
1649 formatter.AddHexField("new_color", replacement_color, 4);
1650 formatter.AddField("preflight_status", "success");
1651
1652 if (!do_write) {
1653 formatter.AddField("status", "dry-run");
1654 formatter.EndObject();
1655 return absl::OkStatus();
1656 }
1657
1658 rom::WriteFence write_fence;
1660 write_fence.Allow(color_pc, color_pc + 2u, "DungeonPaletteColor"));
1661 const std::vector<uint8_t> before = rom->vector();
1662 ScopedRomTransaction transaction(*rom);
1663 {
1664 rom::ScopedWriteFence scoped_fence(rom, &write_fence);
1666 rom->WriteShort(color_pc, static_cast<uint16_t>(replacement_color)));
1667 }
1668
1669 std::vector<uint32_t> changed_bytes;
1670 ASSIGN_OR_RETURN(changed_bytes,
1671 ValidateDungeonPaletteWholeRomDiff(before, *rom, color_pc));
1672 uint16_t memory_color = 0;
1673 ASSIGN_OR_RETURN(memory_color, ReadDungeonPaletteColor(
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");
1678 }
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()));
1683 formatter.BeginArray("changed_byte_pcs");
1684 for (uint32_t changed_pc : changed_bytes) {
1685 formatter.AddArrayItem(absl::StrFormat("0x%06X", changed_pc));
1686 }
1687 formatter.EndArray();
1688 formatter.AddField("memory_readback_status", "verified");
1689
1690 const absl::Status save_status = SaveRomWithBackup(rom, formatter);
1691 if (!save_status.ok()) {
1692 formatter.AddField("status", "error");
1693 formatter.EndObject();
1694 return save_status;
1695 }
1696
1697 // The atomic disk replacement has succeeded. Commit the caller's in-memory
1698 // transaction before external verification so a later reopen/readback
1699 // failure cannot roll memory back while leaving the persisted file changed.
1700 transaction.Commit();
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");
1708 formatter.EndObject();
1709 return reopen_status;
1710 }
1711
1712 formatter.AddField("readback_status", "external_reopen_verified");
1713 formatter.AddField("status", "success");
1714 formatter.EndObject();
1715 return absl::OkStatus();
1716}
1717
1718// ---------------------------------------------------------------------------
1719// dungeon-set-door-type
1720// ---------------------------------------------------------------------------
1721
1723 Rom* rom, const resources::ArgumentParser& parser,
1724 resources::OutputFormatter& formatter) {
1725 int room_id = 0;
1726 ASSIGN_OR_RETURN(room_id, GetRequiredHex(parser, "room"));
1727 RETURN_IF_ERROR(ValidateRoomId(room_id));
1728
1729 int x = 0;
1730 int y = 0;
1731 ASSIGN_OR_RETURN(x, GetRequiredInt(parser, "x"));
1732 ASSIGN_OR_RETURN(y, GetRequiredInt(parser, "y"));
1733 if (x < 0 || x > 63 || y < 0 || y > 63) {
1734 return absl::InvalidArgumentError(
1735 "Door tile coordinates must be in range 0-63");
1736 }
1737
1738 int new_type = 0;
1739 int expected_type = 0;
1740 ASSIGN_OR_RETURN(new_type, GetRequiredHex(parser, "type"));
1741 ASSIGN_OR_RETURN(expected_type, GetRequiredHex(parser, "expect-type"));
1742 RETURN_IF_ERROR(ValidateDoorType(new_type, "type"));
1743 RETURN_IF_ERROR(ValidateDoorType(expected_type, "expect-type"));
1744
1745 std::optional<ObjectSaveManifestContext> manifest_context;
1746 ASSIGN_OR_RETURN(manifest_context, LoadObjectSaveManifestContext(parser));
1747 if (!manifest_context.has_value()) {
1748 return absl::FailedPreconditionError(
1749 "--manifest is required for dungeon-set-door-type");
1750 }
1751
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, "
1759 "found 0x%02X",
1760 x, y, expected_type, old_type));
1761 }
1762 if (new_type == old_type) {
1763 return absl::InvalidArgumentError(
1764 "--type must differ from the current door type");
1765 }
1766
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");
1770 }
1771
1772 formatter.BeginObject("Set Door Type");
1773 formatter.AddHexField("room_id", room_id, 3);
1774 formatter.AddField("door_index", target.door_index);
1775 formatter.AddField("x", x);
1776 formatter.AddField("y", y);
1777 formatter.AddField("position", target.door.position);
1778 formatter.AddField("direction", std::string(target.door.GetDirectionName()));
1779 formatter.AddHexField("old_type", old_type, 2);
1780 formatter.AddField("old_type_name", std::string(target.door.GetTypeName()));
1781 formatter.AddHexField("new_type", new_type, 2);
1782 formatter.AddField(
1783 "new_type_name",
1784 std::string(zelda3::GetDoorTypeName(
1785 zelda3::DoorTypeFromRaw(static_cast<uint8_t>(new_type)))));
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);
1791 formatter.AddHexField("type_snes", PcToSnes(target.type_pc), 6);
1792 formatter.AddField("manifest", *parser.GetString("manifest"));
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");
1797
1798 if (!do_write) {
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");
1803 formatter.EndObject();
1804 return absl::OkStatus();
1805 }
1806
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()));
1811 formatter.EndObject();
1812 return status;
1813 };
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");
1823 }
1824 return absl::OkStatus();
1825 };
1826
1827 rom::WriteFence write_fence;
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");
1832 }
1833
1834 const std::vector<uint8_t> before = rom->vector();
1835 ScopedRomTransaction transaction(*rom);
1836 {
1837 rom::ScopedWriteFence write_scope(rom, &write_fence);
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");
1842 }
1843 }
1844
1845 if (rom->size() != before.size()) {
1846 return finish_error(
1847 absl::DataLossError("Door type edit unexpectedly resized the ROM"),
1848 "write_error");
1849 }
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]) {
1854 ++changed_bytes;
1855 changed_pc = pc;
1856 }
1857 }
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)),
1863 "write_error");
1864 }
1865
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");
1870 }
1871 const absl::Status memory_verify = verify_readback(*memory_readback);
1872 if (!memory_verify.ok()) {
1873 return finish_error(memory_verify, "readback_error");
1874 }
1875 formatter.AddField("readback_status", "pre_save_verified");
1876 formatter.AddField("write_status", "success");
1877
1878 const absl::Status save_status = SaveRomWithBackup(rom, formatter);
1879 if (!save_status.ok()) {
1880 formatter.AddField("status", "error");
1881 formatter.EndObject();
1882 return save_status;
1883 }
1884
1885 transaction.Commit();
1886 formatter.AddField("status", "success");
1887 formatter.EndObject();
1888 return absl::OkStatus();
1889}
1890
1891// ---------------------------------------------------------------------------
1892// dungeon-list-pot-items
1893// ---------------------------------------------------------------------------
1894
1896 Rom* rom, const resources::ArgumentParser& parser,
1897 resources::OutputFormatter& formatter) {
1898 int room_id = 0;
1899 ASSIGN_OR_RETURN(room_id, GetRequiredHex(parser, "room"));
1900 RETURN_IF_ERROR(ValidateRoomId(room_id));
1901
1902 PotItemStreamView stream;
1903 ASSIGN_OR_RETURN(stream, ResolveRuntimePotItemStream(rom, room_id));
1904
1905 formatter.BeginObject("Dungeon Pot Items");
1906 formatter.AddHexField("room_id", room_id, 3);
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()));
1911 formatter.BeginArray("items");
1912 for (const PotItemEntryView& entry : stream.entries) {
1913 formatter.BeginObject();
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);
1920 formatter.AddHexField("item_snes", PcToSnes(entry.item_pc), 6);
1921 formatter.EndObject();
1922 }
1923 formatter.EndArray();
1924 formatter.AddField("status", "success");
1925 formatter.EndObject();
1926 return absl::OkStatus();
1927}
1928
1929// ---------------------------------------------------------------------------
1930// dungeon-set-pot-item
1931// ---------------------------------------------------------------------------
1932
1934 Rom* rom, const resources::ArgumentParser& parser,
1935 resources::OutputFormatter& formatter) {
1936 int room_id = 0;
1937 int index = 0;
1938 int expected_position = 0;
1939 int expected_item = 0;
1940 int new_item = 0;
1941 ASSIGN_OR_RETURN(room_id, GetRequiredHex(parser, "room"));
1942 RETURN_IF_ERROR(ValidateRoomId(room_id));
1943 ASSIGN_OR_RETURN(index, GetRequiredInt(parser, "index"));
1944 ASSIGN_OR_RETURN(expected_position,
1945 GetRequiredHex(parser, "expect-position"));
1946 ASSIGN_OR_RETURN(expected_item, GetRequiredHex(parser, "expect-item"));
1947 ASSIGN_OR_RETURN(new_item, GetRequiredHex(parser, "item"));
1948 if (expected_position < 0 || expected_position > 0xFFFF) {
1949 return absl::InvalidArgumentError(
1950 "--expect-position must be in range 0x0000-0xFFFF");
1951 }
1952 if (expected_item < 0 || expected_item > 0xFF) {
1953 return absl::InvalidArgumentError(
1954 "--expect-item must be in range 0x00-0xFF");
1955 }
1956 if (new_item < 0 || new_item > 0xFF) {
1957 return absl::InvalidArgumentError("--item must be in range 0x00-0xFF");
1958 }
1959
1960 PotItemManifestContext manifest_context;
1961 ASSIGN_OR_RETURN(manifest_context, LoadPotItemManifestContext(parser));
1962 PotItemTarget target;
1963 ASSIGN_OR_RETURN(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));
1972 }
1973 if (new_item == expected_item) {
1974 return absl::InvalidArgumentError(
1975 "--item must differ from the current pot item");
1976 }
1977
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");
1981 }
1982
1983 formatter.BeginObject("Set Pot Item");
1984 formatter.AddHexField("room_id", room_id, 3);
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);
1990 formatter.AddHexField("new_item", new_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);
1995 formatter.AddHexField("item_snes", PcToSnes(target.entry.item_pc), 6);
1996 formatter.AddField("manifest", *parser.GetString("manifest"));
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");
2001
2002 if (!do_write) {
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");
2007 formatter.EndObject();
2008 return absl::OkStatus();
2009 }
2010
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()));
2015 formatter.EndObject();
2016 return status;
2017 };
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");
2029 }
2030 return absl::OkStatus();
2031 };
2032
2033 rom::WriteFence write_fence;
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");
2038 }
2039
2040 const std::vector<uint8_t> before = rom->vector();
2041 ScopedRomTransaction transaction(*rom);
2042 {
2043 rom::ScopedWriteFence write_scope(rom, &write_fence);
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");
2048 }
2049 }
2050
2051 if (rom->size() != before.size()) {
2052 return finish_error(
2053 absl::DataLossError("Pot-item edit unexpectedly resized the ROM"),
2054 "write_error");
2055 }
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]) {
2060 ++changed_bytes;
2061 changed_pc = pc;
2062 }
2063 }
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)),
2069 "write_error");
2070 }
2071
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");
2076 }
2077 const absl::Status memory_verify = verify_readback(*memory_readback);
2078 if (!memory_verify.ok()) {
2079 return finish_error(memory_verify, "readback_error");
2080 }
2081 formatter.AddField("readback_status", "pre_save_verified");
2082 formatter.AddField("write_status", "success");
2083
2084 const absl::Status save_status = SaveRomWithBackup(rom, formatter);
2085 if (!save_status.ok()) {
2086 formatter.AddField("status", "error");
2087 formatter.EndObject();
2088 return save_status;
2089 }
2090
2091 transaction.Commit();
2092 formatter.AddField("status", "success");
2093 formatter.EndObject();
2094 return absl::OkStatus();
2095}
2096
2097// ---------------------------------------------------------------------------
2098// dungeon-set-collision-tile
2099// ---------------------------------------------------------------------------
2100
2102 Rom* rom, const resources::ArgumentParser& parser,
2103 resources::OutputFormatter& formatter) {
2104 auto room_id_str_or = GetRequiredString(parser, "room");
2105 if (!room_id_str_or.ok()) {
2106 return room_id_str_or.status();
2107 }
2108 auto tiles_str_or = GetRequiredString(parser, "tiles");
2109 if (!tiles_str_or.ok()) {
2110 return tiles_str_or.status();
2111 }
2112 const std::string room_id_str = room_id_str_or.value();
2113 const std::string tiles_str = tiles_str_or.value();
2114
2115 int room_id;
2116 if (!ParseHexString(room_id_str, &room_id)) {
2117 return absl::InvalidArgumentError("Invalid room ID. Must be hex.");
2118 }
2119 auto room_status = ValidateRoomId(room_id);
2120 if (!room_status.ok()) {
2121 return room_status;
2122 }
2123
2124 bool do_write = parser.HasFlag("write");
2125
2126 // Parse tile specifications: "x,y,tile;x,y,tile;..."
2127 struct TileSpec {
2128 int x, y, tile;
2129 };
2130 std::vector<TileSpec> specs;
2131
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));
2139 }
2140 TileSpec spec;
2141
2142 // x and y are decimal tile coords
2143 if (!absl::SimpleAtoi(parts[0], &spec.x)) {
2144 return absl::InvalidArgumentError(
2145 absl::StrFormat("Invalid X coord '%s'", parts[0]));
2146 }
2147 if (!absl::SimpleAtoi(parts[1], &spec.y)) {
2148 return absl::InvalidArgumentError(
2149 absl::StrFormat("Invalid Y coord '%s'", parts[1]));
2150 }
2151 if (!ParseHexString(parts[2], &spec.tile)) {
2152 return absl::InvalidArgumentError(
2153 absl::StrFormat("Invalid tile value '%s'. Must be hex.", parts[2]));
2154 }
2155
2156 // Validate ranges
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));
2160 }
2161 if (spec.tile < 0 || spec.tile > 0xFF) {
2162 return absl::InvalidArgumentError("Tile value must be 0x00-0xFF");
2163 }
2164
2165 specs.push_back(spec);
2166 }
2167
2168 if (specs.empty()) {
2169 return absl::InvalidArgumentError("No tile specs provided");
2170 }
2171
2172 // Load room with custom collision data
2173 zelda3::Room room = zelda3::LoadRoomFromRom(rom, room_id);
2174
2175 formatter.BeginObject("Set Collision Tiles");
2176 formatter.AddHexField("room_id", room_id, 2);
2177 formatter.AddField("had_custom_collision", room.has_custom_collision());
2178 formatter.AddField("tile_count", static_cast<int>(specs.size()));
2179 formatter.AddField("mode", do_write ? "write" : "dry-run");
2180
2181 // Apply each tile change
2182 formatter.BeginArray("changes");
2183 for (const auto& spec : specs) {
2184 uint8_t old_value = room.GetCollisionTile(spec.x, spec.y);
2185 room.SetCollisionTile(spec.x, spec.y, static_cast<uint8_t>(spec.tile));
2186
2187 formatter.BeginObject();
2188 formatter.AddField("x", spec.x);
2189 formatter.AddField("y", spec.y);
2190 formatter.AddHexField("old_tile", old_value, 2);
2191 formatter.AddHexField("new_tile", spec.tile, 2);
2192 formatter.EndObject();
2193 }
2194 formatter.EndArray();
2195
2196 if (do_write) {
2197 auto save_status =
2198 MutateAndSaveRomWithBackup(rom, formatter, [&rom, &room]() mutable {
2199 // Flush collision changes to ROM.
2200 std::array<zelda3::Room, 1> rooms_arr = {std::move(room)};
2201 return zelda3::SaveAllCollision(rom, absl::MakeSpan(rooms_arr));
2202 });
2203 if (!save_status.ok()) {
2204 formatter.EndObject();
2205 return save_status;
2206 }
2207 }
2208
2209 formatter.EndObject();
2210 return absl::OkStatus();
2211}
2212
2213} // namespace handlers
2214} // namespace cli
2215} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:227
auto filename() const
Definition rom.h:157
absl::Status WriteByte(int addr, uint8_t value)
Definition rom.cc:586
absl::StatusOr< uint8_t > ReadByte(int offset) const
Definition rom.cc:518
const auto & vector() const
Definition rom.h:155
absl::Status SaveToFile(const SaveSettings &settings)
Definition rom.cc:371
absl::StatusOr< uint16_t > ReadWord(int offset) const
Definition rom.cc:526
auto data() const
Definition rom.h:151
auto size() const
Definition rom.h:150
bool dirty() const
Definition rom.h:145
absl::Status LoadFromData(const std::vector< uint8_t > &data, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:335
absl::Status WriteShort(int addr, uint16_t value)
Definition rom.cc:628
bool is_loaded() const
Definition rom.h:144
absl::StatusOr< uint32_t > ReadLong(int offset) const
Definition rom.cc:533
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)
Utility for consistent output formatting across commands.
void BeginArray(const std::string &key)
Begin an array.
void AddArrayItem(const std::string &item)
Add an item to current array.
void BeginObject(const std::string &title="")
Start a JSON object or text section.
void EndObject()
End a JSON object or text section.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
void AddHexField(const std::string &key, uint64_t value, int width=2)
Add a hex-formatted field.
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)
Definition write_fence.h:32
static int DetermineObjectType(uint8_t b1, uint8_t b3)
bool ArePotItemsLoaded() const
Definition room.h:877
uint8_t GetCollisionTile(int x, int y) const
Definition room.h:509
const std::vector< Door > & GetDoors() const
Definition room.h:351
void MarkSpritesDirty()
Definition room.h:257
absl::Status SaveObjects(const DungeonStreamLayout *layout=nullptr)
Definition room.cc:2023
std::vector< uint8_t > EncodeObjects() const
Definition room.cc:1798
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:254
absl::Status SaveSprites(const DungeonStreamLayout *layout=nullptr)
Definition room.cc:2217
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:383
void LoadPotItems()
Definition room.cc:4100
void SetCollisionTile(int x, int y, uint8_t tile)
Definition room.h:515
void LoadSprites()
Definition room.cc:2494
absl::Status AddObject(const RoomObject &object)
Definition room.cc:2381
bool has_custom_collision() const
Definition room.h:501
const std::vector< PotItem > & GetPotItems() const
Definition room.h:367
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
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::StatusOr< DungeonPaletteSelection > ResolveDungeonPaletteSelection(const Rom &rom, int room_id)
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)
absl::StatusOr< int > GetRequiredHex(const resources::ArgumentParser &parser, const char *name)
absl::StatusOr< std::optional< ObjectSaveManifestContext > > LoadObjectSaveManifestContext(const resources::ArgumentParser &parser)
uint32_t DungeonPaletteColorPc(const DungeonPaletteRoomMapping &mapping, int color_index)
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::StatusOr< PotItemManifestContext > LoadPotItemManifestContext(const resources::ArgumentParser &parser)
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)
Definition hex_util.h:17
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
Definition game_data.h:44
constexpr int kDoorPointers
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
Definition room.cc:546
constexpr uint32_t kNumPalettesets
Definition game_data.h:33
constexpr int kDungeonPaletteBytes
Definition game_data.h:46
uint8_t CanonicalRoomObjectSize(int object_id, uint8_t requested_size)
constexpr int kRoomItemsPointers
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:521
constexpr DoorType DoorTypeFromRaw(uint8_t raw_type)
Convert raw type byte to DoorType enum.
Definition door_types.h:269
constexpr uint32_t kDungeonPalettePointerTable
Definition game_data.h:45
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.
Definition door_types.h:110
constexpr int kNumberOfRooms
constexpr int kRoomHeaderPointer
constexpr int kRoomHeaderPointerBank
absl::Status SaveAllCollision(Rom *rom, absl::Span< Room > rooms)
Definition room.cc:3422
const char * ResolveSpriteName(uint16_t id)
Definition sprite.cc:284
constexpr int kRoomObjectPointer
uint32_t PcToSnes(uint32_t addr)
Definition snes.h:17
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
bool load_resource_labels
Definition rom.h:43
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
uint16_t position
Definition room.h:109
Represents a door in a dungeon room.
Definition room.h:277
DoorType type
Door type (determines appearance/behavior)
Definition room.h:279
static Door FromRomBytes(uint8_t b1, uint8_t b2)
Definition room.h:335
DoorDirection direction
Which wall the door is on.
Definition room.h:280
uint8_t position
Encoded position (5-bit, 0-31)
Definition room.h:278
std::pair< int, int > GetTileCoords() const
Get tile coordinates for this door.
Definition room.h:286