Implement Overworld Map Inspection Commands
- Added `OverworldDescribeMap` command to summarize metadata for a specified overworld map. - Introduced `OverworldListWarps` command to list overworld entrances and holes with their coordinates. - Refactored existing `overworld.cc` to utilize new helper functions for parsing and validation. - Created `overworld_inspect.cc` and `overworld_inspect.h` to encapsulate map and warp-related functionalities. - Updated `ModernCLI` to register new commands and handle their execution. - Modified `tool_dispatcher.cc` to support dispatching the new commands. - Updated CMake configuration to include new source files for the inspection commands.
This commit is contained in:
@@ -257,8 +257,10 @@ class Overworld {
|
|||||||
auto current_graphics() const {
|
auto current_graphics() const {
|
||||||
return overworld_maps_[current_map_].current_graphics();
|
return overworld_maps_[current_map_].current_graphics();
|
||||||
}
|
}
|
||||||
|
const std::vector<OverworldEntrance> &entrances() const { return all_entrances_; }
|
||||||
auto &entrances() { return all_entrances_; }
|
auto &entrances() { return all_entrances_; }
|
||||||
auto mutable_entrances() { return &all_entrances_; }
|
auto mutable_entrances() { return &all_entrances_; }
|
||||||
|
const std::vector<OverworldEntrance> &holes() const { return all_holes_; }
|
||||||
auto &holes() { return all_holes_; }
|
auto &holes() { return all_holes_; }
|
||||||
auto mutable_holes() { return &all_holes_; }
|
auto mutable_holes() { return &all_holes_; }
|
||||||
auto deleted_entrances() const { return deleted_entrances_; }
|
auto deleted_entrances() const { return deleted_entrances_; }
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ absl::Status HandleResourceListCommand(
|
|||||||
absl::Status HandleDungeonListSpritesCommand(
|
absl::Status HandleDungeonListSpritesCommand(
|
||||||
const std::vector<std::string>& arg_vec,
|
const std::vector<std::string>& arg_vec,
|
||||||
Rom* rom_context = nullptr);
|
Rom* rom_context = nullptr);
|
||||||
|
absl::Status HandleOverworldDescribeMapCommand(
|
||||||
|
const std::vector<std::string>& arg_vec,
|
||||||
|
Rom* rom_context = nullptr);
|
||||||
|
absl::Status HandleOverworldListWarpsCommand(
|
||||||
|
const std::vector<std::string>& arg_vec,
|
||||||
|
Rom* rom_context = nullptr);
|
||||||
absl::Status HandleChatCommand(Rom& rom);
|
absl::Status HandleChatCommand(Rom& rom);
|
||||||
|
|
||||||
} // namespace agent
|
} // namespace agent
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
#include "cli/handlers/agent/commands.h"
|
#include "cli/handlers/agent/commands.h"
|
||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "absl/flags/declare.h"
|
#include "absl/flags/declare.h"
|
||||||
#include "absl/flags/flag.h"
|
#include "absl/flags/flag.h"
|
||||||
@@ -13,8 +15,11 @@
|
|||||||
#include "absl/strings/numbers.h"
|
#include "absl/strings/numbers.h"
|
||||||
#include "absl/strings/str_cat.h"
|
#include "absl/strings/str_cat.h"
|
||||||
#include "absl/strings/str_format.h"
|
#include "absl/strings/str_format.h"
|
||||||
|
#include "absl/strings/str_join.h"
|
||||||
#include "app/rom.h"
|
#include "app/rom.h"
|
||||||
#include "app/zelda3/dungeon/room.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"
|
#include "cli/service/resources/resource_context_builder.h"
|
||||||
|
|
||||||
ABSL_DECLARE_FLAG(std::string, rom);
|
ABSL_DECLARE_FLAG(std::string, rom);
|
||||||
@@ -42,6 +47,21 @@ absl::StatusOr<Rom> LoadRomFromFlag() {
|
|||||||
return rom;
|
return rom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
absl::StatusOr<Rom> LoadRomFromPathOrFlag(
|
||||||
|
const std::optional<std::string>& 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
|
} // namespace
|
||||||
|
|
||||||
absl::Status HandleResourceListCommand(
|
absl::Status HandleResourceListCommand(
|
||||||
@@ -189,6 +209,376 @@ absl::Status HandleDungeonListSpritesCommand(
|
|||||||
return absl::OkStatus();
|
return absl::OkStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
absl::Status HandleOverworldDescribeMapCommand(
|
||||||
|
const std::vector<std::string>& arg_vec, Rom* rom_context) {
|
||||||
|
std::optional<std::string> map_value;
|
||||||
|
std::string format = "json";
|
||||||
|
std::optional<std::string> 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 <id> [--format <json|text>]");
|
||||||
|
}
|
||||||
|
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 <id> [--format <json|text>]");
|
||||||
|
}
|
||||||
|
|
||||||
|
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<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 HandleOverworldListWarpsCommand(
|
||||||
|
const std::vector<std::string>& arg_vec, Rom* rom_context) {
|
||||||
|
std::optional<std::string> map_value;
|
||||||
|
std::optional<std::string> world_value;
|
||||||
|
std::optional<std::string> type_value;
|
||||||
|
std::string format = "json";
|
||||||
|
std::optional<std::string> 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<int> 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<int> world_filter;
|
||||||
|
if (world_value.has_value()) {
|
||||||
|
ASSIGN_OR_RETURN(int world_id,
|
||||||
|
overworld::ParseWorldSpecifier(*world_value));
|
||||||
|
world_filter = world_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<overworld::WarpType> 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 agent
|
||||||
} // namespace cli
|
} // namespace cli
|
||||||
} // namespace yaze
|
} // namespace yaze
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "cli/z3ed.h"
|
#include "cli/z3ed.h"
|
||||||
#include "app/zelda3/overworld/overworld.h"
|
#include "app/zelda3/overworld/overworld.h"
|
||||||
|
#include "cli/handlers/overworld_inspect.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
@@ -120,66 +121,6 @@ namespace {
|
|||||||
constexpr absl::string_view kFindTileUsage =
|
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>]";
|
"Usage: overworld find-tile --tile <tile_id> [--map <map_id>] [--world <light|dark|special|0|1|2>] [--format <json|text>]";
|
||||||
|
|
||||||
absl::StatusOr<int> 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<int> 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<int> 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
|
} // namespace
|
||||||
|
|
||||||
absl::Status OverworldFindTile::Run(const std::vector<std::string>& arg_vec) {
|
absl::Status OverworldFindTile::Run(const std::vector<std::string>& arg_vec) {
|
||||||
@@ -226,7 +167,8 @@ absl::Status OverworldFindTile::Run(const std::vector<std::string>& arg_vec) {
|
|||||||
absl::StrCat("Missing required --tile argument\n", kFindTileUsage));
|
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) {
|
if (tile_value < 0 || tile_value > 0xFFFF) {
|
||||||
return absl::InvalidArgumentError(
|
return absl::InvalidArgumentError(
|
||||||
absl::StrCat("Tile ID must be between 0x0000 and 0xFFFF (got ",
|
absl::StrCat("Tile ID must be between 0x0000 and 0xFFFF (got ",
|
||||||
@@ -236,7 +178,8 @@ absl::Status OverworldFindTile::Run(const std::vector<std::string>& arg_vec) {
|
|||||||
|
|
||||||
std::optional<int> map_filter;
|
std::optional<int> map_filter;
|
||||||
if (auto map_it = options.find("map"); map_it != options.end()) {
|
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) {
|
if (map_value < 0 || map_value >= 0xA0) {
|
||||||
return absl::InvalidArgumentError(
|
return absl::InvalidArgumentError(
|
||||||
absl::StrCat("Map ID out of range: ", map_it->second));
|
absl::StrCat("Map ID out of range: ", map_it->second));
|
||||||
@@ -246,18 +189,22 @@ absl::Status OverworldFindTile::Run(const std::vector<std::string>& arg_vec) {
|
|||||||
|
|
||||||
std::optional<int> world_filter;
|
std::optional<int> world_filter;
|
||||||
if (auto world_it = options.find("world"); world_it != options.end()) {
|
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;
|
world_filter = parsed_world;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (map_filter.has_value()) {
|
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) {
|
if (world_filter.has_value() && inferred_world != *world_filter) {
|
||||||
return absl::InvalidArgumentError(
|
return absl::InvalidArgumentError(
|
||||||
absl::StrCat("Map 0x",
|
absl::StrCat("Map 0x",
|
||||||
absl::StrFormat("%02X", *map_filter),
|
absl::StrFormat("%02X", *map_filter),
|
||||||
" belongs to the ", WorldName(inferred_world),
|
" belongs to the ",
|
||||||
" World but --world requested ", WorldName(*world_filter)));
|
overworld::WorldName(inferred_world),
|
||||||
|
" World but --world requested ",
|
||||||
|
overworld::WorldName(*world_filter)));
|
||||||
}
|
}
|
||||||
if (!world_filter.has_value()) {
|
if (!world_filter.has_value()) {
|
||||||
world_filter = inferred_world;
|
world_filter = inferred_world;
|
||||||
@@ -378,7 +325,8 @@ absl::Status OverworldFindTile::Run(const std::vector<std::string>& arg_vec) {
|
|||||||
" {\"map\": \"0x%02X\", \"world\": \"%s\", "
|
" {\"map\": \"0x%02X\", \"world\": \"%s\", "
|
||||||
"\"local\": {\"x\": %d, \"y\": %d}, "
|
"\"local\": {\"x\": %d, \"y\": %d}, "
|
||||||
"\"global\": {\"x\": %d, \"y\": %d}}%s\n",
|
"\"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,
|
match.global_x, match.global_y,
|
||||||
(i + 1 == matches.size()) ? "" : ",");
|
(i + 1 == matches.size()) ? "" : ",");
|
||||||
}
|
}
|
||||||
@@ -395,7 +343,8 @@ absl::Status OverworldFindTile::Run(const std::vector<std::string>& arg_vec) {
|
|||||||
for (const auto& match : matches) {
|
for (const auto& match : matches) {
|
||||||
std::cout << absl::StrFormat(
|
std::cout << absl::StrFormat(
|
||||||
" • Map 0x%02X (%s World) local(%2d,%2d) global(%3d,%3d)\n",
|
" • 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);
|
match.global_x, match.global_y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,5 +352,401 @@ absl::Status OverworldFindTile::Run(const std::vector<std::string>& arg_vec) {
|
|||||||
return absl::OkStatus();
|
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 cli
|
||||||
} // namespace yaze
|
} // namespace yaze
|
||||||
|
|||||||
300
src/cli/handlers/overworld_inspect.cc
Normal file
300
src/cli/handlers/overworld_inspect.cc
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
#include "cli/handlers/overworld_inspect.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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<int>(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<int>(map_pos >> 1);
|
||||||
|
entry.tile16_x = tile_index & 0x3F;
|
||||||
|
entry.tile16_y = tile_index >> 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
absl::StatusOr<int> 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<int> 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<int> 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<MapSummary> 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<std::vector<WarpEntry>> CollectWarpEntries(
|
||||||
|
const zelda3::Overworld& overworld, const WarpQuery& query) {
|
||||||
|
std::vector<WarpEntry> 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<int>(a.type) < static_cast<int>(b.type);
|
||||||
|
});
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace overworld
|
||||||
|
} // namespace cli
|
||||||
|
} // namespace yaze
|
||||||
100
src/cli/handlers/overworld_inspect.h
Normal file
100
src/cli/handlers/overworld_inspect.h
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
#ifndef YAZE_CLI_HANDLERS_OVERWORLD_INSPECT_H_
|
||||||
|
#define YAZE_CLI_HANDLERS_OVERWORLD_INSPECT_H_
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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<uint8_t> sprite_graphics;
|
||||||
|
std::vector<uint8_t> sprite_palettes;
|
||||||
|
std::vector<uint8_t> area_music;
|
||||||
|
std::vector<uint8_t> 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<uint8_t> entrance_id;
|
||||||
|
std::optional<std::string> entrance_name;
|
||||||
|
std::optional<uint16_t> room_id;
|
||||||
|
std::optional<uint16_t> door_type_1;
|
||||||
|
std::optional<uint16_t> door_type_2;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WarpQuery {
|
||||||
|
std::optional<int> world;
|
||||||
|
std::optional<int> map_id;
|
||||||
|
std::optional<WarpType> type;
|
||||||
|
};
|
||||||
|
|
||||||
|
absl::StatusOr<int> ParseNumeric(std::string_view value, int base = 0);
|
||||||
|
absl::StatusOr<int> ParseWorldSpecifier(std::string_view value);
|
||||||
|
absl::StatusOr<int> InferWorldFromMapId(int map_id);
|
||||||
|
std::string WorldName(int world);
|
||||||
|
std::string WarpTypeName(WarpType type);
|
||||||
|
|
||||||
|
absl::StatusOr<MapSummary> BuildMapSummary(zelda3::Overworld& overworld,
|
||||||
|
int map_id);
|
||||||
|
|
||||||
|
absl::StatusOr<std::vector<WarpEntry>> CollectWarpEntries(
|
||||||
|
const zelda3::Overworld& overworld, const WarpQuery& query);
|
||||||
|
|
||||||
|
} // namespace overworld
|
||||||
|
} // namespace cli
|
||||||
|
} // namespace yaze
|
||||||
|
|
||||||
|
#endif // YAZE_CLI_HANDLERS_OVERWORLD_INSPECT_H_
|
||||||
@@ -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 <map_id> [--format json|text]",
|
||||||
|
.handler = [this](const std::vector<std::string>& 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 <map_id>] [--world light|dark|special] [--type entrance|hole|exit|all] [--format json|text]",
|
||||||
|
.handler = [this](const std::vector<std::string>& args) -> absl::Status {
|
||||||
|
return HandleOverworldListWarpsCommand(args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
commands_["overworld set-tile"] = {
|
commands_["overworld set-tile"] = {
|
||||||
.name = "overworld set-tile",
|
.name = "overworld set-tile",
|
||||||
.description = "Set a tile in the overworld",
|
.description = "Set a tile in the overworld",
|
||||||
@@ -448,6 +466,16 @@ absl::Status ModernCLI::HandleOverworldFindTileCommand(const std::vector<std::st
|
|||||||
return handler.Run(args);
|
return handler.Run(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
absl::Status ModernCLI::HandleOverworldDescribeMapCommand(const std::vector<std::string>& args) {
|
||||||
|
OverworldDescribeMap handler;
|
||||||
|
return handler.Run(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
absl::Status ModernCLI::HandleOverworldListWarpsCommand(const std::vector<std::string>& args) {
|
||||||
|
OverworldListWarps handler;
|
||||||
|
return handler.Run(args);
|
||||||
|
}
|
||||||
|
|
||||||
absl::Status ModernCLI::HandleOverworldSetTileCommand(const std::vector<std::string>& args) {
|
absl::Status ModernCLI::HandleOverworldSetTileCommand(const std::vector<std::string>& args) {
|
||||||
OverworldSetTile handler;
|
OverworldSetTile handler;
|
||||||
return handler.Run(args);
|
return handler.Run(args);
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ class ModernCLI {
|
|||||||
absl::Status HandleRomValidateCommand(const std::vector<std::string>& args);
|
absl::Status HandleRomValidateCommand(const std::vector<std::string>& args);
|
||||||
absl::Status HandleOverworldGetTileCommand(const std::vector<std::string>& args);
|
absl::Status HandleOverworldGetTileCommand(const std::vector<std::string>& args);
|
||||||
absl::Status HandleOverworldFindTileCommand(const std::vector<std::string>& args);
|
absl::Status HandleOverworldFindTileCommand(const std::vector<std::string>& args);
|
||||||
|
absl::Status HandleOverworldDescribeMapCommand(const std::vector<std::string>& args);
|
||||||
|
absl::Status HandleOverworldListWarpsCommand(const std::vector<std::string>& args);
|
||||||
absl::Status HandleOverworldSetTileCommand(const std::vector<std::string>& args);
|
absl::Status HandleOverworldSetTileCommand(const std::vector<std::string>& args);
|
||||||
absl::Status HandleSpriteCreateCommand(const std::vector<std::string>& args);
|
absl::Status HandleSpriteCreateCommand(const std::vector<std::string>& args);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ absl::StatusOr<std::string> ToolDispatcher::Dispatch(
|
|||||||
status = HandleResourceListCommand(args, rom_context_);
|
status = HandleResourceListCommand(args, rom_context_);
|
||||||
} else if (tool_call.tool_name == "dungeon-list-sprites") {
|
} else if (tool_call.tool_name == "dungeon-list-sprites") {
|
||||||
status = HandleDungeonListSpritesCommand(args, rom_context_);
|
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 {
|
} else {
|
||||||
status = absl::UnimplementedError(
|
status = absl::UnimplementedError(
|
||||||
absl::StrFormat("Unknown tool: %s", tool_call.tool_name));
|
absl::StrFormat("Unknown tool: %s", tool_call.tool_name));
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ add_executable(
|
|||||||
cli/handlers/palette.cc
|
cli/handlers/palette.cc
|
||||||
cli/handlers/rom.cc
|
cli/handlers/rom.cc
|
||||||
cli/handlers/overworld.cc
|
cli/handlers/overworld.cc
|
||||||
|
cli/handlers/overworld_inspect.cc
|
||||||
cli/handlers/sprite.cc
|
cli/handlers/sprite.cc
|
||||||
cli/tui/tui_component.h
|
cli/tui/tui_component.h
|
||||||
cli/tui/asar_patch.cc
|
cli/tui/asar_patch.cc
|
||||||
|
|||||||
@@ -152,6 +152,16 @@ class OverworldFindTile : public CommandHandler {
|
|||||||
absl::Status Run(const std::vector<std::string>& arg_vec) override;
|
absl::Status Run(const std::vector<std::string>& arg_vec) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class OverworldDescribeMap : public CommandHandler {
|
||||||
|
public:
|
||||||
|
absl::Status Run(const std::vector<std::string>& arg_vec) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
class OverworldListWarps : public CommandHandler {
|
||||||
|
public:
|
||||||
|
absl::Status Run(const std::vector<std::string>& arg_vec) override;
|
||||||
|
};
|
||||||
|
|
||||||
class SpriteCreate : public CommandHandler {
|
class SpriteCreate : public CommandHandler {
|
||||||
public:
|
public:
|
||||||
absl::Status Run(const std::vector<std::string>& arg_vec) override;
|
absl::Status Run(const std::vector<std::string>& arg_vec) override;
|
||||||
|
|||||||
Reference in New Issue
Block a user