#include #include #include #include #include #include #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 "absl/strings/str_cat.h" #include "cli/z3ed.h" #include "cli/tui.h" #include "app/core/asar_wrapper.h" #include "app/gfx/arena.h" #include "app/rom.h" #include "app/zelda3/overworld/overworld.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&)> handler; }; class ModernCLI { public: ModernCLI() { SetupCommands(); } void SetupCommands() { commands_["asar"] = { .name = "asar", .description = "Apply Asar 65816 assembly patch to ROM", .usage = "z3ed asar [--rom=] [--output=]", .handler = [this](const std::vector& args) -> absl::Status { return HandleAsarCommand(args); } }; commands_["patch"] = { .name = "patch", .description = "Apply BPS patch to ROM", .usage = "z3ed patch [--rom=] [--output=]", .handler = [this](const std::vector& args) -> absl::Status { return HandlePatchCommand(args); } }; commands_["extract"] = { .name = "extract", .description = "Extract symbols from assembly file", .usage = "z3ed extract ", .handler = [this](const std::vector& args) -> absl::Status { return HandleExtractCommand(args); } }; commands_["validate"] = { .name = "validate", .description = "Validate assembly file syntax", .usage = "z3ed validate ", .handler = [this](const std::vector& args) -> absl::Status { return HandleValidateCommand(args); } }; commands_["info"] = { .name = "info", .description = "Show ROM information", .usage = "z3ed info [--rom=]", .handler = [this](const std::vector& args) -> absl::Status { return HandleInfoCommand(args); } }; commands_["convert"] = { .name = "convert", .description = "Convert between SNES and PC addresses", .usage = "z3ed convert
[--to-pc|--to-snes]", .handler = [this](const std::vector& args) -> absl::Status { return HandleConvertCommand(args); } }; commands_["test"] = { .name = "test", .description = "Run comprehensive asset loading tests on ROM", .usage = "z3ed test [--rom=] [--graphics] [--overworld] [--dungeons]", .handler = [this](const std::vector& args) -> absl::Status { return HandleTestCommand(args); } }; commands_["help"] = { .name = "help", .description = "Show help information", .usage = "z3ed help [command]", .handler = [this](const std::vector& args) -> absl::Status { return HandleHelpCommand(args); } }; } void ShowVersion() { std::cout << "z3ed v0.3.1 - 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= Specify ROM file to use" << std::endl; std::cout << " --output= Specify output file path" << std::endl; std::cout << " --dry-run Perform operations without making changes" << std::endl; std::cout << " --backup= 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 " << std::endl; } absl::Status RunCommand(const std::string& command, const std::vector& 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 commands_; absl::Status HandleAsarCommand(const std::vector& args) { if (args.empty()) { return absl::InvalidArgumentError("Asar command requires a patch file"); } AsarPatch handler; std::vector 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& args) { if (args.empty()) { return absl::InvalidArgumentError("Patch command requires a BPS file"); } ApplyPatch handler; std::vector 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& 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& 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& 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= or provide as argument)"); } Open handler; return handler.Run({rom_file}); } absl::Status HandleConvertCommand(const std::vector& 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 HandleTestCommand(const std::vector& args) { std::string rom_file = absl::GetFlag(FLAGS_rom); if (args.size() > 0 && args[0].find("--rom=") == 0) { rom_file = args[0].substr(6); } if (rom_file.empty()) { rom_file = "zelda3.sfc"; // Default ROM file } std::cout << "πŸ§ͺ YAZE Asset Loading Test Suite" << std::endl; std::cout << "ROM: " << rom_file << std::endl; std::cout << "=================================" << std::endl; // Initialize SDL for graphics tests if (SDL_Init(SDL_INIT_VIDEO) != 0) { return absl::InternalError(absl::StrCat("Failed to initialize SDL: ", SDL_GetError())); } int tests_passed = 0; int tests_total = 0; // Test 1: ROM Loading std::cout << "πŸ“ Testing ROM loading..." << std::flush; tests_total++; Rom test_rom; auto status = test_rom.LoadFromFile(rom_file); if (status.ok()) { std::cout << " βœ… PASSED" << std::endl; tests_passed++; std::cout << " Title: " << test_rom.title() << std::endl; std::cout << " Size: " << test_rom.size() << " bytes" << std::endl; } else { std::cout << " ❌ FAILED: " << status.message() << std::endl; SDL_Quit(); return status; } // Test 2: Graphics Arena Resource Tracking std::cout << "🎨 Testing graphics arena..." << std::flush; tests_total++; try { auto& arena = gfx::Arena::Get(); size_t initial_textures = arena.GetTextureCount(); size_t initial_surfaces = arena.GetSurfaceCount(); std::cout << " βœ… PASSED" << std::endl; std::cout << " Initial textures: " << initial_textures << std::endl; std::cout << " Initial surfaces: " << initial_surfaces << std::endl; tests_passed++; } catch (const std::exception& e) { std::cout << " ❌ FAILED: " << e.what() << std::endl; } // Test 3: Graphics Data Loading bool test_graphics = true; for (const auto& arg : args) { if (arg == "--no-graphics") test_graphics = false; } if (test_graphics) { std::cout << "πŸ–ΌοΈ Testing graphics data loading..." << std::flush; tests_total++; try { auto graphics_result = LoadAllGraphicsData(test_rom); if (graphics_result.ok()) { std::cout << " βœ… PASSED" << std::endl; std::cout << " Loaded " << graphics_result.value().size() << " graphics sheets" << std::endl; tests_passed++; } else { std::cout << " ❌ FAILED: " << graphics_result.status().message() << std::endl; } } catch (const std::exception& e) { std::cout << " ❌ FAILED: " << e.what() << std::endl; } } // Test 4: Overworld Loading bool test_overworld = true; for (const auto& arg : args) { if (arg == "--no-overworld") test_overworld = false; } if (test_overworld) { std::cout << "πŸ—ΊοΈ Testing overworld loading..." << std::flush; tests_total++; try { zelda3::Overworld overworld(&test_rom); auto ow_status = overworld.Load(&test_rom); if (ow_status.ok()) { std::cout << " βœ… PASSED" << std::endl; std::cout << " Loaded overworld data successfully" << std::endl; tests_passed++; } else { std::cout << " ❌ FAILED: " << ow_status.message() << std::endl; } } catch (const std::exception& e) { std::cout << " ❌ FAILED: " << e.what() << std::endl; } } // Test 5: Arena Shutdown Test std::cout << "πŸ”„ Testing arena shutdown..." << std::flush; tests_total++; try { auto& arena = gfx::Arena::Get(); size_t final_textures = arena.GetTextureCount(); size_t final_surfaces = arena.GetSurfaceCount(); // Test the shutdown method (this should not crash) arena.Shutdown(); std::cout << " βœ… PASSED" << std::endl; std::cout << " Final textures: " << final_textures << std::endl; std::cout << " Final surfaces: " << final_surfaces << std::endl; tests_passed++; } catch (const std::exception& e) { std::cout << " ❌ FAILED: " << e.what() << std::endl; } // Cleanup SDL_Quit(); // Summary std::cout << "=================================" << std::endl; std::cout << "πŸ“Š Test Results: " << tests_passed << "/" << tests_total << " passed" << std::endl; if (tests_passed == tests_total) { std::cout << "πŸŽ‰ All tests passed!" << std::endl; return absl::OkStatus(); } else { std::cout << "❌ Some tests failed." << std::endl; return absl::InternalError("Test failures detected"); } } absl::Status HandleHelpCommand(const std::vector& 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 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; }