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:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
416
src/cli/handlers/message.cc
Normal 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
|
||||
57
src/cli/handlers/message.h
Normal file
57
src/cli/handlers/message.h
Normal 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_
|
||||
@@ -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));
|
||||
|
||||
110
src/cli/z3ed.cc
110
src/cli/z3ed.cc
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user