From 7fee38be551424c0ce79a90c49cd6e19b0330336 Mon Sep 17 00:00:00 2001 From: modeco80 Date: Wed, 15 Jan 2025 18:43:33 -0500 Subject: [PATCH] tools/eupak: Heavily refactor tasks. This starts the beginning of probably one of the bigger refactor jobs, and one I've been hinting at and FIXME jabbing for a while now. Basically, tasks now are a lot closer coupled to their arguments. This is a good thing since it means that changing or adding features only requires modifying the task, not needlessly modifying main over and over. CreateTask is the only one that works currently for this commit and has been converted to the new ITask model. --- src/tools/eupak/CMakeLists.txt | 1 + src/tools/eupak/main.cpp | 86 +++++----------------- src/tools/eupak/tasks/CreateTask.cpp | 106 ++++++++++++++++++++++++++- src/tools/eupak/tasks/CreateTask.hpp | 40 +++++++--- src/tools/eupak/tasks/InfoTask.hpp | 2 + src/tools/eupak/tasks/Task.cpp | 17 +++++ src/tools/eupak/tasks/Task.hpp | 66 +++++++++++++++++ 7 files changed, 238 insertions(+), 80 deletions(-) create mode 100644 src/tools/eupak/tasks/Task.cpp create mode 100644 src/tools/eupak/tasks/Task.hpp diff --git a/src/tools/eupak/CMakeLists.txt b/src/tools/eupak/CMakeLists.txt index b4b603e..da2a62b 100644 --- a/src/tools/eupak/CMakeLists.txt +++ b/src/tools/eupak/CMakeLists.txt @@ -12,6 +12,7 @@ add_executable(eupak Utils.cpp # Tasks + tasks/Task.cpp tasks/InfoTask.cpp tasks/CreateTask.cpp tasks/ExtractTask.cpp diff --git a/src/tools/eupak/main.cpp b/src/tools/eupak/main.cpp index 5686645..485a25d 100644 --- a/src/tools/eupak/main.cpp +++ b/src/tools/eupak/main.cpp @@ -13,6 +13,7 @@ #include #include "europa/structs/Pak.hpp" +#include "tasks/Task.hpp" int main(int argc, char** argv) { // FIXME: At some point we should just have task classes register their arguments @@ -50,35 +51,12 @@ int main(int argc, char** argv) { .default_value(false) .implicit_value(true); - argparse::ArgumentParser createParser("create", EUPAK_VERSION_STR, argparse::default_arguments::help); - createParser.add_description("Create a package file."); - createParser.add_argument("-d", "--directory") - .required() - .metavar("DIRECTORY") - .help("Directory to create archive from"); - - createParser.add_argument("-V", "--archive-version") - .default_value("starfighter") - .help(R"(Output archive version. Either "pmdl", "starfighter" or "jedistarfighter".)") - .metavar("VERSION"); - - createParser.add_argument("-s", "--sector-aligned") - .help(R"(Aligns all files in this new package to CD-ROM sector boundaries. Only valid for -V jedistarfighter.)") - .flag(); - - createParser.add_argument("output") - .required() - .help("Output archive") - .metavar("ARCHIVE"); - - createParser.add_argument("--verbose") - .help("Increase creation output verbosity") - .default_value(false) - .implicit_value(true); - parser.add_subparser(infoParser); parser.add_subparser(extractParser); - parser.add_subparser(createParser); + + auto tasks = std::vector { + eupak::tasks::TaskFactory::CreateNamed("create", parser) + }; try { // No command was specified, display the help and then exit with a failure code. @@ -100,6 +78,17 @@ int main(int argc, char** argv) { // Run the given task + for(auto task : tasks) { + if(task->ShouldRun(parser)) { + if(auto res = task->Parse(); res != 0) + return res; + + return task->Run(); + } + } + +#if 0 + if(parser.is_subcommand_used("extract")) { eupak::tasks::ExtractTask task; eupak::tasks::ExtractTask::Arguments args; @@ -132,52 +121,15 @@ int main(int argc, char** argv) { return task.Run(std::move(args)); } - // FIXME: At some point for accurate rebuilds we should also accept a JSON manifest file - // that contains: Package version, sector alignment, package build time, order of all files (as original) and their modtime, so on. - // Then a user can just do `eupak create --manifest manifest.json` and it'll all be figured out - // (I have not dreamt up the schema for this yet and this relies on other FIXMEs being done so this will have to wait.) + if(parser.is_subcommand_used("create")) { eupak::tasks::CreateTask task; eupak::tasks::CreateTask::Arguments args; - args.verbose = createParser.get("--verbose"); - args.inputDirectory = eupak::fs::path(createParser.get("--directory")); - args.outputFile = eupak::fs::path(createParser.get("output")); - - if(createParser.is_used("--archive-version")) { - const auto& versionStr = createParser.get("--archive-version"); - - if(versionStr == "pmdl") { - args.pakVersion = europa::structs::PakVersion::Ver3; - } else if(versionStr == "starfighter") { - args.pakVersion = europa::structs::PakVersion::Ver4; - } else if(versionStr == "jedistarfighter") { - args.pakVersion = europa::structs::PakVersion::Ver5; - } else { - std::cout << "Error: Invalid version \"" << versionStr << "\"\n" - << createParser; - return 1; - } - } else { - args.pakVersion = europa::structs::PakVersion::Ver4; - } - - args.sectorAligned = createParser.get("--sector-aligned"); - - if(args.sectorAligned && args.pakVersion != eupak::estructs::PakVersion::Ver5) { - std::cout << "Error: --sector-aligned is only valid for creating a package with \"-V jedistarfighter\".\n" - << createParser; - return 1; - } - - if(!eupak::fs::is_directory(args.inputDirectory)) { - std::cout << "Error: Provided input isn't a directory\n" - << createParser; - return 1; - } - + return task.Run(std::move(args)); } +#endif return 0; } diff --git a/src/tools/eupak/tasks/CreateTask.cpp b/src/tools/eupak/tasks/CreateTask.cpp index 37b4dd1..7982950 100644 --- a/src/tools/eupak/tasks/CreateTask.cpp +++ b/src/tools/eupak/tasks/CreateTask.cpp @@ -7,6 +7,7 @@ // #include +#include #include #include #include @@ -15,7 +16,10 @@ #include #include +#include "argparse/argparse.hpp" #include "europa/io/PakFile.hpp" +#include "europa/structs/Pak.hpp" +#include "tasks/Task.hpp" namespace eupak::tasks { @@ -77,7 +81,105 @@ namespace eupak::tasks { }; }; - int CreateTask::Run(Arguments&& args) { + std::optional ParsePakVersion(const std::string& str) { + // FIXME: PMDL should be "starfighter-prerelease" + if(str == "pmdl") { + return estructs::PakVersion::Ver3; + } else if(str == "starfighter") { + return estructs::PakVersion::Ver4; + } else if(str == "jedistarfighter") { + return estructs::PakVersion::Ver5; + } + + return std::nullopt; + } + + CreateTask::CreateTask() + : parser("create", EUPAK_VERSION_STR, argparse::default_arguments::help) { + // Setup argparse + // clang-format off + parser.add_description("Create a package file."); + parser.add_argument("-d", "--directory") + .required() + .metavar("DIRECTORY") + .help("Directory to create archive from"); + + parser.add_argument("-V", "--archive-version") + .default_value("starfighter") + .help(R"(Output archive version. Either "pmdl", "starfighter" or "jedistarfighter".)") + .metavar("VERSION"); + + parser.add_argument("-s", "--sector-aligned") + .help(R"(Aligns all files in this new package to CD-ROM sector boundaries. Only valid for -V jedistarfighter.)") + .flag(); + + parser.add_argument("output") + .required() + .help("Output archive") + .metavar("ARCHIVE"); + + parser.add_argument("--verbose") + .help("Increase creation output verbosity") + .default_value(false) + .implicit_value(true); + + // FIXME: At some point for accurate rebuilds we should also accept a JSON manifest file + // that contains: Package version, sector alignment, package build time, order of all files (as original) and their modtime, so on. + // Then a user can just do `eupak create --manifest manifest.json` and it'll all be figured out + // (I have not dreamt up the schema for this yet and this relies on other FIXMEs being done so this will have to wait.) + + // clang-format on + } + + void CreateTask::Init(argparse::ArgumentParser& parentParser) { + parentParser.add_subparser(parser); + } + + bool CreateTask::ShouldRun(argparse::ArgumentParser& parentParser) const { + return parentParser.is_subcommand_used("create"); + } + + int CreateTask::Parse() { + auto& args = currentArgs; + + args.verbose = parser.get("--verbose"); + args.inputDirectory = eupak::fs::path(parser.get("--directory")); + args.outputFile = eupak::fs::path(parser.get("output")); + + if(parser.is_used("--archive-version")) { + const auto& versionStr = parser.get("--archive-version"); + + if(auto opt = ParsePakVersion(versionStr); opt.has_value()) { + } else { + std::cout << "Error: Invalid version \"" << versionStr << "\"\n" + << parser; + return 1; + } + } else { + args.pakVersion = europa::structs::PakVersion::Ver4; + } + + args.sectorAligned = parser.get("--sector-aligned"); + + if(args.sectorAligned && args.pakVersion != eupak::estructs::PakVersion::Ver5) { + std::cout << "Error: --sector-aligned is only valid for creating a package with \"-V jedistarfighter\".\n" + << parser; + return 1; + } + + if(!eupak::fs::is_directory(args.inputDirectory)) { + std::cout << "Error: Provided input isn't a directory\n" + << parser; + return 1; + } + + return 0; + } + + int CreateTask::Run() { + // we should not be modifying arguments past this point + const auto& args = currentArgs; + auto currFile = 0; auto fileCount = 0; @@ -171,4 +273,6 @@ namespace eupak::tasks { return 0; } + EUPAK_REGISTER_TASK("create", CreateTask); + } // namespace eupak::tasks \ No newline at end of file diff --git a/src/tools/eupak/tasks/CreateTask.hpp b/src/tools/eupak/tasks/CreateTask.hpp index d5eb7a0..a42b857 100644 --- a/src/tools/eupak/tasks/CreateTask.hpp +++ b/src/tools/eupak/tasks/CreateTask.hpp @@ -10,25 +10,41 @@ #define EUROPA_EUPAK_TASKS_CREATETASK_HPP #include - #include +#include +#include "argparse/argparse.hpp" namespace eupak::tasks { - struct CreateTask { - struct Arguments { - fs::path inputDirectory; - fs::path outputFile; + struct CreateTask : ITask { + /// Arguments. + /// This is no longer directly used by clients + /// and should later be factored out of surface. + struct Arguments { + fs::path inputDirectory; + fs::path outputFile; - bool verbose; - europa::structs::PakVersion pakVersion; - bool sectorAligned; - }; - - int Run(Arguments&& args); + bool verbose; + europa::structs::PakVersion pakVersion; + bool sectorAligned; }; -} // namespace europa + CreateTask(); + + void Init(argparse::ArgumentParser& parentParser) override; + + bool ShouldRun(argparse::ArgumentParser& parentParser) const override; + + int Parse() override; + + int Run() override; + + private: + argparse::ArgumentParser parser; + Arguments currentArgs; + }; + +} // namespace eupak::tasks #endif // EUROPA_EUPAK_TASKS_CREATETASK_HPP diff --git a/src/tools/eupak/tasks/InfoTask.hpp b/src/tools/eupak/tasks/InfoTask.hpp index 5d6ff74..6021dce 100644 --- a/src/tools/eupak/tasks/InfoTask.hpp +++ b/src/tools/eupak/tasks/InfoTask.hpp @@ -20,6 +20,8 @@ namespace eupak::tasks { bool verbose; }; + + int Run(Arguments&& args); }; diff --git a/src/tools/eupak/tasks/Task.cpp b/src/tools/eupak/tasks/Task.cpp new file mode 100644 index 0000000..1ec511d --- /dev/null +++ b/src/tools/eupak/tasks/Task.cpp @@ -0,0 +1,17 @@ +#pragma once +#include "Task.hpp" +#include + +namespace eupak::tasks { + + std::shared_ptr TaskFactory::CreateNamed(const std::string& name, argparse::ArgumentParser& parentParser) { + const auto& m = factoryMap(); + if(m.contains(name)) { + auto task = m.at(name)(); + task->Init(parentParser); + return task; + } else + throw std::runtime_error("Invalid task factory creation request"); + } + +} // namespace eupak::tasks \ No newline at end of file diff --git a/src/tools/eupak/tasks/Task.hpp b/src/tools/eupak/tasks/Task.hpp new file mode 100644 index 0000000..0aaa5e7 --- /dev/null +++ b/src/tools/eupak/tasks/Task.hpp @@ -0,0 +1,66 @@ +// +// EuropaTools +// +// (C) 2021-2025 modeco80 +// +// SPDX-License-Identifier: MIT +// + +#pragma once +#include +#include +#include + +#include "argparse/argparse.hpp" + +namespace eupak::tasks { + + /// Base-class for all eupak tasks. + struct ITask { + virtual ~ITask() = default; + + /// Do any creation-time initalizatiion. + virtual void Init(argparse::ArgumentParser& parentParser) = 0; + + /// Query if this task has been selected + virtual bool ShouldRun(argparse::ArgumentParser& parentParser) const = 0; + + /// Parse arguments from the user + virtual int Parse() = 0; + + /// Run the task. + virtual int Run() = 0; + }; + + /// Creates tasks. + struct TaskFactory { + using FactoryMethod = std::shared_ptr (*)(); + + /// Creates a task. + static std::shared_ptr CreateNamed(const std::string& name, argparse::ArgumentParser& parentParser); + + private: + template + friend struct TaskFactoryRegister; + + static auto& factoryMap() { + static std::unordered_map m; + return m; + } + }; + + template + struct TaskFactoryRegister { + TaskFactoryRegister(const std::string& name) { + static_assert(std::is_base_of_v, "cannot register a type which does not derive from ITask"); + TaskFactory::factoryMap().insert({ name, + []() -> std::shared_ptr { + return std::make_shared(); + } }); + } + }; + +#define EUPAK_REGISTER_TASK(Name, TTask) \ + static ::eupak::tasks::TaskFactoryRegister __register__##TTask(Name) + +} // namespace eupak::tasks \ No newline at end of file