eupak: Implement creation command

Ditto. Also has a cute little progress indicator (more detailed
progress will probably have to be done later..)

Removes pakcreate, as it's now replaced by eupak for good.
This commit is contained in:
Lily Tsuru 2022-09-22 12:32:28 -05:00
parent a27ab63c96
commit e82d693dfc
11 changed files with 256 additions and 150 deletions

View File

@ -22,6 +22,9 @@ namespace europa::io {
struct PakWriter { struct PakWriter {
void Init(structs::PakHeader::Version version); void Init(structs::PakHeader::Version version);
// TODO: accessor for header
// use flattened vector format anyhow (less allocs, higher perf)
std::unordered_map<std::string, PakFile>& GetFiles(); std::unordered_map<std::string, PakFile>& GetFiles();
/** /**

View File

@ -9,13 +9,6 @@
add_subdirectory(eupak) add_subdirectory(eupak)
# Most of these utilities are being merged into 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) add_executable(texdump texdump.cpp)
target_link_libraries(texdump PUBLIC target_link_libraries(texdump PUBLIC
europa europa

View File

@ -9,6 +9,8 @@
add_executable(eupak add_executable(eupak
main.cpp main.cpp
Utils.cpp
# Tasks # Tasks
tasks/InfoTask.cpp tasks/InfoTask.cpp
tasks/CreateTask.cpp tasks/CreateTask.cpp

62
src/tools/eupak/Utils.cpp Normal file
View File

@ -0,0 +1,62 @@
//
// EuropaTools
//
// (C) 2021-2022 modeco80 <lily.modeco80@protonmail.ch>
//
// 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 <Utils.hpp>
#include <algorithm>
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 };
}
}

35
src/tools/eupak/Utils.hpp Normal file
View File

@ -0,0 +1,35 @@
//
// EuropaTools
//
// (C) 2021-2022 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
#ifndef EUROPA_EUPAK_UTILS_HPP
#define EUROPA_EUPAK_UTILS_HPP
#include <string>
#include <ctime>
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

View File

@ -10,6 +10,7 @@
#include <tasks/InfoTask.hpp> #include <tasks/InfoTask.hpp>
#include <tasks/ExtractTask.hpp> #include <tasks/ExtractTask.hpp>
#include <tasks/CreateTask.hpp>
#include <argparse/argparse.hpp> #include <argparse/argparse.hpp>
@ -46,17 +47,24 @@ int main(int argc, char** argv) {
argparse::ArgumentParser createParser("create", EUPAK_VERSION_STR, argparse::default_arguments::help); argparse::ArgumentParser createParser("create", EUPAK_VERSION_STR, argparse::default_arguments::help);
createParser.add_description("Create a package file."); createParser.add_description("Create a package file.");
createParser.add_argument("-d", "--directory") createParser.add_argument("-d", "--directory")
.required() .required()
.metavar("DIRECTORY") .metavar("DIRECTORY")
.help("Directory to create archive from"); .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") createParser.add_argument("output")
.help("Output archive") .required()
.metavar("ARCHIVE"); .help("Output archive")
.metavar("ARCHIVE");
createParser.add_argument("--verbose") createParser.add_argument("--verbose")
.help("Increase creation output verbosity") .help("Increase creation output verbosity")
.default_value(false) .default_value(false)
.implicit_value(true); .implicit_value(true);
parser.add_subparser(infoParser); parser.add_subparser(infoParser);
@ -105,8 +113,35 @@ int main(int argc, char** argv) {
} }
if(parser.is_subcommand_used("create")) { if(parser.is_subcommand_used("create")) {
std::cout << "Create command is currently unimplemented for now. Use pakcreate until it is\n"; eupak::tasks::CreateTask task;
return 1; eupak::tasks::CreateTask::Arguments args;
args.verbose = createParser.get<bool>("--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; return 0;

View File

@ -6,4 +6,101 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// //
#include <europa/io/PakWriter.hpp>
#include <fstream>
#include <indicators/cursor_control.hpp>
#include <indicators/progress_bar.hpp>
#include <iostream>
#include <tasks/CreateTask.hpp> #include <tasks/CreateTask.hpp>
#include <Utils.hpp>
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<char*>(&pakData[0]), pakData.size());
file.SetData(std::move(pakData));
file.FillTOCEntry();
file.GetTOCEntry().creationUnixTime = static_cast<std::uint32_t>(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

View File

@ -11,13 +11,21 @@
#include <CommonDefs.hpp> #include <CommonDefs.hpp>
#include <europa/structs/Pak.hpp>
namespace eupak::tasks { namespace eupak::tasks {
struct CreateTask { struct CreateTask {
struct Arguments { struct Arguments {
fs::path inputDirectory;
fs::path outputFile;
bool verbose;
europa::structs::PakHeader::Version pakVersion;
}; };
int Run(Arguments&& args);
}; };
} // namespace europa } // namespace europa

View File

@ -18,7 +18,7 @@
namespace eupak::tasks { namespace eupak::tasks {
int ExtractTask::Run(ExtractTask::Arguments&& args) { int ExtractTask::Run(Arguments&& args) {
std::ifstream ifs(args.inputPath.string(), std::ifstream::binary); std::ifstream ifs(args.inputPath.string(), std::ifstream::binary);
if(!ifs) { if(!ifs) {

View File

@ -6,12 +6,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later // 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 <tasks/InfoTask.hpp> #include <tasks/InfoTask.hpp>
#include <europa/io/PakReader.hpp> #include <europa/io/PakReader.hpp>
@ -19,55 +13,13 @@
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <Utils.hpp>
namespace eupak::tasks { 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"; 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); std::ifstream ifs(args.inputPath.string(), std::ifstream::binary);
if(!ifs) { if(!ifs) {

View File

@ -1,81 +0,0 @@
//
// EuropaTools
//
// (C) 2021-2022 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
// A test utility to regurgitate a pak.
#include <europa/io/PakReader.hpp>
#include <europa/io/PakWriter.hpp>
#include <filesystem>
#include <fstream>
#include <iostream>
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<char*>(&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;
}