feat: Implement message handling commands for agent tool

- Added functionality for listing, reading, and searching messages in the ROM.
- Introduced new commands: `message-list`, `message-read`, and `message-search` with appropriate parameters and descriptions.
- Enhanced the CLI to support these commands, including JSON and text output formats.
- Updated system prompts and function schemas to reflect the new message handling capabilities.
This commit is contained in:
scawful
2025-10-04 20:53:13 -04:00
parent b3fee1b62e
commit f38946118c
11 changed files with 830 additions and 75 deletions

View File

@@ -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"]
}
}
]

View File

@@ -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"]
}
}
]
```

View File

@@ -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
)

View File

@@ -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> # 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> 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 <category> 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 <command>\n";
std::cout << "For all commands: z3ed --list-commands\n\n";
}
struct ParsedGlobals {
std::vector<char*> 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<int>(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;
}
}

View File

@@ -46,6 +46,15 @@ absl::Status HandleOverworldDescribeMapCommand(
absl::Status HandleOverworldListWarpsCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
absl::Status HandleMessageListCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
absl::Status HandleMessageReadCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
absl::Status HandleMessageSearchCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
absl::Status HandleChatCommand(Rom& rom);
absl::Status HandleSimpleChatCommand(const std::vector<std::string>&, Rom* rom, bool quiet);
absl::Status HandleTestConversationCommand(

View File

@@ -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<std::string>& arg_vec, Rom* rom_context) {
return yaze::cli::message::HandleMessageListCommand(arg_vec, rom_context);
}
absl::Status HandleMessageReadCommand(
const std::vector<std::string>& arg_vec, Rom* rom_context) {
return yaze::cli::message::HandleMessageReadCommand(arg_vec, rom_context);
}
absl::Status HandleMessageSearchCommand(
const std::vector<std::string>& arg_vec, Rom* rom_context) {
return yaze::cli::message::HandleMessageSearchCommand(arg_vec, rom_context);
}
} // namespace agent
} // namespace cli
} // namespace yaze

416
src/cli/handlers/message.cc Normal file
View File

@@ -0,0 +1,416 @@
#include "cli/handlers/message.h"
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
#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<Rom> LoadRomFromFlag() {
std::string rom_path = absl::GetFlag(FLAGS_rom);
if (rom_path.empty()) {
return absl::FailedPreconditionError(
"No ROM loaded. Use --rom=<path> 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<editor::MessageData> LoadMessages(Rom* rom) {
return editor::ReadAllTextData(rom->data(), editor::kTextData);
}
} // namespace
absl::Status HandleMessageListCommand(const std::vector<std::string>& 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<int>(messages.size()) - 1;
}
start_id = std::max(0, std::min(start_id, static_cast<int>(messages.size()) - 1));
end_id = std::max(start_id, std::min(end_id, static_cast<int>(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<std::string>& 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 <message_id> [--format <json|text>]");
}
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<int>(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<std::string>& 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 <text> [--format <json|text>]");
}
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<int> 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<std::string>& 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<double>(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

View File

@@ -0,0 +1,57 @@
#ifndef YAZE_CLI_HANDLERS_MESSAGE_H_
#define YAZE_CLI_HANDLERS_MESSAGE_H_
#include <string>
#include <vector>
#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 <json|text>] [--range <start-end>]
* @param rom_context Optional ROM context to avoid reloading
*/
absl::Status HandleMessageListCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
/**
* @brief Read a specific message by ID
* @param arg_vec Command arguments: --id <message_id> [--format <json|text>]
* @param rom_context Optional ROM context to avoid reloading
*/
absl::Status HandleMessageReadCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
/**
* @brief Search for messages containing specific text
* @param arg_vec Command arguments: --query <text> [--format <json|text>]
* @param rom_context Optional ROM context to avoid reloading
*/
absl::Status HandleMessageSearchCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
/**
* @brief Get message statistics and overview
* @param arg_vec Command arguments: [--format <json|text>]
* @param rom_context Optional ROM context to avoid reloading
*/
absl::Status HandleMessageStatsCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
} // namespace message
} // namespace cli
} // namespace yaze
#endif // YAZE_CLI_HANDLERS_MESSAGE_H_

View File

@@ -48,6 +48,12 @@ absl::StatusOr<std::string> ToolDispatcher::Dispatch(
status = HandleOverworldDescribeMapCommand(args, rom_context_);
} else if (tool_call.tool_name == "overworld-list-warps") {
status = HandleOverworldListWarpsCommand(args, rom_context_);
} else if (tool_call.tool_name == "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));

View File

@@ -7,30 +7,106 @@
#include <unordered_map>
#include <vector>
#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=<path> [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> 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<char*> 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;
}
}

View File

@@ -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