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
|
add_executable(eupak
|
||||||
main.cpp
|
main.cpp
|
||||||
|
|
||||||
Utils.cpp
|
Utils.cpp
|
||||||
|
|
||||||
# Tasks
|
# eupak commands
|
||||||
tasks/Task.cpp
|
CreateCommand.cpp
|
||||||
tasks/InfoTask.cpp
|
InfoCommand.cpp
|
||||||
tasks/CreateTask.cpp
|
ExtractCommand.cpp
|
||||||
tasks/ExtractTask.cpp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(eupak PUBLIC
|
target_link_libraries(eupak PUBLIC
|
||||||
europa
|
europa
|
||||||
argparse::argparse
|
toollib
|
||||||
indicators::indicators
|
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
|
// SPDX-License-Identifier: MIT
|
||||||
//
|
//
|
||||||
|
|
||||||
#include <argparse/argparse.hpp>
|
|
||||||
#include <EupakConfig.hpp>
|
#include <EupakConfig.hpp>
|
||||||
#include <tasks/Task.hpp>
|
#include <toollib/ToolCommand.hpp>
|
||||||
|
#include <toollib/ToolMain.hpp>
|
||||||
using namespace eupak;
|
|
||||||
using namespace eupak::tasks;
|
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
argparse::ArgumentParser parser("eupak", EUPAK_VERSION_STR);
|
const tool::ToolInfo info {
|
||||||
parser.add_description("Eupak (Europa Package Multi-Tool) v" EUPAK_VERSION_STR);
|
.name = "eupak",
|
||||||
|
.version = EUPAK_VERSION_STR,
|
||||||
auto tasks = std::vector {
|
.description = "Europa Package Tool v" EUPAK_VERSION_STR
|
||||||
TaskFactory::CreateNamed("create", parser),
|
|
||||||
TaskFactory::CreateNamed("info", parser),
|
|
||||||
TaskFactory::CreateNamed("extract", parser),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
auto toolCommands = std::vector {
|
||||||
// No command was specified, display the help and then exit with a failure code.
|
tool::ToolCommandFactory::CreateNamed("eupak_create"),
|
||||||
// For some reason the `argparse` library does not have something like this on its own.
|
tool::ToolCommandFactory::CreateNamed("eupak_extract"),
|
||||||
//
|
tool::ToolCommandFactory::CreateNamed("eupak_info"),
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
parser.parse_args(argc, argv);
|
// clang-format off
|
||||||
} catch(std::runtime_error& error) {
|
return tool::ToolMain(info, {
|
||||||
std::cout << error.what() << '\n'
|
.toolCommands = toolCommands,
|
||||||
<< parser;
|
.argc = argc,
|
||||||
return 1;
|
.argv = argv
|
||||||
}
|
});
|
||||||
|
// clang-format on
|
||||||
for(auto& task : tasks) {
|
|
||||||
if(task->ShouldRun(parser)) {
|
|
||||||
if(auto res = task->Parse(); res != 0)
|
|
||||||
return res;
|
|
||||||
|
|
||||||
return task->Run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
struct IToolCommand;
|
||||||
|
|
||||||
|
/// Provides information about a tool.
|
||||||
struct ToolInfo {
|
struct ToolInfo {
|
||||||
std::string_view name; // "Eupak"
|
std::string_view name; // "Eupak"
|
||||||
std::string_view version; // v1.0.0
|
std::string_view version; // v1.0.0
|
||||||
|
@ -24,7 +25,7 @@ namespace tool {
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ToolMainInput {
|
struct ToolMainInput {
|
||||||
/// Tool commands to run.
|
/// Tool commands to provide to ToolMain().
|
||||||
std::span<std::shared_ptr<IToolCommand>> toolCommands;
|
std::span<std::shared_ptr<IToolCommand>> toolCommands;
|
||||||
|
|
||||||
// C arguments
|
// C arguments
|
||||||
|
|
Loading…
Reference in a new issue