diff --git a/src/app/zelda3/overworld/overworld.h b/src/app/zelda3/overworld/overworld.h index c1897595..e3048d80 100644 --- a/src/app/zelda3/overworld/overworld.h +++ b/src/app/zelda3/overworld/overworld.h @@ -257,8 +257,10 @@ class Overworld { auto current_graphics() const { return overworld_maps_[current_map_].current_graphics(); } + const std::vector &entrances() const { return all_entrances_; } auto &entrances() { return all_entrances_; } auto mutable_entrances() { return &all_entrances_; } + const std::vector &holes() const { return all_holes_; } auto &holes() { return all_holes_; } auto mutable_holes() { return &all_holes_; } auto deleted_entrances() const { return deleted_entrances_; } diff --git a/src/cli/handlers/agent/commands.h b/src/cli/handlers/agent/commands.h index ff8c285e..e786b036 100644 --- a/src/cli/handlers/agent/commands.h +++ b/src/cli/handlers/agent/commands.h @@ -31,6 +31,12 @@ absl::Status HandleResourceListCommand( absl::Status HandleDungeonListSpritesCommand( const std::vector& arg_vec, Rom* rom_context = nullptr); +absl::Status HandleOverworldDescribeMapCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleOverworldListWarpsCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); absl::Status HandleChatCommand(Rom& rom); } // namespace agent diff --git a/src/cli/handlers/agent/tool_commands.cc b/src/cli/handlers/agent/tool_commands.cc index f5987ab9..adbe0969 100644 --- a/src/cli/handlers/agent/tool_commands.cc +++ b/src/cli/handlers/agent/tool_commands.cc @@ -1,8 +1,10 @@ #include "cli/handlers/agent/commands.h" #include +#include #include #include +#include #include "absl/flags/declare.h" #include "absl/flags/flag.h" @@ -13,8 +15,11 @@ #include "absl/strings/numbers.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" +#include "absl/strings/str_join.h" #include "app/rom.h" #include "app/zelda3/dungeon/room.h" +#include "app/zelda3/overworld/overworld.h" +#include "cli/handlers/overworld_inspect.h" #include "cli/service/resources/resource_context_builder.h" ABSL_DECLARE_FLAG(std::string, rom); @@ -42,6 +47,21 @@ absl::StatusOr LoadRomFromFlag() { return rom; } +absl::StatusOr LoadRomFromPathOrFlag( + const std::optional& override_path) { + if (override_path.has_value()) { + Rom rom; + auto status = rom.LoadFromFile(*override_path); + if (!status.ok()) { + return absl::FailedPreconditionError(absl::StrFormat( + "Failed to load ROM from '%s': %s", *override_path, + status.message())); + } + return rom; + } + return LoadRomFromFlag(); +} + } // namespace absl::Status HandleResourceListCommand( @@ -189,6 +209,376 @@ absl::Status HandleDungeonListSpritesCommand( return absl::OkStatus(); } +absl::Status HandleOverworldDescribeMapCommand( + const std::vector& arg_vec, Rom* rom_context) { + std::optional map_value; + std::string format = "json"; + std::optional rom_override; + + for (size_t i = 0; i < arg_vec.size(); ++i) { + const std::string& token = arg_vec[i]; + if (token == "--map") { + if (i + 1 >= arg_vec.size()) { + return absl::InvalidArgumentError( + "--map requires a value. Usage: agent overworld-describe-map --map [--format ]"); + } + map_value = arg_vec[++i]; + } else if (absl::StartsWith(token, "--map=")) { + map_value = token.substr(6); + } else if (token == "--format") { + if (i + 1 >= arg_vec.size()) { + return absl::InvalidArgumentError("--format requires a value."); + } + format = absl::AsciiStrToLower(arg_vec[++i]); + } else if (absl::StartsWith(token, "--format=")) { + format = absl::AsciiStrToLower(token.substr(9)); + } else if (token == "--rom") { + if (i + 1 >= arg_vec.size()) { + return absl::InvalidArgumentError("--rom requires a value."); + } + rom_override = arg_vec[++i]; + } else if (absl::StartsWith(token, "--rom=")) { + rom_override = token.substr(6); + } + } + + if (!map_value.has_value()) { + return absl::InvalidArgumentError( + "Usage: agent overworld-describe-map --map [--format ]"); + } + + ASSIGN_OR_RETURN(int map_id, + overworld::ParseNumeric(*map_value)); + if (map_id < 0 || map_id >= zelda3::kNumOverworldMaps) { + return absl::InvalidArgumentError( + absl::StrCat("Map ID out of range: ", *map_value)); + } + + if (format != "json" && format != "text") { + return absl::InvalidArgumentError( + absl::StrCat("Unsupported format: ", format)); + } + + Rom rom_storage; + Rom* rom = nullptr; + if (rom_context != nullptr && rom_context->is_loaded() && !rom_override.has_value()) { + rom = rom_context; + } else { + ASSIGN_OR_RETURN(auto rom_or, + LoadRomFromPathOrFlag(rom_override)); + rom_storage = std::move(rom_or); + rom = &rom_storage; + } + + zelda3::Overworld overworld_data(rom); + auto load_status = overworld_data.Load(rom); + if (!load_status.ok()) { + return load_status; + } + + ASSIGN_OR_RETURN(auto summary, + overworld::BuildMapSummary(overworld_data, map_id)); + + auto join_hex = [](const std::vector& values) { + std::vector parts; + parts.reserve(values.size()); + for (uint8_t v : values) { + parts.push_back(absl::StrFormat("0x%02X", v)); + } + return absl::StrJoin(parts, ", "); + }; + + auto join_hex_json = [](const std::vector& values) { + std::vector parts; + parts.reserve(values.size()); + for (uint8_t v : values) { + parts.push_back(absl::StrFormat("\"0x%02X\"", v)); + } + return absl::StrCat("[", absl::StrJoin(parts, ", "), "]"); + }; + + if (format == "json") { + std::cout << "{\n"; + std::cout << absl::StrFormat(" \"map\": \"0x%02X\",\n", summary.map_id); + std::cout << absl::StrFormat(" \"world\": \"%s\",\n", + overworld::WorldName(summary.world)); + std::cout << absl::StrFormat( + " \"grid\": {\"x\": %d, \"y\": %d, \"index\": %d},\n", + summary.map_x, summary.map_y, summary.local_index); + std::cout << absl::StrFormat( + " \"size\": {\"label\": \"%s\", \"is_large\": %s, \"parent\": \"0x%02X\", \"quadrant\": %d},\n", + summary.area_size, summary.is_large_map ? "true" : "false", + summary.parent_map, summary.large_quadrant); + std::cout << absl::StrFormat( + " \"message\": \"0x%04X\",\n", summary.message_id); + std::cout << absl::StrFormat( + " \"area_graphics\": \"0x%02X\",\n", summary.area_graphics); + std::cout << absl::StrFormat( + " \"area_palette\": \"0x%02X\",\n", summary.area_palette); + std::cout << absl::StrFormat( + " \"main_palette\": \"0x%02X\",\n", summary.main_palette); + std::cout << absl::StrFormat( + " \"animated_gfx\": \"0x%02X\",\n", summary.animated_gfx); + std::cout << absl::StrFormat( + " \"subscreen_overlay\": \"0x%04X\",\n", + summary.subscreen_overlay); + std::cout << absl::StrFormat( + " \"area_specific_bg_color\": \"0x%04X\",\n", + summary.area_specific_bg_color); + std::cout << absl::StrFormat( + " \"sprite_graphics\": %s,\n", join_hex_json(summary.sprite_graphics)); + std::cout << absl::StrFormat( + " \"sprite_palettes\": %s,\n", join_hex_json(summary.sprite_palettes)); + std::cout << absl::StrFormat( + " \"area_music\": %s,\n", join_hex_json(summary.area_music)); + std::cout << absl::StrFormat( + " \"static_graphics\": %s,\n", + join_hex_json(summary.static_graphics)); + std::cout << absl::StrFormat( + " \"overlay\": {\"enabled\": %s, \"id\": \"0x%04X\"}\n", + summary.has_overlay ? "true" : "false", summary.overlay_id); + std::cout << "}\n"; + } else { + std::cout << absl::StrFormat("πŸ—ΊοΈ Map 0x%02X (%s World)\n", summary.map_id, + overworld::WorldName(summary.world)); + std::cout << absl::StrFormat(" Grid: (%d, %d) local-index %d\n", + summary.map_x, summary.map_y, + summary.local_index); + std::cout << absl::StrFormat( + " Size: %s%s | Parent: 0x%02X | Quadrant: %d\n", + summary.area_size, summary.is_large_map ? " (large)" : "", + summary.parent_map, summary.large_quadrant); + std::cout << absl::StrFormat( + " Message: 0x%04X | Area GFX: 0x%02X | Area Palette: 0x%02X\n", + summary.message_id, summary.area_graphics, summary.area_palette); + std::cout << absl::StrFormat( + " Main Palette: 0x%02X | Animated GFX: 0x%02X | Overlay: %s (0x%04X)\n", + summary.main_palette, summary.animated_gfx, + summary.has_overlay ? "yes" : "no", summary.overlay_id); + std::cout << absl::StrFormat( + " Subscreen Overlay: 0x%04X | BG Color: 0x%04X\n", + summary.subscreen_overlay, summary.area_specific_bg_color); + std::cout << absl::StrFormat(" Sprite GFX: [%s]\n", + join_hex(summary.sprite_graphics)); + std::cout << absl::StrFormat(" Sprite Palettes: [%s]\n", + join_hex(summary.sprite_palettes)); + std::cout << absl::StrFormat(" Area Music: [%s]\n", + join_hex(summary.area_music)); + std::cout << absl::StrFormat(" Static GFX: [%s]\n", + join_hex(summary.static_graphics)); + } + + return absl::OkStatus(); +} + +absl::Status HandleOverworldListWarpsCommand( + const std::vector& arg_vec, Rom* rom_context) { + std::optional map_value; + std::optional world_value; + std::optional type_value; + std::string format = "json"; + std::optional rom_override; + + for (size_t i = 0; i < arg_vec.size(); ++i) { + const std::string& token = arg_vec[i]; + if (token == "--map") { + if (i + 1 >= arg_vec.size()) { + return absl::InvalidArgumentError("--map requires a value."); + } + map_value = arg_vec[++i]; + } else if (absl::StartsWith(token, "--map=")) { + map_value = token.substr(6); + } else if (token == "--world") { + if (i + 1 >= arg_vec.size()) { + return absl::InvalidArgumentError("--world requires a value."); + } + world_value = arg_vec[++i]; + } else if (absl::StartsWith(token, "--world=")) { + world_value = token.substr(8); + } else if (token == "--type") { + if (i + 1 >= arg_vec.size()) { + return absl::InvalidArgumentError("--type requires a value."); + } + type_value = arg_vec[++i]; + } else if (absl::StartsWith(token, "--type=")) { + type_value = token.substr(7); + } else if (token == "--format") { + if (i + 1 >= arg_vec.size()) { + return absl::InvalidArgumentError("--format requires a value."); + } + format = absl::AsciiStrToLower(arg_vec[++i]); + } else if (absl::StartsWith(token, "--format=")) { + format = absl::AsciiStrToLower(token.substr(9)); + } else if (token == "--rom") { + if (i + 1 >= arg_vec.size()) { + return absl::InvalidArgumentError("--rom requires a value."); + } + rom_override = arg_vec[++i]; + } else if (absl::StartsWith(token, "--rom=")) { + rom_override = token.substr(6); + } + } + + if (format != "json" && format != "text") { + return absl::InvalidArgumentError( + absl::StrCat("Unsupported format: ", format)); + } + + std::optional map_filter; + if (map_value.has_value()) { + ASSIGN_OR_RETURN(int map_id, + overworld::ParseNumeric(*map_value)); + if (map_id < 0 || map_id >= zelda3::kNumOverworldMaps) { + return absl::InvalidArgumentError( + absl::StrCat("Map ID out of range: ", *map_value)); + } + map_filter = map_id; + } + + std::optional world_filter; + if (world_value.has_value()) { + ASSIGN_OR_RETURN(int world_id, + overworld::ParseWorldSpecifier(*world_value)); + world_filter = world_id; + } + + std::optional type_filter; + if (type_value.has_value()) { + std::string lower = absl::AsciiStrToLower(*type_value); + if (lower == "entrance" || lower == "entrances") { + type_filter = overworld::WarpType::kEntrance; + } else if (lower == "hole" || lower == "holes") { + type_filter = overworld::WarpType::kHole; + } else if (lower == "exit" || lower == "exits") { + type_filter = overworld::WarpType::kExit; + } else if (lower == "all" || lower.empty()) { + type_filter.reset(); + } else { + return absl::InvalidArgumentError( + absl::StrCat("Unknown warp type: ", *type_value)); + } + } + + if (map_filter.has_value()) { + ASSIGN_OR_RETURN(int inferred_world, + overworld::InferWorldFromMapId(*map_filter)); + if (world_filter.has_value() && inferred_world != *world_filter) { + return absl::InvalidArgumentError( + absl::StrCat("Map 0x", + absl::StrFormat("%02X", *map_filter), + " belongs to the ", + overworld::WorldName(inferred_world), + " World but --world requested ", + overworld::WorldName(*world_filter))); + } + if (!world_filter.has_value()) { + world_filter = inferred_world; + } + } + + Rom rom_storage; + Rom* rom = nullptr; + if (rom_context != nullptr && rom_context->is_loaded() && !rom_override.has_value()) { + rom = rom_context; + } else { + ASSIGN_OR_RETURN(auto rom_or, + LoadRomFromPathOrFlag(rom_override)); + rom_storage = std::move(rom_or); + rom = &rom_storage; + } + + zelda3::Overworld overworld_data(rom); + auto load_status = overworld_data.Load(rom); + if (!load_status.ok()) { + return load_status; + } + + overworld::WarpQuery query; + query.map_id = map_filter; + query.world = world_filter; + query.type = type_filter; + + ASSIGN_OR_RETURN(auto entries, + overworld::CollectWarpEntries(overworld_data, query)); + + if (format == "json") { + std::cout << "{\n"; + std::cout << absl::StrFormat(" \"count\": %zu,\n", entries.size()); + std::cout << " \"entries\": [\n"; + for (size_t i = 0; i < entries.size(); ++i) { + const auto& entry = entries[i]; + std::cout << " {\n"; + std::cout << absl::StrFormat( + " \"type\": \"%s\",\n", + overworld::WarpTypeName(entry.type)); + std::cout << absl::StrFormat( + " \"map\": \"0x%02X\",\n", entry.map_id); + std::cout << absl::StrFormat( + " \"world\": \"%s\",\n", + overworld::WorldName(entry.world)); + std::cout << absl::StrFormat( + " \"grid\": {\"x\": %d, \"y\": %d, \"index\": %d},\n", + entry.map_x, entry.map_y, entry.local_index); + std::cout << absl::StrFormat( + " \"tile16\": {\"x\": %d, \"y\": %d},\n", + entry.tile16_x, entry.tile16_y); + std::cout << absl::StrFormat( + " \"pixel\": {\"x\": %d, \"y\": %d},\n", + entry.pixel_x, entry.pixel_y); + std::cout << absl::StrFormat( + " \"map_pos\": \"0x%04X\",\n", entry.map_pos); + std::cout << absl::StrFormat( + " \"deleted\": %s,\n", entry.deleted ? "true" : "false"); + std::cout << absl::StrFormat( + " \"is_hole\": %s", + entry.is_hole ? "true" : "false"); + if (entry.entrance_id.has_value()) { + std::cout << absl::StrFormat( + ",\n \"entrance_id\": \"0x%02X\"", + *entry.entrance_id); + } + if (entry.entrance_name.has_value()) { + std::cout << absl::StrFormat( + ",\n \"entrance_name\": \"%s\"", + *entry.entrance_name); + } + std::cout << "\n }" << (i + 1 == entries.size() ? "" : ",") << "\n"; + } + std::cout << " ]\n"; + std::cout << "}\n"; + } else { + if (entries.empty()) { + std::cout << "No overworld warps match the specified filters." << std::endl; + return absl::OkStatus(); + } + + std::cout << absl::StrFormat("🌐 Overworld warps (%zu)\n", entries.size()); + for (const auto& entry : entries) { + std::string line = absl::StrFormat( + " β€’ %-9s map 0x%02X (%s World) tile16(%02d,%02d) pixel(%4d,%4d)", + overworld::WarpTypeName(entry.type), entry.map_id, + overworld::WorldName(entry.world), entry.tile16_x, entry.tile16_y, + entry.pixel_x, entry.pixel_y); + if (entry.entrance_id.has_value()) { + line = absl::StrCat(line, + absl::StrFormat(" id=0x%02X", *entry.entrance_id)); + } + if (entry.entrance_name.has_value()) { + line = absl::StrCat(line, " (", *entry.entrance_name, ")"); + } + if (entry.deleted) { + line = absl::StrCat(line, " [deleted]"); + } + if (entry.is_hole && entry.type != overworld::WarpType::kHole) { + line = absl::StrCat(line, " [hole]"); + } + std::cout << line << std::endl; + } + } + + return absl::OkStatus(); +} + } // namespace agent } // namespace cli } // namespace yaze diff --git a/src/cli/handlers/overworld.cc b/src/cli/handlers/overworld.cc index b2c45571..925275a8 100644 --- a/src/cli/handlers/overworld.cc +++ b/src/cli/handlers/overworld.cc @@ -1,5 +1,6 @@ #include "cli/z3ed.h" #include "app/zelda3/overworld/overworld.h" +#include "cli/handlers/overworld_inspect.h" #include #include @@ -120,66 +121,6 @@ namespace { constexpr absl::string_view kFindTileUsage = "Usage: overworld find-tile --tile [--map ] [--world ] [--format ]"; -absl::StatusOr ParseNumeric(const std::string& value, int base = 0) { - try { - size_t processed = 0; - int result = std::stoi(value, &processed, base); - if (processed != value.size()) { - return absl::InvalidArgumentError( - absl::StrCat("Invalid numeric value: ", value)); - } - return result; - } catch (const std::exception&) { - return absl::InvalidArgumentError( - absl::StrCat("Invalid numeric value: ", value)); - } -} - -absl::StatusOr WorldFromString(const std::string& value) { - std::string lower = absl::AsciiStrToLower(value); - if (lower == "0" || lower == "light") { - return 0; - } - if (lower == "1" || lower == "dark") { - return 1; - } - if (lower == "2" || lower == "special") { - return 2; - } - return absl::InvalidArgumentError( - absl::StrCat("Unknown world value: ", value)); -} - -absl::StatusOr WorldFromMapId(int map_id) { - if (map_id < 0) { - return absl::InvalidArgumentError("Map ID must be non-negative"); - } - if (map_id < 0x40) { - return 0; - } - if (map_id < 0x80) { - return 1; - } - if (map_id < 0xA0) { - return 2; - } - return absl::InvalidArgumentError( - absl::StrCat("Map ID out of range: 0x", absl::StrFormat("%02X", map_id))); -} - -std::string WorldName(int world) { - switch (world) { - case 0: - return "Light"; - case 1: - return "Dark"; - case 2: - return "Special"; - default: - return absl::StrCat("Unknown(", world, ")"); - } -} - } // namespace absl::Status OverworldFindTile::Run(const std::vector& arg_vec) { @@ -226,7 +167,8 @@ absl::Status OverworldFindTile::Run(const std::vector& arg_vec) { absl::StrCat("Missing required --tile argument\n", kFindTileUsage)); } - ASSIGN_OR_RETURN(int tile_value, ParseNumeric(tile_it->second)); + ASSIGN_OR_RETURN(int tile_value, + overworld::ParseNumeric(tile_it->second)); if (tile_value < 0 || tile_value > 0xFFFF) { return absl::InvalidArgumentError( absl::StrCat("Tile ID must be between 0x0000 and 0xFFFF (got ", @@ -236,7 +178,8 @@ absl::Status OverworldFindTile::Run(const std::vector& arg_vec) { std::optional map_filter; if (auto map_it = options.find("map"); map_it != options.end()) { - ASSIGN_OR_RETURN(int map_value, ParseNumeric(map_it->second)); + ASSIGN_OR_RETURN(int map_value, + overworld::ParseNumeric(map_it->second)); if (map_value < 0 || map_value >= 0xA0) { return absl::InvalidArgumentError( absl::StrCat("Map ID out of range: ", map_it->second)); @@ -246,18 +189,22 @@ absl::Status OverworldFindTile::Run(const std::vector& arg_vec) { std::optional world_filter; if (auto world_it = options.find("world"); world_it != options.end()) { - ASSIGN_OR_RETURN(int parsed_world, WorldFromString(world_it->second)); + ASSIGN_OR_RETURN(int parsed_world, + overworld::ParseWorldSpecifier(world_it->second)); world_filter = parsed_world; } if (map_filter.has_value()) { - ASSIGN_OR_RETURN(int inferred_world, WorldFromMapId(*map_filter)); + ASSIGN_OR_RETURN(int inferred_world, + overworld::InferWorldFromMapId(*map_filter)); if (world_filter.has_value() && inferred_world != *world_filter) { return absl::InvalidArgumentError( absl::StrCat("Map 0x", absl::StrFormat("%02X", *map_filter), - " belongs to the ", WorldName(inferred_world), - " World but --world requested ", WorldName(*world_filter))); + " belongs to the ", + overworld::WorldName(inferred_world), + " World but --world requested ", + overworld::WorldName(*world_filter))); } if (!world_filter.has_value()) { world_filter = inferred_world; @@ -378,7 +325,8 @@ absl::Status OverworldFindTile::Run(const std::vector& arg_vec) { " {\"map\": \"0x%02X\", \"world\": \"%s\", " "\"local\": {\"x\": %d, \"y\": %d}, " "\"global\": {\"x\": %d, \"y\": %d}}%s\n", - match.map_id, WorldName(match.world), match.local_x, match.local_y, + match.map_id, overworld::WorldName(match.world), match.local_x, + match.local_y, match.global_x, match.global_y, (i + 1 == matches.size()) ? "" : ","); } @@ -395,7 +343,8 @@ absl::Status OverworldFindTile::Run(const std::vector& arg_vec) { for (const auto& match : matches) { std::cout << absl::StrFormat( " β€’ Map 0x%02X (%s World) local(%2d,%2d) global(%3d,%3d)\n", - match.map_id, WorldName(match.world), match.local_x, match.local_y, + match.map_id, overworld::WorldName(match.world), match.local_x, + match.local_y, match.global_x, match.global_y); } } @@ -403,5 +352,401 @@ absl::Status OverworldFindTile::Run(const std::vector& arg_vec) { return absl::OkStatus(); } +absl::Status OverworldDescribeMap::Run( + const std::vector& arg_vec) { + constexpr absl::string_view kUsage = + "Usage: overworld describe-map --map [--format ]"; + + std::unordered_map options; + std::vector positional; + options.reserve(arg_vec.size()); + + for (size_t i = 0; i < arg_vec.size(); ++i) { + const std::string& token = arg_vec[i]; + if (absl::StartsWith(token, "--")) { + std::string key; + std::string value; + auto eq_pos = token.find('='); + if (eq_pos != std::string::npos) { + key = token.substr(2, eq_pos - 2); + value = token.substr(eq_pos + 1); + } else { + key = token.substr(2); + if (i + 1 >= arg_vec.size()) { + return absl::InvalidArgumentError( + absl::StrCat("Missing value for --", key, "\n", kUsage)); + } + value = arg_vec[++i]; + } + if (value.empty()) { + return absl::InvalidArgumentError( + absl::StrCat("Missing value for --", key, "\n", kUsage)); + } + options[key] = value; + } else { + positional.push_back(token); + } + } + + if (!positional.empty()) { + return absl::InvalidArgumentError( + absl::StrCat("Unexpected positional arguments: ", + absl::StrJoin(positional, ", "), "\n", kUsage)); + } + + auto map_it = options.find("map"); + if (map_it == options.end()) { + return absl::InvalidArgumentError(std::string(kUsage)); + } + + ASSIGN_OR_RETURN(int map_value, + overworld::ParseNumeric(map_it->second)); + if (map_value < 0 || map_value >= zelda3::kNumOverworldMaps) { + return absl::InvalidArgumentError( + absl::StrCat("Map ID out of range: ", map_it->second)); + } + + std::string format = "text"; + if (auto it = options.find("format"); it != options.end()) { + format = absl::AsciiStrToLower(it->second); + if (format != "text" && format != "json") { + return absl::InvalidArgumentError( + absl::StrCat("Unsupported format: ", it->second)); + } + } + + std::string rom_file = absl::GetFlag(FLAGS_rom); + if (auto it = options.find("rom"); it != options.end()) { + rom_file = it->second; + } + + if (rom_file.empty()) { + return absl::InvalidArgumentError( + "ROM file must be provided via --rom flag."); + } + + auto load_status = rom_.LoadFromFile(rom_file); + if (!load_status.ok()) { + return load_status; + } + if (!rom_.is_loaded()) { + return absl::AbortedError("Failed to load ROM."); + } + + zelda3::Overworld overworld_rom(&rom_); + auto ow_status = overworld_rom.Load(&rom_); + if (!ow_status.ok()) { + return ow_status; + } + + ASSIGN_OR_RETURN(auto summary, + overworld::BuildMapSummary(overworld_rom, map_value)); + + auto join_hex = [](const std::vector& values) { + std::vector parts; + parts.reserve(values.size()); + for (uint8_t v : values) { + parts.push_back(absl::StrFormat("0x%02X", v)); + } + return absl::StrJoin(parts, ", "); + }; + + auto join_hex_json = [](const std::vector& values) { + std::vector parts; + parts.reserve(values.size()); + for (uint8_t v : values) { + parts.push_back(absl::StrFormat("\"0x%02X\"", v)); + } + return absl::StrCat("[", absl::StrJoin(parts, ", "), "]"); + }; + + if (format == "json") { + std::cout << "{\n"; + std::cout << absl::StrFormat(" \"map\": \"0x%02X\",\n", summary.map_id); + std::cout << absl::StrFormat(" \"world\": \"%s\",\n", + overworld::WorldName(summary.world)); + std::cout << absl::StrFormat( + " \"grid\": {\"x\": %d, \"y\": %d, \"index\": %d},\n", + summary.map_x, summary.map_y, summary.local_index); + std::cout << absl::StrFormat( + " \"size\": {\"label\": \"%s\", \"is_large\": %s, \"parent\": \"0x%02X\", \"quadrant\": %d},\n", + summary.area_size, summary.is_large_map ? "true" : "false", + summary.parent_map, summary.large_quadrant); + std::cout << absl::StrFormat( + " \"message\": \"0x%04X\",\n", summary.message_id); + std::cout << absl::StrFormat( + " \"area_graphics\": \"0x%02X\",\n", summary.area_graphics); + std::cout << absl::StrFormat( + " \"area_palette\": \"0x%02X\",\n", summary.area_palette); + std::cout << absl::StrFormat( + " \"main_palette\": \"0x%02X\",\n", summary.main_palette); + std::cout << absl::StrFormat( + " \"animated_gfx\": \"0x%02X\",\n", summary.animated_gfx); + std::cout << absl::StrFormat( + " \"subscreen_overlay\": \"0x%04X\",\n", + summary.subscreen_overlay); + std::cout << absl::StrFormat( + " \"area_specific_bg_color\": \"0x%04X\",\n", + summary.area_specific_bg_color); + std::cout << absl::StrFormat( + " \"sprite_graphics\": %s,\n", join_hex_json(summary.sprite_graphics)); + std::cout << absl::StrFormat( + " \"sprite_palettes\": %s,\n", join_hex_json(summary.sprite_palettes)); + std::cout << absl::StrFormat( + " \"area_music\": %s,\n", join_hex_json(summary.area_music)); + std::cout << absl::StrFormat( + " \"static_graphics\": %s,\n", + join_hex_json(summary.static_graphics)); + std::cout << absl::StrFormat( + " \"overlay\": {\"enabled\": %s, \"id\": \"0x%04X\"}\n", + summary.has_overlay ? "true" : "false", summary.overlay_id); + std::cout << "}\n"; + } else { + std::cout << absl::StrFormat("πŸ—ΊοΈ Map 0x%02X (%s World)\n", summary.map_id, + overworld::WorldName(summary.world)); + std::cout << absl::StrFormat(" Grid: (%d, %d) local-index %d\n", + summary.map_x, summary.map_y, + summary.local_index); + std::cout << absl::StrFormat( + " Size: %s%s | Parent: 0x%02X | Quadrant: %d\n", + summary.area_size, summary.is_large_map ? " (large)" : "", + summary.parent_map, summary.large_quadrant); + std::cout << absl::StrFormat( + " Message: 0x%04X | Area GFX: 0x%02X | Area Palette: 0x%02X\n", + summary.message_id, summary.area_graphics, summary.area_palette); + std::cout << absl::StrFormat( + " Main Palette: 0x%02X | Animated GFX: 0x%02X | Overlay: %s (0x%04X)\n", + summary.main_palette, summary.animated_gfx, + summary.has_overlay ? "yes" : "no", summary.overlay_id); + std::cout << absl::StrFormat( + " Subscreen Overlay: 0x%04X | BG Color: 0x%04X\n", + summary.subscreen_overlay, summary.area_specific_bg_color); + std::cout << absl::StrFormat(" Sprite GFX: [%s]\n", + join_hex(summary.sprite_graphics)); + std::cout << absl::StrFormat(" Sprite Palettes: [%s]\n", + join_hex(summary.sprite_palettes)); + std::cout << absl::StrFormat(" Area Music: [%s]\n", + join_hex(summary.area_music)); + std::cout << absl::StrFormat(" Static GFX: [%s]\n", + join_hex(summary.static_graphics)); + } + + return absl::OkStatus(); +} + +absl::Status OverworldListWarps::Run( + const std::vector& arg_vec) { + constexpr absl::string_view kUsage = + "Usage: overworld list-warps [--map ] [--world ] " + "[--type ] [--format ]"; + + std::unordered_map options; + std::vector positional; + options.reserve(arg_vec.size()); + + for (size_t i = 0; i < arg_vec.size(); ++i) { + const std::string& token = arg_vec[i]; + if (absl::StartsWith(token, "--")) { + std::string key; + std::string value; + auto eq_pos = token.find('='); + if (eq_pos != std::string::npos) { + key = token.substr(2, eq_pos - 2); + value = token.substr(eq_pos + 1); + } else { + key = token.substr(2); + if (i + 1 >= arg_vec.size()) { + return absl::InvalidArgumentError( + absl::StrCat("Missing value for --", key, "\n", kUsage)); + } + value = arg_vec[++i]; + } + if (value.empty()) { + return absl::InvalidArgumentError( + absl::StrCat("Missing value for --", key, "\n", kUsage)); + } + options[key] = value; + } else { + positional.push_back(token); + } + } + + if (!positional.empty()) { + return absl::InvalidArgumentError( + absl::StrCat("Unexpected positional arguments: ", + absl::StrJoin(positional, ", "), "\n", kUsage)); + } + + std::optional map_filter; + if (auto it = options.find("map"); it != options.end()) { + ASSIGN_OR_RETURN(int map_value, + overworld::ParseNumeric(it->second)); + if (map_value < 0 || map_value >= zelda3::kNumOverworldMaps) { + return absl::InvalidArgumentError( + absl::StrCat("Map ID out of range: ", it->second)); + } + map_filter = map_value; + } + + std::optional world_filter; + if (auto it = options.find("world"); it != options.end()) { + ASSIGN_OR_RETURN(int parsed_world, + overworld::ParseWorldSpecifier(it->second)); + world_filter = parsed_world; + } + + std::optional type_filter; + if (auto it = options.find("type"); it != options.end()) { + std::string lower = absl::AsciiStrToLower(it->second); + if (lower == "entrance" || lower == "entrances") { + type_filter = overworld::WarpType::kEntrance; + } else if (lower == "hole" || lower == "holes") { + type_filter = overworld::WarpType::kHole; + } else if (lower == "exit" || lower == "exits") { + type_filter = overworld::WarpType::kExit; + } else if (lower == "all" || lower.empty()) { + type_filter.reset(); + } else { + return absl::InvalidArgumentError( + absl::StrCat("Unknown warp type: ", it->second)); + } + } + + if (map_filter.has_value()) { + ASSIGN_OR_RETURN(int inferred_world, + overworld::InferWorldFromMapId(*map_filter)); + if (world_filter.has_value() && inferred_world != *world_filter) { + return absl::InvalidArgumentError( + absl::StrCat("Map 0x", + absl::StrFormat("%02X", *map_filter), + " belongs to the ", + overworld::WorldName(inferred_world), + " World but --world requested ", + overworld::WorldName(*world_filter))); + } + if (!world_filter.has_value()) { + world_filter = inferred_world; + } + } + + std::string format = "text"; + if (auto it = options.find("format"); it != options.end()) { + format = absl::AsciiStrToLower(it->second); + if (format != "text" && format != "json") { + return absl::InvalidArgumentError( + absl::StrCat("Unsupported format: ", it->second)); + } + } + + std::string rom_file = absl::GetFlag(FLAGS_rom); + if (auto it = options.find("rom"); it != options.end()) { + rom_file = it->second; + } + + if (rom_file.empty()) { + return absl::InvalidArgumentError( + "ROM file must be provided via --rom flag."); + } + + auto load_status = rom_.LoadFromFile(rom_file); + if (!load_status.ok()) { + return load_status; + } + if (!rom_.is_loaded()) { + return absl::AbortedError("Failed to load ROM."); + } + + zelda3::Overworld overworld_rom(&rom_); + auto ow_status = overworld_rom.Load(&rom_); + if (!ow_status.ok()) { + return ow_status; + } + + overworld::WarpQuery query; + query.map_id = map_filter; + query.world = world_filter; + query.type = type_filter; + + ASSIGN_OR_RETURN(auto entries, + overworld::CollectWarpEntries(overworld_rom, query)); + + if (format == "json") { + std::cout << "{\n"; + std::cout << absl::StrFormat(" \"count\": %zu,\n", entries.size()); + std::cout << " \"entries\": [\n"; + for (size_t i = 0; i < entries.size(); ++i) { + const auto& entry = entries[i]; + std::cout << " {\n"; + std::cout << absl::StrFormat( + " \"type\": \"%s\",\n", + overworld::WarpTypeName(entry.type)); + std::cout << absl::StrFormat( + " \"map\": \"0x%02X\",\n", entry.map_id); + std::cout << absl::StrFormat( + " \"world\": \"%s\",\n", + overworld::WorldName(entry.world)); + std::cout << absl::StrFormat( + " \"grid\": {\"x\": %d, \"y\": %d, \"index\": %d},\n", + entry.map_x, entry.map_y, entry.local_index); + std::cout << absl::StrFormat( + " \"tile16\": {\"x\": %d, \"y\": %d},\n", + entry.tile16_x, entry.tile16_y); + std::cout << absl::StrFormat( + " \"pixel\": {\"x\": %d, \"y\": %d},\n", + entry.pixel_x, entry.pixel_y); + std::cout << absl::StrFormat( + " \"map_pos\": \"0x%04X\",\n", entry.map_pos); + std::cout << absl::StrFormat( + " \"deleted\": %s,\n", entry.deleted ? "true" : "false"); + std::cout << absl::StrFormat( + " \"is_hole\": %s", + entry.is_hole ? "true" : "false"); + if (entry.entrance_id.has_value()) { + std::cout << absl::StrFormat( + ",\n \"entrance_id\": \"0x%02X\"", + *entry.entrance_id); + } + if (entry.entrance_name.has_value()) { + std::cout << absl::StrFormat( + ",\n \"entrance_name\": \"%s\"", + *entry.entrance_name); + } + std::cout << "\n }" << (i + 1 == entries.size() ? "" : ",") << "\n"; + } + std::cout << " ]\n"; + std::cout << "}\n"; + } else { + if (entries.empty()) { + std::cout << "No overworld warps match the specified filters." << std::endl; + return absl::OkStatus(); + } + + std::cout << absl::StrFormat("🌐 Overworld warps (%zu)\n", entries.size()); + for (const auto& entry : entries) { + std::string line = absl::StrFormat( + " β€’ %-9s map 0x%02X (%s World) tile16(%02d,%02d) pixel(%4d,%4d)", + overworld::WarpTypeName(entry.type), entry.map_id, + overworld::WorldName(entry.world), entry.tile16_x, entry.tile16_y, + entry.pixel_x, entry.pixel_y); + if (entry.entrance_id.has_value()) { + line = absl::StrCat(line, + absl::StrFormat(" id=0x%02X", *entry.entrance_id)); + } + if (entry.entrance_name.has_value()) { + line = absl::StrCat(line, " (", *entry.entrance_name, ")"); + } + if (entry.deleted) { + line = absl::StrCat(line, " [deleted]"); + } + if (entry.is_hole && entry.type != overworld::WarpType::kHole) { + line = absl::StrCat(line, " [hole]"); + } + std::cout << line << std::endl; + } + } + + return absl::OkStatus(); +} + } // namespace cli } // namespace yaze diff --git a/src/cli/handlers/overworld_inspect.cc b/src/cli/handlers/overworld_inspect.cc new file mode 100644 index 00000000..c504a108 --- /dev/null +++ b/src/cli/handlers/overworld_inspect.cc @@ -0,0 +1,300 @@ +#include "cli/handlers/overworld_inspect.h" + +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/strings/ascii.h" +#include "absl/strings/numbers.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "app/zelda3/common.h" +#include "app/zelda3/overworld/overworld.h" +#include "app/zelda3/overworld/overworld_entrance.h" +#include "app/zelda3/overworld/overworld_exit.h" +#include "app/zelda3/overworld/overworld_map.h" +#include "util/macro.h" + +namespace yaze { +namespace cli { +namespace overworld { + +namespace { + +constexpr int kLightWorldOffset = 0x00; +constexpr int kDarkWorldOffset = 0x40; +constexpr int kSpecialWorldOffset = 0x80; + +int NormalizeMapId(uint16_t raw_map_id) { + return static_cast(raw_map_id & 0x00FF); +} + +int WorldOffset(int world) { + switch (world) { + case 0: + return kLightWorldOffset; + case 1: + return kDarkWorldOffset; + case 2: + return kSpecialWorldOffset; + default: + return 0; + } +} + +absl::Status ValidateMapId(int map_id) { + if (map_id < 0 || map_id >= zelda3::kNumOverworldMaps) { + return absl::InvalidArgumentError( + absl::StrFormat("Map ID out of range: 0x%02X", map_id)); + } + return absl::OkStatus(); +} + +std::string AreaSizeToString(zelda3::AreaSizeEnum size) { + switch (size) { + case zelda3::AreaSizeEnum::SmallArea: + return "Small"; + case zelda3::AreaSizeEnum::LargeArea: + return "Large"; + case zelda3::AreaSizeEnum::WideArea: + return "Wide"; + case zelda3::AreaSizeEnum::TallArea: + return "Tall"; + default: + return "Unknown"; + } +} + +std::string EntranceLabel(uint8_t id) { + constexpr size_t kEntranceCount = + sizeof(zelda3::kEntranceNames) / sizeof(zelda3::kEntranceNames[0]); + if (id < kEntranceCount) { + return zelda3::kEntranceNames[id]; + } + return absl::StrFormat("Entrance %d", id); +} + +void PopulateCommonWarpFields(WarpEntry& entry, uint16_t raw_map_id, + uint16_t map_pos, int pixel_x, int pixel_y) { + entry.raw_map_id = raw_map_id; + entry.map_id = NormalizeMapId(raw_map_id); + if (entry.map_id >= zelda3::kNumOverworldMaps) { + // Some ROM hacks use sentinel values. Clamp to valid range for reporting + entry.map_id %= zelda3::kNumOverworldMaps; + } + entry.world = (entry.map_id >= kSpecialWorldOffset) + ? 2 + : (entry.map_id >= kDarkWorldOffset ? 1 : 0); + entry.local_index = entry.map_id - WorldOffset(entry.world); + entry.map_x = entry.local_index % 8; + entry.map_y = entry.local_index / 8; + entry.map_pos = map_pos; + entry.pixel_x = pixel_x; + entry.pixel_y = pixel_y; + int tile_index = static_cast(map_pos >> 1); + entry.tile16_x = tile_index & 0x3F; + entry.tile16_y = tile_index >> 6; +} + +} // namespace + +absl::StatusOr ParseNumeric(std::string_view value, int base) { + try { + size_t processed = 0; + int result = std::stoi(std::string(value), &processed, base); + if (processed != value.size()) { + return absl::InvalidArgumentError( + absl::StrCat("Invalid numeric value: ", value)); + } + return result; + } catch (const std::exception&) { + return absl::InvalidArgumentError( + absl::StrCat("Invalid numeric value: ", value)); + } +} + +absl::StatusOr ParseWorldSpecifier(std::string_view value) { + std::string lower = absl::AsciiStrToLower(value); + if (lower == "0" || lower == "light") { + return 0; + } + if (lower == "1" || lower == "dark") { + return 1; + } + if (lower == "2" || lower == "special") { + return 2; + } + return absl::InvalidArgumentError( + absl::StrCat("Unknown world value: ", value)); +} + +absl::StatusOr InferWorldFromMapId(int map_id) { + RETURN_IF_ERROR(ValidateMapId(map_id)); + if (map_id < kDarkWorldOffset) { + return 0; + } + if (map_id < kSpecialWorldOffset) { + return 1; + } + return 2; +} + +std::string WorldName(int world) { + switch (world) { + case 0: + return "Light"; + case 1: + return "Dark"; + case 2: + return "Special"; + default: + return absl::StrCat("Unknown(", world, ")"); + } +} + +std::string WarpTypeName(WarpType type) { + switch (type) { + case WarpType::kEntrance: + return "entrance"; + case WarpType::kHole: + return "hole"; + case WarpType::kExit: + return "exit"; + default: + return "unknown"; + } +} + +absl::StatusOr BuildMapSummary(zelda3::Overworld& overworld, + int map_id) { + RETURN_IF_ERROR(ValidateMapId(map_id)); + ASSIGN_OR_RETURN(int world, InferWorldFromMapId(map_id)); + + // Ensure map data is built before accessing metadata. + RETURN_IF_ERROR(overworld.EnsureMapBuilt(map_id)); + + const auto* map = overworld.overworld_map(map_id); + if (map == nullptr) { + return absl::InternalError( + absl::StrFormat("Failed to retrieve overworld map 0x%02X", map_id)); + } + + MapSummary summary; + summary.map_id = map_id; + summary.world = world; + summary.local_index = map_id - WorldOffset(world); + summary.map_x = summary.local_index % 8; + summary.map_y = summary.local_index / 8; + summary.is_large_map = map->is_large_map(); + summary.parent_map = map->parent(); + summary.large_quadrant = map->large_index(); + summary.area_size = AreaSizeToString(map->area_size()); + summary.message_id = map->message_id(); + summary.area_graphics = map->area_graphics(); + summary.area_palette = map->area_palette(); + summary.main_palette = map->main_palette(); + summary.animated_gfx = map->animated_gfx(); + summary.subscreen_overlay = map->subscreen_overlay(); + summary.area_specific_bg_color = map->area_specific_bg_color(); + + summary.sprite_graphics.clear(); + summary.sprite_palettes.clear(); + summary.area_music.clear(); + summary.static_graphics.clear(); + + for (int i = 0; i < 3; ++i) { + summary.sprite_graphics.push_back(map->sprite_graphics(i)); + summary.sprite_palettes.push_back(map->sprite_palette(i)); + } + + for (int i = 0; i < 4; ++i) { + summary.area_music.push_back(map->area_music(i)); + } + + for (int i = 0; i < 16; ++i) { + summary.static_graphics.push_back(map->static_graphics(i)); + } + + summary.has_overlay = map->has_overlay(); + summary.overlay_id = map->overlay_id(); + + return summary; +} + +absl::StatusOr> CollectWarpEntries( + const zelda3::Overworld& overworld, const WarpQuery& query) { + std::vector entries; + + const auto& entrances = overworld.entrances(); + for (const auto& entrance : entrances) { + WarpEntry entry; + entry.type = WarpType::kEntrance; + entry.deleted = entrance.deleted; + entry.is_hole = entrance.is_hole_; + entry.entrance_id = entrance.entrance_id_; + entry.entrance_name = EntranceLabel(entrance.entrance_id_); + PopulateCommonWarpFields(entry, entrance.map_id_, entrance.map_pos_, + entrance.x_, entrance.y_); + + if (query.type.has_value() && *query.type != entry.type) { + continue; + } + if (query.world.has_value() && *query.world != entry.world) { + continue; + } + if (query.map_id.has_value() && *query.map_id != entry.map_id) { + continue; + } + + entries.push_back(std::move(entry)); + } + + const auto& holes = overworld.holes(); + for (const auto& hole : holes) { + WarpEntry entry; + entry.type = WarpType::kHole; + entry.deleted = false; + entry.is_hole = true; + entry.entrance_id = hole.entrance_id_; + entry.entrance_name = EntranceLabel(hole.entrance_id_); + PopulateCommonWarpFields(entry, hole.map_id_, hole.map_pos_, hole.x_, + hole.y_); + + if (query.type.has_value() && *query.type != entry.type) { + continue; + } + if (query.world.has_value() && *query.world != entry.world) { + continue; + } + if (query.map_id.has_value() && *query.map_id != entry.map_id) { + continue; + } + + entries.push_back(std::move(entry)); + } + + std::sort(entries.begin(), entries.end(), [](const WarpEntry& a, + const WarpEntry& b) { + if (a.world != b.world) { + return a.world < b.world; + } + if (a.map_id != b.map_id) { + return a.map_id < b.map_id; + } + if (a.tile16_y != b.tile16_y) { + return a.tile16_y < b.tile16_y; + } + if (a.tile16_x != b.tile16_x) { + return a.tile16_x < b.tile16_x; + } + return static_cast(a.type) < static_cast(b.type); + }); + + return entries; +} + +} // namespace overworld +} // namespace cli +} // namespace yaze diff --git a/src/cli/handlers/overworld_inspect.h b/src/cli/handlers/overworld_inspect.h new file mode 100644 index 00000000..f38ad16f --- /dev/null +++ b/src/cli/handlers/overworld_inspect.h @@ -0,0 +1,100 @@ +#ifndef YAZE_CLI_HANDLERS_OVERWORLD_INSPECT_H_ +#define YAZE_CLI_HANDLERS_OVERWORLD_INSPECT_H_ + +#include +#include +#include +#include +#include + +#include "absl/status/statusor.h" + +namespace yaze { +class Rom; + +namespace zelda3 { +class Overworld; +class OverworldEntrance; +class OverworldExit; +class OverworldMap; +} + +namespace cli { +namespace overworld { + +enum class WarpType { + kEntrance, + kHole, + kExit, +}; + +struct MapSummary { + int map_id; + int world; + int local_index; + int map_x; + int map_y; + bool is_large_map; + int parent_map; + int large_quadrant; + std::string area_size; + uint16_t message_id; + uint8_t area_graphics; + uint8_t area_palette; + uint8_t main_palette; + uint8_t animated_gfx; + uint16_t subscreen_overlay; + uint16_t area_specific_bg_color; + std::vector sprite_graphics; + std::vector sprite_palettes; + std::vector area_music; + std::vector static_graphics; + bool has_overlay; + uint16_t overlay_id; +}; + +struct WarpEntry { + WarpType type; + uint16_t raw_map_id; + int map_id; + int world; + int local_index; + int map_x; + int map_y; + int tile16_x; + int tile16_y; + int pixel_x; + int pixel_y; + uint16_t map_pos; + bool deleted; + bool is_hole; + std::optional entrance_id; + std::optional entrance_name; + std::optional room_id; + std::optional door_type_1; + std::optional door_type_2; +}; + +struct WarpQuery { + std::optional world; + std::optional map_id; + std::optional type; +}; + +absl::StatusOr ParseNumeric(std::string_view value, int base = 0); +absl::StatusOr ParseWorldSpecifier(std::string_view value); +absl::StatusOr InferWorldFromMapId(int map_id); +std::string WorldName(int world); +std::string WarpTypeName(WarpType type); + +absl::StatusOr BuildMapSummary(zelda3::Overworld& overworld, + int map_id); + +absl::StatusOr> CollectWarpEntries( + const zelda3::Overworld& overworld, const WarpQuery& query); + +} // namespace overworld +} // namespace cli +} // namespace yaze + +#endif // YAZE_CLI_HANDLERS_OVERWORLD_INSPECT_H_ diff --git a/src/cli/modern_cli.cc b/src/cli/modern_cli.cc index 0257280c..3f3dad77 100644 --- a/src/cli/modern_cli.cc +++ b/src/cli/modern_cli.cc @@ -210,6 +210,24 @@ void ModernCLI::SetupCommands() { } }; + commands_["overworld describe-map"] = { + .name = "overworld describe-map", + .description = "Summarize metadata for an overworld map", + .usage = "z3ed overworld describe-map --map [--format json|text]", + .handler = [this](const std::vector& args) -> absl::Status { + return HandleOverworldDescribeMapCommand(args); + } + }; + + commands_["overworld list-warps"] = { + .name = "overworld list-warps", + .description = "List overworld entrances and holes with coordinates", + .usage = "z3ed overworld list-warps [--map ] [--world light|dark|special] [--type entrance|hole|exit|all] [--format json|text]", + .handler = [this](const std::vector& args) -> absl::Status { + return HandleOverworldListWarpsCommand(args); + } + }; + commands_["overworld set-tile"] = { .name = "overworld set-tile", .description = "Set a tile in the overworld", @@ -448,6 +466,16 @@ absl::Status ModernCLI::HandleOverworldFindTileCommand(const std::vector& args) { + OverworldDescribeMap handler; + return handler.Run(args); +} + +absl::Status ModernCLI::HandleOverworldListWarpsCommand(const std::vector& args) { + OverworldListWarps handler; + return handler.Run(args); +} + absl::Status ModernCLI::HandleOverworldSetTileCommand(const std::vector& args) { OverworldSetTile handler; return handler.Run(args); diff --git a/src/cli/modern_cli.h b/src/cli/modern_cli.h index aebc6c94..b69d6f05 100644 --- a/src/cli/modern_cli.h +++ b/src/cli/modern_cli.h @@ -53,6 +53,8 @@ class ModernCLI { absl::Status HandleRomValidateCommand(const std::vector& args); absl::Status HandleOverworldGetTileCommand(const std::vector& args); absl::Status HandleOverworldFindTileCommand(const std::vector& args); + absl::Status HandleOverworldDescribeMapCommand(const std::vector& args); + absl::Status HandleOverworldListWarpsCommand(const std::vector& args); absl::Status HandleOverworldSetTileCommand(const std::vector& args); absl::Status HandleSpriteCreateCommand(const std::vector& args); }; diff --git a/src/cli/service/agent/tool_dispatcher.cc b/src/cli/service/agent/tool_dispatcher.cc index 22c21847..0cfe0ee4 100644 --- a/src/cli/service/agent/tool_dispatcher.cc +++ b/src/cli/service/agent/tool_dispatcher.cc @@ -38,6 +38,10 @@ absl::StatusOr ToolDispatcher::Dispatch( status = HandleResourceListCommand(args, rom_context_); } else if (tool_call.tool_name == "dungeon-list-sprites") { status = HandleDungeonListSpritesCommand(args, rom_context_); + } else if (tool_call.tool_name == "overworld-describe-map") { + status = HandleOverworldDescribeMapCommand(args, rom_context_); + } else if (tool_call.tool_name == "overworld-list-warps") { + status = HandleOverworldListWarpsCommand(args, rom_context_); } else { status = absl::UnimplementedError( absl::StrFormat("Unknown tool: %s", tool_call.tool_name)); diff --git a/src/cli/z3ed.cmake b/src/cli/z3ed.cmake index 66d72acd..56829de8 100644 --- a/src/cli/z3ed.cmake +++ b/src/cli/z3ed.cmake @@ -33,6 +33,7 @@ add_executable( cli/handlers/palette.cc cli/handlers/rom.cc cli/handlers/overworld.cc + cli/handlers/overworld_inspect.cc cli/handlers/sprite.cc cli/tui/tui_component.h cli/tui/asar_patch.cc diff --git a/src/cli/z3ed.h b/src/cli/z3ed.h index 57307fec..dda77852 100644 --- a/src/cli/z3ed.h +++ b/src/cli/z3ed.h @@ -152,6 +152,16 @@ class OverworldFindTile : public CommandHandler { absl::Status Run(const std::vector& arg_vec) override; }; +class OverworldDescribeMap : public CommandHandler { + public: + absl::Status Run(const std::vector& arg_vec) override; +}; + +class OverworldListWarps : public CommandHandler { + public: + absl::Status Run(const std::vector& arg_vec) override; +}; + class SpriteCreate : public CommandHandler { public: absl::Status Run(const std::vector& arg_vec) override;