diff --git a/assets/agent/function_schemas.json b/assets/agent/function_schemas.json index 77e10c66..696728c1 100644 --- a/assets/agent/function_schemas.json +++ b/assets/agent/function_schemas.json @@ -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"] + } } ] diff --git a/assets/agent/system_prompt_v2.txt b/assets/agent/system_prompt_v2.txt index fd720fcf..fdb5760a 100644 --- a/assets/agent/system_prompt_v2.txt +++ b/assets/agent/system_prompt_v2.txt @@ -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"] + } } ] ``` diff --git a/src/cli/flags.cc b/src/cli/flags.cc index 0db719f5..ceb3b52c 100644 --- a/src/cli/flags.cc +++ b/src/cli/flags.cc @@ -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)"); diff --git a/src/cli/handlers/agent/commands.h b/src/cli/handlers/agent/commands.h index 57897449..a69eebfc 100644 --- a/src/cli/handlers/agent/commands.h +++ b/src/cli/handlers/agent/commands.h @@ -46,6 +46,15 @@ absl::Status HandleOverworldDescribeMapCommand( absl::Status HandleOverworldListWarpsCommand( const std::vector& arg_vec, Rom* rom_context = nullptr); +absl::Status HandleOverworldListSpritesCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleOverworldGetEntranceCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleOverworldTileStatsCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); absl::Status HandleMessageListCommand( const std::vector& arg_vec, Rom* rom_context = nullptr); diff --git a/src/cli/handlers/agent/tool_commands.cc b/src/cli/handlers/agent/tool_commands.cc index a6d57cf3..f0d2fccc 100644 --- a/src/cli/handlers/agent/tool_commands.cc +++ b/src/cli/handlers/agent/tool_commands.cc @@ -1180,6 +1180,346 @@ absl::Status HandleOverworldListWarpsCommand( return absl::OkStatus(); } +absl::Status HandleOverworldListSpritesCommand( + const std::vector& arg_vec, Rom* rom_context) { + std::optional map_value; + std::optional world_value; + std::string format = "json"; + std::optional 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& arg_vec, Rom* rom_context) { + std::optional entrance_id_str; + std::string format = "json"; + std::optional 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 [--format ]"); + } + + 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(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& arg_vec, Rom* rom_context) { + std::optional tile_value; + std::optional map_value; + std::optional world_value; + std::string format = "json"; + std::optional 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 [--map ] [--world ] [--format ]"); + } + + 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(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(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(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& arg_vec, Rom* rom_context) { return yaze::cli::message::HandleMessageListCommand(arg_vec, rom_context); diff --git a/src/cli/handlers/message.cc b/src/cli/handlers/message.cc index 238edfac..960dec8f 100644 --- a/src/cli/handlers/message.cc +++ b/src/cli/handlers/message.cc @@ -43,7 +43,8 @@ absl::StatusOr LoadRomFromFlag() { } std::vector 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(rom->data()), editor::kTextData); } } // namespace diff --git a/src/cli/handlers/overworld_inspect.cc b/src/cli/handlers/overworld_inspect.cc index 902477d7..fd03617e 100644 --- a/src/cli/handlers/overworld_inspect.cc +++ b/src/cli/handlers/overworld_inspect.cc @@ -386,6 +386,115 @@ absl::StatusOr> FindTileMatches( return matches; } +absl::StatusOr> CollectOverworldSprites( + const zelda3::Overworld& overworld, const SpriteQuery& query) { + std::vector 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 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 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(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 diff --git a/src/cli/handlers/overworld_inspect.h b/src/cli/handlers/overworld_inspect.h index 128e2432..464e2d4e 100644 --- a/src/cli/handlers/overworld_inspect.h +++ b/src/cli/handlers/overworld_inspect.h @@ -95,6 +95,42 @@ struct TileSearchOptions { std::optional world; }; +struct OverworldSprite { + uint8_t sprite_id; + int map_id; + int world; + int x; + int y; + std::optional sprite_name; +}; + +struct SpriteQuery { + std::optional map_id; + std::optional world; + std::optional 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 entrance_name; +}; + +struct TileStatistics { + int map_id; + int world; + uint16_t tile_id; + int count; + std::vector> positions; // (x, y) positions +}; + absl::StatusOr ParseNumeric(std::string_view value, int base = 0); absl::StatusOr ParseWorldSpecifier(std::string_view value); absl::StatusOr InferWorldFromMapId(int map_id); @@ -111,6 +147,16 @@ absl::StatusOr> FindTileMatches( zelda3::Overworld& overworld, uint16_t tile_id, const TileSearchOptions& options = {}); +absl::StatusOr> CollectOverworldSprites( + const zelda3::Overworld& overworld, const SpriteQuery& query); + +absl::StatusOr GetEntranceDetails( + const zelda3::Overworld& overworld, uint8_t entrance_id); + +absl::StatusOr AnalyzeTileUsage( + zelda3::Overworld& overworld, uint16_t tile_id, + const TileSearchOptions& options = {}); + } // namespace overworld } // namespace cli } // namespace yaze diff --git a/src/cli/service/agent/tool_dispatcher.cc b/src/cli/service/agent/tool_dispatcher.cc index d2ac2b7b..8663ff70 100644 --- a/src/cli/service/agent/tool_dispatcher.cc +++ b/src/cli/service/agent/tool_dispatcher.cc @@ -48,6 +48,12 @@ absl::StatusOr 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") { diff --git a/src/cli/service/ai/service_factory.cc b/src/cli/service/ai/service_factory.cc index a8d7b24a..d93c23ed 100644 --- a/src/cli/service/ai/service_factory.cc +++ b/src/cli/service/ai/service_factory.cc @@ -45,10 +45,41 @@ std::unique_ptr CreateAIService() { } std::unique_ptr 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(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 CreateAIService(const AIServiceConfig& config) { } std::cout << " Using model: " << ollama_config.model << std::endl; - return service; + return std::unique_ptr(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= or GEMINI_API_KEY environment variable" << std::endl; @@ -86,8 +117,7 @@ std::unique_ptr 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 CreateAIService(const AIServiceConfig& config) { // return std::make_unique(); // } - 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(); } diff --git a/src/cli/service/ai/service_factory.h b/src/cli/service/ai/service_factory.h index d5f2dac0..4eae4359 100644 --- a/src/cli/service/ai/service_factory.h +++ b/src/cli/service/ai/service_factory.h @@ -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