*: Introduce "eupak" utility

I have been preparing for this for a while. Instead of having a bunch
of strewn out utilities, let's just have one solid multitool
which is nice to use.

This commit also removes europa_pack_extractor, as it's now unnesscary
and replaced with a better utility, that does more.

Creation wasn't implemented yet, but I really need to sleep. It can be done later, and pakcreate can be used as a temporary stopgap.
This commit is contained in:
Lily Tsuru 2022-09-22 05:43:35 -05:00
parent 87b02d8659
commit a95d104e7f
14 changed files with 469 additions and 98 deletions

View File

@ -16,7 +16,7 @@ endif()
include(cmake/Policies.cmake) include(cmake/Policies.cmake)
project(EuropaTools project(EuropaTools
VERSION 0.0.1 # Placeholder for sem-ver usage. Replace with real value later. VERSION 1.0.0
LANGUAGES C CXX LANGUAGES C CXX
DESCRIPTION "Tools for working with LEC Europa based games (Star Wars: Starfighter & Star Wars: Jedi Starfighter)" DESCRIPTION "Tools for working with LEC Europa based games (Star Wars: Starfighter & Star Wars: Jedi Starfighter)"
) )

View File

@ -38,6 +38,9 @@ namespace europa::io {
MapType& GetFiles(); MapType& GetFiles();
const MapType& GetFiles() const; const MapType& GetFiles() const;
// implement in cpp later, lazy and just wanna get this out :vvv
const structs::PakHeader& GetHeader() const { return header; }
private: private:
std::istream& stream; std::istream& stream;
bool invalid { false }; bool invalid { false };

View File

@ -6,28 +6,22 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
add_executable(europa_pack_extractor europa_pack_extractor.cpp) add_subdirectory(eupak)
target_link_libraries(europa_pack_extractor PUBLIC # Most of these utilities are being merged into eupak.
europa
indicators::indicators
)
add_executable(pakcreate pakcreate.cpp) add_executable(pakcreate pakcreate.cpp)
target_link_libraries(pakcreate PUBLIC target_link_libraries(pakcreate PUBLIC
europa europa
indicators::indicators indicators::indicators
) )
add_executable(texdump texdump.cpp) add_executable(texdump texdump.cpp)
target_link_libraries(texdump PUBLIC target_link_libraries(texdump PUBLIC
europa europa
) )
add_executable(paktest paktest.cpp) add_executable(paktest paktest.cpp)
target_link_libraries(paktest PUBLIC target_link_libraries(paktest PUBLIC
europa europa
) )

View File

@ -0,0 +1,29 @@
#
# EuropaTools
#
# (C) 2021-2022 modeco80 <lily.modeco80@protonmail.ch>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
add_executable(eupak
main.cpp
# Tasks
tasks/InfoTask.cpp
tasks/CreateTask.cpp
tasks/ExtractTask.cpp
)
target_link_libraries(eupak PUBLIC
europa
argparse::argparse
indicators::indicators
)
configure_file(EupakConfig.hpp.in
${CMAKE_CURRENT_BINARY_DIR}/EupakConfig.hpp
)
target_include_directories(eupak PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(eupak PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

View File

@ -0,0 +1,20 @@
//
// EuropaTools
//
// (C) 2021-2022 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
#ifndef EUROPA_EUPAK_COMMONDEFS_HPP
#define EUROPA_EUPAK_COMMONDEFS_HPP
#include <filesystem>
namespace eupak {
namespace fs = std::filesystem;
}
#endif // EUROPA_EUPAK_COMMONDEFS_HPP

View File

@ -0,0 +1,13 @@
//
// EuropaTools
//
// (C) 2021-2022 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
#define EUPAK_VERSION_STR "@PROJECT_VERSION@"
#define EUPAK_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define EUPAK_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define EUPAK_VERSION_PATCH @PROJECT_VERSION_PATCH@

113
src/tools/eupak/main.cpp Normal file
View File

@ -0,0 +1,113 @@
//
// EuropaTools
//
// (C) 2021-2022 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
#include <EupakConfig.hpp>
#include <tasks/InfoTask.hpp>
#include <tasks/ExtractTask.hpp>
#include <argparse/argparse.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);
argparse::ArgumentParser infoParser("info", EUPAK_VERSION_STR, argparse::default_arguments::help);
infoParser.add_description("Print information about a package file.");
infoParser.add_argument("input")
.help("Input archive")
.metavar("ARCHIVE");
infoParser.add_argument("--verbose")
.help("Increase information output verbosity (print a list of files).")
.default_value(false)
.implicit_value(true);
argparse::ArgumentParser extractParser("extract", EUPAK_VERSION_STR, argparse::default_arguments::help);
extractParser.add_description("Extract a package file.");
extractParser.add_argument("-d", "--directory")
.default_value("")
.metavar("DIRECTORY")
.help("Directory to extract to.");
extractParser.add_argument("input")
.help("Input archive")
.metavar("ARCHIVE");
extractParser.add_argument("--verbose")
.help("Increase extraction output verbosity")
.default_value(false)
.implicit_value(true);
argparse::ArgumentParser createParser("create", EUPAK_VERSION_STR, argparse::default_arguments::help);
createParser.add_description("Create a package file.");
createParser.add_argument("-d", "--directory")
.required()
.metavar("DIRECTORY")
.help("Directory to create archive from");
createParser.add_argument("output")
.help("Output archive")
.metavar("ARCHIVE");
createParser.add_argument("--verbose")
.help("Increase creation output verbosity")
.default_value(false)
.implicit_value(true);
parser.add_subparser(infoParser);
parser.add_subparser(extractParser);
parser.add_subparser(createParser);
try {
parser.parse_args(argc, argv);
} catch(std::runtime_error& error) {
std::cout << error.what() << '\n' << parser;
return 1;
}
// Run the given task
if(parser.is_subcommand_used("extract")) {
eupak::tasks::ExtractTask task;
eupak::tasks::ExtractTask::Arguments args;
args.verbose = extractParser.get<bool>("--verbose");
args.inputPath = eupak::fs::path(extractParser.get("input"));
if(extractParser.is_used("--directory")) {
args.outputDirectory = eupak::fs::path(extractParser.get("--directory"));
} else {
// Default to the basename appended to current path
// as a "relatively sane" default path to extract to.
// Should be okay.
args.outputDirectory = eupak::fs::current_path() / args.inputPath.stem();
}
std::cout << "Input PAK/PMDL: " << args.inputPath << '\n';
std::cout << "Output Directory: " << args.outputDirectory << '\n';
return task.Run(std::move(args));
}
if(parser.is_subcommand_used("info")) {
eupak::tasks::InfoTask task;
eupak::tasks::InfoTask::Arguments args;
args.verbose = infoParser.get<bool>("--verbose");
args.inputPath = eupak::fs::path(infoParser.get("input"));
return task.Run(std::move(args));
}
if(parser.is_subcommand_used("create")) {
std::cout << "Create command is currently unimplemented for now. Use pakcreate until it is\n";
return 1;
}
return 0;
}

View File

@ -0,0 +1,9 @@
//
// EuropaTools
//
// (C) 2021-2022 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
#include <tasks/CreateTask.hpp>

View File

@ -0,0 +1,25 @@
//
// EuropaTools
//
// (C) 2021-2022 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
#ifndef EUROPA_EUPAK_TASKS_CREATETASK_HPP
#define EUROPA_EUPAK_TASKS_CREATETASK_HPP
#include <CommonDefs.hpp>
namespace eupak::tasks {
struct CreateTask {
struct Arguments {
};
};
} // namespace europa
#endif // EUROPA_EUPAK_TASKS_CREATETASK_HPP

View File

@ -0,0 +1,92 @@
//
// EuropaTools
//
// (C) 2021-2022 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
#include <tasks/ExtractTask.hpp>
#include <europa/io/PakReader.hpp>
#include <fstream>
#include <indicators/cursor_control.hpp>
#include <indicators/progress_bar.hpp>
#include <iostream>
// this actually is pretty fast so maybe I won't bother doing crazy thread optimizations..
namespace eupak::tasks {
int ExtractTask::Run(ExtractTask::Arguments&& args) {
std::ifstream ifs(args.inputPath.string(), std::ifstream::binary);
if(!ifs) {
std::cout << "Error: Could not open file " << args.inputPath << ".\n";
return 1;
}
europa::io::PakReader reader(ifs);
reader.ReadData();
if(reader.Invalid()) {
std::cout << "Error: Invalid PAK/PMDL file " << args.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 = (args.outputDirectory / nameCopy);
if(!fs::exists(outpath.parent_path()))
fs::create_directories(outpath.parent_path());
reader.ReadFile(filename);
std::ofstream ofs(outpath.string(), std::ofstream::binary);
if(!ofs) {
std::cerr << "Could not open " << outpath << " for writing.\n";
continue;
}
if(args.verbose) {
std::cerr << "Extracting file \"" << filename << "\"...\n";
}
ofs.write(reinterpret_cast<const char*>(file.GetData().data()), static_cast<std::streampos>(file.GetTOCEntry().size));
ofs.flush();
progress.tick();
}
indicators::show_console_cursor(true);
return 0;
}
}

View File

@ -0,0 +1,30 @@
//
// EuropaTools
//
// (C) 2021-2022 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
#ifndef EUROPA_EUPAK_TASKS_EXTRACTTASK_HPP
#define EUROPA_EUPAK_TASKS_EXTRACTTASK_HPP
#include <CommonDefs.hpp>
namespace eupak::tasks {
struct ExtractTask {
struct Arguments {
fs::path inputPath;
fs::path outputDirectory;
bool verbose;
};
int Run(Arguments&& args);
};
}
#endif // EUROPATOOLS_EXTRACTTASK_H

View File

@ -0,0 +1,104 @@
//
// EuropaTools
//
// (C) 2021-2022 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
#include <tasks/InfoTask.hpp>
#include <europa/io/PakReader.hpp>
#include <algorithm>
#include <fstream>
#include <iostream>
namespace eupak::tasks {
namespace {
/**
* Format a raw amount of bytes to a human-readable unit.
* \param[in] bytes Size in bytes.
*/
std::string FormatUnit(std::uint64_t bytes) {
char buf[1024];
constexpr auto unit = 1024;
std::size_t exp {};
std::size_t div = unit;
if(bytes < unit) {
sprintf(buf, "%zu B", bytes);
return buf;
} else {
for(std::uint64_t i = bytes / unit; i >= unit; i /= unit) {
div *= unit;
exp++; // TODO: break if too big
}
}
#define CHECKED_LIT(literal, expression) (literal)[std::clamp(expression, std::size_t(0), sizeof(literal) - 1)]
sprintf(buf, "%0.2f %cB", float(bytes) / float(div), CHECKED_LIT("kMG", exp));
#undef CHECKED_LIT
return buf;
}
std::string FormatUnixTimestamp(time_t time, const std::string_view format) {
char buf[1024]{};
tm tmObject{};
localtime_r(&time, &tmObject);
auto count = std::strftime(&buf[0], sizeof(buf), format.data(), &tmObject);
// an error occured, probably.
if(count == -1)
return "";
return { buf, count };
}
}
constexpr static auto DATE_FORMAT = "%m/%d/%Y %r";
int InfoTask::Run(InfoTask::Arguments&& args) {
std::ifstream ifs(args.inputPath.string(), std::ifstream::binary);
if(!ifs) {
std::cout << "Error: Could not open file " << args.inputPath << ".\n";
return 1;
}
europa::io::PakReader reader(ifs);
reader.ReadData();
if(reader.Invalid()) {
std::cout << "Error: Invalid PAK/PMDL file " << args.inputPath << ".\n";
return 1;
}
std::string version = "Version 4 (Starfighter)";
if(reader.GetHeader().version == europa::structs::PakHeader::Version::Ver5)
version = "Version 5 (Jedi Starfighter)";
std::cout << "Archive " << args.inputPath << ":\n";
std::cout << " Created: " << FormatUnixTimestamp(reader.GetHeader().creationUnixTime, DATE_FORMAT) << '\n';
std::cout << " Version: " << version << '\n';
std::cout << " Size: " << FormatUnit(reader.GetHeader().tocOffset + reader.GetHeader().tocSize) << '\n';
std::cout << " File Count: " << reader.GetHeader().fileCount << " files\n";
// Print a detailed file list if verbose.
if(args.verbose) {
for(auto& [ filename, file ] : reader.GetFiles()) {
std::cout << "File \"" << filename << "\":\n";
std::cout << " Created: " << FormatUnixTimestamp(file.GetTOCEntry().creationUnixTime, DATE_FORMAT) << '\n';
std::cout << " Size: " << FormatUnit(file.GetTOCEntry().size) << '\n';
}
}
return 0;
}
}

View File

@ -0,0 +1,28 @@
//
// EuropaTools
//
// (C) 2021-2022 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
#ifndef EUROPA_EUPAK_TASKS_INFOTASK_HPP
#define EUROPA_EUPAK_TASKS_INFOTASK_HPP
#include <CommonDefs.hpp>
namespace eupak::tasks {
struct InfoTask {
struct Arguments {
fs::path inputPath;
bool verbose;
};
int Run(Arguments&& args);
};
}
#endif // EUROPA_EUPAK_TASKS_INFOTASK_HPP

View File

@ -1,89 +0,0 @@
//
// EuropaTools
//
// (C) 2021-2022 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
#include <europa/io/PakReader.hpp>
#include <filesystem>
#include <fstream>
#include <indicators/cursor_control.hpp>
#include <indicators/progress_bar.hpp>
#include <iostream>
namespace fs = std::filesystem;
int main(int argc, char** argv) {
if(argc != 2) {
std::cout << "Usage: " << argv[0] << " [path to Europa PAK file]";
return 1;
}
std::ifstream ifs(argv[1], std::ifstream::binary);
if(!ifs) {
std::cout << "Invalid file \"" << argv[1] << "\"\n";
return 1;
}
europa::io::PakReader reader(ifs);
auto baseDirectory = fs::path(argv[1]).stem();
reader.ReadData();
if(reader.Invalid()) {
std::cout << "Invalid pak data in file \"" << argv[1] << "\"\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 = (baseDirectory / nameCopy);
if(!fs::exists(outpath.parent_path()))
fs::create_directories(outpath.parent_path());
reader.ReadFile(filename);
std::ofstream ofs(outpath.string(), std::ofstream::binary);
if(!ofs) {
std::cerr << "Could not open \"" << outpath.string() << "\" for writing.\n";
continue;
}
ofs.write(reinterpret_cast<const char*>(file.GetData().data()), static_cast<std::streampos>(file.GetTOCEntry().size));
progress.tick();
}
indicators::show_console_cursor(true);
return 0;
}