Files
yaze/src/cli/handlers/overworld.cc
scawful 467b0926e5 Add Overworld tile search command and enhance related documentation
- Implemented `overworld-find-tile` command in the agent for searching tiles by ID.
- Updated `README.md` and `AGENT-ROADMAP.md` to reflect new command and usage.
- Enhanced `overworld_inspect` module with tile matching functionality.
2025-10-03 15:57:42 -04:00

693 lines
23 KiB
C++

#include "cli/z3ed.h"
#include "app/zelda3/overworld/overworld.h"
#include "cli/handlers/overworld_inspect.h"
#include <algorithm>
#include <cctype>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
#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<std::string>& arg_vec) {
if (arg_vec.size() < 3) {
return absl::InvalidArgumentError("Usage: overworld get-tile --map <map_id> --x <x> --y <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<std::string>& arg_vec) {
if (arg_vec.size() < 4) {
return absl::InvalidArgumentError("Usage: overworld set-tile --map <map_id> --x <x> --y <y> --tile <tile_id>");
}
// 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<uint16_t>(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 <tile_id> [--map <map_id>] [--world <light|dark|special|0|1|2>] [--format <json|text>]";
} // namespace
absl::Status OverworldFindTile::Run(const std::vector<std::string>& arg_vec) {
std::unordered_map<std::string, std::string> options;
std::vector<std::string> 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<uint16_t>(tile_value);
std::optional<int> 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<int> 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<std::string>& arg_vec) {
constexpr absl::string_view kUsage =
"Usage: overworld describe-map --map <map_id> [--format <json|text>]";
std::unordered_map<std::string, std::string> options;
std::vector<std::string> 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<uint8_t>& values) {
std::vector<std::string> 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<uint8_t>& values) {
std::vector<std::string> 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<std::string>& arg_vec) {
constexpr absl::string_view kUsage =
"Usage: overworld list-warps [--map <map_id>] [--world <light|dark|special>] "
"[--type <entrance|hole|exit|all>] [--format <json|text>]";
std::unordered_map<std::string, std::string> options;
std::vector<std::string> 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<int> 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<int> 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<overworld::WarpType> 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