diff --git a/include/europa/io/PakWriter.hpp b/include/europa/io/PakWriter.hpp index e3cc6d1..7d91502 100644 --- a/include/europa/io/PakWriter.hpp +++ b/include/europa/io/PakWriter.hpp @@ -22,6 +22,9 @@ namespace europa::io { struct PakWriter { void Init(structs::PakHeader::Version version); + // TODO: accessor for header + // use flattened vector format anyhow (less allocs, higher perf) + std::unordered_map& GetFiles(); /** diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index 4a3e364..2e4531e 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -9,13 +9,6 @@ add_subdirectory(eupak) # 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 diff --git a/src/tools/eupak/CMakeLists.txt b/src/tools/eupak/CMakeLists.txt index 6555586..6e648f9 100644 --- a/src/tools/eupak/CMakeLists.txt +++ b/src/tools/eupak/CMakeLists.txt @@ -9,6 +9,8 @@ add_executable(eupak main.cpp + Utils.cpp + # Tasks tasks/InfoTask.cpp tasks/CreateTask.cpp diff --git a/src/tools/eupak/Utils.cpp b/src/tools/eupak/Utils.cpp new file mode 100644 index 0000000..dff66e3 --- /dev/null +++ b/src/tools/eupak/Utils.cpp @@ -0,0 +1,62 @@ +// +// EuropaTools +// +// (C) 2021-2022 modeco80 +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +// MinGW bodges are cool. +#if defined(_WIN32) && !defined(_MSC_VER) + #define _POSIX_THREAD_SAFE_FUNCTIONS +#endif + +#include +#include + +namespace eupak { + + 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) { + // Break out if we're gonna set the exponent too high + if((exp + 1) > 2) + break; + + 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(std::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 }; + } + +} \ No newline at end of file diff --git a/src/tools/eupak/Utils.hpp b/src/tools/eupak/Utils.hpp new file mode 100644 index 0000000..02ec797 --- /dev/null +++ b/src/tools/eupak/Utils.hpp @@ -0,0 +1,35 @@ +// +// EuropaTools +// +// (C) 2021-2022 modeco80 +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#ifndef EUROPA_EUPAK_UTILS_HPP +#define EUROPA_EUPAK_UTILS_HPP + +#include +#include + +namespace eupak { + + /** + * Format a raw amount of bytes to a human-readable unit, if possible. + * \param[in] bytes Size in bytes. + */ + std::string FormatUnit(std::uint64_t bytes); + + /** + * Formats a Unix timestamp using the strftime() C function. + * + * \param[in] time The Unix timestamp time to format + * \param[in] format The format string + * \return A formatted string corresponding to user input. + */ + std::string FormatUnixTimestamp(std::time_t time, const std::string_view format); + +} + + +#endif // EUROPA_EUPAK_UTILS_HPP diff --git a/src/tools/eupak/main.cpp b/src/tools/eupak/main.cpp index 5903a27..dd8b13d 100644 --- a/src/tools/eupak/main.cpp +++ b/src/tools/eupak/main.cpp @@ -10,6 +10,7 @@ #include #include +#include #include @@ -46,17 +47,24 @@ int main(int argc, char** argv) { 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"); + .required() + .metavar("DIRECTORY") + .help("Directory to create archive from"); + + createParser.add_argument("-V","--archive-version") + .default_value("starfighter") + .help(R"(Output archive version. Either "starfighter" or "jedistarfighter".)") + .metavar("VERSION"); createParser.add_argument("output") - .help("Output archive") - .metavar("ARCHIVE"); + .required() + .help("Output archive") + .metavar("ARCHIVE"); + createParser.add_argument("--verbose") - .help("Increase creation output verbosity") - .default_value(false) - .implicit_value(true); + .help("Increase creation output verbosity") + .default_value(false) + .implicit_value(true); parser.add_subparser(infoParser); @@ -105,8 +113,35 @@ int main(int argc, char** argv) { } if(parser.is_subcommand_used("create")) { - std::cout << "Create command is currently unimplemented for now. Use pakcreate until it is\n"; - return 1; + eupak::tasks::CreateTask task; + eupak::tasks::CreateTask::Arguments args; + + args.verbose = createParser.get("--verbose"); + args.inputDirectory = eupak::fs::path(createParser.get("--directory")); + args.outputFile = eupak::fs::path(createParser.get("output")); + + if(createParser.is_used("--archive-version")) { + auto& versionStr = createParser.get("--archive-version"); + + if(versionStr == "starfighter") { + args.pakVersion = europa::structs::PakHeader::Version::Ver4; + } else if(versionStr == "jedistarfighter") { + args.pakVersion = europa::structs::PakHeader::Version::Ver5; + } else { + std::cout << "Error: Invalid version \"" << versionStr << "\"\n" << createParser; + return 1; + } + } else { + args.pakVersion = europa::structs::PakHeader::Version::Ver4; + } + + + if(!eupak::fs::is_directory(args.inputDirectory)) { + std::cout << "Error: Provided input isn't a directory\n" << createParser; + return 1; + } + + return task.Run(std::move(args)); } return 0; diff --git a/src/tools/eupak/tasks/CreateTask.cpp b/src/tools/eupak/tasks/CreateTask.cpp index 32ab99d..a9c842e 100644 --- a/src/tools/eupak/tasks/CreateTask.cpp +++ b/src/tools/eupak/tasks/CreateTask.cpp @@ -6,4 +6,101 @@ // SPDX-License-Identifier: GPL-3.0-or-later // +#include +#include +#include +#include +#include #include +#include + +namespace eupak::tasks { + + int CreateTask::Run(Arguments&& args) { + europa::io::PakWriter writer; + + writer.Init(args.pakVersion); + + auto currFile = 0; + auto fileCount = 0; + + // Count how many files we're gonna add to the archive + for(auto& ent : fs::recursive_directory_iterator(args.inputDirectory)) { + if(ent.is_directory()) + continue; + fileCount++; + } + + std::cout << "Going to write " << fileCount << " files into " << args.outputFile << '\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 { "Creating 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 + + for(auto& ent : fs::recursive_directory_iterator(args.inputDirectory)) { + if(ent.is_directory()) + continue; + + auto relativePathName = fs::relative(ent.path(), args.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) + ")"}); + + std::ifstream ifs(ent.path(), std::ifstream::binary); + + if(!ifs) { + std::cout << "Error: Couldn't open file for archive path \"" << relativePathName << "\"\n"; + return 1; + } + + europa::io::PakFile file; + europa::io::PakFile::DataType pakData; + + ifs.seekg(0, std::ifstream::end); + pakData.resize(ifs.tellg()); + ifs.seekg(0, std::ifstream::beg); + + ifs.read(reinterpret_cast(&pakData[0]), pakData.size()); + + file.SetData(std::move(pakData)); + file.FillTOCEntry(); + + file.GetTOCEntry().creationUnixTime = static_cast(lastModified.time_since_epoch().count()); + + writer.GetFiles()[relativePathName] = std::move(file); + + progress.tick(); + currFile++; + } + + std::ofstream ofs(args.outputFile.string(), std::ofstream::binary); + + if(!ofs) { + std::cout << "Error: Couldn't open " << args.outputFile << " for writing\n"; + return 1; + } + + writer.Write(ofs); + indicators::show_console_cursor(true); + return 0; + } + +} // namespace eupak::tasks \ No newline at end of file diff --git a/src/tools/eupak/tasks/CreateTask.hpp b/src/tools/eupak/tasks/CreateTask.hpp index 1bdd5fe..a65bb8c 100644 --- a/src/tools/eupak/tasks/CreateTask.hpp +++ b/src/tools/eupak/tasks/CreateTask.hpp @@ -11,13 +11,21 @@ #include +#include + + namespace eupak::tasks { struct CreateTask { struct Arguments { + fs::path inputDirectory; + fs::path outputFile; + bool verbose; + europa::structs::PakHeader::Version pakVersion; }; + int Run(Arguments&& args); }; } // namespace europa diff --git a/src/tools/eupak/tasks/ExtractTask.cpp b/src/tools/eupak/tasks/ExtractTask.cpp index 1a40e6c..532ab6e 100644 --- a/src/tools/eupak/tasks/ExtractTask.cpp +++ b/src/tools/eupak/tasks/ExtractTask.cpp @@ -18,7 +18,7 @@ namespace eupak::tasks { - int ExtractTask::Run(ExtractTask::Arguments&& args) { + int ExtractTask::Run(Arguments&& args) { std::ifstream ifs(args.inputPath.string(), std::ifstream::binary); if(!ifs) { diff --git a/src/tools/eupak/tasks/InfoTask.cpp b/src/tools/eupak/tasks/InfoTask.cpp index 36d3ce5..fab0a9d 100644 --- a/src/tools/eupak/tasks/InfoTask.cpp +++ b/src/tools/eupak/tasks/InfoTask.cpp @@ -6,12 +6,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later // - -// MinGW bodges are cool. -#if defined(_WIN32) && !defined(_MSC_VER) - #define _POSIX_THREAD_SAFE_FUNCTIONS -#endif - #include #include @@ -19,55 +13,13 @@ #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) { + int InfoTask::Run(Arguments&& args) { std::ifstream ifs(args.inputPath.string(), std::ifstream::binary); if(!ifs) { diff --git a/src/tools/pakcreate.cpp b/src/tools/pakcreate.cpp deleted file mode 100644 index 508fd83..0000000 --- a/src/tools/pakcreate.cpp +++ /dev/null @@ -1,81 +0,0 @@ -// -// EuropaTools -// -// (C) 2021-2022 modeco80 -// -// SPDX-License-Identifier: GPL-3.0-or-later -// - -// A test utility to regurgitate a pak. - -#include -#include -#include -#include -#include - -namespace fs = std::filesystem; - -using namespace europa; - -int main(int argc, char** argv) { - std::ofstream ofs(argv[2], std::ofstream::binary); - - if(!ofs) { - std::cout << "Couldn't open output PAK file\n"; - return 1; - } - - io::PakWriter writer; - - if(argv[3] != nullptr) { - if(!strcmp(argv[3], "--jedi")) { - std::cout << "Writing Jedi Starfighter archive\n"; - writer.Init(structs::PakHeader::Version::Ver5); - } - } else { - std::cout << "Writing Starfighter archive\n"; - writer.Init(structs::PakHeader::Version::Ver4); - } - - for(auto& ent : fs::recursive_directory_iterator(argv[1])) { - if(ent.is_directory()) - continue; - - auto relativePathName = fs::relative(ent.path(), argv[1]).string(); - - // Convert to Windows path separator always (that's what the game wants, after all) - for(auto& c : relativePathName) - if(c == '/') - c = '\\'; - - std::ifstream ifs(ent.path(), std::ifstream::binary); - - if(!ifs) { - std::cout << "ERROR: Couldn't open file for archive path \"" << relativePathName << "\"\n"; - return 1; - } - - io::PakFile file; - io::PakFile::DataType pakData; - - ifs.seekg(0, std::ifstream::end); - pakData.resize(ifs.tellg()); - ifs.seekg(0, std::ifstream::beg); - - ifs.read(reinterpret_cast(&pakData[0]), pakData.size()); - - file.SetData(std::move(pakData)); - file.FillTOCEntry(); - - file.GetTOCEntry().creationUnixTime = 0; - - //std::cout << "File \"" << relativePathName << "\"\n"; - writer.GetFiles()[relativePathName] = std::move(file); - } - - writer.Write(ofs); - - std::cout << "Wrote archive to \"" << argv[2] << "\"!\n"; - return 0; -} \ No newline at end of file