From 8d5a8d4adc458bc703cbb9ba69b2852c9f151a10 Mon Sep 17 00:00:00 2001 From: modeco80 Date: Fri, 17 Jan 2025 17:58:08 -0500 Subject: [PATCH] tools/eupak: Refactor to use toollib This also flattens out eupak's source code a bit. Idk if i will keep that, but we don't need headers anymore. --- src/tools/eupak/CMakeLists.txt | 12 +- src/tools/eupak/CreateCommand.cpp | 298 +++++++++++++++++++++++++ src/tools/eupak/ExtractCommand.cpp | 167 ++++++++++++++ src/tools/eupak/InfoCommand.cpp | 146 ++++++++++++ src/tools/eupak/main.cpp | 57 ++--- src/tools/eupak/tasks/CreateTask.cpp | 282 ----------------------- src/tools/eupak/tasks/CreateTask.hpp | 46 ---- src/tools/eupak/tasks/ExtractTask.cpp | 157 ------------- src/tools/eupak/tasks/ExtractTask.hpp | 41 ---- src/tools/eupak/tasks/InfoTask.cpp | 134 ----------- src/tools/eupak/tasks/InfoTask.hpp | 40 ---- src/tools/eupak/tasks/Task.cpp | 25 --- src/tools/eupak/tasks/Task.hpp | 68 ------ src/tools/toollib/README.md | 3 + src/tools/toollib/toollib/ToolMain.hpp | 3 +- 15 files changed, 639 insertions(+), 840 deletions(-) create mode 100644 src/tools/eupak/CreateCommand.cpp create mode 100644 src/tools/eupak/ExtractCommand.cpp create mode 100644 src/tools/eupak/InfoCommand.cpp delete mode 100644 src/tools/eupak/tasks/CreateTask.cpp delete mode 100644 src/tools/eupak/tasks/CreateTask.hpp delete mode 100644 src/tools/eupak/tasks/ExtractTask.cpp delete mode 100644 src/tools/eupak/tasks/ExtractTask.hpp delete mode 100644 src/tools/eupak/tasks/InfoTask.cpp delete mode 100644 src/tools/eupak/tasks/InfoTask.hpp delete mode 100644 src/tools/eupak/tasks/Task.cpp delete mode 100644 src/tools/eupak/tasks/Task.hpp create mode 100644 src/tools/toollib/README.md diff --git a/src/tools/eupak/CMakeLists.txt b/src/tools/eupak/CMakeLists.txt index da2a62b..066446d 100644 --- a/src/tools/eupak/CMakeLists.txt +++ b/src/tools/eupak/CMakeLists.txt @@ -8,19 +8,17 @@ add_executable(eupak main.cpp - Utils.cpp - # Tasks - tasks/Task.cpp - tasks/InfoTask.cpp - tasks/CreateTask.cpp - tasks/ExtractTask.cpp + # eupak commands + CreateCommand.cpp + InfoCommand.cpp + ExtractCommand.cpp ) target_link_libraries(eupak PUBLIC europa - argparse::argparse + toollib indicators::indicators ) diff --git a/src/tools/eupak/CreateCommand.cpp b/src/tools/eupak/CreateCommand.cpp new file mode 100644 index 0000000..4a17ff3 --- /dev/null +++ b/src/tools/eupak/CreateCommand.cpp @@ -0,0 +1,298 @@ +// +// EuropaTools +// +// (C) 2021-2025 modeco80 +// +// SPDX-License-Identifier: MIT +// + +#include + +// Common stuff +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "europa/structs/Pak.hpp" + +namespace eupak { + + struct CreateArchiveReportSink : public eio::pak::WriterProgressReportSink { + CreateArchiveReportSink(int fileCount = 0) + : eio::pak::WriterProgressReportSink() { + indicators::show_console_cursor(false); + progress.set_option(indicators::option::MaxProgress { fileCount }); + } + + ~CreateArchiveReportSink() { + indicators::show_console_cursor(true); + } + + void OnEvent(const PakEvent& event) override { + using enum PakEvent::EventCode; + switch(event.eventCode) { + case WritingHeader: + progress.set_option(indicators::option::PostfixText { "Writing header" }); + progress.print_progress(); + break; + + case FillInHeader: + progress.set_option(indicators::option::PostfixText { "Filling in header" }); + progress.print_progress(); + break; + + case WritingToc: + progress.set_option(indicators::option::PostfixText { "Writing TOC" }); + progress.print_progress(); + break; + } + } + + void OnEvent(const FileEvent& event) override { + using enum FileEvent::EventCode; + switch(event.eventCode) { + case FileWriteBegin: + progress.set_option(indicators::option::PostfixText { "Writing " + event.targetFileName }); + progress.print_progress(); + break; + + case FileWriteEnd: + progress.set_option(indicators::option::PostfixText { "Written " + event.targetFileName }); + progress.tick(); + break; + } + } + + private: + indicators::ProgressBar progress { + indicators::option::BarWidth { 50 }, + indicators::option::ForegroundColor { indicators::Color::yellow }, + indicators::option::ShowPercentage { true }, + indicators::option::ShowElapsedTime { true }, + indicators::option::ShowRemainingTime { true }, + + indicators::option::PrefixText { "Writing archive " } + }; + }; + + std::optional ParsePakVersion(const std::string& str) { + if(str == "europa-prerelease") { + return estructs::PakVersion::Ver3; + } else if(str == "starfighter") { + return estructs::PakVersion::Ver4; + } else if(str == "jedistarfighter") { + return estructs::PakVersion::Ver5; + } + + return std::nullopt; + } + + struct CreateCommand : tool::IToolCommand { + CreateCommand() + : 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 "europa-prerelease", "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 bit-accurate rebuilds we should also accept a JSON manifest file + // that contains: + // - Package version, + // - sector alignment (for v5), + // - package build time, + // - data order of all files + // - TOC order of all files + // - file TOC data (modtime, TOC index, so on) + // Then a user can just do `eupak create --manifest manifest.json` and it'll all be done for them + // + // `eupak extract` should optionally generate this manifest for the user + // (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 Init(argparse::ArgumentParser& parentParser) override { + parentParser.add_subparser(parser); + } + + bool ShouldRun(argparse::ArgumentParser& parentParser) const override { + return parentParser.is_subcommand_used("create"); + } + + int Parse() override { + currentArgs.verbose = parser.get("--verbose"); + currentArgs.inputDirectory = fs::path(parser.get("--directory")); + currentArgs.outputFile = 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()) { + currentArgs.pakVersion = *opt; + } else { + std::cout << "Error: Invalid version \"" << versionStr << "\"\n" + << parser; + return 1; + } + } else { + currentArgs.pakVersion = estructs::PakVersion::Ver4; + } + + currentArgs.sectorAligned = parser.get("--sector-aligned"); + + if(currentArgs.sectorAligned && currentArgs.pakVersion != 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(currentArgs.inputDirectory)) { + std::cout << "Error: Provided input isn't a directory\n" + << parser; + return 1; + } + + return 0; + } + + int Run() override { + auto currFile = 0; + auto fileCount = 0; + + // Count how many files we're gonna add to the archive + for(auto& ent : fs::recursive_directory_iterator(currentArgs.inputDirectory)) { + if(ent.is_directory()) + continue; + fileCount++; + } + + std::cout << "Going to write " << fileCount << " files into " << currentArgs.outputFile << '\n'; + + if(currentArgs.sectorAligned) { + std::cout << "Writing a sector aligned package\n"; + } + + indicators::ProgressBar progress { + indicators::option::BarWidth { 50 }, + indicators::option::ForegroundColor { indicators::Color::green }, + indicators::option::MaxProgress { fileCount }, + indicators::option::ShowPercentage { true }, + indicators::option::ShowElapsedTime { true }, + indicators::option::ShowRemainingTime { true }, + + indicators::option::PrefixText { "Adding files to archive " } + }; + + indicators::show_console_cursor(false); + + // TODO: use time to write in the header + // also: is there any point to verbosity? could add archive written size ig + + std::vector files; + files.reserve(fileCount); + + for(auto& ent : fs::recursive_directory_iterator(currentArgs.inputDirectory)) { + if(ent.is_directory()) + continue; + + auto relativePathName = fs::relative(ent.path(), currentArgs.inputDirectory).string(); + auto lastModified = fs::last_write_time(ent.path()); + + // Convert to Windows path separator always (that's what the game wants, after all) + for(auto& c : relativePathName) + if(c == '/') + c = '\\'; + + progress.set_option(indicators::option::PostfixText { relativePathName + " (" + std::to_string(currFile + 1) + '/' + std::to_string(fileCount) + ")" }); + + eio::pak::File file; + eio::pak::FileData pakData = eio::pak::FileData::InitAsPath(ent.path()); + + file.InitAs(currentArgs.pakVersion, currentArgs.sectorAligned); + + // Add data + file.SetData(std::move(pakData)); + + // Setup other stuff like modtime + file.VisitTocEntry([&](auto& tocEntry) { + // Kinda stupid but works + auto sys = std::chrono::file_clock::to_sys(lastModified); + auto seconds = std::chrono::time_point_cast(sys); + tocEntry.creationUnixTime = static_cast(seconds.time_since_epoch().count()); + }); + + files.emplace_back(std::make_pair(relativePathName, std::move(file))); + progress.tick(); + currFile++; + } + + indicators::show_console_cursor(true); + + std::ofstream ofs(currentArgs.outputFile.string(), std::ofstream::binary); + + if(!ofs) { + std::cout << "Error: Couldn't open " << currentArgs.outputFile << " for writing\n"; + return 1; + } + + CreateArchiveReportSink reportSink(fileCount); + eio::pak::Writer writer(currentArgs.pakVersion); + + using enum eio::pak::Writer::SectorAlignment; + + eio::pak::Writer::SectorAlignment alignment = DoNotAlign; + + if(currentArgs.sectorAligned) + alignment = Align; + + writer.Write(ofs, std::move(files), reportSink, alignment); + return 0; + } + + private: + struct Arguments { + fs::path inputDirectory; + fs::path outputFile; + + bool verbose; + europa::structs::PakVersion pakVersion; + bool sectorAligned; + }; + + argparse::ArgumentParser parser; + Arguments currentArgs; + }; + + TOOLLIB_REGISTER_TOOLCOMMAND("eupak_create", CreateCommand); + +} // namespace eupak \ No newline at end of file diff --git a/src/tools/eupak/ExtractCommand.cpp b/src/tools/eupak/ExtractCommand.cpp new file mode 100644 index 0000000..f7f4112 --- /dev/null +++ b/src/tools/eupak/ExtractCommand.cpp @@ -0,0 +1,167 @@ +// +// EuropaTools +// +// (C) 2021-2025 modeco80 +// +// SPDX-License-Identifier: MIT +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace eupak { + + struct ExtractCommand : tool::IToolCommand { + ExtractCommand() + : parser("extract", EUPAK_VERSION_STR, argparse::default_arguments::help) { + // clang-format off + parser + .add_description("Extract a package file."); + parser + .add_argument("-d", "--directory") + .default_value("") + .metavar("DIRECTORY") + .help("Directory to extract to."); + + parser + .add_argument("input") + .help("Input archive") + .metavar("ARCHIVE"); + + parser + .add_argument("--verbose") + .help("Increase extraction output verbosity") + .default_value(false) + .implicit_value(true); + // clang-format on + } + + void Init(argparse::ArgumentParser& parentParser) override { + parentParser.add_subparser(parser); + } + + bool ShouldRun(argparse::ArgumentParser& parentParser) const override { + return parentParser.is_subcommand_used("extract"); + } + + int Parse() override { + currentArgs.verbose = parser.get("--verbose"); + currentArgs.inputPath = eupak::fs::path(parser.get("input")); + + if(parser.is_used("--directory")) { + currentArgs.outputDirectory = eupak::fs::path(parser.get("--directory")); + } else { + // Default to the basename appended to current path + // as a "relatively sane" default path to extract to. + // Should be okay. + currentArgs.outputDirectory = eupak::fs::current_path() / currentArgs.inputPath.stem(); + } + + return 0; + } + + int Run() override { + std::cout << "Input PAK/PMDL: " << currentArgs.inputPath << '\n'; + std::cout << "Output Directory: " << currentArgs.outputDirectory << '\n'; + + std::ifstream ifs(currentArgs.inputPath.string(), std::ifstream::binary); + + if(!ifs) { + std::cout << "Error: Could not open file " << currentArgs.inputPath << ".\n"; + return 1; + } + + eio::pak::Reader reader(ifs); + + reader.ReadHeaderAndTOC(); + + if(reader.Invalid()) { + std::cout << "Error: Invalid PAK/PMDL file " << currentArgs.inputPath << ".\n"; + return 1; + } + + indicators::ProgressBar progress { + indicators::option::BarWidth { 50 }, + indicators::option::ForegroundColor { indicators::Color::green }, + indicators::option::MaxProgress { reader.GetFiles().size() }, + indicators::option::ShowPercentage { true }, + indicators::option::ShowElapsedTime { true }, + indicators::option::ShowRemainingTime { true }, + + indicators::option::PrefixText { "Extracting archive " } + }; + + indicators::show_console_cursor(false); + + for(auto& [filename, file] : reader.GetFiles()) { + auto nameCopy = filename; + +#ifndef _WIN32 + if(nameCopy.find('\\') != std::string::npos) { + // Grody, but eh. Should work. + for(auto& c : nameCopy) + if(c == '\\') + c = '/'; + } +#endif + + progress.set_option(indicators::option::PostfixText { filename }); + + auto outpath = (currentArgs.outputDirectory / nameCopy); + + if(!fs::exists(outpath.parent_path())) + fs::create_directories(outpath.parent_path()); + + std::ofstream ofs(outpath.string(), std::ofstream::binary); + + if(!ofs) { + std::cerr << "Could not open " << outpath << " for writing.\n"; + continue; + } + + if(currentArgs.verbose) { + std::cerr << "Extracting file \"" << filename << "\"...\n"; + } + + reader.ReadFile(filename); + + { + auto& fileData = file.GetData(); + if(auto* buffer = fileData.GetIf>(); buffer) { + ofs.write(reinterpret_cast((*buffer).data()), (*buffer).size()); + ofs.flush(); + } else { + throw std::runtime_error("???? why are we getting paths here?"); + } + } + + // We no longer need the file data anymore, so let's purge it to save memory + file.PurgeData(); + + progress.tick(); + } + + indicators::show_console_cursor(true); + return 0; + } + + private: + struct Arguments { + fs::path inputPath; + fs::path outputDirectory; + bool verbose; + }; + + argparse::ArgumentParser parser; + Arguments currentArgs; + }; + + TOOLLIB_REGISTER_TOOLCOMMAND("eupak_extract", ExtractCommand); +} // namespace eupak \ No newline at end of file diff --git a/src/tools/eupak/InfoCommand.cpp b/src/tools/eupak/InfoCommand.cpp new file mode 100644 index 0000000..be33b20 --- /dev/null +++ b/src/tools/eupak/InfoCommand.cpp @@ -0,0 +1,146 @@ +// +// EuropaTools +// +// (C) 2021-2025 modeco80 +// +// SPDX-License-Identifier: MIT +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace eupak { + + constexpr static auto DATE_FORMAT = "%m/%d/%Y %r"; + + struct InfoCommand : tool::IToolCommand { + InfoCommand() + : parser("info", EUPAK_VERSION_STR, argparse::default_arguments::help) { + // clang-format off + parser + .add_description("Print information about a package file."); + parser + .add_argument("input") + .help("Input archive") + .metavar("ARCHIVE"); + + // FIXME: Probably just print this always, in a thinner format, but use + // the existing thicker format for verbosity. + parser + .add_argument("--verbose") + .help("Increase information output verbosity (print a list of files).") + .default_value(false) + .implicit_value(true); + // clang-format on + } + + void Init(argparse::ArgumentParser& parentParser) override { + parentParser.add_subparser(parser); + } + + bool ShouldRun(argparse::ArgumentParser& parentParser) const override { + return parentParser.is_subcommand_used("info"); + } + + int Parse() override { + try { + currentArgs.verbose = parser.get("--verbose"); + currentArgs.inputPath = eupak::fs::path(parser.get("input")); + + if(fs::is_directory(currentArgs.inputPath)) { + std::cout << "Error: " << currentArgs.inputPath << " appears to be a directory, not a file.\n"; + return 1; + } + + } catch(...) { + return 1; + } + + return 0; + } + + int Run() override { + std::ifstream ifs(currentArgs.inputPath.string(), std::ifstream::binary); + + if(!ifs) { + std::cout << "Error: Could not open file " << currentArgs.inputPath << ".\n"; + return 1; + } + + eio::pak::Reader reader(ifs); + + reader.ReadHeaderAndTOC(); + + if(reader.Invalid()) { + std::cout << "Error: Invalid PAK/PMDL file " << currentArgs.inputPath << ".\n"; + return 1; + } + + std::visit([&](auto& header) { + std::string_view version = "???"; + + // This is the best other than just duplicating the body for each pak version.. :( + if constexpr(std::decay_t::VERSION == estructs::PakVersion::Ver3) + version = "Version 3 (Starfighter/Europa pre-release, May-July 2000?)"; + else if constexpr(std::decay_t::VERSION == estructs::PakVersion::Ver4) + version = "Version 4 (Starfighter)"; + else if constexpr(std::decay_t::VERSION == estructs::PakVersion::Ver5) + version = "Version 5 (Jedi Starfighter)"; + + std::cout << "Archive " << currentArgs.inputPath << ":\n"; + std::cout << " Created: " << FormatUnixTimestamp(header.creationUnixTime, DATE_FORMAT) << '\n'; + std::cout << " Version: " << version << '\n'; + std::cout << " Size: " << FormatUnit(header.tocOffset + header.tocSize) << '\n'; + std::cout << " File Count: " << header.fileCount << " files\n"; + }, + reader.GetHeader()); + + // Print a detailed file list if verbose. + + for(auto& [filename, file] : reader.GetFiles()) { + if(currentArgs.verbose) { + std::cout << "File \"" << filename << "\":\n"; + file.VisitTocEntry([&](auto& tocEntry) { + std::cout << " Created: " << FormatUnixTimestamp(tocEntry.creationUnixTime, DATE_FORMAT) << '\n'; + std::cout << " Size: " << FormatUnit(tocEntry.size) << '\n'; + + if constexpr(std::is_same_v, estructs::PakHeader_V5::TocEntry_SectorAligned>) { + std::cout << " Start LBA (CD-ROM Sector): " << tocEntry.startLBA << '\n'; + } + }); + } else { + file.VisitTocEntry([&](auto& tocEntry) { + std::printf("%16s %10s %8s", FormatUnixTimestamp(tocEntry.creationUnixTime, DATE_FORMAT).c_str(), FormatUnit(tocEntry.size).c_str(), filename.c_str()); + + if constexpr(std::is_same_v, estructs::PakHeader_V5::TocEntry_SectorAligned>) { + std::printf(" (LBA %u)", tocEntry.startLBA); + } + + std::printf("\n"); + }); + } + } + + return 0; + } + + private: + struct Arguments { + fs::path inputPath; + bool verbose; + }; + + argparse::ArgumentParser parser; + Arguments currentArgs; + }; + + TOOLLIB_REGISTER_TOOLCOMMAND("eupak_info", InfoCommand); + +} // namespace eupak \ No newline at end of file diff --git a/src/tools/eupak/main.cpp b/src/tools/eupak/main.cpp index 6ae3f72..351bbf8 100644 --- a/src/tools/eupak/main.cpp +++ b/src/tools/eupak/main.cpp @@ -6,49 +6,28 @@ // SPDX-License-Identifier: MIT // -#include #include -#include - -using namespace eupak; -using namespace eupak::tasks; +#include +#include int main(int argc, char** argv) { - argparse::ArgumentParser parser("eupak", EUPAK_VERSION_STR); - parser.add_description("Eupak (Europa Package Multi-Tool) v" EUPAK_VERSION_STR); - - auto tasks = std::vector { - TaskFactory::CreateNamed("create", parser), - TaskFactory::CreateNamed("info", parser), - TaskFactory::CreateNamed("extract", parser), + const tool::ToolInfo info { + .name = "eupak", + .version = EUPAK_VERSION_STR, + .description = "Europa Package Tool v" EUPAK_VERSION_STR }; - try { - // No command was specified, display the help and then exit with a failure code. - // For some reason the `argparse` library does not have something like this on its own. - // - // I guess it's simple though so I can't really complain that much - if(argc == 1) { - auto s = parser.help(); - printf("%s\n", s.str().c_str()); - return 1; - } + auto toolCommands = std::vector { + tool::ToolCommandFactory::CreateNamed("eupak_create"), + tool::ToolCommandFactory::CreateNamed("eupak_extract"), + tool::ToolCommandFactory::CreateNamed("eupak_info"), + }; - parser.parse_args(argc, argv); - } catch(std::runtime_error& error) { - std::cout << error.what() << '\n' - << parser; - return 1; - } - - for(auto& task : tasks) { - if(task->ShouldRun(parser)) { - if(auto res = task->Parse(); res != 0) - return res; - - return task->Run(); - } - } - - return 0; + // clang-format off + return tool::ToolMain(info, { + .toolCommands = toolCommands, + .argc = argc, + .argv = argv + }); + // clang-format on } diff --git a/src/tools/eupak/tasks/CreateTask.cpp b/src/tools/eupak/tasks/CreateTask.cpp deleted file mode 100644 index d7f131b..0000000 --- a/src/tools/eupak/tasks/CreateTask.cpp +++ /dev/null @@ -1,282 +0,0 @@ -// -// EuropaTools -// -// (C) 2021-2025 modeco80 -// -// SPDX-License-Identifier: MIT -// - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "argparse/argparse.hpp" -#include "europa/structs/Pak.hpp" -#include "tasks/Task.hpp" - -namespace eupak::tasks { - - struct CreateArchiveReportSink : public eio::pak::WriterProgressReportSink { - CreateArchiveReportSink(int fileCount = 0) - : eio::pak::WriterProgressReportSink() { - indicators::show_console_cursor(false); - progress.set_option(indicators::option::MaxProgress { fileCount }); - } - - ~CreateArchiveReportSink() { - indicators::show_console_cursor(true); - } - - void OnEvent(const PakEvent& event) override { - using enum PakEvent::EventCode; - switch(event.eventCode) { - case WritingHeader: - progress.set_option(indicators::option::PostfixText { "Writing header" }); - progress.print_progress(); - break; - - case FillInHeader: - progress.set_option(indicators::option::PostfixText { "Filling in header" }); - progress.print_progress(); - break; - - case WritingToc: - progress.set_option(indicators::option::PostfixText { "Writing TOC" }); - progress.print_progress(); - break; - } - } - - void OnEvent(const FileEvent& event) override { - using enum FileEvent::EventCode; - switch(event.eventCode) { - case FileWriteBegin: - progress.set_option(indicators::option::PostfixText { "Writing " + event.targetFileName }); - progress.print_progress(); - break; - - case FileWriteEnd: - progress.set_option(indicators::option::PostfixText { "Written " + event.targetFileName }); - progress.tick(); - break; - } - } - - private: - indicators::ProgressBar progress { - indicators::option::BarWidth { 50 }, - indicators::option::ForegroundColor { indicators::Color::yellow }, - indicators::option::ShowPercentage { true }, - indicators::option::ShowElapsedTime { true }, - indicators::option::ShowRemainingTime { true }, - - indicators::option::PrefixText { "Writing archive " } - }; - }; - - std::optional ParsePakVersion(const std::string& str) { - if(str == "europa-prerelease") { - 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 "europa-prerelease", "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 bit-accurate rebuilds we should also accept a JSON manifest file - // that contains: - // - Package version, - // - sector alignment (for v5), - // - package build time, - // - data order of all files - // - TOC order of all files - // - file TOC data (modtime, TOC index, so on) - // Then a user can just do `eupak create --manifest manifest.json` and it'll all be done for them - // - // `eupak extract` should optionally generate this manifest for the user - // (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() { - currentArgs.verbose = parser.get("--verbose"); - currentArgs.inputDirectory = fs::path(parser.get("--directory")); - currentArgs.outputFile = 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()) { - currentArgs.pakVersion = *opt; - } else { - std::cout << "Error: Invalid version \"" << versionStr << "\"\n" - << parser; - return 1; - } - } else { - currentArgs.pakVersion = estructs::PakVersion::Ver4; - } - - currentArgs.sectorAligned = parser.get("--sector-aligned"); - - if(currentArgs.sectorAligned && currentArgs.pakVersion != 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(currentArgs.inputDirectory)) { - std::cout << "Error: Provided input isn't a directory\n" - << parser; - return 1; - } - - return 0; - } - - int CreateTask::Run() { - auto currFile = 0; - auto fileCount = 0; - - // Count how many files we're gonna add to the archive - for(auto& ent : fs::recursive_directory_iterator(currentArgs.inputDirectory)) { - if(ent.is_directory()) - continue; - fileCount++; - } - - std::cout << "Going to write " << fileCount << " files into " << currentArgs.outputFile << '\n'; - - if(currentArgs.sectorAligned) { - std::cout << "Writing a sector aligned package\n"; - } - - indicators::ProgressBar progress { - indicators::option::BarWidth { 50 }, - indicators::option::ForegroundColor { indicators::Color::green }, - indicators::option::MaxProgress { fileCount }, - indicators::option::ShowPercentage { true }, - indicators::option::ShowElapsedTime { true }, - indicators::option::ShowRemainingTime { true }, - - indicators::option::PrefixText { "Adding files to archive " } - }; - - indicators::show_console_cursor(false); - - // TODO: use time to write in the header - // also: is there any point to verbosity? could add archive written size ig - - std::vector files; - files.reserve(fileCount); - - for(auto& ent : fs::recursive_directory_iterator(currentArgs.inputDirectory)) { - if(ent.is_directory()) - continue; - - auto relativePathName = fs::relative(ent.path(), currentArgs.inputDirectory).string(); - auto lastModified = fs::last_write_time(ent.path()); - - // Convert to Windows path separator always (that's what the game wants, after all) - for(auto& c : relativePathName) - if(c == '/') - c = '\\'; - - progress.set_option(indicators::option::PostfixText { relativePathName + " (" + std::to_string(currFile + 1) + '/' + std::to_string(fileCount) + ")" }); - - eio::pak::File file; - eio::pak::FileData pakData = eio::pak::FileData::InitAsPath(ent.path()); - - file.InitAs(currentArgs.pakVersion, currentArgs.sectorAligned); - - // Add data - file.SetData(std::move(pakData)); - - // Setup other stuff like modtime - file.VisitTocEntry([&](auto& tocEntry) { - // Kinda stupid but works - auto sys = std::chrono::file_clock::to_sys(lastModified); - auto seconds = std::chrono::time_point_cast(sys); - tocEntry.creationUnixTime = static_cast(seconds.time_since_epoch().count()); - }); - - files.emplace_back(std::make_pair(relativePathName, std::move(file))); - progress.tick(); - currFile++; - } - - indicators::show_console_cursor(true); - - std::ofstream ofs(currentArgs.outputFile.string(), std::ofstream::binary); - - if(!ofs) { - std::cout << "Error: Couldn't open " << currentArgs.outputFile << " for writing\n"; - return 1; - } - - CreateArchiveReportSink reportSink(fileCount); - eio::pak::Writer writer(currentArgs.pakVersion); - - using enum eio::pak::Writer::SectorAlignment; - - eio::pak::Writer::SectorAlignment alignment = DoNotAlign; - - if(currentArgs.sectorAligned) - alignment = Align; - - writer.Write(ofs, std::move(files), reportSink, alignment); - 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 deleted file mode 100644 index 044738d..0000000 --- a/src/tools/eupak/tasks/CreateTask.hpp +++ /dev/null @@ -1,46 +0,0 @@ -// -// EuropaTools -// -// (C) 2021-2025 modeco80 -// -// SPDX-License-Identifier: MIT -// - -#ifndef EUROPA_EUPAK_TASKS_CREATETASK_HPP -#define EUROPA_EUPAK_TASKS_CREATETASK_HPP - -#include -#include -#include -#include - -namespace eupak::tasks { - - struct CreateTask : ITask { - CreateTask(); - - void Init(argparse::ArgumentParser& parentParser) override; - - bool ShouldRun(argparse::ArgumentParser& parentParser) const override; - - int Parse() override; - - int Run() override; - - private: - struct Arguments { - fs::path inputDirectory; - fs::path outputFile; - - bool verbose; - europa::structs::PakVersion pakVersion; - bool sectorAligned; - }; - - argparse::ArgumentParser parser; - Arguments currentArgs; - }; - -} // namespace eupak::tasks - -#endif // EUROPA_EUPAK_TASKS_CREATETASK_HPP diff --git a/src/tools/eupak/tasks/ExtractTask.cpp b/src/tools/eupak/tasks/ExtractTask.cpp deleted file mode 100644 index 7515a09..0000000 --- a/src/tools/eupak/tasks/ExtractTask.cpp +++ /dev/null @@ -1,157 +0,0 @@ -// -// EuropaTools -// -// (C) 2021-2025 modeco80 -// -// SPDX-License-Identifier: MIT -// - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "tasks/Task.hpp" - -namespace eupak::tasks { - ExtractTask::ExtractTask() - : parser("extract", EUPAK_VERSION_STR, argparse::default_arguments::help) { - // clang-format off - parser - .add_description("Extract a package file."); - parser - .add_argument("-d", "--directory") - .default_value("") - .metavar("DIRECTORY") - .help("Directory to extract to."); - - parser - .add_argument("input") - .help("Input archive") - .metavar("ARCHIVE"); - - parser - .add_argument("--verbose") - .help("Increase extraction output verbosity") - .default_value(false) - .implicit_value(true); - // clang-format on - } - - void ExtractTask::Init(argparse::ArgumentParser& parentParser) { - parentParser.add_subparser(parser); - } - - bool ExtractTask::ShouldRun(argparse::ArgumentParser& parentParser) const { - return parentParser.is_subcommand_used("extract"); - }; - - int ExtractTask::Parse() { - eupak::tasks::ExtractTask task; - - currentArgs.verbose = parser.get("--verbose"); - currentArgs.inputPath = eupak::fs::path(parser.get("input")); - - if(parser.is_used("--directory")) { - currentArgs.outputDirectory = eupak::fs::path(parser.get("--directory")); - } else { - // Default to the basename appended to current path - // as a "relatively sane" default path to extract to. - // Should be okay. - currentArgs.outputDirectory = eupak::fs::current_path() / currentArgs.inputPath.stem(); - } - - return 0; - } - - int ExtractTask::Run() { - std::cout << "Input PAK/PMDL: " << currentArgs.inputPath << '\n'; - std::cout << "Output Directory: " << currentArgs.outputDirectory << '\n'; - - std::ifstream ifs(currentArgs.inputPath.string(), std::ifstream::binary); - - if(!ifs) { - std::cout << "Error: Could not open file " << currentArgs.inputPath << ".\n"; - return 1; - } - - eio::pak::Reader reader(ifs); - - reader.ReadHeaderAndTOC(); - - if(reader.Invalid()) { - std::cout << "Error: Invalid PAK/PMDL file " << currentArgs.inputPath << ".\n"; - return 1; - } - - indicators::ProgressBar progress { - indicators::option::BarWidth { 50 }, - indicators::option::ForegroundColor { indicators::Color::green }, - indicators::option::MaxProgress { reader.GetFiles().size() }, - indicators::option::ShowPercentage { true }, - indicators::option::ShowElapsedTime { true }, - indicators::option::ShowRemainingTime { true }, - - indicators::option::PrefixText { "Extracting archive " } - }; - - indicators::show_console_cursor(false); - - for(auto& [filename, file] : reader.GetFiles()) { - auto nameCopy = filename; - -#ifndef _WIN32 - if(nameCopy.find('\\') != std::string::npos) { - // Grody, but eh. Should work. - for(auto& c : nameCopy) - if(c == '\\') - c = '/'; - } -#endif - - progress.set_option(indicators::option::PostfixText { filename }); - - auto outpath = (currentArgs.outputDirectory / nameCopy); - - if(!fs::exists(outpath.parent_path())) - fs::create_directories(outpath.parent_path()); - - std::ofstream ofs(outpath.string(), std::ofstream::binary); - - if(!ofs) { - std::cerr << "Could not open " << outpath << " for writing.\n"; - continue; - } - - if(currentArgs.verbose) { - std::cerr << "Extracting file \"" << filename << "\"...\n"; - } - - reader.ReadFile(filename); - - { - auto& fileData = file.GetData(); - if(auto* buffer = fileData.GetIf>(); buffer) { - ofs.write(reinterpret_cast((*buffer).data()), (*buffer).size()); - ofs.flush(); - } else { - throw std::runtime_error("???? why are we getting paths here?"); - } - } - - // We no longer need the file data anymore, so let's purge it to save memory - file.PurgeData(); - - progress.tick(); - } - - indicators::show_console_cursor(true); - return 0; - } - - EUPAK_REGISTER_TASK("extract", ExtractTask); -} // namespace eupak::tasks \ No newline at end of file diff --git a/src/tools/eupak/tasks/ExtractTask.hpp b/src/tools/eupak/tasks/ExtractTask.hpp deleted file mode 100644 index 8aebc46..0000000 --- a/src/tools/eupak/tasks/ExtractTask.hpp +++ /dev/null @@ -1,41 +0,0 @@ -// -// EuropaTools -// -// (C) 2021-2025 modeco80 -// -// SPDX-License-Identifier: MIT -// - -#ifndef EUROPA_EUPAK_TASKS_EXTRACTTASK_HPP -#define EUROPA_EUPAK_TASKS_EXTRACTTASK_HPP - -#include -#include - -namespace eupak::tasks { - - struct ExtractTask : ITask { - ExtractTask(); - - void Init(argparse::ArgumentParser& parentParser) override; - - bool ShouldRun(argparse::ArgumentParser& parentParser) const override; - - int Parse() override; - - int Run() override; - - private: - struct Arguments { - fs::path inputPath; - fs::path outputDirectory; - bool verbose; - }; - - argparse::ArgumentParser parser; - Arguments currentArgs; - }; - -} // namespace eupak::tasks - -#endif // EUROPATOOLS_EXTRACTTASK_H diff --git a/src/tools/eupak/tasks/InfoTask.cpp b/src/tools/eupak/tasks/InfoTask.cpp deleted file mode 100644 index f99ec67..0000000 --- a/src/tools/eupak/tasks/InfoTask.cpp +++ /dev/null @@ -1,134 +0,0 @@ -// -// EuropaTools -// -// (C) 2021-2025 modeco80 -// -// SPDX-License-Identifier: MIT -// - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace eupak::tasks { - - constexpr static auto DATE_FORMAT = "%m/%d/%Y %r"; - - InfoTask::InfoTask() - : parser("info", EUPAK_VERSION_STR, argparse::default_arguments::help) { - // clang-format off - parser - .add_description("Print information about a package file."); - parser - .add_argument("input") - .help("Input archive") - .metavar("ARCHIVE"); - - // FIXME: Probably just print this always, in a thinner format, but use - // the existing thicker format for verbosity. - parser - .add_argument("--verbose") - .help("Increase information output verbosity (print a list of files).") - .default_value(false) - .implicit_value(true); - // clang-format on - } - - void InfoTask::Init(argparse::ArgumentParser& parentParser) { - parentParser.add_subparser(parser); - } - - int InfoTask::Parse() { - try { - currentArgs.verbose = parser.get("--verbose"); - currentArgs.inputPath = eupak::fs::path(parser.get("input")); - - if(fs::is_directory(currentArgs.inputPath)) { - std::cout << "Error: " << currentArgs.inputPath << " appears to be a directory, not a file.\n"; - return 1; - } - - } catch(...) { - return 1; - } - - return 0; - } - - bool InfoTask::ShouldRun(argparse::ArgumentParser& parentParser) const { - return parentParser.is_subcommand_used("info"); - } - - int InfoTask::Run() { - std::ifstream ifs(currentArgs.inputPath.string(), std::ifstream::binary); - - if(!ifs) { - std::cout << "Error: Could not open file " << currentArgs.inputPath << ".\n"; - return 1; - } - - eio::pak::Reader reader(ifs); - - reader.ReadHeaderAndTOC(); - - if(reader.Invalid()) { - std::cout << "Error: Invalid PAK/PMDL file " << currentArgs.inputPath << ".\n"; - return 1; - } - - std::visit([&](auto& header) { - std::string_view version = "???"; - - // This is the best other than just duplicating the body for each pak version.. :( - if constexpr(std::decay_t::VERSION == estructs::PakVersion::Ver3) - version = "Version 3 (Starfighter/Europa pre-release, May-July 2000?)"; - else if constexpr(std::decay_t::VERSION == estructs::PakVersion::Ver4) - version = "Version 4 (Starfighter)"; - else if constexpr(std::decay_t::VERSION == estructs::PakVersion::Ver5) - version = "Version 5 (Jedi Starfighter)"; - - std::cout << "Archive " << currentArgs.inputPath << ":\n"; - std::cout << " Created: " << FormatUnixTimestamp(header.creationUnixTime, DATE_FORMAT) << '\n'; - std::cout << " Version: " << version << '\n'; - std::cout << " Size: " << FormatUnit(header.tocOffset + header.tocSize) << '\n'; - std::cout << " File Count: " << header.fileCount << " files\n"; - }, - reader.GetHeader()); - - // Print a detailed file list if verbose. - - for(auto& [filename, file] : reader.GetFiles()) { - if(currentArgs.verbose) { - std::cout << "File \"" << filename << "\":\n"; - file.VisitTocEntry([&](auto& tocEntry) { - std::cout << " Created: " << FormatUnixTimestamp(tocEntry.creationUnixTime, DATE_FORMAT) << '\n'; - std::cout << " Size: " << FormatUnit(tocEntry.size) << '\n'; - - if constexpr(std::is_same_v, estructs::PakHeader_V5::TocEntry_SectorAligned>) { - std::cout << " Start LBA (CD-ROM Sector): " << tocEntry.startLBA << '\n'; - } - }); - } else { - file.VisitTocEntry([&](auto& tocEntry) { - std::printf("%16s %10s %8s", FormatUnixTimestamp(tocEntry.creationUnixTime, DATE_FORMAT).c_str(), FormatUnit(tocEntry.size).c_str(), filename.c_str()); - - if constexpr(std::is_same_v, estructs::PakHeader_V5::TocEntry_SectorAligned>) { - std::printf(" (LBA %u)", tocEntry.startLBA); - } - - std::printf("\n"); - }); - } - } - - return 0; - } - - EUPAK_REGISTER_TASK("info", InfoTask); - -} // namespace eupak::tasks \ No newline at end of file diff --git a/src/tools/eupak/tasks/InfoTask.hpp b/src/tools/eupak/tasks/InfoTask.hpp deleted file mode 100644 index c62480c..0000000 --- a/src/tools/eupak/tasks/InfoTask.hpp +++ /dev/null @@ -1,40 +0,0 @@ -// -// EuropaTools -// -// (C) 2021-2025 modeco80 -// -// SPDX-License-Identifier: MIT -// - -#ifndef EUROPA_EUPAK_TASKS_INFOTASK_HPP -#define EUROPA_EUPAK_TASKS_INFOTASK_HPP - -#include -#include - -namespace eupak::tasks { - - struct InfoTask : ITask { - InfoTask(); - - void Init(argparse::ArgumentParser& parentParser) override; - - bool ShouldRun(argparse::ArgumentParser& parentParser) const override; - - int Parse() override; - - int Run() override; - - private: - struct Arguments { - fs::path inputPath; - bool verbose; - }; - - argparse::ArgumentParser parser; - Arguments currentArgs; - }; - -} // namespace eupak::tasks - -#endif // EUROPA_EUPAK_TASKS_INFOTASK_HPP diff --git a/src/tools/eupak/tasks/Task.cpp b/src/tools/eupak/tasks/Task.cpp deleted file mode 100644 index 46805e8..0000000 --- a/src/tools/eupak/tasks/Task.cpp +++ /dev/null @@ -1,25 +0,0 @@ -// -// EuropaTools -// -// (C) 2021-2025 modeco80 -// -// SPDX-License-Identifier: MIT -// - -#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 deleted file mode 100644 index cbc9064..0000000 --- a/src/tools/eupak/tasks/Task.hpp +++ /dev/null @@ -1,68 +0,0 @@ -// -// EuropaTools -// -// (C) 2021-2025 modeco80 -// -// SPDX-License-Identifier: MIT -// - -#pragma once -#include -#include -#include -#include - -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 ITask instances for clients. - 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; - } - }; - - /// Helper template to register into the [TaskFactory]. - 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(); - } }); - } - }; - - /// Registers a task. Should be put in the .cpp implementation source file of the - /// task object itself. -#define EUPAK_REGISTER_TASK(Name, TTask) \ - static ::eupak::tasks::TaskFactoryRegister __register__##TTask(Name) - -} // namespace eupak::tasks \ No newline at end of file diff --git a/src/tools/toollib/README.md b/src/tools/toollib/README.md new file mode 100644 index 0000000..d1e4b38 --- /dev/null +++ b/src/tools/toollib/README.md @@ -0,0 +1,3 @@ +# toollib + +This is a library (to be) shared by all the tools here that want to have multiple subcommands. \ No newline at end of file diff --git a/src/tools/toollib/toollib/ToolMain.hpp b/src/tools/toollib/toollib/ToolMain.hpp index ec11800..a4769c2 100644 --- a/src/tools/toollib/toollib/ToolMain.hpp +++ b/src/tools/toollib/toollib/ToolMain.hpp @@ -16,6 +16,7 @@ namespace tool { struct IToolCommand; + /// Provides information about a tool. struct ToolInfo { std::string_view name; // "Eupak" std::string_view version; // v1.0.0 @@ -24,7 +25,7 @@ namespace tool { }; struct ToolMainInput { - /// Tool commands to run. + /// Tool commands to provide to ToolMain(). std::span> toolCommands; // C arguments