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:
parent
b99c044cbd
commit
8d5a8d4adc
15 changed files with 639 additions and 840 deletions
|
@ -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
|
||||
)
|
||||
|
||||
|
|
298
src/tools/eupak/CreateCommand.cpp
Normal file
298
src/tools/eupak/CreateCommand.cpp
Normal 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
|
167
src/tools/eupak/ExtractCommand.cpp
Normal file
167
src/tools/eupak/ExtractCommand.cpp
Normal 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
|
146
src/tools/eupak/InfoCommand.cpp
Normal file
146
src/tools/eupak/InfoCommand.cpp
Normal 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
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
3
src/tools/toollib/README.md
Normal file
3
src/tools/toollib/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# toollib
|
||||
|
||||
This is a library (to be) shared by all the tools here that want to have multiple subcommands.
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue