9#include "absl/strings/numbers.h"
10#include "absl/strings/str_format.h"
11#include "absl/strings/str_split.h"
38 if (!value.has_value()) {
39 return absl::InvalidArgumentError(
40 absl::StrFormat(
"Missing required argument '--%s'",
name));
49 return parsed.status();
51 return parsed.value();
55 const char*
name,
int default_value) {
62 return parsed.status();
64 return parsed.value();
69 return absl::InvalidArgumentError(
70 absl::StrFormat(
"Room ID out of range: 0x%X (expected 0x00-0x%02X)",
73 return absl::OkStatus();
77 if (value < 0 || value > 31) {
78 return absl::InvalidArgumentError(
79 absl::StrFormat(
"%c must be 0-31 (5-bit tile coord)", axis));
81 return absl::OkStatus();
88 auto disk_status = rom->
SaveToFile(save_settings);
89 if (!disk_status.ok()) {
90 formatter.
AddField(
"save_error", std::string(disk_status.message()));
94 formatter.
AddField(
"save_status",
"saved");
95 return absl::OkStatus();
98template <
typename Mutation>
101 Mutation&& mutation) {
104 auto write_status = std::forward<Mutation>(mutation)();
105 if (!write_status.ok()) {
106 formatter.
AddField(
"write_error", std::string(write_status.message()));
109 formatter.
AddField(
"write_status",
"success");
113 return absl::OkStatus();
125absl::StatusOr<std::optional<ObjectSaveManifestContext>>
127 const auto manifest_path = parser.
GetString(
"manifest");
128 if (!manifest_path.has_value()) {
137 if (manifest_layout ==
nullptr) {
138 return absl::FailedPreconditionError(
139 "Manifest does not define dungeon_stream_regions.objects");
142 return absl::FailedPreconditionError(
143 "dungeon_stream_regions.objects must use copy_on_write");
149 return std::optional<ObjectSaveManifestContext>(std::move(context));
155 std::vector<std::pair<uint32_t, uint32_t>> ranges;
160 uint32_t object_pointer_table_snes = 0;
163 const uint32_t object_pointer_table_pc =
SnesToPc(object_pointer_table_snes);
164 const uint64_t current_pointer_slot =
165 static_cast<uint64_t
>(object_pointer_table_pc) +
166 static_cast<uint64_t
>(room_id) * 3u;
167 if (current_pointer_slot + 3u > source_rom.
size()) {
168 return absl::OutOfRangeError(
169 "Selected object pointer slot is outside the ROM");
172 uint32_t current_stream_snes = 0;
174 source_rom.
ReadLong(
static_cast<int>(current_pointer_slot)));
175 const uint32_t current_stream_pc =
SnesToPc(current_stream_snes);
176 const uint64_t current_stream_end =
static_cast<uint64_t
>(current_stream_pc) +
178 if (current_stream_end > std::numeric_limits<uint32_t>::max()) {
179 return absl::OutOfRangeError(
"Selected object write range overflows");
181 ranges.emplace_back(current_stream_pc,
182 static_cast<uint32_t
>(current_stream_end));
184 const uint64_t door_pointer_slot =
186 static_cast<uint64_t
>(room_id) * 3u;
187 if (door_pointer_slot + 3u > std::numeric_limits<uint32_t>::max()) {
188 return absl::OutOfRangeError(
"Selected door pointer range overflows");
190 ranges.emplace_back(
static_cast<uint32_t
>(door_pointer_slot),
191 static_cast<uint32_t
>(door_pointer_slot + 3u));
194 ranges.emplace_back(range.begin, range.end);
200 const uint64_t cow_pointer_slot =
202 static_cast<uint64_t
>(room_id) * pointer_width;
203 if (cow_pointer_slot + pointer_width > std::numeric_limits<uint32_t>::max()) {
204 return absl::OutOfRangeError(
"Selected COW pointer range overflows");
206 ranges.emplace_back(
static_cast<uint32_t
>(cow_pointer_slot),
207 static_cast<uint32_t
>(cow_pointer_slot + pointer_width));
210 if (conflicts.empty() ||
212 return absl::OkStatus();
214 return absl::PermissionDeniedError(
"Write conflict with Hack Manifest");
221 if (manifest_context !=
nullptr) {
223 source_rom, room_id, pending_room, *manifest_context));
237 return scratch_room.
SaveObjects(manifest_context !=
nullptr
251 auto room_id_str_or = GetRequiredString(parser,
"room");
252 if (!room_id_str_or.ok()) {
253 return room_id_str_or.status();
255 auto sprite_id_str_or = GetRequiredString(parser,
"id");
256 if (!sprite_id_str_or.ok()) {
257 return sprite_id_str_or.status();
259 const std::string room_id_str = room_id_str_or.value();
260 const std::string sprite_id_str = sprite_id_str_or.value();
262 int room_id, sprite_id;
263 if (!ParseHexString(room_id_str, &room_id)) {
264 return absl::InvalidArgumentError(
"Invalid room ID. Must be hex.");
266 if (!ParseHexString(sprite_id_str, &sprite_id)) {
267 return absl::InvalidArgumentError(
"Invalid sprite ID. Must be hex.");
269 auto room_status = ValidateRoomId(room_id);
270 if (!room_status.ok()) {
274 auto x_or = GetRequiredInt(parser,
"x");
276 return x_or.status();
278 auto y_or = GetRequiredInt(parser,
"y");
280 return y_or.status();
282 auto subtype_or = GetOptionalInt(parser,
"subtype", 0);
283 if (!subtype_or.ok()) {
284 return subtype_or.status();
286 auto layer_or = GetOptionalInt(parser,
"layer", 0);
287 if (!layer_or.ok()) {
288 return layer_or.status();
291 int x = x_or.value();
292 int y = y_or.value();
293 int subtype = subtype_or.value();
294 int layer = layer_or.value();
295 bool do_write = parser.
HasFlag(
"write");
300 if (sprite_id < 0 || sprite_id > 0xFF) {
301 return absl::InvalidArgumentError(
"Sprite ID must be 0x00-0xFF");
303 if (subtype < 0 || subtype > 0x1F) {
304 return absl::InvalidArgumentError(
"Subtype must be 0-31 (5-bit flags)");
306 if (layer < 0 || layer > 1) {
307 return absl::InvalidArgumentError(
"Layer must be 0 or 1");
314 int count_before =
static_cast<int>(room.
GetSprites().size());
318 static_cast<uint8_t
>(sprite_id),
static_cast<uint8_t
>(x),
319 static_cast<uint8_t
>(y),
static_cast<uint8_t
>(subtype),
320 static_cast<uint8_t
>(layer));
329 formatter.
AddField(
"subtype", subtype);
331 formatter.
AddField(
"sprites_before", count_before);
334 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
337 auto save_status = MutateAndSaveRomWithBackup(
338 rom, formatter, [&room]() {
return room.
SaveSprites(); });
339 if (!save_status.ok()) {
346 return absl::OkStatus();
356 auto room_id_str_or = GetRequiredString(parser,
"room");
357 if (!room_id_str_or.ok()) {
358 return room_id_str_or.status();
360 const std::string room_id_str = room_id_str_or.value();
363 if (!ParseHexString(room_id_str, &room_id)) {
364 return absl::InvalidArgumentError(
"Invalid room ID. Must be hex.");
366 auto room_status = ValidateRoomId(room_id);
367 if (!room_status.ok()) {
371 bool do_write = parser.
HasFlag(
"write");
378 int count_before =
static_cast<int>(sprites.size());
381 const bool has_index = parser.
GetString(
"index").has_value();
382 const bool has_x = parser.
GetString(
"x").has_value();
383 const bool has_y = parser.
GetString(
"y").has_value();
384 if (has_index && (has_x || has_y)) {
385 return absl::InvalidArgumentError(
386 "Use either --index or --x/--y, not both");
388 if (!has_index && has_x != has_y) {
389 return absl::InvalidArgumentError(
390 "Both --x and --y are required when removing by position");
392 if (!has_index && !has_x) {
393 return absl::InvalidArgumentError(
394 "Either --index or both --x and --y are required");
397 int remove_index = -1;
399 auto index_or = GetRequiredInt(parser,
"index");
400 if (!index_or.ok()) {
401 return index_or.status();
403 remove_index = index_or.value();
405 auto x_or = GetRequiredInt(parser,
"x");
407 return x_or.status();
409 auto y_or = GetRequiredInt(parser,
"y");
411 return y_or.status();
414 const int x = x_or.value();
415 const int y = y_or.value();
419 for (
int i = 0; i < static_cast<int>(sprites.size()); ++i) {
420 if (sprites[i].x() == x && sprites[i].y() == y) {
425 if (remove_index < 0) {
426 return absl::NotFoundError(absl::StrFormat(
427 "No sprite at (%d, %d) in room 0x%02X", x, y, room_id));
431 if (remove_index < 0 || remove_index >=
static_cast<int>(sprites.size())) {
432 return absl::OutOfRangeError(
433 absl::StrFormat(
"Sprite index %d out of range (room has %d sprites)",
434 remove_index, count_before));
438 const auto& target = sprites[remove_index];
441 formatter.
AddField(
"removed_index", remove_index);
442 formatter.
AddHexField(
"sprite_id", target.id(), 2);
444 formatter.
AddField(
"x", target.x());
445 formatter.
AddField(
"y", target.y());
446 formatter.
AddField(
"sprites_before", count_before);
449 sprites.erase(sprites.begin() + remove_index);
451 formatter.
AddField(
"sprites_after",
static_cast<int>(sprites.size()));
452 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
455 auto save_status = MutateAndSaveRomWithBackup(
456 rom, formatter, [&room]() {
return room.
SaveSprites(); });
457 if (!save_status.ok()) {
464 return absl::OkStatus();
474 auto room_id_str_or = GetRequiredString(parser,
"room");
475 if (!room_id_str_or.ok()) {
476 return room_id_str_or.status();
478 auto object_id_str_or = GetRequiredString(parser,
"id");
479 if (!object_id_str_or.ok()) {
480 return object_id_str_or.status();
482 const std::string room_id_str = room_id_str_or.value();
483 const std::string object_id_str = object_id_str_or.value();
485 int room_id, object_id;
486 if (!ParseHexString(room_id_str, &room_id)) {
487 return absl::InvalidArgumentError(
"Invalid room ID. Must be hex.");
489 if (!ParseHexString(object_id_str, &object_id)) {
490 return absl::InvalidArgumentError(
"Invalid object ID. Must be hex.");
492 auto room_status = ValidateRoomId(room_id);
493 if (!room_status.ok()) {
496 if (object_id < 0 || object_id > 0xFFFF) {
497 return absl::InvalidArgumentError(
"Object ID must be 0x0000-0xFFFF");
500 auto x_or = GetRequiredInt(parser,
"x");
502 return x_or.status();
504 auto y_or = GetRequiredInt(parser,
"y");
506 return y_or.status();
508 auto size_or = GetOptionalInt(parser,
"size", 0);
510 return size_or.status();
512 auto layer_or = GetOptionalInt(parser,
"layer", 0);
513 if (!layer_or.ok()) {
514 return layer_or.status();
517 int x = x_or.value();
518 int y = y_or.value();
519 int size = size_or.value();
520 int layer = layer_or.value();
521 bool do_write = parser.
HasFlag(
"write");
524 if (x < 0 || x > 63) {
525 return absl::InvalidArgumentError(
"X must be 0-63");
527 if (y < 0 || y > 63) {
528 return absl::InvalidArgumentError(
"Y must be 0-63");
530 if (layer < 0 || layer > 2) {
531 return absl::InvalidArgumentError(
"Layer must be 0, 1, or 2");
533 if (size < 0 || size > 0xFF) {
534 return absl::InvalidArgumentError(
"Size must be 0-255");
537 std::optional<ObjectSaveManifestContext> manifest_context;
540 manifest_context.has_value() ? &manifest_context->allocator_layout
546 int count_before =
static_cast<int>(room.
GetTileObjects().size());
550 static_cast<uint8_t
>(x),
static_cast<uint8_t
>(y),
551 static_cast<uint8_t
>(size),
552 static_cast<uint8_t
>(layer));
562 formatter.
AddField(
"object_type", type);
567 formatter.
AddField(
"objects_before", count_before);
568 if (
const auto manifest_path = parser.
GetString(
"manifest");
569 manifest_path.has_value()) {
570 formatter.
AddField(
"manifest", *manifest_path);
571 formatter.
AddField(
"manifest_write_policy",
"block");
573 formatter.
AddField(
"allocator_capability",
574 layout !=
nullptr ?
"copy_on_write" :
"none");
578 if (!add_status.ok()) {
579 formatter.
AddField(
"error", std::string(add_status.message()));
586 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
588 const auto preflight_status = PreflightObjectSave(
589 *rom, room_id, obj, room,
590 manifest_context.has_value() ? &*manifest_context :
nullptr);
591 if (!preflight_status.ok()) {
592 formatter.
AddField(
"preflight_status",
"failed");
593 formatter.
AddField(
"preflight_error",
594 std::string(preflight_status.message()));
596 return preflight_status;
598 formatter.
AddField(
"preflight_status",
"success");
601 auto save_status = MutateAndSaveRomWithBackup(
602 rom, formatter, [&room, layout]() {
return room.
SaveObjects(layout); });
603 if (!save_status.ok()) {
610 return absl::OkStatus();
620 auto room_id_str_or = GetRequiredString(parser,
"room");
621 if (!room_id_str_or.ok()) {
622 return room_id_str_or.status();
624 auto tiles_str_or = GetRequiredString(parser,
"tiles");
625 if (!tiles_str_or.ok()) {
626 return tiles_str_or.status();
628 const std::string room_id_str = room_id_str_or.value();
629 const std::string tiles_str = tiles_str_or.value();
632 if (!ParseHexString(room_id_str, &room_id)) {
633 return absl::InvalidArgumentError(
"Invalid room ID. Must be hex.");
635 auto room_status = ValidateRoomId(room_id);
636 if (!room_status.ok()) {
640 bool do_write = parser.
HasFlag(
"write");
646 std::vector<TileSpec> specs;
648 for (absl::string_view entry :
649 absl::StrSplit(tiles_str,
';', absl::SkipEmpty())) {
650 std::vector<std::string> parts =
651 absl::StrSplit(entry,
',', absl::SkipEmpty());
652 if (parts.size() != 3) {
653 return absl::InvalidArgumentError(absl::StrFormat(
654 "Invalid tile spec '%s'. Expected x,y,tile (e.g. 10,5,0xB7)", entry));
659 if (!absl::SimpleAtoi(parts[0], &spec.x)) {
660 return absl::InvalidArgumentError(
661 absl::StrFormat(
"Invalid X coord '%s'", parts[0]));
663 if (!absl::SimpleAtoi(parts[1], &spec.y)) {
664 return absl::InvalidArgumentError(
665 absl::StrFormat(
"Invalid Y coord '%s'", parts[1]));
667 if (!ParseHexString(parts[2], &spec.tile)) {
668 return absl::InvalidArgumentError(
669 absl::StrFormat(
"Invalid tile value '%s'. Must be hex.", parts[2]));
673 if (spec.x < 0 || spec.x > 63 || spec.y < 0 || spec.y > 63) {
674 return absl::InvalidArgumentError(absl::StrFormat(
675 "Tile coords (%d,%d) out of range (0-63)", spec.x, spec.y));
677 if (spec.tile < 0 || spec.tile > 0xFF) {
678 return absl::InvalidArgumentError(
"Tile value must be 0x00-0xFF");
681 specs.push_back(spec);
685 return absl::InvalidArgumentError(
"No tile specs provided");
694 formatter.
AddField(
"tile_count",
static_cast<int>(specs.size()));
695 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
699 for (
const auto& spec : specs) {
714 MutateAndSaveRomWithBackup(rom, formatter, [&rom, &room]()
mutable {
716 std::array<zelda3::Room, 1> rooms_arr = {std::move(room)};
719 if (!save_status.ok()) {
726 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
const auto & vector() const
absl::Status SaveToFile(const SaveSettings &settings)
absl::Status LoadFromData(const std::vector< uint8_t > &data, const LoadOptions &options=LoadOptions::Defaults())
absl::StatusOr< uint32_t > ReadLong(int offset) const
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
Utility for parsing common CLI argument patterns.
std::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
bool HasFlag(const std::string &name) const
Check if a flag is present.
absl::StatusOr< int > GetInt(const std::string &name) const
Parse an integer argument (supports hex with 0x prefix)
Loads and queries the hack manifest JSON for yaze-ASM integration.
const DungeonStreamLayout * GetDungeonStreamLayout(DungeonStreamType stream) const
Get an explicitly declared dungeon stream layout.
absl::Status LoadFromFile(const std::string &filepath)
Load manifest from a JSON file path.
std::vector< WriteConflict > AnalyzePcWriteRanges(const std::vector< std::pair< uint32_t, uint32_t > > &pc_ranges) const
Analyze a set of PC-offset ranges for write conflicts.
static int DetermineObjectType(uint8_t b1, uint8_t b3)
uint8_t GetCollisionTile(int x, int y) const
absl::Status SaveObjects(const DungeonStreamLayout *layout=nullptr)
std::vector< uint8_t > EncodeObjects() const
const std::vector< zelda3::Sprite > & GetSprites() const
absl::Status SaveSprites(const DungeonStreamLayout *layout=nullptr)
const std::vector< RoomObject > & GetTileObjects() const
void SetCollisionTile(int x, int y, uint8_t tile)
absl::Status AddObject(const RoomObject &object)
bool has_custom_collision() const
#define ASSIGN_OR_RETURN(type_variable_name, expression)
absl::Status ValidateRoomId(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< std::optional< ObjectSaveManifestContext > > LoadObjectSaveManifestContext(const resources::ArgumentParser &parser)
absl::Status ValidateSpriteCoord(int value, char axis)
absl::StatusOr< int > GetRequiredInt(const resources::ArgumentParser &parser, const char *name)
absl::Status PreflightObjectSave(const Rom &source_rom, int room_id, const zelda3::RoomObject &object, const zelda3::Room &pending_room, const ObjectSaveManifestContext *manifest_context)
absl::Status MutateAndSaveRomWithBackup(Rom *rom, resources::OutputFormatter &formatter, Mutation &&mutation)
absl::Status SaveRomWithBackup(Rom *rom, resources::OutputFormatter &formatter)
bool ParseHexString(absl::string_view str, int *out)
absl::StatusOr< zelda3::DungeonStreamLayout > ToDungeonStreamAllocatorLayout(DungeonStreamType stream_type, const DungeonStreamLayout &manifest_layout)
constexpr int kDoorPointers
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
Room LoadRoomFromRom(Rom *rom, int room_id)
std::string GetObjectName(int object_id)
constexpr int kNumberOfRooms
absl::Status SaveAllCollision(Rom *rom, absl::Span< Room > rooms)
const char * ResolveSpriteName(uint16_t id)
constexpr int kRoomObjectPointer
uint32_t SnesToPc(uint32_t addr) noexcept
#define RETURN_IF_ERROR(expr)
bool load_resource_labels
core::HackManifest manifest
zelda3::DungeonStreamLayout allocator_layout
project::RomWritePolicy write_policy
Explicit pointer and storage layout for one dungeon stream.
DungeonWriteStrategy strategy
std::vector< DungeonStreamPcRange > allocation_ranges
DungeonPointerEncoding pointer_encoding
uint32_t pointer_table_pc