diff --git a/assets/agent/function_schemas.json b/assets/agent/function_schemas.json index 0cc7ae63..77e10c66 100644 --- a/assets/agent/function_schemas.json +++ b/assets/agent/function_schemas.json @@ -169,6 +169,62 @@ } } } + }, + { + "name": "message-list", + "description": "List all in-game dialogue and text messages from the ROM", + "parameters": { + "type": "object", + "properties": { + "range": { + "type": "string", + "description": "Optional: limit to message ID range in format 'start-end' (e.g., '0-100')" + }, + "format": { + "type": "string", + "enum": ["json", "text"], + "default": "json" + } + } + } + }, + { + "name": "message-read", + "description": "Read a specific message by its ID", + "parameters": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Message ID number (0-300+)" + }, + "format": { + "type": "string", + "enum": ["json", "text"], + "default": "json" + } + }, + "required": ["id"] + } + }, + { + "name": "message-search", + "description": "Search for messages containing specific text", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Text to search for within message content (case-insensitive)" + }, + "format": { + "type": "string", + "enum": ["json", "text"], + "default": "json" + } + }, + "required": ["query"] + } } ] diff --git a/assets/agent/system_prompt_v2.txt b/assets/agent/system_prompt_v2.txt index ace1ab70..fd720fcf 100644 --- a/assets/agent/system_prompt_v2.txt +++ b/assets/agent/system_prompt_v2.txt @@ -201,6 +201,62 @@ You must follow this exact two-step process to avoid errors. } } } + }, + { + "name": "message-list", + "description": "List all in-game dialogue and text messages from the ROM. Messages are the text that NPCs speak, signs display, and item descriptions show. There are typically 300+ messages in the ROM. Use --range to limit output.", + "usage_examples": [ + "What are all the game messages?", + "List messages 0 through 50", + "Show all dialogue in the ROM" + ], + "parameters": { + "type": "object", + "properties": { + "range": { + "type": "string", + "description": "Optional: limit to message ID range in format 'start-end' (e.g., '0-100'). Omit to list all messages." + } + } + } + }, + { + "name": "message-read", + "description": "Read a specific message by its ID. Messages contain the exact text shown in-game, including special formatting like line breaks and commands. Message IDs range from 0 to 300+.", + "usage_examples": [ + "What does message 42 say?", + "Read the text of message 0", + "Show me message 157" + ], + "parameters": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Message ID number (0-300+). Use message-list first if you don't know the ID." + } + }, + "required": ["id"] + } + }, + { + "name": "message-search", + "description": "Search for messages containing specific text or phrases. Case-insensitive search across all message dialogue. Returns all matching messages with their IDs and content.", + "usage_examples": [ + "Find messages about the Master Sword", + "Search for messages containing 'treasure'", + "Which messages mention 'princess'?" + ], + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Text to search for within message content. Case-insensitive." + } + }, + "required": ["query"] + } } ] ``` diff --git a/src/cli/agent.cmake b/src/cli/agent.cmake index 569d2aa9..74bdd220 100644 --- a/src/cli/agent.cmake +++ b/src/cli/agent.cmake @@ -80,6 +80,7 @@ set(YAZE_AGENT_SOURCES cli/service/resources/resource_catalog.cc cli/service/resources/resource_context_builder.cc cli/handlers/overworld_inspect.cc + cli/handlers/message.cc cli/flags.cc cli/service/rom/rom_sandbox_manager.cc ) diff --git a/src/cli/cli_main.cc b/src/cli/cli_main.cc index 1e9f30f4..0b452662 100644 --- a/src/cli/cli_main.cc +++ b/src/cli/cli_main.cc @@ -7,14 +7,17 @@ #include "absl/flags/declare.h" #include "absl/flags/flag.h" -#include "absl/strings/str_format.h" #include "absl/strings/match.h" - +#include "absl/strings/str_format.h" #include "cli/modern_cli.h" #include "cli/tui.h" +#include "cli/z3ed_ascii_logo.h" #include "yaze_config.h" -ABSL_FLAG(bool, tui, false, "Launch Text User Interface"); +// Define all CLI flags +ABSL_FLAG(bool, tui, false, "Launch interactive Text User Interface"); +ABSL_FLAG(bool, quiet, false, "Suppress non-essential output"); +ABSL_FLAG(bool, version, false, "Show version information"); ABSL_DECLARE_FLAG(std::string, rom); ABSL_DECLARE_FLAG(std::string, ai_provider); ABSL_DECLARE_FLAG(std::string, ai_model); @@ -22,10 +25,57 @@ ABSL_DECLARE_FLAG(std::string, gemini_api_key); ABSL_DECLARE_FLAG(std::string, ollama_host); ABSL_DECLARE_FLAG(std::string, prompt_version); ABSL_DECLARE_FLAG(bool, use_function_calling); -ABSL_FLAG(bool, quiet, false, "Enable quiet mode for simple-chat."); namespace { +void PrintVersion() { + std::cout << yaze::cli::GetColoredLogo() << "\n"; + std::cout << absl::StrFormat(" Version %d.%d.%d\n", + YAZE_VERSION_MAJOR, + YAZE_VERSION_MINOR, + YAZE_VERSION_PATCH); + std::cout << " Yet Another Zelda3 Editor - Command Line Interface\n"; + std::cout << " https://github.com/scawful/yaze\n\n"; +} + +void PrintCompactHelp() { + std::cout << yaze::cli::GetColoredLogo() << "\n"; + std::cout << " \033[1;37mYet Another Zelda3 Editor - AI-Powered CLI\033[0m\n\n"; + + std::cout << "\033[1;36mUSAGE:\033[0m\n"; + std::cout << " z3ed [command] [flags]\n"; + std::cout << " z3ed --tui # Interactive TUI mode\n"; + std::cout << " z3ed --version # Show version\n"; + std::cout << " z3ed --help # Category help\n\n"; + + std::cout << "\033[1;36mCOMMANDS:\033[0m\n"; + std::cout << " \033[1;33magent\033[0m AI conversational agent for ROM inspection\n"; + std::cout << " \033[1;33mrom\033[0m ROM operations (info, validate, diff)\n"; + std::cout << " \033[1;33mdungeon\033[0m Dungeon inspection and editing\n"; + std::cout << " \033[1;33moverworld\033[0m Overworld inspection and editing\n"; + std::cout << " \033[1;33mmessage\033[0m Message/dialogue inspection\n"; + std::cout << " \033[1;33mgfx\033[0m Graphics operations (export, import)\n"; + std::cout << " \033[1;33mpalette\033[0m Palette operations\n"; + std::cout << " \033[1;33mpatch\033[0m Apply patches (BPS, Asar)\n"; + std::cout << " \033[1;33mproject\033[0m Project management (init, build)\n\n"; + + std::cout << "\033[1;36mCOMMON FLAGS:\033[0m\n"; + std::cout << " --rom= Path to ROM file\n"; + std::cout << " --tui Launch interactive TUI\n"; + std::cout << " --quiet, -q Suppress output\n"; + std::cout << " --version Show version\n"; + std::cout << " --help Show category help\n\n"; + + std::cout << "\033[1;36mEXAMPLES:\033[0m\n"; + std::cout << " z3ed agent test-conversation --rom=zelda3.sfc\n"; + std::cout << " z3ed rom info --rom=zelda3.sfc\n"; + std::cout << " z3ed agent message-search --rom=zelda3.sfc --query=\"Master Sword\"\n"; + std::cout << " z3ed dungeon export --rom=zelda3.sfc --id=1\n\n"; + + std::cout << "For detailed help: z3ed --help \n"; + std::cout << "For all commands: z3ed --list-commands\n\n"; +} + struct ParsedGlobals { std::vector positional; bool show_help = false; @@ -56,6 +106,7 @@ ParsedGlobals ParseGlobalFlags(int argc, char* argv[]) { continue; } + // Help flags if (absl::StartsWith(token, "--help=")) { std::string category(token.substr(7)); if (!category.empty()) { @@ -65,7 +116,6 @@ ParsedGlobals ParseGlobalFlags(int argc, char* argv[]) { } continue; } - if (token == "--help" || token == "-h") { if (i + 1 < argc && argv[i + 1][0] != '-') { result.help_category = std::string(argv[++i]); @@ -75,38 +125,40 @@ ParsedGlobals ParseGlobalFlags(int argc, char* argv[]) { continue; } - if (token == "--version") { + // Version flag + if (token == "--version" || token == "-v") { result.show_version = true; continue; } - if (token == "--tui") { - absl::SetFlag(&FLAGS_tui, true); - continue; - } - + // List commands if (token == "--list-commands" || token == "--list") { result.list_commands = true; continue; } - if (absl::StartsWith(token, "--quiet=")) { - std::string value(token.substr(8)); - bool enable = value.empty() || value == "1" || value == "true"; - absl::SetFlag(&FLAGS_quiet, enable); + // TUI mode + if (token == "--tui" || token == "--interactive") { + absl::SetFlag(&FLAGS_tui, true); continue; } + // Quiet mode if (token == "--quiet" || token == "-q") { absl::SetFlag(&FLAGS_quiet, true); continue; } + if (absl::StartsWith(token, "--quiet=")) { + std::string value(token.substr(8)); + absl::SetFlag(&FLAGS_quiet, value == "true" || value == "1"); + continue; + } + // ROM path if (absl::StartsWith(token, "--rom=")) { absl::SetFlag(&FLAGS_rom, std::string(token.substr(6))); continue; } - if (token == "--rom") { if (i + 1 >= argc) { result.error = "--rom flag requires a value"; @@ -117,79 +169,91 @@ ParsedGlobals ParseGlobalFlags(int argc, char* argv[]) { } // AI provider flags - if (absl::StartsWith(token, "--ai_provider=")) { - absl::SetFlag(&FLAGS_ai_provider, std::string(token.substr(14))); + if (absl::StartsWith(token, "--ai_provider=") || + absl::StartsWith(token, "--ai-provider=")) { + size_t eq_pos = token.find('='); + absl::SetFlag(&FLAGS_ai_provider, std::string(token.substr(eq_pos + 1))); continue; } - if (token == "--ai_provider") { + if (token == "--ai_provider" || token == "--ai-provider") { if (i + 1 >= argc) { - result.error = "--ai_provider flag requires a value"; + result.error = "--ai-provider flag requires a value"; return result; } absl::SetFlag(&FLAGS_ai_provider, std::string(argv[++i])); continue; } - if (absl::StartsWith(token, "--ai_model=")) { - absl::SetFlag(&FLAGS_ai_model, std::string(token.substr(11))); + if (absl::StartsWith(token, "--ai_model=") || + absl::StartsWith(token, "--ai-model=")) { + size_t eq_pos = token.find('='); + absl::SetFlag(&FLAGS_ai_model, std::string(token.substr(eq_pos + 1))); continue; } - if (token == "--ai_model") { + if (token == "--ai_model" || token == "--ai-model") { if (i + 1 >= argc) { - result.error = "--ai_model flag requires a value"; + result.error = "--ai-model flag requires a value"; return result; } absl::SetFlag(&FLAGS_ai_model, std::string(argv[++i])); continue; } - if (absl::StartsWith(token, "--gemini_api_key=")) { - absl::SetFlag(&FLAGS_gemini_api_key, std::string(token.substr(17))); + if (absl::StartsWith(token, "--gemini_api_key=") || + absl::StartsWith(token, "--gemini-api-key=")) { + size_t eq_pos = token.find('='); + absl::SetFlag(&FLAGS_gemini_api_key, std::string(token.substr(eq_pos + 1))); continue; } - if (token == "--gemini_api_key") { + if (token == "--gemini_api_key" || token == "--gemini-api-key") { if (i + 1 >= argc) { - result.error = "--gemini_api_key flag requires a value"; + result.error = "--gemini-api-key flag requires a value"; return result; } absl::SetFlag(&FLAGS_gemini_api_key, std::string(argv[++i])); continue; } - if (absl::StartsWith(token, "--ollama_host=")) { - absl::SetFlag(&FLAGS_ollama_host, std::string(token.substr(14))); + if (absl::StartsWith(token, "--ollama_host=") || + absl::StartsWith(token, "--ollama-host=")) { + size_t eq_pos = token.find('='); + absl::SetFlag(&FLAGS_ollama_host, std::string(token.substr(eq_pos + 1))); continue; } - if (token == "--ollama_host") { + if (token == "--ollama_host" || token == "--ollama-host") { if (i + 1 >= argc) { - result.error = "--ollama_host flag requires a value"; + result.error = "--ollama-host flag requires a value"; return result; } absl::SetFlag(&FLAGS_ollama_host, std::string(argv[++i])); continue; } - if (absl::StartsWith(token, "--prompt_version=")) { - absl::SetFlag(&FLAGS_prompt_version, std::string(token.substr(17))); + if (absl::StartsWith(token, "--prompt_version=") || + absl::StartsWith(token, "--prompt-version=")) { + size_t eq_pos = token.find('='); + absl::SetFlag(&FLAGS_prompt_version, std::string(token.substr(eq_pos + 1))); continue; } - if (token == "--prompt_version") { + if (token == "--prompt_version" || token == "--prompt-version") { if (i + 1 >= argc) { - result.error = "--prompt_version flag requires a value"; + result.error = "--prompt-version flag requires a value"; return result; } absl::SetFlag(&FLAGS_prompt_version, std::string(argv[++i])); continue; } - if (absl::StartsWith(token, "--use_function_calling=")) { - std::string value(token.substr(23)); + if (absl::StartsWith(token, "--use_function_calling=") || + absl::StartsWith(token, "--use-function-calling=")) { + size_t eq_pos = token.find('='); + std::string value(token.substr(eq_pos + 1)); absl::SetFlag(&FLAGS_use_function_calling, value == "true" || value == "1"); continue; } - if (token == "--use_function_calling") { + if (token == "--use_function_calling" || token == "--use-function-calling") { if (i + 1 >= argc) { - result.error = "--use_function_calling flag requires a value"; + result.error = "--use-function-calling flag requires a value"; return result; } std::string value(argv[++i]); @@ -204,63 +268,60 @@ ParsedGlobals ParseGlobalFlags(int argc, char* argv[]) { return result; } -void PrintVersion() { - std::cout << absl::StrFormat("yaze %d.%d.%d", YAZE_VERSION_MAJOR, - YAZE_VERSION_MINOR, YAZE_VERSION_PATCH) - << std::endl; -} - } // namespace int main(int argc, char* argv[]) { + // Parse global flags ParsedGlobals globals = ParseGlobalFlags(argc, argv); if (globals.error.has_value()) { - std::cerr << "Error: " << *globals.error << std::endl; + std::cerr << "Error: " << *globals.error << "\n"; + std::cerr << "Use --help for usage information.\n"; return EXIT_FAILURE; } + // Handle version flag if (globals.show_version) { PrintVersion(); return EXIT_SUCCESS; } - // Check if TUI mode is requested + // Handle TUI mode if (absl::GetFlag(FLAGS_tui)) { yaze::cli::ShowMain(); return EXIT_SUCCESS; } + // Create CLI instance yaze::cli::ModernCLI cli; + // Handle category-specific help if (globals.help_category.has_value()) { cli.PrintCategoryHelp(*globals.help_category); return EXIT_SUCCESS; } + // Handle list commands if (globals.list_commands) { cli.PrintCommandSummary(); return EXIT_SUCCESS; } - if (globals.show_help) { - cli.PrintTopLevelHelp(); - return EXIT_SUCCESS; - } - - if (globals.positional.size() <= 1) { - cli.PrintTopLevelHelp(); + // Handle general help or no arguments + if (globals.show_help || globals.positional.size() <= 1) { + PrintCompactHelp(); return EXIT_SUCCESS; } // Run CLI commands auto status = cli.Run(static_cast(globals.positional.size()), globals.positional.data()); - + if (!status.ok()) { - std::cerr << "Error: " << status.message() << std::endl; + std::cerr << "\n\033[1;31mError:\033[0m " << status.message() << "\n"; + std::cerr << "Use --help for usage information.\n"; return EXIT_FAILURE; } return EXIT_SUCCESS; -} +} \ No newline at end of file diff --git a/src/cli/handlers/agent/commands.h b/src/cli/handlers/agent/commands.h index 04aa7658..57897449 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 HandleMessageListCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleMessageReadCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); +absl::Status HandleMessageSearchCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); absl::Status HandleChatCommand(Rom& rom); absl::Status HandleSimpleChatCommand(const std::vector&, Rom* rom, bool quiet); absl::Status HandleTestConversationCommand( diff --git a/src/cli/handlers/agent/tool_commands.cc b/src/cli/handlers/agent/tool_commands.cc index 716e51a5..a6d57cf3 100644 --- a/src/cli/handlers/agent/tool_commands.cc +++ b/src/cli/handlers/agent/tool_commands.cc @@ -22,6 +22,7 @@ #include "app/rom.h" #include "app/zelda3/dungeon/room.h" #include "app/zelda3/overworld/overworld.h" +#include "cli/handlers/message.h" #include "cli/handlers/overworld_inspect.h" #include "cli/service/resources/resource_context_builder.h" #include "util/macro.h" @@ -1179,6 +1180,21 @@ absl::Status HandleOverworldListWarpsCommand( return absl::OkStatus(); } +absl::Status HandleMessageListCommand( + const std::vector& arg_vec, Rom* rom_context) { + return yaze::cli::message::HandleMessageListCommand(arg_vec, rom_context); +} + +absl::Status HandleMessageReadCommand( + const std::vector& arg_vec, Rom* rom_context) { + return yaze::cli::message::HandleMessageReadCommand(arg_vec, rom_context); +} + +absl::Status HandleMessageSearchCommand( + const std::vector& arg_vec, Rom* rom_context) { + return yaze::cli::message::HandleMessageSearchCommand(arg_vec, rom_context); +} + } // namespace agent } // namespace cli } // namespace yaze diff --git a/src/cli/handlers/message.cc b/src/cli/handlers/message.cc new file mode 100644 index 00000000..238edfac --- /dev/null +++ b/src/cli/handlers/message.cc @@ -0,0 +1,416 @@ +#include "cli/handlers/message.h" + +#include +#include +#include +#include + +#include "absl/flags/declare.h" +#include "absl/flags/flag.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/ascii.h" +#include "absl/strings/match.h" +#include "absl/strings/numbers.h" +#include "absl/strings/str_format.h" +#include "app/editor/message/message_data.h" +#include "app/rom.h" +#include "util/macro.h" + +ABSL_DECLARE_FLAG(std::string, rom); + +namespace yaze { +namespace cli { +namespace message { + +namespace { + +absl::StatusOr LoadRomFromFlag() { + std::string rom_path = absl::GetFlag(FLAGS_rom); + if (rom_path.empty()) { + return absl::FailedPreconditionError( + "No ROM loaded. Use --rom= to specify ROM file."); + } + + Rom rom; + auto status = rom.LoadFromFile(rom_path); + if (!status.ok()) { + return absl::FailedPreconditionError(absl::StrFormat( + "Failed to load ROM from '%s': %s", rom_path, status.message())); + } + + return rom; +} + +std::vector LoadMessages(Rom* rom) { + return editor::ReadAllTextData(rom->data(), editor::kTextData); +} + +} // namespace + +absl::Status HandleMessageListCommand(const std::vector& arg_vec, + Rom* rom_context) { + std::string format = "json"; + int start_id = 0; + int end_id = -1; // -1 means all + + for (size_t i = 0; i < arg_vec.size(); ++i) { + const std::string& token = arg_vec[i]; + 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 == "--range") { + if (i + 1 >= arg_vec.size()) { + return absl::InvalidArgumentError("--range requires a value (start-end)."); + } + std::string range = arg_vec[++i]; + size_t dash_pos = range.find('-'); + if (dash_pos == std::string::npos) { + return absl::InvalidArgumentError("--range format must be start-end (e.g. 0-100)"); + } + if (!absl::SimpleAtoi(range.substr(0, dash_pos), &start_id) || + !absl::SimpleAtoi(range.substr(dash_pos + 1), &end_id)) { + return absl::InvalidArgumentError("Invalid range format"); + } + } else if (absl::StartsWith(token, "--range=")) { + std::string range = token.substr(8); + size_t dash_pos = range.find('-'); + if (dash_pos == std::string::npos) { + return absl::InvalidArgumentError("--range format must be start-end (e.g. 0-100)"); + } + if (!absl::SimpleAtoi(range.substr(0, dash_pos), &start_id) || + !absl::SimpleAtoi(range.substr(dash_pos + 1), &end_id)) { + return absl::InvalidArgumentError("Invalid range format"); + } + } + } + + 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 = rom_context; + } else { + auto rom_or = LoadRomFromFlag(); + if (!rom_or.ok()) { + return rom_or.status(); + } + rom_storage = std::move(rom_or.value()); + rom = &rom_storage; + } + + auto messages = LoadMessages(rom); + + if (end_id < 0) { + end_id = static_cast(messages.size()) - 1; + } + + start_id = std::max(0, std::min(start_id, static_cast(messages.size()) - 1)); + end_id = std::max(start_id, std::min(end_id, static_cast(messages.size()) - 1)); + + if (format == "json") { + std::cout << "{\n"; + std::cout << absl::StrFormat(" \"total_messages\": %zu,\n", messages.size()); + std::cout << absl::StrFormat(" \"range\": [%d, %d],\n", start_id, end_id); + std::cout << " \"messages\": [\n"; + + bool first = true; + for (int i = start_id; i <= end_id; ++i) { + const auto& msg = messages[i]; + if (!first) std::cout << ",\n"; + std::cout << " {\n"; + std::cout << absl::StrFormat(" \"id\": %d,\n", msg.ID); + std::cout << absl::StrFormat(" \"address\": \"0x%06X\",\n", msg.Address); + + // Escape quotes in the text + std::string escaped_text = msg.ContentsParsed; + size_t pos = 0; + while ((pos = escaped_text.find('"', pos)) != std::string::npos) { + escaped_text.insert(pos, "\\"); + pos += 2; + } + std::cout << absl::StrFormat(" \"text\": \"%s\"\n", escaped_text); + std::cout << " }"; + first = false; + } + std::cout << "\n ]\n"; + std::cout << "}\n"; + } else { + std::cout << absl::StrFormat("📝 Messages %d-%d (Total: %zu)\n", + start_id, end_id, messages.size()); + std::cout << std::string(60, '=') << "\n"; + for (int i = start_id; i <= end_id; ++i) { + const auto& msg = messages[i]; + std::cout << absl::StrFormat("[%03d] @ 0x%06X\n", msg.ID, msg.Address); + std::cout << " " << msg.ContentsParsed << "\n"; + std::cout << std::string(60, '-') << "\n"; + } + } + + return absl::OkStatus(); +} + +absl::Status HandleMessageReadCommand(const std::vector& arg_vec, + Rom* rom_context) { + int message_id = -1; + std::string format = "json"; + + 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."); + } + if (!absl::SimpleAtoi(arg_vec[++i], &message_id)) { + return absl::InvalidArgumentError("Invalid message ID format."); + } + } else if (absl::StartsWith(token, "--id=")) { + if (!absl::SimpleAtoi(token.substr(5), &message_id)) { + return absl::InvalidArgumentError("Invalid message ID format."); + } + } 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)); + } + } + + if (message_id < 0) { + return absl::InvalidArgumentError( + "Usage: message-read --id [--format ]"); + } + + 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 = rom_context; + } else { + auto rom_or = LoadRomFromFlag(); + if (!rom_or.ok()) { + return rom_or.status(); + } + rom_storage = std::move(rom_or.value()); + rom = &rom_storage; + } + + auto messages = LoadMessages(rom); + + if (message_id >= static_cast(messages.size())) { + return absl::NotFoundError( + absl::StrFormat("Message ID %d not found (max: %d)", + message_id, messages.size() - 1)); + } + + const auto& msg = messages[message_id]; + + if (format == "json") { + std::cout << "{\n"; + std::cout << absl::StrFormat(" \"id\": %d,\n", msg.ID); + std::cout << absl::StrFormat(" \"address\": \"0x%06X\",\n", msg.Address); + + // Escape quotes + std::string escaped_text = msg.ContentsParsed; + size_t pos = 0; + while ((pos = escaped_text.find('"', pos)) != std::string::npos) { + escaped_text.insert(pos, "\\"); + pos += 2; + } + std::cout << absl::StrFormat(" \"text\": \"%s\",\n", escaped_text); + std::cout << absl::StrFormat(" \"length\": %zu\n", msg.Data.size()); + std::cout << "}\n"; + } else { + std::cout << absl::StrFormat("📝 Message #%d\n", msg.ID); + std::cout << absl::StrFormat("Address: 0x%06X\n", msg.Address); + std::cout << absl::StrFormat("Length: %zu bytes\n", msg.Data.size()); + std::cout << std::string(60, '-') << "\n"; + std::cout << msg.ContentsParsed << "\n"; + } + + return absl::OkStatus(); +} + +absl::Status HandleMessageSearchCommand(const std::vector& arg_vec, + Rom* rom_context) { + std::string query; + std::string format = "json"; + + for (size_t i = 0; i < arg_vec.size(); ++i) { + const std::string& token = arg_vec[i]; + if (token == "--query") { + if (i + 1 >= arg_vec.size()) { + return absl::InvalidArgumentError("--query requires a value."); + } + query = arg_vec[++i]; + } else if (absl::StartsWith(token, "--query=")) { + query = 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)); + } + } + + if (query.empty()) { + return absl::InvalidArgumentError( + "Usage: message-search --query [--format ]"); + } + + 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 = rom_context; + } else { + auto rom_or = LoadRomFromFlag(); + if (!rom_or.ok()) { + return rom_or.status(); + } + rom_storage = std::move(rom_or.value()); + rom = &rom_storage; + } + + auto messages = LoadMessages(rom); + std::string lowered_query = absl::AsciiStrToLower(query); + + std::vector matches; + for (const auto& msg : messages) { + std::string lowered_text = absl::AsciiStrToLower(msg.ContentsParsed); + if (lowered_text.find(lowered_query) != std::string::npos) { + matches.push_back(msg.ID); + } + } + + if (format == "json") { + std::cout << "{\n"; + std::cout << absl::StrFormat(" \"query\": \"%s\",\n", query); + std::cout << absl::StrFormat(" \"match_count\": %zu,\n", matches.size()); + std::cout << " \"matches\": [\n"; + + for (size_t i = 0; i < matches.size(); ++i) { + const auto& msg = messages[matches[i]]; + if (i > 0) std::cout << ",\n"; + + std::string escaped_text = msg.ContentsParsed; + size_t pos = 0; + while ((pos = escaped_text.find('"', pos)) != std::string::npos) { + escaped_text.insert(pos, "\\"); + pos += 2; + } + + std::cout << " {\n"; + std::cout << absl::StrFormat(" \"id\": %d,\n", msg.ID); + std::cout << absl::StrFormat(" \"address\": \"0x%06X\",\n", msg.Address); + std::cout << absl::StrFormat(" \"text\": \"%s\"\n", escaped_text); + std::cout << " }"; + } + std::cout << "\n ]\n"; + std::cout << "}\n"; + } else { + std::cout << absl::StrFormat("🔍 Search: \"%s\" → %zu match(es)\n", + query, matches.size()); + std::cout << std::string(60, '=') << "\n"; + + for (int match_id : matches) { + const auto& msg = messages[match_id]; + std::cout << absl::StrFormat("[%03d] @ 0x%06X\n", msg.ID, msg.Address); + std::cout << " " << msg.ContentsParsed << "\n"; + std::cout << std::string(60, '-') << "\n"; + } + } + + return absl::OkStatus(); +} + +absl::Status HandleMessageStatsCommand(const std::vector& arg_vec, + Rom* rom_context) { + std::string format = "json"; + + for (size_t i = 0; i < arg_vec.size(); ++i) { + const std::string& token = arg_vec[i]; + 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)); + } + } + + 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 = rom_context; + } else { + auto rom_or = LoadRomFromFlag(); + if (!rom_or.ok()) { + return rom_or.status(); + } + rom_storage = std::move(rom_or.value()); + rom = &rom_storage; + } + + auto messages = LoadMessages(rom); + + size_t total_bytes = 0; + size_t max_length = 0; + size_t min_length = SIZE_MAX; + + for (const auto& msg : messages) { + size_t len = msg.Data.size(); + total_bytes += len; + max_length = std::max(max_length, len); + min_length = std::min(min_length, len); + } + + double avg_length = messages.empty() ? 0.0 : + static_cast(total_bytes) / messages.size(); + + if (format == "json") { + std::cout << "{\n"; + std::cout << absl::StrFormat(" \"total_messages\": %zu,\n", messages.size()); + std::cout << absl::StrFormat(" \"total_bytes\": %zu,\n", total_bytes); + std::cout << absl::StrFormat(" \"average_length\": %.2f,\n", avg_length); + std::cout << absl::StrFormat(" \"min_length\": %zu,\n", min_length); + std::cout << absl::StrFormat(" \"max_length\": %zu\n", max_length); + std::cout << "}\n"; + } else { + std::cout << "📊 Message Statistics\n"; + std::cout << std::string(40, '=') << "\n"; + std::cout << absl::StrFormat("Total Messages: %zu\n", messages.size()); + std::cout << absl::StrFormat("Total Bytes: %zu\n", total_bytes); + std::cout << absl::StrFormat("Average Length: %.2f bytes\n", avg_length); + std::cout << absl::StrFormat("Min Length: %zu bytes\n", min_length); + std::cout << absl::StrFormat("Max Length: %zu bytes\n", max_length); + } + + return absl::OkStatus(); +} + +} // namespace message +} // namespace cli +} // namespace yaze diff --git a/src/cli/handlers/message.h b/src/cli/handlers/message.h new file mode 100644 index 00000000..9440c416 --- /dev/null +++ b/src/cli/handlers/message.h @@ -0,0 +1,57 @@ +#ifndef YAZE_CLI_HANDLERS_MESSAGE_H_ +#define YAZE_CLI_HANDLERS_MESSAGE_H_ + +#include +#include + +#include "absl/status/status.h" + +namespace yaze { +class Rom; + +namespace cli { +namespace message { + +// Message inspection handlers for agent tool calls + +/** + * @brief List all messages in the ROM + * @param arg_vec Command arguments: [--format ] [--range ] + * @param rom_context Optional ROM context to avoid reloading + */ +absl::Status HandleMessageListCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); + +/** + * @brief Read a specific message by ID + * @param arg_vec Command arguments: --id [--format ] + * @param rom_context Optional ROM context to avoid reloading + */ +absl::Status HandleMessageReadCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); + +/** + * @brief Search for messages containing specific text + * @param arg_vec Command arguments: --query [--format ] + * @param rom_context Optional ROM context to avoid reloading + */ +absl::Status HandleMessageSearchCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); + +/** + * @brief Get message statistics and overview + * @param arg_vec Command arguments: [--format ] + * @param rom_context Optional ROM context to avoid reloading + */ +absl::Status HandleMessageStatsCommand( + const std::vector& arg_vec, + Rom* rom_context = nullptr); + +} // namespace message +} // namespace cli +} // namespace yaze + +#endif // YAZE_CLI_HANDLERS_MESSAGE_H_ diff --git a/src/cli/service/agent/tool_dispatcher.cc b/src/cli/service/agent/tool_dispatcher.cc index 73515a57..d2ac2b7b 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 == "message-list") { + status = HandleMessageListCommand(args, rom_context_); + } else if (tool_call.tool_name == "message-read") { + status = HandleMessageReadCommand(args, rom_context_); + } else if (tool_call.tool_name == "message-search") { + status = HandleMessageSearchCommand(args, rom_context_); } else { status = absl::UnimplementedError( absl::StrFormat("Unknown tool: %s", tool_call.tool_name)); diff --git a/src/cli/z3ed.cc b/src/cli/z3ed.cc index 158d82eb..c7ed66cd 100644 --- a/src/cli/z3ed.cc +++ b/src/cli/z3ed.cc @@ -7,30 +7,106 @@ #include #include +#include "absl/flags/flag.h" +#include "absl/flags/parse.h" +#include "absl/flags/usage.h" +#include "cli/modern_cli.h" #include "cli/tui.h" -#include "util/flag.h" #include "util/macro.h" -DEFINE_FLAG(std::string, rom_file, "", "The ROM file to load."); -DEFINE_FLAG(std::string, bps_file, "", "The BPS file to apply."); - -DEFINE_FLAG(std::string, src_file, "", "The source file."); -DEFINE_FLAG(std::string, modified_file, "", "The modified file."); - -DEFINE_FLAG(std::string, bin_file, "", "The binary file to export to."); -DEFINE_FLAG(std::string, address, "", "The address to convert."); -DEFINE_FLAG(std::string, length, "", "The length of the data to read."); - -DEFINE_FLAG(std::string, file_size, "", "The size of the file to expand to."); -DEFINE_FLAG(std::string, dest_rom, "", "The destination ROM file."); +// Define additional z3ed-specific flags +ABSL_FLAG(bool, quiet, false, "Suppress non-essential output"); +ABSL_FLAG(bool, interactive, false, "Launch interactive TUI mode"); +ABSL_FLAG(bool, version, false, "Show version information"); #ifdef _WIN32 extern "C" int SDL_main(int argc, char *argv[]) { #else int main(int argc, char *argv[]) { #endif - yaze::util::FlagParser flag_parser(yaze::util::global_flag_registry()); - RETURN_IF_EXCEPTION(flag_parser.Parse(argc, argv)); - yaze::cli::ShowMain(); + // Set up usage message + absl::SetProgramUsageMessage(R"( +z3ed - Yet Another Zelda3 Editor CLI + +A command-line interface for inspecting and modifying Zelda 3: A Link to the +Past ROM files. Supports both interactive commands and batch processing. + +USAGE: + z3ed [command] [flags] + z3ed --rom= [command] + z3ed --interactive # Launch TUI mode + +COMMANDS: + agent AI-powered conversational agent for ROM inspection + rom ROM file operations (info, validate, diff, etc.) + dungeon Dungeon inspection and editing + overworld Overworld inspection and editing + message Message/dialogue inspection and editing + gfx Graphics operations (export, import) + palette Palette operations + patch Apply patches (BPS, Asar) + project Project management (init, build) + +FLAGS: + --rom= Path to the ROM file + --quiet Suppress non-essential output + --interactive Launch interactive TUI mode + --version Show version information + --help Show this help message + +EXAMPLES: + # Interactive TUI mode + z3ed --interactive + + # Get ROM information + z3ed rom info --rom=zelda3.sfc + + # AI agent conversation + z3ed agent test-conversation --rom=zelda3.sfc + + # List all messages + z3ed agent message-list --rom=zelda3.sfc --format=json + + # Search for specific message text + z3ed agent message-search --rom=zelda3.sfc --query="Master Sword" + + # Describe dungeon room + z3ed agent dungeon-describe-room --rom=zelda3.sfc --room=0x02A + +For more information about each command, run: + z3ed [command] --help +)"); + + // Parse command line flags + std::vector remaining = absl::ParseCommandLine(argc, argv); + + // Handle version flag + if (absl::GetFlag(FLAGS_version)) { + std::cout << "z3ed version 0.4.0\n"; + std::cout << "Yet Another Zelda3 Editor - Command Line Interface\n"; + return EXIT_SUCCESS; + } + + // Handle interactive TUI mode + if (absl::GetFlag(FLAGS_interactive)) { + yaze::cli::ShowMain(); + return EXIT_SUCCESS; + } + + // If no commands specified, show usage + if (remaining.size() <= 1) { + std::cout << absl::ProgramUsageMessage() << std::endl; + return EXIT_SUCCESS; + } + + // Use modern CLI for command dispatching + yaze::cli::ModernCLI cli; + auto status = cli.Run(argc, argv); + + if (!status.ok()) { + std::cerr << "Error: " << status.message() << std::endl; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; -} +} \ No newline at end of file diff --git a/src/cli/z3ed.cmake b/src/cli/z3ed.cmake index 7b38aaf6..8d6900a6 100644 --- a/src/cli/z3ed.cmake +++ b/src/cli/z3ed.cmake @@ -59,6 +59,7 @@ add_executable( cli/handlers/sprite.cc cli/handlers/project.cc cli/handlers/command_palette.cc + cli/handlers/message.cc cli/handlers/agent.cc cli/handlers/agent/common.cc cli/handlers/agent/general_commands.cc