7#include "absl/strings/ascii.h"
8#include "absl/strings/str_cat.h"
9#include "absl/strings/str_format.h"
10#include "absl/strings/str_split.h"
44 for (
int i = 0; i < static_cast<int>(existing.
tiles.size()); ++i) {
53 auto registry_path = parser.
GetString(
"sprite-registry");
54 if (!registry_path.has_value()) {
55 return absl::OkStatus();
58 std::ifstream file(registry_path.value());
59 if (!file.is_open()) {
60 return absl::NotFoundError(absl::StrFormat(
61 "Could not open sprite registry: %s", registry_path.value()));
64 std::stringstream buffer;
65 buffer << file.rdbuf();
75 auto registry_status = MaybeLoadSpriteRegistry(parser);
76 if (!registry_status.ok()) {
77 return registry_status;
80 auto room_id_str = parser.
GetString(
"room").value();
83 if (!ParseHexString(room_id_str, &room_id)) {
84 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
88 formatter.
AddField(
"room_id", room_id);
94 formatter.
AddField(
"total_sprites",
static_cast<int>(sprites.size()));
95 formatter.
AddField(
"status",
"success");
98 for (
const auto& sprite : sprites) {
100 formatter.
AddHexField(
"sprite_id", sprite.id(), 2);
102 formatter.
AddField(
"x", sprite.x());
103 formatter.
AddField(
"y", sprite.y());
104 formatter.
AddField(
"subtype", sprite.subtype());
105 formatter.
AddField(
"layer", sprite.layer());
106 if (sprite.key_drop() > 0) {
107 formatter.
AddField(
"key_drop", sprite.key_drop());
114 return absl::OkStatus();
121 auto registry_status = MaybeLoadSpriteRegistry(parser);
122 if (!registry_status.ok()) {
123 return registry_status;
126 auto room_id_str = parser.
GetString(
"room").value();
129 if (!ParseHexString(room_id_str, &room_id)) {
130 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
133 formatter.
AddField(
"room_id", room_id);
138 formatter.
AddField(
"status",
"success");
139 formatter.
AddField(
"name", absl::StrFormat(
"Room %d", room.
id()));
141 formatter.
AddField(
"room_type",
"Dungeon Room");
152 formatter.
AddField(
"tag1",
static_cast<int>(room.
tag1()));
153 formatter.
AddField(
"tag2",
static_cast<int>(room.
tag2()));
163 for (
const auto& door : room.
GetDoors()) {
165 formatter.
AddField(
"position", door.position);
166 formatter.
AddField(
"direction", std::string(door.GetDirectionName()));
167 formatter.
AddField(
"type", std::string(door.GetTypeName()));
168 auto [tx, ty] = door.GetTileCoords();
177 for (
const auto& stair : room.
GetStairs()) {
182 "tile_y", stair.room);
183 formatter.
AddField(
"label", stair.label);
199 return absl::OkStatus();
205 auto room_id_opt = parser.
GetString(
"room");
207 bool has_room_filter = room_id_opt.has_value();
208 int room_filter = -1;
209 if (has_room_filter) {
210 if (!ParseHexString(room_id_opt.value(), &room_filter)) {
211 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
216 int rooms_with_chests = 0;
217 int total_chests = 0;
218 std::map<int, int> item_counts;
221 formatter.
AddField(
"room_filter", has_room_filter
222 ? absl::StrFormat(
"0x%02X", room_filter)
226 int start_room = has_room_filter ? room_filter : 0;
229 for (
int room_id = start_room; room_id <= end_room; ++room_id) {
235 if (chests.empty()) {
240 total_chests +=
static_cast<int>(chests.size());
243 formatter.
AddField(
"room_id", absl::StrFormat(
"0x%02X", room_id));
245 formatter.
AddField(
"chest_count",
static_cast<int>(chests.size()));
249 for (
const auto&
chest : chests) {
251 formatter.
AddField(
"index", chest_index++);
258 item_counts[
chest.id]++;
267 formatter.
AddField(
"total_rooms", total_rooms);
268 formatter.
AddField(
"rooms_with_chests", rooms_with_chests);
269 formatter.
AddField(
"total_chests", total_chests);
270 formatter.
AddField(
"unique_items",
static_cast<int>(item_counts.size()));
273 for (
const auto& [item_id, count] : item_counts) {
287 return absl::OkStatus();
293 auto entrance_id_str = parser.
GetString(
"entrance").value();
294 bool is_spawn_point = parser.
HasFlag(
"spawn");
297 if (!ParseHexString(entrance_id_str, &entrance_id)) {
298 return absl::InvalidArgumentError(
299 "Invalid entrance ID format. Must be hex.");
305 formatter.
AddField(
"entrance_id", absl::StrFormat(
"0x%02X", entrance_id));
306 formatter.
AddField(
"is_spawn_point", is_spawn_point);
307 formatter.
AddField(
"room_id", absl::StrFormat(
"0x%04X", entrance.
room_));
308 formatter.
AddField(
"exit_id", absl::StrFormat(
"0x%04X", entrance.
exit_));
324 formatter.
AddField(
"floor", absl::StrFormat(
"0x%02X", entrance.
floor_));
327 formatter.
AddField(
"door", absl::StrFormat(
"0x%02X", entrance.
door_));
329 absl::StrFormat(
"0x%02X", entrance.
ladder_bg_));
331 absl::StrFormat(
"0x%02X", entrance.
scrolling_));
332 formatter.
AddField(
"scroll_quadrant",
334 formatter.
AddField(
"music", absl::StrFormat(
"0x%02X", entrance.
music_));
356 return absl::OkStatus();
362 auto room_id_str = parser.
GetString(
"room").value();
365 if (!ParseHexString(room_id_str, &room_id)) {
366 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
370 formatter.
AddField(
"room_id", room_id);
374 auto room_or = dungeon_editor.
GetRoom(room_id);
376 formatter.
AddField(
"status",
"error");
377 formatter.
AddField(
"error", room_or.status().ToString());
379 return room_or.status();
382 auto& room = room_or.value();
385 formatter.
AddField(
"status",
"success");
386 formatter.
AddField(
"room_width",
"Unknown");
387 formatter.
AddField(
"room_height",
"Unknown");
388 formatter.
AddField(
"room_name", absl::StrFormat(
"Room %d", room.id()));
392 formatter.
AddField(
"tiles",
"Room tile data would be exported here");
393 formatter.
AddField(
"sprites",
"Room sprite data would be exported here");
394 formatter.
AddField(
"doors",
"Room door data would be exported here");
399 return absl::OkStatus();
405 auto room_id_str = parser.
GetString(
"room").value();
408 if (!ParseHexString(room_id_str, &room_id)) {
409 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
413 formatter.
AddField(
"room_id", room_id);
417 auto room_or = dungeon_editor.
GetRoom(room_id);
419 formatter.
AddField(
"status",
"error");
420 formatter.
AddField(
"error", room_or.status().ToString());
422 return room_or.status();
425 auto& room = room_or.value();
430 const auto& objects = room.GetTileObjects();
431 formatter.
AddField(
"total_objects",
static_cast<int>(objects.size()));
432 formatter.
AddField(
"status",
"success");
435 for (
const auto& obj : objects) {
438 formatter.
AddField(
"id_hex", absl::StrFormat(
"0x%04X", obj.id_));
441 formatter.
AddField(
"size", obj.size_);
442 formatter.
AddField(
"layer",
static_cast<int>(obj.layer_));
452 return absl::OkStatus();
458 auto room_id_str = parser.
GetString(
"room").value();
461 if (!ParseHexString(room_id_str, &room_id)) {
462 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
466 formatter.
AddField(
"room_id", room_id);
470 auto room_or = dungeon_editor.
GetRoom(room_id);
472 formatter.
AddField(
"status",
"error");
473 formatter.
AddField(
"error", room_or.status().ToString());
475 return room_or.status();
478 auto& room = room_or.value();
482 formatter.
AddField(
"room_width", 64);
483 formatter.
AddField(
"room_height", 64);
484 formatter.
AddField(
"total_tiles", 64 * 64);
485 formatter.
AddField(
"has_custom_collision", room.has_custom_collision());
487 static_cast<int>(room.GetTileObjects().size()));
488 formatter.
AddField(
"status",
"success");
493 const auto& collision = room.custom_collision().tiles;
494 for (
int y = 0; y < 64; ++y) {
497 for (
int x = 0; x < 64; ++x) {
501 absl::StrAppend(&row, absl::StrFormat(
"%02X", collision[y * 64 + x]));
508 return absl::OkStatus();
514 auto room_id_str = parser.
GetString(
"room").value();
515 auto property = parser.
GetString(
"property").value();
516 auto value = parser.
GetString(
"value").value();
519 if (!ParseHexString(room_id_str, &room_id)) {
520 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
523 formatter.
BeginObject(
"Dungeon Room Property Set");
524 formatter.
AddField(
"room_id", room_id);
525 formatter.
AddField(
"property", property);
530 auto room_or = dungeon_editor.
GetRoom(room_id);
532 formatter.
AddField(
"status",
"error");
533 formatter.
AddField(
"error", room_or.status().ToString());
535 return room_or.status();
538 auto& room = room_or.value();
540 int parsed_value = 0;
541 if (!ParseHexString(value, &parsed_value)) {
542 return absl::InvalidArgumentError(absl::StrFormat(
543 "Invalid value format: %s (expected integer/hex)", value));
546 const std::string prop = absl::AsciiStrToLower(property);
547 if (prop ==
"palette") {
548 room.SetPalette(
static_cast<uint8_t
>(parsed_value & 0xFF));
549 }
else if (prop ==
"blockset") {
550 room.SetBlockset(
static_cast<uint8_t
>(parsed_value & 0xFF));
551 }
else if (prop ==
"spriteset") {
552 room.SetSpriteset(
static_cast<uint8_t
>(parsed_value & 0xFF));
553 }
else if (prop ==
"layout" || prop ==
"layout_id") {
554 room.SetLayoutId(
static_cast<uint8_t
>(parsed_value & 0xFF));
555 }
else if (prop ==
"floor1") {
556 room.set_floor1(
static_cast<uint8_t
>(parsed_value & 0xFF));
557 }
else if (prop ==
"floor2") {
558 room.set_floor2(
static_cast<uint8_t
>(parsed_value & 0xFF));
559 }
else if (prop ==
"effect") {
561 }
else if (prop ==
"tag1") {
563 }
else if (prop ==
"tag2") {
565 }
else if (prop ==
"holewarp") {
566 room.SetHolewarp(
static_cast<uint8_t
>(parsed_value & 0xFF));
568 return absl::InvalidArgumentError(
569 absl::StrFormat(
"Unsupported property: %s", property));
574 formatter.
AddField(
"status",
"success");
575 if (parser.
HasFlag(
"mock-rom")) {
576 formatter.
AddField(
"save_status",
"mock-rom-skipped");
579 save_settings.
backup =
true;
581 formatter.
AddField(
"save_status",
"saved");
585 return absl::OkStatus();
591 auto room_id_str = parser.
GetString(
"room").value();
594 if (!ParseHexString(room_id_str, &room_id)) {
595 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
599 formatter.
AddField(
"room_id", room_id);
613 int header_pointer_pc =
SnesToPc(header_pointer);
616 formatter.
AddHexField(
"snes_address", header_pointer, 6);
617 formatter.
AddHexField(
"pc_address", header_pointer_pc, 6);
621 int table_offset = header_pointer_pc + (room_id * 2);
623 (rom->
data()[table_offset + 1] << 8) +
624 rom->
data()[table_offset];
625 int room_header_pc =
SnesToPc(room_header_addr);
628 formatter.
AddHexField(
"table_offset_pc", table_offset, 6);
629 formatter.
AddHexField(
"snes_address", room_header_addr, 6);
630 formatter.
AddHexField(
"pc_address", room_header_pc, 6);
635 for (
int i = 0; i < 14; ++i) {
636 if (room_header_pc + i <
static_cast<int>(rom->
size())) {
638 absl::StrFormat(
"0x%02X", rom->
data()[room_header_pc + i]));
644 if (room_header_pc >= 0 &&
645 room_header_pc + 13 <
static_cast<int>(rom->
size())) {
646 uint8_t byte0 = rom->
data()[room_header_pc];
647 uint8_t byte1 = rom->
data()[room_header_pc + 1];
648 uint8_t byte2 = rom->
data()[room_header_pc + 2];
649 uint8_t byte3 = rom->
data()[room_header_pc + 3];
652 formatter.
AddField(
"bg2", (byte0 >> 5) & 0x07);
653 formatter.
AddField(
"collision", (byte0 >> 2) & 0x07);
654 formatter.
AddField(
"is_light", (byte0 & 0x01) == 1);
655 formatter.
AddField(
"palette", byte1 & 0x3F);
656 formatter.
AddField(
"blockset", byte2);
657 formatter.
AddField(
"spriteset", byte3);
658 formatter.
AddField(
"effect", rom->
data()[room_header_pc + 4]);
659 formatter.
AddField(
"tag1", rom->
data()[room_header_pc + 5]);
660 formatter.
AddField(
"tag2", rom->
data()[room_header_pc + 6]);
661 formatter.
AddField(
"holewarp", rom->
data()[room_header_pc + 9]);
662 formatter.
AddField(
"stair1_room", rom->
data()[room_header_pc + 10]);
663 formatter.
AddField(
"stair2_room", rom->
data()[room_header_pc + 11]);
664 formatter.
AddField(
"stair3_room", rom->
data()[room_header_pc + 12]);
665 formatter.
AddField(
"stair4_room", rom->
data()[room_header_pc + 13]);
668 formatter.
AddField(
"error",
"Room header address out of range");
672 return absl::OkStatus();
682 auto switch_str = parser.
GetString(
"promote-switch");
683 if (switch_str.has_value()) {
684 for (absl::string_view pair :
685 absl::StrSplit(switch_str.value(),
' ', absl::SkipEmpty())) {
686 std::vector<std::string> coords =
687 absl::StrSplit(pair,
',', absl::SkipEmpty());
688 if (coords.size() == 2) {
690 if (ParseHexString(coords[0], &sx) && ParseHexString(coords[1], &sy)) {
697 bool do_write = parser.
HasFlag(
"write");
698 bool do_preserve_stops = parser.
HasFlag(
"preserve-stops");
699 bool do_visualize = parser.
HasFlag(
"visualize");
702 std::vector<int> room_ids;
703 auto rooms_arg = parser.
GetString(
"rooms");
704 auto room_arg = parser.
GetString(
"room");
706 if (rooms_arg.has_value()) {
708 for (absl::string_view token :
709 absl::StrSplit(rooms_arg.value(),
',', absl::SkipEmpty())) {
711 std::string token_str(token);
712 if (!ParseHexString(token_str, &rid)) {
713 return absl::InvalidArgumentError(absl::StrFormat(
714 "Invalid room ID '%s' in --rooms list. Must be hex.", token_str));
716 room_ids.push_back(rid);
718 if (room_ids.empty()) {
719 return absl::InvalidArgumentError(
"--rooms list is empty.");
721 }
else if (room_arg.has_value()) {
724 if (!ParseHexString(room_arg.value(), &rid)) {
725 return absl::InvalidArgumentError(
"Invalid room ID format. Must be hex.");
727 room_ids.push_back(rid);
729 return absl::InvalidArgumentError(
"Either --room or --rooms is required.");
732 bool is_batch = room_ids.size() > 1;
736 formatter.
BeginObject(
"Batch Track Collision Generation");
737 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
738 formatter.
AddField(
"room_count",
static_cast<int>(room_ids.size()));
742 int total_corners = 0;
743 int total_switches = 0;
744 int rooms_succeeded = 0;
747 for (
int room_id : room_ids) {
756 formatter.
AddField(
"error", std::string(result.status().message()));
758 return absl::InternalError(
759 absl::StrFormat(
"Generation failed for room 0x%03X: %s", room_id,
760 result.status().message()));
763 if (do_preserve_stops && do_write) {
765 if (existing.ok() && existing->has_data) {
766 MergeStopTiles(*existing, result->collision_map);
772 formatter.
AddField(
"tiles_generated", result->tiles_generated);
773 formatter.
AddField(
"stop_count", result->stop_count);
774 formatter.
AddField(
"corner_count", result->corner_count);
775 formatter.
AddField(
"switch_count", result->switch_count);
776 if (do_preserve_stops) {
777 formatter.
AddField(
"stops_preserved",
true);
781 formatter.
AddField(
"visualization", result->ascii_visualization);
787 if (!write_status.ok()) {
790 std::string(write_status.message()));
794 return absl::InternalError(
795 absl::StrFormat(
"Write failed for room 0x%03X: %s", room_id,
796 write_status.message()));
798 formatter.
AddField(
"write_status",
"success");
801 total_tiles += result->tiles_generated;
802 total_stops += result->stop_count;
803 total_corners += result->corner_count;
804 total_switches += result->switch_count;
814 save_settings.
backup =
true;
815 auto save_status = rom->
SaveToFile(save_settings);
816 if (!save_status.ok()) {
817 formatter.
AddField(
"save_error", std::string(save_status.message()));
819 formatter.
AddField(
"save_status",
"saved");
825 formatter.
AddField(
"rooms_succeeded", rooms_succeeded);
826 formatter.
AddField(
"tiles_generated", total_tiles);
827 formatter.
AddField(
"stop_count", total_stops);
828 formatter.
AddField(
"corner_count", total_corners);
829 formatter.
AddField(
"switch_count", total_switches);
835 int room_id = room_ids[0];
842 return result.status();
845 if (do_preserve_stops && do_write) {
847 if (existing.ok() && existing->has_data) {
848 MergeStopTiles(*existing, result->collision_map);
852 formatter.
BeginObject(
"Track Collision Generation");
854 formatter.
AddField(
"tiles_generated", result->tiles_generated);
855 formatter.
AddField(
"stop_count", result->stop_count);
856 formatter.
AddField(
"corner_count", result->corner_count);
857 formatter.
AddField(
"switch_count", result->switch_count);
858 formatter.
AddField(
"mode", do_write ?
"write" :
"dry-run");
859 if (do_preserve_stops) {
860 formatter.
AddField(
"stops_preserved",
true);
863 if (do_visualize || !do_write) {
864 formatter.
AddField(
"visualization", result->ascii_visualization);
870 if (!write_status.ok()) {
871 formatter.
AddField(
"write_error", std::string(write_status.message()));
873 formatter.
AddField(
"write_status",
"success");
876 save_settings.
backup =
true;
877 auto save_status = rom->
SaveToFile(save_settings);
878 if (!save_status.ok()) {
879 formatter.
AddField(
"save_error", std::string(save_status.message()));
881 formatter.
AddField(
"save_status",
"saved");
889 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
absl::Status SaveToFile(const SaveSettings &settings)
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
Utility for parsing common CLI argument patterns.
std::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
bool HasFlag(const std::string &name) const
Check if a flag is present.
Core dungeon editing system.
absl::StatusOr< Room > GetRoom(int room_id)
absl::Status ImportOracleSpriteRegistry(const std::string &csv_content)
Import sprite labels from Oracle of Secrets registry.csv format.
Dungeon Room Entrance or Spawn Point.
uint8_t camera_boundary_fw_
uint8_t camera_boundary_qe_
uint8_t camera_boundary_fe_
uint8_t camera_boundary_fs_
uint8_t camera_boundary_qs_
uint8_t camera_boundary_qw_
uint16_t camera_trigger_y_
uint8_t camera_boundary_fn_
uint16_t camera_trigger_x_
uint8_t camera_boundary_qn_
static int DetermineObjectType(uint8_t b1, uint8_t b3)
const std::vector< chest_data > & GetChests() const
const std::vector< Door > & GetDoors() const
const std::vector< staircase > & GetStairs() const
uint8_t spriteset() const
const std::vector< zelda3::Sprite > & GetSprites() const
const std::vector< RoomObject > & GetTileObjects() const
uint8_t layout_id() const
absl::Status MaybeLoadSpriteRegistry(const resources::ArgumentParser &parser)
bool IsStopTile(uint8_t v)
constexpr uint8_t kStopTileMin
constexpr uint8_t kStopTileMax
void MergeStopTiles(const zelda3::CustomCollisionMap &existing, zelda3::CustomCollisionMap &generated)
bool ParseHexString(absl::string_view str, int *out)
absl::Status WriteTrackCollision(Rom *rom, int room_id, const CustomCollisionMap &map)
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
std::string GetRoomLabel(int id)
Convenience function to get a room label.
std::string GetItemLabel(int id)
Convenience function to get an item label.
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
Room LoadRoomFromRom(Rom *rom, int room_id)
constexpr int kNumberOfRooms
constexpr int kRoomHeaderPointer
constexpr int kRoomHeaderPointerBank
const char * ResolveSpriteName(uint16_t id)
absl::StatusOr< TrackCollisionResult > GenerateTrackCollision(Room *room, const GeneratorOptions &options)
ResourceLabelProvider & GetResourceLabels()
Get the global ResourceLabelProvider instance.
uint32_t SnesToPc(uint32_t addr) noexcept
#define RETURN_IF_ERROR(expr)
std::array< uint8_t, 64 *64 > tiles
std::vector< std::pair< int, int > > switch_promotions