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:
scawful
2025-10-03 15:39:04 -04:00
parent 720d55fc43
commit 8935363eae
11 changed files with 1256 additions and 68 deletions

View 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