diff --git a/CMakeLists.txt b/CMakeLists.txt index ef3ca99..451de7b 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,7 +16,7 @@ endif() include(cmake/Policies.cmake) project(EuropaTools - VERSION 0.0.1 # Placeholder for sem-ver usage. Replace with real value later. + VERSION 1.0.0 LANGUAGES C CXX DESCRIPTION "Tools for working with LEC Europa based games (Star Wars: Starfighter & Star Wars: Jedi Starfighter)" ) diff --git a/include/europa/io/PakReader.hpp b/include/europa/io/PakReader.hpp index 3b8cfe8..8917ecd 100644 --- a/include/europa/io/PakReader.hpp +++ b/include/europa/io/PakReader.hpp @@ -38,6 +38,9 @@ namespace europa::io { MapType& GetFiles(); const MapType& GetFiles() const; + // implement in cpp later, lazy and just wanna get this out :vvv + const structs::PakHeader& GetHeader() const { return header; } + private: std::istream& stream; bool invalid { false }; diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index 6bf5a3e..4a3e364 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -6,28 +6,22 @@ # 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 - europa - indicators::indicators - ) +# Most of these utilities are being merged into eupak. add_executable(pakcreate pakcreate.cpp) - target_link_libraries(pakcreate PUBLIC europa indicators::indicators ) add_executable(texdump texdump.cpp) - target_link_libraries(texdump PUBLIC europa ) add_executable(paktest paktest.cpp) - target_link_libraries(paktest PUBLIC europa ) diff --git a/src/tools/eupak/CMakeLists.txt b/src/tools/eupak/CMakeLists.txt new file mode 100644 index 0000000..6555586 --- /dev/null +++ b/src/tools/eupak/CMakeLists.txt @@ -0,0 +1,29 @@ +# +# EuropaTools +# +# (C) 2021-2022 modeco80 +# +# 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}) \ No newline at end of file diff --git a/src/tools/eupak/CommonDefs.hpp b/src/tools/eupak/CommonDefs.hpp new file mode 100644 index 0000000..33f1735 --- /dev/null +++ b/src/tools/eupak/CommonDefs.hpp @@ -0,0 +1,20 @@ +// +// EuropaTools +// +// (C) 2021-2022 modeco80 +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#ifndef EUROPA_EUPAK_COMMONDEFS_HPP +#define EUROPA_EUPAK_COMMONDEFS_HPP + +#include + +namespace eupak { + + namespace fs = std::filesystem; + +} + +#endif // EUROPA_EUPAK_COMMONDEFS_HPP diff --git a/src/tools/eupak/EupakConfig.hpp.in b/src/tools/eupak/EupakConfig.hpp.in new file mode 100644 index 0000000..9b3ca10 --- /dev/null +++ b/src/tools/eupak/EupakConfig.hpp.in @@ -0,0 +1,13 @@ +// +// EuropaTools +// +// (C) 2021-2022 modeco80 +// +// 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@ \ No newline at end of file diff --git a/src/tools/eupak/main.cpp b/src/tools/eupak/main.cpp new file mode 100644 index 0000000..5903a27 --- /dev/null +++ b/src/tools/eupak/main.cpp @@ -0,0 +1,113 @@ +// +// EuropaTools +// +// (C) 2021-2022 modeco80 +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#include + +#include +#include + +#include + +int main(int argc, char** argv) { + argparse::ArgumentParser parser("eupak", EUPAK_VERSION_STR); + parser.add_description("Eupak (Europa Package Multi-Tool) v" EUPAK_VERSION_STR); + + 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("--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("--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; +} \ No newline at end of file diff --git a/src/tools/eupak/tasks/CreateTask.cpp b/src/tools/eupak/tasks/CreateTask.cpp new file mode 100644 index 0000000..32ab99d --- /dev/null +++ b/src/tools/eupak/tasks/CreateTask.cpp @@ -0,0 +1,9 @@ +// +// EuropaTools +// +// (C) 2021-2022 modeco80 +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#include diff --git a/src/tools/eupak/tasks/CreateTask.hpp b/src/tools/eupak/tasks/CreateTask.hpp new file mode 100644 index 0000000..1bdd5fe --- /dev/null +++ b/src/tools/eupak/tasks/CreateTask.hpp @@ -0,0 +1,25 @@ +// +// EuropaTools +// +// (C) 2021-2022 modeco80 +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#ifndef EUROPA_EUPAK_TASKS_CREATETASK_HPP +#define EUROPA_EUPAK_TASKS_CREATETASK_HPP + +#include + +namespace eupak::tasks { + + struct CreateTask { + struct Arguments { + + }; + + }; + +} // namespace europa + +#endif // EUROPA_EUPAK_TASKS_CREATETASK_HPP diff --git a/src/tools/eupak/tasks/ExtractTask.cpp b/src/tools/eupak/tasks/ExtractTask.cpp new file mode 100644 index 0000000..1a40e6c --- /dev/null +++ b/src/tools/eupak/tasks/ExtractTask.cpp @@ -0,0 +1,92 @@ +// +// EuropaTools +// +// (C) 2021-2022 modeco80 +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#include + +#include +#include +#include +#include +#include + +// 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(file.GetData().data()), static_cast(file.GetTOCEntry().size)); + ofs.flush(); + progress.tick(); + } + + indicators::show_console_cursor(true); + return 0; + } + +} \ No newline at end of file diff --git a/src/tools/eupak/tasks/ExtractTask.hpp b/src/tools/eupak/tasks/ExtractTask.hpp new file mode 100644 index 0000000..92cebbe --- /dev/null +++ b/src/tools/eupak/tasks/ExtractTask.hpp @@ -0,0 +1,30 @@ +// +// EuropaTools +// +// (C) 2021-2022 modeco80 +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#ifndef EUROPA_EUPAK_TASKS_EXTRACTTASK_HPP +#define EUROPA_EUPAK_TASKS_EXTRACTTASK_HPP + +#include + +namespace eupak::tasks { + + struct ExtractTask { + + struct Arguments { + fs::path inputPath; + fs::path outputDirectory; + bool verbose; + }; + + int Run(Arguments&& args); + }; + + +} + +#endif // EUROPATOOLS_EXTRACTTASK_H diff --git a/src/tools/eupak/tasks/InfoTask.cpp b/src/tools/eupak/tasks/InfoTask.cpp new file mode 100644 index 0000000..13167d7 --- /dev/null +++ b/src/tools/eupak/tasks/InfoTask.cpp @@ -0,0 +1,104 @@ +// +// EuropaTools +// +// (C) 2021-2022 modeco80 +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#include + +#include +#include +#include +#include + +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; + } + +} \ No newline at end of file diff --git a/src/tools/eupak/tasks/InfoTask.hpp b/src/tools/eupak/tasks/InfoTask.hpp new file mode 100644 index 0000000..f4d93a2 --- /dev/null +++ b/src/tools/eupak/tasks/InfoTask.hpp @@ -0,0 +1,28 @@ +// +// EuropaTools +// +// (C) 2021-2022 modeco80 +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#ifndef EUROPA_EUPAK_TASKS_INFOTASK_HPP +#define EUROPA_EUPAK_TASKS_INFOTASK_HPP + +#include + +namespace eupak::tasks { + + struct InfoTask { + + struct Arguments { + fs::path inputPath; + bool verbose; + }; + + int Run(Arguments&& args); + }; + +} + +#endif // EUROPA_EUPAK_TASKS_INFOTASK_HPP diff --git a/src/tools/europa_pack_extractor.cpp b/src/tools/europa_pack_extractor.cpp deleted file mode 100644 index 34c33ec..0000000 --- a/src/tools/europa_pack_extractor.cpp +++ /dev/null @@ -1,89 +0,0 @@ -// -// EuropaTools -// -// (C) 2021-2022 modeco80 -// -// SPDX-License-Identifier: GPL-3.0-or-later -// - -#include -#include -#include -#include -#include -#include - -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(file.GetData().data()), static_cast(file.GetTOCEntry().size)); - progress.tick(); - } - - indicators::show_console_cursor(true); - return 0; -} \ No newline at end of file