#include "cli/z3ed.h" #include "app/zelda3/overworld/overworld.h" #include "cli/handlers/overworld_inspect.h" #include #include #include #include #include #include #include #include #include #include "absl/flags/flag.h" #include "absl/flags/declare.h" #include "absl/status/statusor.h" #include "absl/strings/ascii.h" #include "absl/strings/match.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" #include "absl/strings/str_format.h" ABSL_DECLARE_FLAG(std::string, rom); namespace yaze { namespace cli { absl::Status OverworldGetTile::Run(const std::vector& arg_vec) { if (arg_vec.size() < 3) { return absl::InvalidArgumentError("Usage: overworld get-tile --map --x --y "); } // TODO: Implement proper argument parsing int map_id = std::stoi(arg_vec[0]); int x = std::stoi(arg_vec[1]); int y = std::stoi(arg_vec[2]); std::string rom_file = absl::GetFlag(FLAGS_rom); 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_); auto ow_status = overworld.Load(&rom_); if (!ow_status.ok()) { return ow_status; } uint16_t tile = overworld.GetTile(x, y); std::cout << "Tile at (" << x << ", " << y << ") on map " << map_id << " is: 0x" << std::hex << tile << std::endl; return absl::OkStatus(); } absl::Status OverworldSetTile::Run(const std::vector& arg_vec) { if (arg_vec.size() < 4) { return absl::InvalidArgumentError("Usage: overworld set-tile --map --x --y --tile "); } // TODO: Implement proper argument parsing int map_id = std::stoi(arg_vec[0]); int x = std::stoi(arg_vec[1]); int y = std::stoi(arg_vec[2]); int tile_id = std::stoi(arg_vec[3], nullptr, 16); std::string rom_file = absl::GetFlag(FLAGS_rom); 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_); auto status = overworld.Load(&rom_); if (!status.ok()) { return status; } // Set the world based on map_id if (map_id < 0x40) { overworld.set_current_world(0); // Light World } else if (map_id < 0x80) { overworld.set_current_world(1); // Dark World } else { overworld.set_current_world(2); // Special World } // Set the tile overworld.SetTile(x, y, static_cast(tile_id)); // Save the ROM auto save_status = rom_.SaveToFile({.filename = rom_file}); if (!save_status.ok()) { return save_status; } std::cout << "βœ… Set tile at (" << x << ", " << y << ") on map " << map_id << " to: 0x" << std::hex << tile_id << std::dec << std::endl; return absl::OkStatus(); } namespace { constexpr absl::string_view kFindTileUsage = "Usage: overworld find-tile --tile [--map ] [--world ] [--format ]"; } // namespace absl::Status OverworldFindTile::Run(const std::vector& arg_vec) { 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", kFindTileUsage)); } value = arg_vec[++i]; } if (value.empty()) { return absl::InvalidArgumentError( absl::StrCat("Missing value for --", key, "\n", kFindTileUsage)); } options[key] = value; } else { positional.push_back(token); } } if (!positional.empty()) { return absl::InvalidArgumentError( absl::StrCat("Unexpected positional arguments: ", absl::StrJoin(positional, ", "), "\n", kFindTileUsage)); } auto tile_it = options.find("tile"); if (tile_it == options.end()) { return absl::InvalidArgumentError( absl::StrCat("Missing required --tile argument\n", kFindTileUsage)); } 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 ", tile_it->second, ")")); } const uint16_t tile_id = static_cast(tile_value); std::optional map_filter; if (auto map_it = options.find("map"); map_it != options.end()) { 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)); } map_filter = map_value; } std::optional world_filter; if (auto world_it = options.find("world"); world_it != options.end()) { 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, 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 format_it = options.find("format"); format_it != options.end()) { format = absl::AsciiStrToLower(format_it->second); if (format != "text" && format != "json") { return absl::InvalidArgumentError( absl::StrCat("Unsupported format: ", format_it->second)); } } std::string rom_file = absl::GetFlag(FLAGS_rom); if (auto rom_it = options.find("rom"); rom_it != options.end()) { rom_file = rom_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_); auto ow_status = overworld.Load(&rom_); if (!ow_status.ok()) { return ow_status; } overworld::TileSearchOptions search_options; search_options.map_id = map_filter; search_options.world = world_filter; ASSIGN_OR_RETURN(auto matches, overworld::FindTileMatches(overworld, tile_id, search_options)); if (format == "json") { std::cout << "{\n"; std::cout << absl::StrFormat( " \"tile\": \"0x%04X\",\n", tile_id); std::cout << absl::StrFormat( " \"match_count\": %zu,\n", matches.size()); std::cout << " \"matches\": [\n"; for (size_t i = 0; i < matches.size(); ++i) { const auto& match = matches[i]; std::cout << absl::StrFormat( " {\"map\": \"0x%02X\", \"world\": \"%s\", " "\"local\": {\"x\": %d, \"y\": %d}, " "\"global\": {\"x\": %d, \"y\": %d}}%s\n", match.map_id, overworld::WorldName(match.world), match.local_x, match.local_y, match.global_x, match.global_y, (i + 1 == matches.size()) ? "" : ","); } std::cout << " ]\n"; std::cout << "}\n"; } else { std::cout << absl::StrFormat( "πŸ”Ž Tile 0x%04X β†’ %zu match(es)\n", tile_id, matches.size()); if (matches.empty()) { std::cout << " No matches found." << std::endl; return absl::OkStatus(); } for (const auto& match : matches) { std::cout << absl::StrFormat( " β€’ Map 0x%02X (%s World) local(%2d,%2d) global(%3d,%3d)\n", match.map_id, overworld::WorldName(match.world), match.local_x, match.local_y, match.global_x, match.global_y); } } 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