feat: Add overworld sprite and entrance commands to agent tool

- Implemented new commands for listing overworld sprites and retrieving entrance details.
- Enhanced CLI functionality to support filtering by map, world, and sprite ID with JSON and text output formats.
- Introduced tile statistics analysis command for detailed tile usage insights.
- Updated function schemas and system prompts to reflect the new commands and their parameters.
This commit is contained in:
scawful
2025-10-04 21:27:10 -04:00
parent f38946118c
commit 85bc14e93e
11 changed files with 701 additions and 15 deletions

View File

@@ -225,6 +225,78 @@
},
"required": ["query"]
}
},
{
"name": "overworld-list-sprites",
"description": "List sprites on the overworld with optional filters for map, world, or sprite ID",
"parameters": {
"type": "object",
"properties": {
"map": {
"type": "string",
"description": "Optional: filter by map ID (0x00-0x9F)"
},
"world": {
"type": "string",
"description": "Optional: filter by world (0=light, 1=dark, 2=special)"
},
"sprite_id": {
"type": "string",
"description": "Optional: filter by sprite ID (0x00-0xFF)"
},
"format": {
"type": "string",
"enum": ["json", "text"],
"default": "json"
}
}
}
},
{
"name": "overworld-get-entrance",
"description": "Get detailed information about a specific overworld entrance by its ID",
"parameters": {
"type": "object",
"properties": {
"entrance_id": {
"type": "string",
"description": "Entrance ID number (0-128)"
},
"format": {
"type": "string",
"enum": ["json", "text"],
"default": "json"
}
},
"required": ["entrance_id"]
}
},
{
"name": "overworld-tile-stats",
"description": "Analyze usage statistics for a specific tile16 ID across the overworld",
"parameters": {
"type": "object",
"properties": {
"tile_id": {
"type": "string",
"description": "Tile16 ID to analyze (0x0000-0xFFFF, hex or decimal)"
},
"map": {
"type": "string",
"description": "Optional: limit analysis to specific map ID"
},
"world": {
"type": "string",
"description": "Optional: limit analysis to specific world (0=light, 1=dark, 2=special)"
},
"format": {
"type": "string",
"enum": ["json", "text"],
"default": "json"
}
},
"required": ["tile_id"]
}
}
]

View File

