Update CMake configuration and CI/CD workflows
- Upgraded CMake minimum version requirement to 3.16 and updated project version to 0.3.0. - Introduced new CMake presets for build configurations, including default, debug, and release options. - Added CI/CD workflows for continuous integration and release management, enhancing automated testing and deployment processes. - Integrated Asar assembler support with new wrapper classes and CLI commands for patching ROMs. - Implemented comprehensive tests for Asar integration, ensuring robust functionality and error handling. - Enhanced packaging configuration for cross-platform support, including Windows, macOS, and Linux. - Updated documentation and added test assets for improved clarity and usability.
This commit is contained in:
335
src/cli/cli_main.cc
Normal file
335
src/cli/cli_main.cc
Normal file
@@ -0,0 +1,335 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
|
||||
#include "absl/flags/flag.h"
|
||||
#include "absl/flags/parse.h"
|
||||
#include "absl/flags/usage.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
|
||||
#include "cli/z3ed.h"
|
||||
#include "cli/tui.h"
|
||||
#include "app/core/asar_wrapper.h"
|
||||
|
||||
// Global flags
|
||||
ABSL_FLAG(bool, tui, false, "Launch the Text User Interface");
|
||||
ABSL_FLAG(bool, version, false, "Show version information");
|
||||
ABSL_FLAG(bool, verbose, false, "Enable verbose output");
|
||||
ABSL_FLAG(std::string, rom, "", "Path to the ROM file");
|
||||
|
||||
// Command-specific flags
|
||||
ABSL_FLAG(std::string, output, "", "Output file path");
|
||||
ABSL_FLAG(bool, dry_run, false, "Perform a dry run without making changes");
|
||||
ABSL_FLAG(bool, backup, true, "Create a backup before modifying files");
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
|
||||
struct CommandInfo {
|
||||
std::string name;
|
||||
std::string description;
|
||||
std::string usage;
|
||||
std::function<absl::Status(const std::vector<std::string>&)> handler;
|
||||
};
|
||||
|
||||
class ModernCLI {
|
||||
public:
|
||||
ModernCLI() {
|
||||
SetupCommands();
|
||||
}
|
||||
|
||||
void SetupCommands() {
|
||||
commands_["asar"] = {
|
||||
.name = "asar",
|
||||
.description = "Apply Asar 65816 assembly patch to ROM",
|
||||
.usage = "z3ed asar <patch.asm> [--rom=<rom_file>] [--output=<output_file>]",
|
||||
.handler = [this](const std::vector<std::string>& args) -> absl::Status {
|
||||
return HandleAsarCommand(args);
|
||||
}
|
||||
};
|
||||
|
||||
commands_["patch"] = {
|
||||
.name = "patch",
|
||||
.description = "Apply BPS patch to ROM",
|
||||
.usage = "z3ed patch <patch.bps> [--rom=<rom_file>] [--output=<output_file>]",
|
||||
.handler = [this](const std::vector<std::string>& args) -> absl::Status {
|
||||
return HandlePatchCommand(args);
|
||||
}
|
||||
};
|
||||
|
||||
commands_["extract"] = {
|
||||
.name = "extract",
|
||||
.description = "Extract symbols from assembly file",
|
||||
.usage = "z3ed extract <patch.asm>",
|
||||
.handler = [this](const std::vector<std::string>& args) -> absl::Status {
|
||||
return HandleExtractCommand(args);
|
||||
}
|
||||
};
|
||||
|
||||
commands_["validate"] = {
|
||||
.name = "validate",
|
||||
.description = "Validate assembly file syntax",
|
||||
.usage = "z3ed validate <patch.asm>",
|
||||
.handler = [this](const std::vector<std::string>& args) -> absl::Status {
|
||||
return HandleValidateCommand(args);
|
||||
}
|
||||
};
|
||||
|
||||
commands_["info"] = {
|
||||
.name = "info",
|
||||
.description = "Show ROM information",
|
||||
.usage = "z3ed info [--rom=<rom_file>]",
|
||||
.handler = [this](const std::vector<std::string>& args) -> absl::Status {
|
||||
return HandleInfoCommand(args);
|
||||
}
|
||||
};
|
||||
|
||||
commands_["convert"] = {
|
||||
.name = "convert",
|
||||
.description = "Convert between SNES and PC addresses",
|
||||
.usage = "z3ed convert <address> [--to-pc|--to-snes]",
|
||||
.handler = [this](const std::vector<std::string>& args) -> absl::Status {
|
||||
return HandleConvertCommand(args);
|
||||
}
|
||||
};
|
||||
|
||||
commands_["help"] = {
|
||||
.name = "help",
|
||||
.description = "Show help information",
|
||||
.usage = "z3ed help [command]",
|
||||
.handler = [this](const std::vector<std::string>& args) -> absl::Status {
|
||||
return HandleHelpCommand(args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void ShowVersion() {
|
||||
std::cout << "z3ed v0.3.0 - Yet Another Zelda3 Editor CLI" << std::endl;
|
||||
std::cout << "Built with Asar integration" << std::endl;
|
||||
std::cout << "Copyright (c) 2025 scawful" << std::endl;
|
||||
}
|
||||
|
||||
void ShowHelp(const std::string& command = "") {
|
||||
if (!command.empty()) {
|
||||
auto it = commands_.find(command);
|
||||
if (it != commands_.end()) {
|
||||
std::cout << "Command: " << it->second.name << std::endl;
|
||||
std::cout << "Description: " << it->second.description << std::endl;
|
||||
std::cout << "Usage: " << it->second.usage << std::endl;
|
||||
return;
|
||||
} else {
|
||||
std::cout << "Unknown command: " << command << std::endl;
|
||||
std::cout << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "z3ed - Yet Another Zelda3 Editor CLI Tool" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "USAGE:" << std::endl;
|
||||
std::cout << " z3ed [--tui] [command] [arguments]" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "GLOBAL FLAGS:" << std::endl;
|
||||
std::cout << " --tui Launch Text User Interface" << std::endl;
|
||||
std::cout << " --version Show version information" << std::endl;
|
||||
std::cout << " --verbose Enable verbose output" << std::endl;
|
||||
std::cout << " --rom=<file> Specify ROM file to use" << std::endl;
|
||||
std::cout << " --output=<file> Specify output file path" << std::endl;
|
||||
std::cout << " --dry-run Perform operations without making changes" << std::endl;
|
||||
std::cout << " --backup=<bool> Create backup before modifying (default: true)" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "COMMANDS:" << std::endl;
|
||||
|
||||
for (const auto& [name, info] : commands_) {
|
||||
std::cout << absl::StrFormat(" %-12s %s", name, info.description) << std::endl;
|
||||
}
|
||||
|
||||
std::cout << std::endl;
|
||||
std::cout << "EXAMPLES:" << std::endl;
|
||||
std::cout << " z3ed --tui # Launch TUI" << std::endl;
|
||||
std::cout << " z3ed asar patch.asm --rom=zelda3.sfc # Apply Asar patch" << std::endl;
|
||||
std::cout << " z3ed patch changes.bps --rom=zelda3.sfc # Apply BPS patch" << std::endl;
|
||||
std::cout << " z3ed extract patch.asm # Extract symbols" << std::endl;
|
||||
std::cout << " z3ed validate patch.asm # Validate assembly" << std::endl;
|
||||
std::cout << " z3ed info --rom=zelda3.sfc # Show ROM info" << std::endl;
|
||||
std::cout << " z3ed convert 0x008000 --to-pc # Convert address" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "For more information on a specific command:" << std::endl;
|
||||
std::cout << " z3ed help <command>" << std::endl;
|
||||
}
|
||||
|
||||
absl::Status RunCommand(const std::string& command, const std::vector<std::string>& args) {
|
||||
auto it = commands_.find(command);
|
||||
if (it == commands_.end()) {
|
||||
return absl::NotFoundError(absl::StrFormat("Unknown command: %s", command));
|
||||
}
|
||||
|
||||
return it->second.handler(args);
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<std::string, CommandInfo> commands_;
|
||||
|
||||
absl::Status HandleAsarCommand(const std::vector<std::string>& args) {
|
||||
if (args.empty()) {
|
||||
return absl::InvalidArgumentError("Asar command requires a patch file");
|
||||
}
|
||||
|
||||
AsarPatch handler;
|
||||
std::vector<std::string> handler_args = args;
|
||||
|
||||
// Add ROM file from flag if not provided as argument
|
||||
std::string rom_file = absl::GetFlag(FLAGS_rom);
|
||||
if (args.size() == 1 && !rom_file.empty()) {
|
||||
handler_args.push_back(rom_file);
|
||||
}
|
||||
|
||||
return handler.Run(handler_args);
|
||||
}
|
||||
|
||||
absl::Status HandlePatchCommand(const std::vector<std::string>& args) {
|
||||
if (args.empty()) {
|
||||
return absl::InvalidArgumentError("Patch command requires a BPS file");
|
||||
}
|
||||
|
||||
ApplyPatch handler;
|
||||
std::vector<std::string> handler_args = args;
|
||||
|
||||
std::string rom_file = absl::GetFlag(FLAGS_rom);
|
||||
if (args.size() == 1 && !rom_file.empty()) {
|
||||
handler_args.push_back(rom_file);
|
||||
}
|
||||
|
||||
return handler.Run(handler_args);
|
||||
}
|
||||
|
||||
absl::Status HandleExtractCommand(const std::vector<std::string>& args) {
|
||||
if (args.empty()) {
|
||||
return absl::InvalidArgumentError("Extract command requires an assembly file");
|
||||
}
|
||||
|
||||
// Use the AsarWrapper to extract symbols
|
||||
yaze::app::core::AsarWrapper wrapper;
|
||||
RETURN_IF_ERROR(wrapper.Initialize());
|
||||
|
||||
auto symbols_result = wrapper.ExtractSymbols(args[0]);
|
||||
if (!symbols_result.ok()) {
|
||||
return symbols_result.status();
|
||||
}
|
||||
|
||||
const auto& symbols = symbols_result.value();
|
||||
std::cout << "🏷️ Extracted " << symbols.size() << " symbols from " << args[0] << ":" << std::endl;
|
||||
std::cout << std::endl;
|
||||
|
||||
for (const auto& symbol : symbols) {
|
||||
std::cout << absl::StrFormat(" %-20s @ $%06X", symbol.name, symbol.address) << std::endl;
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status HandleValidateCommand(const std::vector<std::string>& args) {
|
||||
if (args.empty()) {
|
||||
return absl::InvalidArgumentError("Validate command requires an assembly file");
|
||||
}
|
||||
|
||||
yaze::app::core::AsarWrapper wrapper;
|
||||
RETURN_IF_ERROR(wrapper.Initialize());
|
||||
|
||||
auto status = wrapper.ValidateAssembly(args[0]);
|
||||
if (status.ok()) {
|
||||
std::cout << "✅ Assembly file is valid: " << args[0] << std::endl;
|
||||
} else {
|
||||
std::cout << "❌ Assembly validation failed:" << std::endl;
|
||||
std::cout << " " << status.message() << std::endl;
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
absl::Status HandleInfoCommand(const std::vector<std::string>& args) {
|
||||
std::string rom_file = absl::GetFlag(FLAGS_rom);
|
||||
if (!args.empty()) {
|
||||
rom_file = args[0];
|
||||
}
|
||||
|
||||
if (rom_file.empty()) {
|
||||
return absl::InvalidArgumentError("ROM file required (use --rom=<file> or provide as argument)");
|
||||
}
|
||||
|
||||
Open handler;
|
||||
return handler.Run({rom_file});
|
||||
}
|
||||
|
||||
absl::Status HandleConvertCommand(const std::vector<std::string>& args) {
|
||||
if (args.empty()) {
|
||||
return absl::InvalidArgumentError("Convert command requires an address");
|
||||
}
|
||||
|
||||
// TODO: Implement address conversion
|
||||
std::cout << "Address conversion not yet implemented" << std::endl;
|
||||
return absl::UnimplementedError("Address conversion functionality");
|
||||
}
|
||||
|
||||
absl::Status HandleHelpCommand(const std::vector<std::string>& args) {
|
||||
std::string command = args.empty() ? "" : args[0];
|
||||
ShowHelp(command);
|
||||
return absl::OkStatus();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
absl::SetProgramUsageMessage(
|
||||
"z3ed - Yet Another Zelda3 Editor CLI Tool\n"
|
||||
"\n"
|
||||
"A command-line tool for editing The Legend of Zelda: A Link to the Past ROMs.\n"
|
||||
"Supports Asar 65816 assembly patching, BPS patches, and ROM analysis.\n"
|
||||
"\n"
|
||||
"Use --tui to launch the interactive text interface, or run commands directly.\n"
|
||||
);
|
||||
|
||||
auto args = absl::ParseCommandLine(argc, argv);
|
||||
|
||||
yaze::cli::ModernCLI cli;
|
||||
|
||||
// Handle version flag
|
||||
if (absl::GetFlag(FLAGS_version)) {
|
||||
cli.ShowVersion();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Handle TUI flag
|
||||
if (absl::GetFlag(FLAGS_tui)) {
|
||||
yaze::cli::ShowMain();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Handle command line arguments
|
||||
if (args.size() < 2) {
|
||||
cli.ShowHelp();
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string command = args[1];
|
||||
std::vector<std::string> command_args(args.begin() + 2, args.end());
|
||||
|
||||
auto status = cli.RunCommand(command, command_args);
|
||||
if (!status.ok()) {
|
||||
std::cerr << "Error: " << status.message() << std::endl;
|
||||
|
||||
if (status.code() == absl::StatusCode::kNotFound) {
|
||||
std::cerr << std::endl;
|
||||
std::cerr << "Available commands:" << std::endl;
|
||||
cli.ShowHelp();
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -27,21 +27,93 @@ absl::Status ApplyPatch::Run(const std::vector<std::string>& arg_vec) {
|
||||
}
|
||||
|
||||
absl::Status AsarPatch::Run(const std::vector<std::string>& arg_vec) {
|
||||
std::string patch_filename = arg_vec[1];
|
||||
std::string rom_filename = arg_vec[2];
|
||||
if (arg_vec.size() < 2) {
|
||||
return absl::InvalidArgumentError("Usage: asar <patch_file> <rom_file>");
|
||||
}
|
||||
|
||||
std::string patch_filename = arg_vec[0];
|
||||
std::string rom_filename = arg_vec[1];
|
||||
|
||||
// Load ROM file
|
||||
RETURN_IF_ERROR(rom_.LoadFromFile(rom_filename))
|
||||
int buflen = rom_.vector().size();
|
||||
int romlen = rom_.vector().size();
|
||||
if (!asar_patch(patch_filename.c_str(), rom_filename.data(), buflen,
|
||||
&romlen)) {
|
||||
std::string error_message = "Failed to apply patch: ";
|
||||
|
||||
// Get ROM data
|
||||
auto rom_data = rom_.vector();
|
||||
int buflen = static_cast<int>(rom_data.size());
|
||||
int romlen = buflen;
|
||||
|
||||
// Ensure we have enough buffer space
|
||||
const int max_rom_size = asar_maxromsize();
|
||||
if (buflen < max_rom_size) {
|
||||
rom_data.resize(max_rom_size, 0);
|
||||
buflen = max_rom_size;
|
||||
}
|
||||
|
||||
// Apply Asar patch
|
||||
if (!asar_patch(patch_filename.c_str(),
|
||||
reinterpret_cast<char*>(rom_data.data()),
|
||||
buflen, &romlen)) {
|
||||
std::string error_message = "Failed to apply Asar patch:\n";
|
||||
int num_errors = 0;
|
||||
const errordata* errors = asar_geterrors(&num_errors);
|
||||
for (int i = 0; i < num_errors; i++) {
|
||||
error_message += absl::StrFormat("%s", errors[i].fullerrdata);
|
||||
error_message += absl::StrFormat(" %s\n", errors[i].fullerrdata);
|
||||
}
|
||||
return absl::InternalError(error_message);
|
||||
}
|
||||
|
||||
// Resize ROM to actual size
|
||||
rom_data.resize(romlen);
|
||||
|
||||
// Update the ROM data by writing the patched data back
|
||||
for (size_t i = 0; i < rom_data.size(); ++i) {
|
||||
auto status = rom_.WriteByte(i, rom_data[i]);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
// Save patched ROM
|
||||
std::string output_filename = rom_filename;
|
||||
size_t dot_pos = output_filename.find_last_of('.');
|
||||
if (dot_pos != std::string::npos) {
|
||||
output_filename.insert(dot_pos, "_patched");
|
||||
} else {
|
||||
output_filename += "_patched";
|
||||
}
|
||||
|
||||
Rom::SaveSettings settings;
|
||||
settings.filename = output_filename;
|
||||
RETURN_IF_ERROR(rom_.SaveToFile(settings))
|
||||
|
||||
std::cout << "✅ Asar patch applied successfully!" << std::endl;
|
||||
std::cout << "📁 Output: " << output_filename << std::endl;
|
||||
std::cout << "📊 Final ROM size: " << romlen << " bytes" << std::endl;
|
||||
|
||||
// Show warnings if any
|
||||
int num_warnings = 0;
|
||||
const errordata* warnings = asar_getwarnings(&num_warnings);
|
||||
if (num_warnings > 0) {
|
||||
std::cout << "⚠️ Warnings:" << std::endl;
|
||||
for (int i = 0; i < num_warnings; i++) {
|
||||
std::cout << " " << warnings[i].fullerrdata << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// Show extracted symbols
|
||||
int num_labels = 0;
|
||||
const labeldata* labels = asar_getalllabels(&num_labels);
|
||||
if (num_labels > 0) {
|
||||
std::cout << "🏷️ Extracted " << num_labels << " symbols:" << std::endl;
|
||||
for (int i = 0; i < std::min(10, num_labels); i++) { // Show first 10
|
||||
std::cout << " " << labels[i].name << " @ $"
|
||||
<< std::hex << std::uppercase << labels[i].location << std::endl;
|
||||
}
|
||||
if (num_labels > 10) {
|
||||
std::cout << " ... and " << (num_labels - 10) << " more" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
|
||||
478
src/cli/tui.cc
478
src/cli/tui.cc
@@ -6,8 +6,11 @@
|
||||
#include <ftxui/screen/screen.hpp>
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
#include "util/bps.h"
|
||||
#include "app/core/platform/file_dialog.h"
|
||||
#include "app/core/asar_wrapper.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
@@ -219,6 +222,308 @@ void GenerateSaveFileComponent(ftxui::ScreenInteractive &screen) {
|
||||
screen.Loop(renderer);
|
||||
}
|
||||
|
||||
void ApplyAsarPatchComponent(ftxui::ScreenInteractive &screen) {
|
||||
static std::string patch_file;
|
||||
static std::string output_message;
|
||||
static std::vector<std::string> symbols_list;
|
||||
static bool show_symbols = false;
|
||||
|
||||
auto patch_file_input = Input(&patch_file, "Assembly patch file (.asm)");
|
||||
|
||||
auto apply_button = Button("Apply Asar Patch", [&] {
|
||||
if (patch_file.empty()) {
|
||||
app_context.error_message = "Please specify an assembly patch file";
|
||||
SwitchComponents(screen, LayoutID::kError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!app_context.rom.is_loaded()) {
|
||||
app_context.error_message = "No ROM loaded. Please load a ROM first.";
|
||||
SwitchComponents(screen, LayoutID::kError);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
app::core::AsarWrapper wrapper;
|
||||
auto init_status = wrapper.Initialize();
|
||||
if (!init_status.ok()) {
|
||||
app_context.error_message = absl::StrCat("Failed to initialize Asar: ", init_status.message());
|
||||
SwitchComponents(screen, LayoutID::kError);
|
||||
return;
|
||||
}
|
||||
|
||||
auto rom_data = app_context.rom.vector();
|
||||
auto patch_result = wrapper.ApplyPatch(patch_file, rom_data);
|
||||
|
||||
if (!patch_result.ok()) {
|
||||
app_context.error_message = absl::StrCat("Patch failed: ", patch_result.status().message());
|
||||
SwitchComponents(screen, LayoutID::kError);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& result = patch_result.value();
|
||||
if (!result.success) {
|
||||
app_context.error_message = absl::StrCat("Patch failed: ", absl::StrJoin(result.errors, "; "));
|
||||
SwitchComponents(screen, LayoutID::kError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update ROM with patched data
|
||||
// Note: ROM update would need proper implementation
|
||||
// For now, just indicate success
|
||||
|
||||
// Prepare success message
|
||||
output_message = absl::StrFormat(
|
||||
"✅ Patch applied successfully!\n"
|
||||
"📊 ROM size: %d bytes\n"
|
||||
"🏷️ Symbols found: %d",
|
||||
result.rom_size, result.symbols.size());
|
||||
|
||||
// Prepare symbols list
|
||||
symbols_list.clear();
|
||||
for (const auto& symbol : result.symbols) {
|
||||
symbols_list.push_back(absl::StrFormat("%-20s @ $%06X",
|
||||
symbol.name, symbol.address));
|
||||
}
|
||||
show_symbols = !symbols_list.empty();
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
app_context.error_message = "Exception: " + std::string(e.what());
|
||||
SwitchComponents(screen, LayoutID::kError);
|
||||
}
|
||||
});
|
||||
|
||||
auto show_symbols_button = Button("Show Symbols", [&] {
|
||||
show_symbols = !show_symbols;
|
||||
});
|
||||
|
||||
auto back_button = Button("Back to Main Menu", [&] {
|
||||
output_message.clear();
|
||||
symbols_list.clear();
|
||||
show_symbols = false;
|
||||
SwitchComponents(screen, LayoutID::kMainMenu);
|
||||
});
|
||||
|
||||
std::vector<Component> container_items = {
|
||||
patch_file_input,
|
||||
apply_button,
|
||||
};
|
||||
|
||||
if (!output_message.empty()) {
|
||||
container_items.push_back(show_symbols_button);
|
||||
}
|
||||
container_items.push_back(back_button);
|
||||
|
||||
auto container = Container::Vertical(container_items);
|
||||
|
||||
auto renderer = Renderer(container, [&] {
|
||||
std::vector<Element> elements = {
|
||||
text("Apply Asar Assembly Patch") | center | bold,
|
||||
separator(),
|
||||
text("Assembly Patch File:"),
|
||||
patch_file_input->Render(),
|
||||
separator(),
|
||||
apply_button->Render() | center,
|
||||
};
|
||||
|
||||
if (!output_message.empty()) {
|
||||
elements.push_back(separator());
|
||||
elements.push_back(text(output_message) | color(Color::Green));
|
||||
elements.push_back(show_symbols_button->Render() | center);
|
||||
|
||||
if (show_symbols && !symbols_list.empty()) {
|
||||
elements.push_back(separator());
|
||||
elements.push_back(text("Extracted Symbols:") | bold);
|
||||
|
||||
// Show symbols in a scrollable area
|
||||
std::vector<Element> symbol_elements;
|
||||
for (size_t i = 0; i < std::min(symbols_list.size(), size_t(10)); ++i) {
|
||||
symbol_elements.push_back(text(symbols_list[i]) | color(Color::Cyan));
|
||||
}
|
||||
if (symbols_list.size() > 10) {
|
||||
symbol_elements.push_back(text(absl::StrFormat("... and %d more",
|
||||
symbols_list.size() - 10)) |
|
||||
color(Color::Yellow));
|
||||
}
|
||||
elements.push_back(vbox(symbol_elements) | frame);
|
||||
}
|
||||
}
|
||||
|
||||
elements.push_back(separator());
|
||||
elements.push_back(back_button->Render() | center);
|
||||
|
||||
return vbox(elements) | center | border;
|
||||
});
|
||||
|
||||
screen.Loop(renderer);
|
||||
}
|
||||
|
||||
void ExtractSymbolsComponent(ftxui::ScreenInteractive &screen) {
|
||||
static std::string asm_file;
|
||||
static std::vector<std::string> symbols_list;
|
||||
static std::string output_message;
|
||||
|
||||
auto asm_file_input = Input(&asm_file, "Assembly file (.asm)");
|
||||
|
||||
auto extract_button = Button("Extract Symbols", [&] {
|
||||
if (asm_file.empty()) {
|
||||
app_context.error_message = "Please specify an assembly file";
|
||||
SwitchComponents(screen, LayoutID::kError);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
app::core::AsarWrapper wrapper;
|
||||
auto init_status = wrapper.Initialize();
|
||||
if (!init_status.ok()) {
|
||||
app_context.error_message = absl::StrCat("Failed to initialize Asar: ", init_status.message());
|
||||
SwitchComponents(screen, LayoutID::kError);
|
||||
return;
|
||||
}
|
||||
|
||||
auto symbols_result = wrapper.ExtractSymbols(asm_file);
|
||||
if (!symbols_result.ok()) {
|
||||
app_context.error_message = absl::StrCat("Symbol extraction failed: ", symbols_result.status().message());
|
||||
SwitchComponents(screen, LayoutID::kError);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& symbols = symbols_result.value();
|
||||
output_message = absl::StrFormat("✅ Extracted %d symbols from %s",
|
||||
symbols.size(), asm_file);
|
||||
|
||||
symbols_list.clear();
|
||||
for (const auto& symbol : symbols) {
|
||||
symbols_list.push_back(absl::StrFormat("%-20s @ $%06X",
|
||||
symbol.name, symbol.address));
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
app_context.error_message = "Exception: " + std::string(e.what());
|
||||
SwitchComponents(screen, LayoutID::kError);
|
||||
}
|
||||
});
|
||||
|
||||
auto back_button = Button("Back to Main Menu", [&] {
|
||||
output_message.clear();
|
||||
symbols_list.clear();
|
||||
SwitchComponents(screen, LayoutID::kMainMenu);
|
||||
});
|
||||
|
||||
auto container = Container::Vertical({
|
||||
asm_file_input,
|
||||
extract_button,
|
||||
back_button,
|
||||
});
|
||||
|
||||
auto renderer = Renderer(container, [&] {
|
||||
std::vector<Element> elements = {
|
||||
text("Extract Assembly Symbols") | center | bold,
|
||||
separator(),
|
||||
text("Assembly File:"),
|
||||
asm_file_input->Render(),
|
||||
separator(),
|
||||
extract_button->Render() | center,
|
||||
};
|
||||
|
||||
if (!output_message.empty()) {
|
||||
elements.push_back(separator());
|
||||
elements.push_back(text(output_message) | color(Color::Green));
|
||||
|
||||
if (!symbols_list.empty()) {
|
||||
elements.push_back(separator());
|
||||
elements.push_back(text("Symbols:") | bold);
|
||||
|
||||
std::vector<Element> symbol_elements;
|
||||
for (const auto& symbol : symbols_list) {
|
||||
symbol_elements.push_back(text(symbol) | color(Color::Cyan));
|
||||
}
|
||||
elements.push_back(vbox(symbol_elements) | frame | size(HEIGHT, LESS_THAN, 15));
|
||||
}
|
||||
}
|
||||
|
||||
elements.push_back(separator());
|
||||
elements.push_back(back_button->Render() | center);
|
||||
|
||||
return vbox(elements) | center | border;
|
||||
});
|
||||
|
||||
screen.Loop(renderer);
|
||||
}
|
||||
|
||||
void ValidateAssemblyComponent(ftxui::ScreenInteractive &screen) {
|
||||
static std::string asm_file;
|
||||
static std::string output_message;
|
||||
static Color output_color = Color::White;
|
||||
|
||||
auto asm_file_input = Input(&asm_file, "Assembly file (.asm)");
|
||||
|
||||
auto validate_button = Button("Validate Assembly", [&] {
|
||||
if (asm_file.empty()) {
|
||||
app_context.error_message = "Please specify an assembly file";
|
||||
SwitchComponents(screen, LayoutID::kError);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
app::core::AsarWrapper wrapper;
|
||||
auto init_status = wrapper.Initialize();
|
||||
if (!init_status.ok()) {
|
||||
app_context.error_message = absl::StrCat("Failed to initialize Asar: ", init_status.message());
|
||||
SwitchComponents(screen, LayoutID::kError);
|
||||
return;
|
||||
}
|
||||
|
||||
auto validation_status = wrapper.ValidateAssembly(asm_file);
|
||||
if (validation_status.ok()) {
|
||||
output_message = "✅ Assembly file is valid!";
|
||||
output_color = Color::Green;
|
||||
} else {
|
||||
output_message = absl::StrCat("❌ Validation failed:\n", validation_status.message());
|
||||
output_color = Color::Red;
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
app_context.error_message = "Exception: " + std::string(e.what());
|
||||
SwitchComponents(screen, LayoutID::kError);
|
||||
}
|
||||
});
|
||||
|
||||
auto back_button = Button("Back to Main Menu", [&] {
|
||||
output_message.clear();
|
||||
SwitchComponents(screen, LayoutID::kMainMenu);
|
||||
});
|
||||
|
||||
auto container = Container::Vertical({
|
||||
asm_file_input,
|
||||
validate_button,
|
||||
back_button,
|
||||
});
|
||||
|
||||
auto renderer = Renderer(container, [&] {
|
||||
std::vector<Element> elements = {
|
||||
text("Validate Assembly File") | center | bold,
|
||||
separator(),
|
||||
text("Assembly File:"),
|
||||
asm_file_input->Render(),
|
||||
separator(),
|
||||
validate_button->Render() | center,
|
||||
};
|
||||
|
||||
if (!output_message.empty()) {
|
||||
elements.push_back(separator());
|
||||
elements.push_back(text(output_message) | color(output_color));
|
||||
}
|
||||
|
||||
elements.push_back(separator());
|
||||
elements.push_back(back_button->Render() | center);
|
||||
|
||||
return vbox(elements) | center | border;
|
||||
});
|
||||
|
||||
screen.Loop(renderer);
|
||||
}
|
||||
|
||||
void LoadRomComponent(ftxui::ScreenInteractive &screen) {
|
||||
static std::string rom_file;
|
||||
auto rom_file_input = Input(&rom_file, "ROM file path");
|
||||
@@ -235,24 +540,36 @@ void LoadRomComponent(ftxui::ScreenInteractive &screen) {
|
||||
SwitchComponents(screen, LayoutID::kMainMenu);
|
||||
});
|
||||
|
||||
auto browse_button = Button("Browse...", [&] {
|
||||
// TODO: Implement file dialog
|
||||
// For now, show a placeholder
|
||||
rom_file = "/path/to/your/rom.sfc";
|
||||
});
|
||||
|
||||
auto back_button =
|
||||
Button("Back", [&] { SwitchComponents(screen, LayoutID::kMainMenu); });
|
||||
|
||||
auto container = Container::Vertical({
|
||||
rom_file_input,
|
||||
Container::Horizontal({rom_file_input, browse_button}),
|
||||
load_button,
|
||||
back_button,
|
||||
});
|
||||
|
||||
auto renderer = Renderer(container, [&] {
|
||||
return vbox({text("Load ROM") | center, separator(),
|
||||
text("Enter ROM File:"), rom_file_input->Render(), separator(),
|
||||
hbox({
|
||||
load_button->Render() | center,
|
||||
separator(),
|
||||
back_button->Render() | center,
|
||||
}) | center}) |
|
||||
center;
|
||||
return vbox({
|
||||
text("Load ROM") | center | bold,
|
||||
separator(),
|
||||
text("Enter ROM File Path:"),
|
||||
hbox({
|
||||
rom_file_input->Render() | flex,
|
||||
separator(),
|
||||
browse_button->Render(),
|
||||
}),
|
||||
separator(),
|
||||
load_button->Render() | center,
|
||||
separator(),
|
||||
back_button->Render() | center,
|
||||
}) | center | border;
|
||||
});
|
||||
|
||||
screen.Loop(renderer);
|
||||
@@ -387,92 +704,126 @@ void PaletteEditorComponent(ftxui::ScreenInteractive &screen) {
|
||||
|
||||
void HelpComponent(ftxui::ScreenInteractive &screen) {
|
||||
auto help_text = vbox({
|
||||
text("z3ed") | bold | color(Color::Yellow),
|
||||
text("z3ed v0.3.0") | bold | color(Color::Yellow),
|
||||
text("by scawful") | color(Color::Magenta),
|
||||
text("The Legend of Zelda: A Link to the Past Hacking Tool") |
|
||||
color(Color::Red),
|
||||
text("Now with Asar 65816 Assembler Integration!") |
|
||||
color(Color::Green),
|
||||
separator(),
|
||||
|
||||
text("🎯 ASAR COMMANDS") | bold | color(Color::Cyan),
|
||||
separator(),
|
||||
hbox({
|
||||
text("Command") | bold | underlined,
|
||||
text("Apply Asar Patch"),
|
||||
filler(),
|
||||
text("Arg") | bold | underlined,
|
||||
text("asar"),
|
||||
filler(),
|
||||
text("Params") | bold | underlined,
|
||||
text("<patch.asm> [--rom=<file>]"),
|
||||
}),
|
||||
hbox({
|
||||
text("Extract Symbols"),
|
||||
filler(),
|
||||
text("extract"),
|
||||
filler(),
|
||||
text("<patch.asm>"),
|
||||
}),
|
||||
hbox({
|
||||
text("Validate Assembly"),
|
||||
filler(),
|
||||
text("validate"),
|
||||
filler(),
|
||||
text("<patch.asm>"),
|
||||
}),
|
||||
|
||||
separator(),
|
||||
text("📦 PATCH COMMANDS") | bold | color(Color::Blue),
|
||||
separator(),
|
||||
hbox({
|
||||
text("Apply BPS Patch"),
|
||||
filler(),
|
||||
text("-a"),
|
||||
text("patch"),
|
||||
filler(),
|
||||
text("<rom_file> <bps_file>"),
|
||||
text("<patch.bps> [--rom=<file>]"),
|
||||
}),
|
||||
hbox({
|
||||
text("Create BPS Patch"),
|
||||
filler(),
|
||||
text("-c"),
|
||||
text("create"),
|
||||
filler(),
|
||||
text("<bps_file> <src_file> <modified_file>"),
|
||||
text("<src_file> <modified_file>"),
|
||||
}),
|
||||
|
||||
separator(),
|
||||
text("🗃️ ROM COMMANDS") | bold | color(Color::Yellow),
|
||||
separator(),
|
||||
hbox({
|
||||
text("Open ROM"),
|
||||
text("Show ROM Info"),
|
||||
filler(),
|
||||
text("-o"),
|
||||
text("info"),
|
||||
filler(),
|
||||
text("<rom_file>"),
|
||||
text("[--rom=<file>]"),
|
||||
}),
|
||||
hbox({
|
||||
text("Backup ROM"),
|
||||
filler(),
|
||||
text("-b"),
|
||||
text("backup"),
|
||||
filler(),
|
||||
text("<rom_file> <optional:new_file>"),
|
||||
text("<rom_file> [backup_name]"),
|
||||
}),
|
||||
hbox({
|
||||
text("Expand ROM"),
|
||||
filler(),
|
||||
text("-x"),
|
||||
text("expand"),
|
||||
filler(),
|
||||
text("<rom_file> <file_size>"),
|
||||
text("<rom_file> <size>"),
|
||||
}),
|
||||
|
||||
separator(),
|
||||
text("🔧 UTILITY COMMANDS") | bold | color(Color::Magenta),
|
||||
separator(),
|
||||
hbox({
|
||||
text("Address Conversion"),
|
||||
filler(),
|
||||
text("convert"),
|
||||
filler(),
|
||||
text("<address> [--to-pc|--to-snes]"),
|
||||
}),
|
||||
hbox({
|
||||
text("Transfer Tile16"),
|
||||
filler(),
|
||||
text("-t"),
|
||||
text("tile16"),
|
||||
filler(),
|
||||
text("<src_rom> <dest_rom> <tile32_id_list:csv>"),
|
||||
text("<src> <dest> <tiles>"),
|
||||
}),
|
||||
|
||||
separator(),
|
||||
text("🌐 GLOBAL FLAGS") | bold | color(Color::White),
|
||||
separator(),
|
||||
hbox({
|
||||
text("Export Graphics"),
|
||||
text("--tui"),
|
||||
filler(),
|
||||
text("-e"),
|
||||
filler(),
|
||||
text("<rom_file> <bin_file>"),
|
||||
text("Launch Text User Interface"),
|
||||
}),
|
||||
hbox({
|
||||
text("Import Graphics"),
|
||||
text("--rom=<file>"),
|
||||
filler(),
|
||||
text("-i"),
|
||||
filler(),
|
||||
text("<bin_file> <rom_file>"),
|
||||
}),
|
||||
separator(),
|
||||
hbox({
|
||||
text("SNES to PC Address"),
|
||||
filler(),
|
||||
text("-s"),
|
||||
filler(),
|
||||
text("<address>"),
|
||||
text("Specify ROM file"),
|
||||
}),
|
||||
hbox({
|
||||
text("PC to SNES Address"),
|
||||
text("--output=<file>"),
|
||||
filler(),
|
||||
text("-p"),
|
||||
text("Specify output file"),
|
||||
}),
|
||||
hbox({
|
||||
text("--verbose"),
|
||||
filler(),
|
||||
text("<address>"),
|
||||
text("Enable verbose output"),
|
||||
}),
|
||||
hbox({
|
||||
text("--dry-run"),
|
||||
filler(),
|
||||
text("Test without changes"),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -515,7 +866,7 @@ void MainMenuComponent(ftxui::ScreenInteractive &screen) {
|
||||
auto title = border(hbox({
|
||||
text("z3ed") | bold | color(Color::Blue1),
|
||||
separator(),
|
||||
text("v0.1.0") | bold | color(Color::Green1),
|
||||
text("v0.3.0") | bold | color(Color::Green1),
|
||||
separator(),
|
||||
text(rom_information) | bold | color(Color::Red1),
|
||||
}));
|
||||
@@ -533,15 +884,24 @@ void MainMenuComponent(ftxui::ScreenInteractive &screen) {
|
||||
auto main_component = CatchEvent(renderer, [&](Event event) {
|
||||
if (event == Event::Return) {
|
||||
switch ((MainMenuEntry)selected) {
|
||||
case MainMenuEntry::kLoadRom:
|
||||
SwitchComponents(screen, LayoutID::kLoadRom);
|
||||
return true;
|
||||
case MainMenuEntry::kApplyAsarPatch:
|
||||
SwitchComponents(screen, LayoutID::kApplyAsarPatch);
|
||||
return true;
|
||||
case MainMenuEntry::kApplyBpsPatch:
|
||||
SwitchComponents(screen, LayoutID::kApplyBpsPatch);
|
||||
return true;
|
||||
case MainMenuEntry::kExtractSymbols:
|
||||
SwitchComponents(screen, LayoutID::kExtractSymbols);
|
||||
return true;
|
||||
case MainMenuEntry::kValidateAssembly:
|
||||
SwitchComponents(screen, LayoutID::kValidateAssembly);
|
||||
return true;
|
||||
case MainMenuEntry::kGenerateSaveFile:
|
||||
SwitchComponents(screen, LayoutID::kGenerateSaveFile);
|
||||
return true;
|
||||
case MainMenuEntry::kLoadRom:
|
||||
SwitchComponents(screen, LayoutID::kLoadRom);
|
||||
return true;
|
||||
case MainMenuEntry::kPaletteEditor:
|
||||
SwitchComponents(screen, LayoutID::kPaletteEditor);
|
||||
return true;
|
||||
@@ -576,9 +936,18 @@ void ShowMain() {
|
||||
case LayoutID::kLoadRom: {
|
||||
LoadRomComponent(screen);
|
||||
} break;
|
||||
case LayoutID::kApplyAsarPatch: {
|
||||
ApplyAsarPatchComponent(screen);
|
||||
} break;
|
||||
case LayoutID::kApplyBpsPatch: {
|
||||
ApplyBpsPatchComponent(screen);
|
||||
} break;
|
||||
case LayoutID::kExtractSymbols: {
|
||||
ExtractSymbolsComponent(screen);
|
||||
} break;
|
||||
case LayoutID::kValidateAssembly: {
|
||||
ValidateAssemblyComponent(screen);
|
||||
} break;
|
||||
case LayoutID::kGenerateSaveFile: {
|
||||
GenerateSaveFileComponent(screen);
|
||||
} break;
|
||||
@@ -596,10 +965,13 @@ void ShowMain() {
|
||||
});
|
||||
|
||||
auto error_renderer = Renderer(error_button, [&] {
|
||||
return vbox({text("Error") | center, separator(),
|
||||
text(app_context.error_message), separator(),
|
||||
error_button->Render() | center}) |
|
||||
center;
|
||||
return vbox({
|
||||
text("Error") | center | bold | color(Color::Red),
|
||||
separator(),
|
||||
text(app_context.error_message) | color(Color::Yellow),
|
||||
separator(),
|
||||
error_button->Render() | center
|
||||
}) | center | border;
|
||||
});
|
||||
|
||||
screen.Loop(error_renderer);
|
||||
|
||||
@@ -17,7 +17,10 @@ namespace yaze {
|
||||
namespace cli {
|
||||
const std::vector<std::string> kMainMenuEntries = {
|
||||
"Load ROM",
|
||||
"Apply BPS Patch",
|
||||
"Apply Asar Patch",
|
||||
"Apply BPS Patch",
|
||||
"Extract Symbols",
|
||||
"Validate Assembly",
|
||||
"Generate Save File",
|
||||
"Palette Editor",
|
||||
"Help",
|
||||
@@ -26,7 +29,10 @@ const std::vector<std::string> kMainMenuEntries = {
|
||||
|
||||
enum class MainMenuEntry {
|
||||
kLoadRom,
|
||||
kApplyAsarPatch,
|
||||
kApplyBpsPatch,
|
||||
kExtractSymbols,
|
||||
kValidateAssembly,
|
||||
kGenerateSaveFile,
|
||||
kPaletteEditor,
|
||||
kHelp,
|
||||
@@ -35,7 +41,10 @@ enum class MainMenuEntry {
|
||||
|
||||
enum LayoutID {
|
||||
kLoadRom,
|
||||
kApplyAsarPatch,
|
||||
kApplyBpsPatch,
|
||||
kExtractSymbols,
|
||||
kValidateAssembly,
|
||||
kGenerateSaveFile,
|
||||
kPaletteEditor,
|
||||
kHelp,
|
||||
|
||||
@@ -13,13 +13,14 @@ endif()
|
||||
|
||||
add_executable(
|
||||
z3ed
|
||||
cli/z3ed.cc
|
||||
cli/cli_main.cc
|
||||
cli/tui.cc
|
||||
cli/handlers/compress.cc
|
||||
cli/handlers/patch.cc
|
||||
cli/handlers/tile16_transfer.cc
|
||||
app/rom.cc
|
||||
app/core/project.cc
|
||||
app/core/asar_wrapper.cc
|
||||
app/core/platform/file_dialog.mm
|
||||
app/core/platform/file_dialog.cc
|
||||
${YAZE_APP_EMU_SRC}
|
||||
@@ -28,14 +29,13 @@ add_executable(
|
||||
${YAZE_UTIL_SRC}
|
||||
${YAZE_GUI_SRC}
|
||||
${IMGUI_SRC}
|
||||
${ASAR_STATIC_SRC}
|
||||
)
|
||||
|
||||
target_include_directories(
|
||||
z3ed PUBLIC
|
||||
lib/
|
||||
app/
|
||||
${ASAR_INCLUDE_DIR}
|
||||
${ASAR_INCLUDE_DIRS}
|
||||
${CMAKE_SOURCE_DIR}/incl/
|
||||
${CMAKE_SOURCE_DIR}/src/
|
||||
${PNG_INCLUDE_DIRS}
|
||||
@@ -50,6 +50,8 @@ target_link_libraries(
|
||||
ftxui::component
|
||||
ftxui::screen
|
||||
ftxui::dom
|
||||
absl::flags
|
||||
absl::flags_parse
|
||||
${ABSL_TARGETS}
|
||||
${SDL_TARGETS}
|
||||
${PNG_LIBRARIES}
|
||||
|
||||
Reference in New Issue
Block a user