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:
@@ -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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
@@ -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)");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user