@@ -257,6 +257,81 @@ You must follow this exact two-step process to avoid errors.
},
"required": ["query"]
}
},
{
"name": "overworld-list-sprites",
"description": "List sprites (enemies, NPCs, objects) on the overworld with optional filters. Sprites are placed on specific maps at pixel coordinates. Each sprite has an ID (0x00-0xFF) that determines what entity it is. You can filter by map, world, or sprite ID.",
"usage_examples": [
"What sprites are on map 0?",
"List all Octorok sprites in the Light World",
"Show me sprite placements in the Dark World",
"Where is sprite ID 0x15?"
],
"parameters": {
"type": "object",
"properties": {
"map": {
"type": "string",
"description": "Optional: filter by map ID (0x00-0x9F). Light World = 0x00-0x3F, Dark World = 0x40-0x7F, Special = 0x80-0x9F."
},
"world": {
"type": "string",
"description": "Optional: filter by world. 0 = Light World, 1 = Dark World, 2 = Special World."
},
"sprite_id": {
"type": "string",
"description": "Optional: filter by specific sprite ID (0x00-0xFF). Use resource-list tool to look up sprite names by ID."
}
}
}
},
{
"name": "overworld-get-entrance",
"description": "Get detailed information about a specific overworld entrance by its entrance ID. Overworld entrances are the doorways, caves, and warps that connect the overworld to dungeons and indoor locations. Each entrance has a unique ID (0-128) and contains information about its map location, pixel coordinates, area position, and whether it's a hole or standard entrance.",
"usage_examples": [
"Tell me about entrance 0",
"What's at entrance ID 67?",
"Show me details for entrance 5",
"Where does entrance 43 lead?"
],
"parameters": {
"type": "object",
"properties": {
"entrance_id": {
"type": "string",
"description": "Entrance ID number (0-128). Use overworld-list-warps or resource-list tool first if you need to find an entrance by name or location."
}
},
"required": ["entrance_id"]
}
},
{
"name": "overworld-tile-stats",
"description": "Analyze usage statistics for a specific tile16 ID across the overworld. Shows how many times a tile appears, where it's used, and on which maps. Useful for understanding tile distribution, finding patterns, or analyzing terrain composition. Can be scoped to a specific map or world.",
"usage_examples": [
"How many times is tile 0x02E used?",
"Where does tile 0x14C appear in the Light World?",
"Analyze tile usage for tile 0x020 on map 0",
"Show me statistics for water tiles"
],
"parameters": {
"type": "object",
"properties": {
"tile_id": {
"type": "string",
"description": "Tile16 ID to analyze (0x0000-0xFFFF, hex or decimal). Common tiles: 0x02E=tree, 0x020=grass, 0x14C=water."
},
"map": {
"type": "string",
"description": "Optional: limit analysis to specific map ID (0x00-0x9F)."
},
"world": {
"type": "string",
"description": "Optional: limit analysis to specific world (0=light, 1=dark, 2=special)."
}
},
"required": ["tile_id"]
}
}
]
```

View File

@@ -5,8 +5,8 @@
ABSL_FLAG(std::string, rom, "", "Path to the ROM file");
// AI Service Configuration Flags
ABSL_FLAG(std::string, ai_provider, "mock",
"AI provider to use: 'mock' (default), 'ollama', or 'gemini'");
ABSL_FLAG(std::string, ai_provider, "auto",
"AI provider to use: 'auto' (try gemini→ollama→mock), 'gemini', 'ollama', or 'mock'");
ABSL_FLAG(std::string, ai_model, "",
"AI model to use (provider-specific, e.g., 'llama3' for Ollama, "
"'gemini-1.5-flash' for Gemini)");

View File

@@ -46,6 +46,15 @@ absl::Status HandleOverworldDescribeMapCommand(
absl::Status HandleOverworldListWarpsCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
absl::Status HandleOverworldListSpritesCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
absl::Status HandleOverworldGetEntranceCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
absl::Status HandleOverworldTileStatsCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
absl::Status HandleMessageListCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);

View File

@@ -1180,6 +1180,346 @@ absl::Status HandleOverworldListWarpsCommand(
return absl::OkStatus();
}
absl::Status HandleOverworldListSpritesCommand(
const std::vector<std::string>& arg_vec, Rom* rom_context) {
std::optional<std::string> map_value;
std::optional<std::string> world_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 == "--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("--format must be either json or text");
}
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::SpriteQuery query;
if (map_value.has_value()) {
ASSIGN_OR_RETURN(int map_id, overworld::ParseNumeric(*map_value));
query.map_id = map_id;
}
if (world_value.has_value()) {
ASSIGN_OR_RETURN(int world_id, overworld::ParseWorldSpecifier(*world_value));
query.world = world_id;
}
ASSIGN_OR_RETURN(auto sprites, overworld::CollectOverworldSprites(overworld_data, query));
if (format == "json") {
std::cout << "{\n";
std::cout << absl::StrFormat(" \"count\": %zu,\n", sprites.size());
std::cout << " \"sprites\": [\n";
for (size_t i = 0; i < sprites.size(); ++i) {
const auto& sprite = sprites[i];
std::cout << " {\n";
std::cout << absl::StrFormat(" \"sprite_id\": \"0x%02X\",\n", sprite.sprite_id);
std::cout << absl::StrFormat(" \"map\": \"0x%02X\",\n", sprite.map_id);
std::cout << absl::StrFormat(" \"world\": \"%s\",\n", overworld::WorldName(sprite.world));
std::cout << absl::StrFormat(" \"position\": {\"x\": %d, \"y\": %d}", sprite.x, sprite.y);
if (sprite.sprite_name.has_value()) {
std::cout << absl::StrFormat(",\n \"name\": \"%s\"", *sprite.sprite_name);
}
std::cout << "\n }" << (i + 1 == sprites.size() ? "" : ",") << "\n";
}
std::cout << " ]\n";
std::cout << "}\n";
} else {
std::cout << absl::StrFormat("🎮 Overworld Sprites (%zu)\n", sprites.size());
for (const auto& sprite : sprites) {
std::cout << absl::StrFormat(" • Sprite 0x%02X @ Map 0x%02X (%s) pos(%d,%d)",
sprite.sprite_id, sprite.map_id,
overworld::WorldName(sprite.world),
sprite.x, sprite.y);
if (sprite.sprite_name.has_value()) {
std::cout << " - " << *sprite.sprite_name;
}
std::cout << "\n";
}
}
return absl::OkStatus();
}
absl::Status HandleOverworldGetEntranceCommand(
const std::vector<std::string>& arg_vec, Rom* rom_context) {
std::optional<std::string> entrance_id_str;
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 == "--id") {
if (i + 1 >= arg_vec.size()) {
return absl::InvalidArgumentError("--id requires a value.");
}
entrance_id_str = arg_vec[++i];
} else if (absl::StartsWith(token, "--id=")) {
entrance_id_str = token.substr(5);
} 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 (!entrance_id_str.has_value()) {
return absl::InvalidArgumentError(
"Usage: overworld-get-entrance --id <entrance_id> [--format <json|text>]");
}
if (format != "json" && format != "text") {
return absl::InvalidArgumentError("--format must be either json or text");
}
ASSIGN_OR_RETURN(int entrance_id, overworld::ParseNumeric(*entrance_id_str));
if (entrance_id < 0 || entrance_id > 255) {
return absl::InvalidArgumentError("Entrance ID must be between 0 and 255");
}
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 entrance, overworld::GetEntranceDetails(overworld_data,
static_cast<uint8_t>(entrance_id)));
if (format == "json") {
std::cout << "{\n";
std::cout << absl::StrFormat(" \"entrance_id\": %d,\n", entrance.entrance_id);
std::cout << absl::StrFormat(" \"map\": \"0x%02X\",\n", entrance.map_id);
std::cout << absl::StrFormat(" \"world\": \"%s\",\n", overworld::WorldName(entrance.world));
std::cout << absl::StrFormat(" \"position\": {\"x\": %d, \"y\": %d},\n", entrance.x, entrance.y);
std::cout << absl::StrFormat(" \"room_id\": \"0x%04X\",\n", entrance.room_id);
std::cout << absl::StrFormat(" \"door_type_1\": \"0x%04X\",\n", entrance.door_type_1);
std::cout << absl::StrFormat(" \"door_type_2\": \"0x%04X\",\n", entrance.door_type_2);
std::cout << absl::StrFormat(" \"is_hole\": %s", entrance.is_hole ? "true" : "false");
if (entrance.entrance_name.has_value()) {
std::cout << absl::StrFormat(",\n \"entrance_name\": \"%s\"", *entrance.entrance_name);
}
if (entrance.room_name.has_value()) {
std::cout << absl::StrFormat(",\n \"room_name\": \"%s\"", *entrance.room_name);
}
std::cout << "\n}\n";
} else {
std::cout << absl::StrFormat("🚪 Entrance #%d\n", entrance.entrance_id);
if (entrance.entrance_name.has_value()) {
std::cout << "Name: " << *entrance.entrance_name << "\n";
}
std::cout << absl::StrFormat("Map: 0x%02X (%s World)\n", entrance.map_id,
overworld::WorldName(entrance.world));
std::cout << absl::StrFormat("Position: (%d, %d)\n", entrance.x, entrance.y);
std::cout << absl::StrFormat("→ Leads to Room 0x%04X", entrance.room_id);
if (entrance.room_name.has_value()) {
std::cout << " (" << *entrance.room_name << ")";
}
std::cout << "\n";
std::cout << absl::StrFormat("Door Types: 0x%04X / 0x%04X\n",
entrance.door_type_1, entrance.door_type_2);
std::cout << absl::StrFormat("Is Hole: %s\n", entrance.is_hole ? "yes" : "no");
}
return absl::OkStatus();
}
absl::Status HandleOverworldTileStatsCommand(
const std::vector<std::string>& arg_vec, Rom* rom_context) {
std::optional<std::string> tile_value;
std::optional<std::string> map_value;
std::optional<std::string> world_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 == "--tile") {
if (i + 1 >= arg_vec.size()) {
return absl::InvalidArgumentError("--tile requires a value.");
}
tile_value = arg_vec[++i];
} else if (absl::StartsWith(token, "--tile=")) {
tile_value = token.substr(7);
} else 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 == "--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 (!tile_value.has_value()) {
return absl::InvalidArgumentError(
"Usage: overworld-tile-stats --tile <id> [--map <id>] [--world <light|dark|special>] [--format <json|text>]");
}
if (format != "json" && format != "text") {
return absl::InvalidArgumentError("--format must be either json or text");
}
ASSIGN_OR_RETURN(int tile_numeric, overworld::ParseNumeric(*tile_value));
if (tile_numeric < 0 || tile_numeric > 0xFFFF) {
return absl::InvalidArgumentError("Tile ID must be between 0x0000 and 0xFFFF");
}
overworld::TileSearchOptions options;
if (map_value.has_value()) {
ASSIGN_OR_RETURN(int map_id, overworld::ParseNumeric(*map_value));
options.map_id = map_id;
}
if (world_value.has_value()) {
ASSIGN_OR_RETURN(int world_id, overworld::ParseWorldSpecifier(*world_value));
options.world = world_id;
}
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 stats, overworld::AnalyzeTileUsage(overworld_data,
static_cast<uint16_t>(tile_numeric),
options));
if (format == "json") {
std::cout << "{\n";
std::cout << absl::StrFormat(" \"tile\": \"0x%04X\",\n", stats.tile_id);
if (stats.map_id >= 0) {
std::cout << absl::StrFormat(" \"map\": \"0x%02X\",\n", stats.map_id);
std::cout << absl::StrFormat(" \"world\": \"%s\",\n", overworld::WorldName(stats.world));
}
std::cout << absl::StrFormat(" \"total_count\": %d,\n", stats.count);
std::cout << " \"sample_positions\": [\n";
size_t limit = std::min<size_t>(stats.positions.size(), 10);
for (size_t i = 0; i < limit; ++i) {
const auto& pos = stats.positions[i];
std::cout << absl::StrFormat(" {\"x\": %d, \"y\": %d}", pos.first, pos.second);
if (i + 1 < limit) std::cout << ",";
std::cout << "\n";
}
std::cout << " ]\n";
std::cout << "}\n";
} else {
std::cout << absl::StrFormat("📊 Tile 0x%04X Statistics\n", stats.tile_id);
if (stats.map_id >= 0) {
std::cout << absl::StrFormat("Map: 0x%02X (%s World)\n", stats.map_id,
overworld::WorldName(stats.world));
}
std::cout << absl::StrFormat("Total Count: %d occurrences\n", stats.count);
if (!stats.positions.empty()) {
std::cout << "Sample Positions (first 10):\n";
size_t limit = std::min<size_t>(stats.positions.size(), 10);
for (size_t i = 0; i < limit; ++i) {
const auto& pos = stats.positions[i];
std::cout << absl::StrFormat(" (%d, %d)\n", pos.first, pos.second);
}
}
}
return absl::OkStatus();
}
absl::Status HandleMessageListCommand(
const std::vector<std::string>& arg_vec, Rom* rom_context) {
return yaze::cli::message::HandleMessageListCommand(arg_vec, rom_context);

View File

@@ -43,7 +43,8 @@ absl::StatusOr<Rom> LoadRomFromFlag() {
}
std::vector<editor::MessageData> LoadMessages(Rom* rom) {
return editor::ReadAllTextData(rom->data(), editor::kTextData);
// Fix: Cast away constness for ReadAllTextData, which expects uint8_t*
return editor::ReadAllTextData(const_cast<uint8_t*>(rom->data()), editor::kTextData);
}
} // namespace

View File

@@ -386,6 +386,115 @@ absl::StatusOr<std::vector<TileMatch>> FindTileMatches(
return matches;
}
absl::StatusOr<std::vector<OverworldSprite>> CollectOverworldSprites(
const zelda3::Overworld& overworld, const SpriteQuery& query) {
std::vector<OverworldSprite> results;
// Iterate through all 3 game states (beginning, zelda, agahnim)
for (int game_state = 0; game_state < 3; ++game_state) {
const auto& sprites = overworld.sprites(game_state);
for (const auto& sprite : sprites) {
// Apply filters
if (query.sprite_id.has_value() && sprite.id() != *query.sprite_id) {
continue;
}
int map_id = sprite.map_id();
if (query.map_id.has_value() && map_id != *query.map_id) {
continue;
}
// Determine world from map_id
int world = (map_id >= kSpecialWorldOffset) ? 2
: (map_id >= kDarkWorldOffset ? 1 : 0);
if (query.world.has_value() && world != *query.world) {
continue;
}
OverworldSprite entry;
entry.sprite_id = sprite.id();
entry.map_id = map_id;
entry.world = world;
entry.x = sprite.x();
entry.y = sprite.y();
// Sprite names would come from a label system if available
// entry.sprite_name = GetSpriteName(sprite.id());
results.push_back(entry);
}
}
return results;
}
absl::StatusOr<EntranceDetails> GetEntranceDetails(
const zelda3::Overworld& overworld, uint8_t entrance_id) {
const auto& entrances = overworld.entrances();
if (entrance_id >= entrances.size()) {
return absl::NotFoundError(
absl::StrFormat("Entrance %d not found (max: %d)",
entrance_id, entrances.size() - 1));
}
const auto& entrance = entrances[entrance_id];
EntranceDetails details;
details.entrance_id = entrance_id;
details.map_id = entrance.map_id_;
// Determine world from map_id
details.world = (details.map_id >= kSpecialWorldOffset) ? 2
: (details.map_id >= kDarkWorldOffset ? 1 : 0);
details.x = entrance.x_;
details.y = entrance.y_;
details.area_x = entrance.area_x_;
details.area_y = entrance.area_y_;
details.map_pos = entrance.map_pos_;
details.is_hole = entrance.is_hole_;
// Get entrance name if available
details.entrance_name = EntranceLabel(entrance_id);
return details;
}
absl::StatusOr<TileStatistics> AnalyzeTileUsage(
zelda3::Overworld& overworld, uint16_t tile_id,
const TileSearchOptions& options) {
// Use FindTileMatches to get all occurrences
ASSIGN_OR_RETURN(auto matches, FindTileMatches(overworld, tile_id, options));
TileStatistics stats;
stats.tile_id = tile_id;
stats.count = static_cast<int>(matches.size());
// If scoped to a specific map, store that info
if (options.map_id.has_value()) {
stats.map_id = *options.map_id;
if (options.world.has_value()) {
stats.world = *options.world;
} else {
ASSIGN_OR_RETURN(stats.world, InferWorldFromMapId(*options.map_id));
}
} else {
stats.map_id = -1; // Indicates all maps
stats.world = -1;
}
// Store positions (convert from TileMatch to pair)
stats.positions.reserve(matches.size());
for (const auto& match : matches) {
stats.positions.emplace_back(match.local_x, match.local_y);
}
return stats;
}
} // namespace overworld
} // namespace cli
} // namespace yaze

View File

@@ -95,6 +95,42 @@ struct TileSearchOptions {
std::optional<int> world;
};
struct OverworldSprite {
uint8_t sprite_id;
int map_id;
int world;
int x;
int y;
std::optional<std::string> sprite_name;
};
struct SpriteQuery {
std::optional<int> map_id;
std::optional<int> world;
std::optional<uint8_t> sprite_id;
};
struct EntranceDetails {
uint8_t entrance_id;
int map_id;
int world;
int x;
int y;
uint8_t area_x;
uint8_t area_y;
uint16_t map_pos;
bool is_hole;
std::optional<std::string> entrance_name;
};
struct TileStatistics {
int map_id;
int world;
uint16_t tile_id;
int count;
std::vector<std::pair<int, int>> positions; // (x, y) positions
};
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);
@@ -111,6 +147,16 @@ absl::StatusOr<std::vector<TileMatch>> FindTileMatches(
zelda3::Overworld& overworld, uint16_t tile_id,
const TileSearchOptions& options = {});
absl::StatusOr<std::vector<OverworldSprite>> CollectOverworldSprites(
const zelda3::Overworld& overworld, const SpriteQuery& query);
absl::StatusOr<EntranceDetails> GetEntranceDetails(
const zelda3::Overworld& overworld, uint8_t entrance_id);
absl::StatusOr<TileStatistics> AnalyzeTileUsage(
zelda3::Overworld& overworld, uint16_t tile_id,
const TileSearchOptions& options = {});
} // namespace overworld
} // namespace cli
} // namespace yaze

View File

@@ -48,6 +48,12 @@ absl::StatusOr<std::string> ToolDispatcher::Dispatch(
status = HandleOverworldDescribeMapCommand(args, rom_context_);
} else if (tool_call.tool_name == "overworld-list-warps") {
status = HandleOverworldListWarpsCommand(args, rom_context_);
} else if (tool_call.tool_name == "overworld-list-sprites") {
status = HandleOverworldListSpritesCommand(args, rom_context_);
} else if (tool_call.tool_name == "overworld-get-entrance") {
status = HandleOverworldGetEntranceCommand(args, rom_context_);
} else if (tool_call.tool_name == "overworld-tile-stats") {
status = HandleOverworldTileStatsCommand(args, rom_context_);
} else if (tool_call.tool_name == "message-list") {
status = HandleMessageListCommand(args, rom_context_);
} else if (tool_call.tool_name == "message-read") {

View File

@@ -45,10 +45,41 @@ std::unique_ptr<AIService> CreateAIService() {
}
std::unique_ptr<AIService> CreateAIService(const AIServiceConfig& config) {
std::cout << "🤖 AI Provider: " << config.provider << "\n";
std::string provider = config.provider;
// Auto-detection: try gemini → ollama → mock
if (provider == "auto") {
// Try Gemini first if API key is available
#ifdef YAZE_WITH_JSON
if (!config.gemini_api_key.empty()) {
std::cout << "🤖 Auto-detecting AI provider...\n";
std::cout << " Found Gemini API key, using Gemini\n";
provider = "gemini";
} else
#endif
{
// Try Ollama next
OllamaConfig test_config;
test_config.base_url = config.ollama_host;
auto test_service = std::make_unique<OllamaAIService>(test_config);
if (test_service->CheckAvailability().ok()) {
std::cout << "🤖 Auto-detecting AI provider...\n";
std::cout << " Ollama available, using Ollama\n";
provider = "ollama";
} else {
std::cout << "🤖 No AI provider configured, using MockAIService\n";
std::cout << " Tip: Set GEMINI_API_KEY or start Ollama for real AI\n";
provider = "mock";
}
}
}
if (provider != "mock") {
std::cout << "🤖 AI Provider: " << provider << "\n";
}
// Ollama provider
if (config.provider == "ollama") {
if (provider == "ollama") {
OllamaConfig ollama_config;
ollama_config.base_url = config.ollama_host;
if (!config.model.empty()) {
@@ -65,12 +96,12 @@ std::unique_ptr<AIService> CreateAIService(const AIServiceConfig& config) {
}
std::cout << " Using model: " << ollama_config.model << std::endl;
return service;
return std::unique_ptr<AIService>(std::move(service));
}
// Gemini provider
#ifdef YAZE_WITH_JSON
if (config.provider == "gemini") {
if (provider == "gemini") {
if (config.gemini_api_key.empty()) {
std::cerr << "⚠️ Gemini API key not provided" << std::endl;
std::cerr << " Use --gemini_api_key=<key> or GEMINI_API_KEY environment variable" << std::endl;
@@ -86,8 +117,7 @@ std::unique_ptr<AIService> CreateAIService(const AIServiceConfig& config) {
gemini_config.use_function_calling = absl::GetFlag(FLAGS_use_function_calling);
gemini_config.verbose = config.verbose;
std::cerr << "🤖 AI Provider: gemini" << std::endl;
std::cerr << " Model: " << gemini_config.model << std::endl;
std::cout << " Model: " << gemini_config.model << std::endl;
if (config.verbose) {
std::cerr << " Prompt: " << gemini_config.prompt_version << std::endl;
}
@@ -100,24 +130,22 @@ std::unique_ptr<AIService> CreateAIService(const AIServiceConfig& config) {
// return std::make_unique<MockAIService>();
// }
std::cout << " Using model: " << gemini_config.model << std::endl;
if (config.verbose) {
std::cerr << "[DEBUG] Gemini service ready" << std::endl;
}
return service;
}
#else
if (config.provider == "gemini") {
if (provider == "gemini") {
std::cerr << "⚠️ Gemini support not available: rebuild with YAZE_WITH_JSON=ON" << std::endl;
std::cerr << " Falling back to MockAIService" << std::endl;
}
#endif
// Default: Mock service
if (config.provider != "mock") {
std::cout << " No LLM configured, using MockAIService" << std::endl;
if (provider == "mock") {
std::cout << " Using MockAIService (no real AI)\n";
}
std::cout << " Tip: Use --ai_provider=ollama or --ai_provider=gemini" << std::endl;
return std::make_unique<MockAIService>();
}

View File

@@ -10,7 +10,7 @@ namespace yaze {
namespace cli {
struct AIServiceConfig {
std::string provider = "mock"; // "mock", "ollama", or "gemini"
std::string provider = "auto"; // "auto" (try gemini→ollama→mock), "gemini", "ollama", or "mock"
std::string model; // Provider-specific model name
std::string gemini_api_key; // For Gemini
std::string ollama_host = "http://localhost:11434"; // For Ollama