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.
This commit is contained in:
Lily Tsuru 2025-01-17 17:58:08 -05:00
parent b99c044cbd
commit 8d5a8d4adc
15 changed files with 639 additions and 840 deletions

View file

@ -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
)

View file

@ -0,0 +1,298 @@
//
// EuropaTools
//
// (C) 2021-2025 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: MIT
//
#include <chrono>
// Common stuff
#include <CommonDefs.hpp>
#include <EupakConfig.hpp>
#include <europa/io/pak/File.hpp>
#include <europa/io/pak/Writer.hpp>
#include <europa/io/pak/WriterProgressReportSink.hpp>
#include <fstream>
#include <indicators/cursor_control.hpp>
#include <indicators/progress_bar.hpp>
#include <iostream>
#include <toollib/ToolCommand.hpp>
#include <Utils.hpp>
#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<estructs::PakVersion> 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<bool>("--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<bool>("--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<eio::pak::Writer::FlattenedType> 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<std::chrono::seconds>(sys);
tocEntry.creationUnixTime = static_cast<std::uint32_t>(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

View file

@ -0,0 +1,167 @@
//
// EuropaTools
//
// (C) 2021-2025 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: MIT
//
#include <CommonDefs.hpp>
#include <EupakConfig.hpp>
#include <europa/io/pak/Reader.hpp>
#include <fstream>
#include <indicators/cursor_control.hpp>
#include <indicators/progress_bar.hpp>
#include <iostream>
#include <stdexcept>
#include <toollib/ToolCommand.hpp>
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<bool>("--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<std::vector<std::uint8_t>>(); buffer) {
ofs.write(reinterpret_cast<const char*>((*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

View file

@ -0,0 +1,146 @@
//
// EuropaTools
//
// (C) 2021-2025 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: MIT
//
#include <argparse/argparse.hpp>
#include <CommonDefs.hpp>
#include <EupakConfig.hpp>
#include <europa/io/pak/Reader.hpp>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <toollib/ToolCommand.hpp>
#include <Utils.hpp>
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<bool>("--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<decltype(header)>::VERSION == estructs::PakVersion::Ver3)
version = "Version 3 (Starfighter/Europa pre-release, May-July 2000?)";
else if constexpr(std::decay_t<decltype(header)>::VERSION == estructs::PakVersion::Ver4)
version = "Version 4 (Starfighter)";
else if constexpr(std::decay_t<decltype(header)>::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<std::decay_t<decltype(tocEntry)>, 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<std::decay_t<decltype(tocEntry)>, 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

View file

@ -6,49 +6,28 @@
// SPDX-License-Identifier: MIT
//
#include <argparse/argparse.hpp>
#include <EupakConfig.hpp>
#include <tasks/Task.hpp>
using namespace eupak;
using namespace eupak::tasks;
#include <toollib/ToolCommand.hpp>
#include <toollib/ToolMain.hpp>
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
}

View file

@ -1,282 +0,0 @@
//
// EuropaTools
//
// (C) 2021-2025 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: MIT
//
#include <chrono>
#include <EupakConfig.hpp>
#include <europa/io/pak/File.hpp>
#include <europa/io/pak/Writer.hpp>
#include <europa/io/pak/WriterProgressReportSink.hpp>
#include <fstream>
#include <indicators/cursor_control.hpp>
#include <indicators/progress_bar.hpp>
#include <iostream>
#include <tasks/CreateTask.hpp>
#include <Utils.hpp>
#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<estructs::PakVersion> 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<bool>("--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<bool>("--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<eio::pak::Writer::FlattenedType> 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<std::chrono::seconds>(sys);
tocEntry.creationUnixTime = static_cast<std::uint32_t>(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

View file

@ -1,46 +0,0 @@
//
// EuropaTools
//
// (C) 2021-2025 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: MIT
//
#ifndef EUROPA_EUPAK_TASKS_CREATETASK_HPP
#define EUROPA_EUPAK_TASKS_CREATETASK_HPP
#include <argparse/argparse.hpp>
#include <CommonDefs.hpp>
#include <europa/structs/Pak.hpp>
#include <tasks/Task.hpp>
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

View file

@ -1,157 +0,0 @@
//
// EuropaTools
//
// (C) 2021-2025 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: MIT
//
#include <EupakConfig.hpp>
#include <europa/io/pak/Reader.hpp>
#include <fstream>
#include <indicators/cursor_control.hpp>
#include <indicators/progress_bar.hpp>
#include <iostream>
#include <stdexcept>
#include <tasks/ExtractTask.hpp>
#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<bool>("--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<std::vector<std::uint8_t>>(); buffer) {
ofs.write(reinterpret_cast<const char*>((*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

View file

@ -1,41 +0,0 @@
//
// EuropaTools
//
// (C) 2021-2025 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: MIT
//
#ifndef EUROPA_EUPAK_TASKS_EXTRACTTASK_HPP
#define EUROPA_EUPAK_TASKS_EXTRACTTASK_HPP
#include <CommonDefs.hpp>
#include <tasks/Task.hpp>
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

View file

@ -1,134 +0,0 @@
//
// EuropaTools
//
// (C) 2021-2025 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: MIT
//
#include <argparse/argparse.hpp>
#include <EupakConfig.hpp>
#include <europa/io/pak/Reader.hpp>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <tasks/InfoTask.hpp>
#include <Utils.hpp>
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<bool>("--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<decltype(header)>::VERSION == estructs::PakVersion::Ver3)
version = "Version 3 (Starfighter/Europa pre-release, May-July 2000?)";
else if constexpr(std::decay_t<decltype(header)>::VERSION == estructs::PakVersion::Ver4)
version = "Version 4 (Starfighter)";
else if constexpr(std::decay_t<decltype(header)>::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<std::decay_t<decltype(tocEntry)>, 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<std::decay_t<decltype(tocEntry)>, estructs::PakHeader_V5::TocEntry_SectorAligned>) {
std::printf(" (LBA %u)", tocEntry.startLBA);
}
std::printf("\n");
});
}
}
return 0;
}
EUPAK_REGISTER_TASK("info", InfoTask);
} // namespace eupak::tasks

View file

@ -1,40 +0,0 @@
//
// EuropaTools
//
// (C) 2021-2025 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: MIT
//
#ifndef EUROPA_EUPAK_TASKS_INFOTASK_HPP
#define EUROPA_EUPAK_TASKS_INFOTASK_HPP
#include <CommonDefs.hpp>
#include <tasks/Task.hpp>
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

View file

@ -1,25 +0,0 @@
//
// EuropaTools
//
// (C) 2021-2025 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: MIT
//
#include "Task.hpp"
#include <stdexcept>
namespace eupak::tasks {
std::shared_ptr<ITask> 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

View file

@ -1,68 +0,0 @@
//
// EuropaTools
//
// (C) 2021-2025 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: MIT
//
#pragma once
#include <argparse/argparse.hpp>
#include <CommonDefs.hpp>
#include <type_traits>
#include <unordered_map>
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<ITask> (*)();
/// Creates a task.
static std::shared_ptr<ITask> CreateNamed(const std::string& name, argparse::ArgumentParser& parentParser);
private:
template <class T>
friend struct TaskFactoryRegister;
static auto& factoryMap() {
static std::unordered_map<std::string, FactoryMethod> m;
return m;
}
};
/// Helper template to register into the [TaskFactory].
template <class T>
struct TaskFactoryRegister {
TaskFactoryRegister(const std::string& name) {
static_assert(std::is_base_of_v<ITask, T>, "cannot register a type which does not derive from ITask");
TaskFactory::factoryMap().insert({ name,
[]() -> std::shared_ptr<ITask> {
return std::make_shared<T>();
} });
}
};
/// 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<TTask> __register__##TTask(Name)
} // namespace eupak::tasks

View file

@ -0,0 +1,3 @@
# toollib
This is a library (to be) shared by all the tools here that want to have multiple subcommands.

View file

@ -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<std::shared_ptr<IToolCommand>> toolCommands;
// C arguments