- Implemented new commands for listing overworld sprites and retrieving entrance details. - Enhanced CLI functionality to support filtering by map, world, and sprite ID with JSON and text output formats. - Introduced tile statistics analysis command for detailed tile usage insights. - Updated function schemas and system prompts to reflect the new commands and their parameters.
418 lines
14 KiB
C++
418 lines
14 KiB
C++
#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) {
|
|
// Fix: Cast away constness for ReadAllTextData, which expects uint8_t*
|
|
return editor::ReadAllTextData(const_cast<uint8_t*>(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
|