461 lines
14 KiB
C++
461 lines
14 KiB
C++
/**
|
|
* @file asset_packer.cpp
|
|
* @brief 资源打包工具
|
|
*
|
|
* 命令行工具,用于创建、查看和提取资源包。
|
|
*
|
|
* 用法:
|
|
* asset_packer create <output.pack> [options] <input files/dirs...>
|
|
* asset_packer list <pack file>
|
|
* asset_packer extract <pack file> <output dir>
|
|
*
|
|
* 选项:
|
|
* -c, --compression <algo> 压缩算法 (none, zstd, lz4, zlib),默认 zstd
|
|
* -l, --level <level> 压缩级别 (1-22),默认 3
|
|
* -e, --encrypt <key> 加密密钥
|
|
* -t, --encrypt-type <type> 加密类型 (xor, aes),默认 xor
|
|
* -v, --verbose 详细输出
|
|
* -h, --help 显示帮助
|
|
*/
|
|
|
|
#include <extra2d/asset/asset_pack.h>
|
|
#include <extra2d/asset/data_processor.h>
|
|
#include <extra2d/core/types.h>
|
|
|
|
#include <chrono>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 命令行参数解析
|
|
// ---------------------------------------------------------------------------
|
|
|
|
struct Options {
|
|
std::string command;
|
|
std::string outputFile;
|
|
std::string packFile;
|
|
std::string outputDir;
|
|
std::vector<std::string> inputs;
|
|
|
|
extra2d::Compression compression = extra2d::Compression::Zstd;
|
|
int compressionLevel = 3;
|
|
std::string encryptKey;
|
|
extra2d::Decryptor::Type encryptType = extra2d::Decryptor::Type::XOR;
|
|
|
|
bool verbose = false;
|
|
bool showHelp = false;
|
|
};
|
|
|
|
/**
|
|
* @brief 打印帮助信息
|
|
*/
|
|
void printHelp(const char* programName) {
|
|
std::cout << "Extra2D 资源打包工具 v1.0.0\n\n";
|
|
std::cout << "用法:\n";
|
|
std::cout << " " << programName << " create <output.pack> [options] <inputs...>\n";
|
|
std::cout << " " << programName << " list <pack file>\n";
|
|
std::cout << " " << programName << " extract <pack file> <output dir>\n\n";
|
|
std::cout << "命令:\n";
|
|
std::cout << " create 创建资源包\n";
|
|
std::cout << " list 列出资源包内容\n";
|
|
std::cout << " extract 提取资源包内容\n\n";
|
|
std::cout << "选项:\n";
|
|
std::cout << " -c, --compression <algo> 压缩算法 (none, zstd, lz4, zlib),默认 zstd\n";
|
|
std::cout << " -l, --level <level> 压缩级别 (1-22),默认 3\n";
|
|
std::cout << " -e, --encrypt <key> 加密密钥\n";
|
|
std::cout << " -t, --encrypt-type <type> 加密类型 (xor, aes),默认 xor\n";
|
|
std::cout << " -v, --verbose 详细输出\n";
|
|
std::cout << " -h, --help 显示帮助\n";
|
|
}
|
|
|
|
/**
|
|
* @brief 解析压缩算法名称
|
|
*/
|
|
extra2d::Compression parseCompression(const std::string& name) {
|
|
if (name == "none" || name == "None" || name == "NONE") {
|
|
return extra2d::Compression::None;
|
|
} else if (name == "zstd" || name == "Zstd" || name == "ZSTD") {
|
|
return extra2d::Compression::Zstd;
|
|
} else if (name == "lz4" || name == "LZ4") {
|
|
return extra2d::Compression::LZ4;
|
|
} else if (name == "zlib" || name == "Zlib" || name == "ZLIB") {
|
|
return extra2d::Compression::Zlib;
|
|
}
|
|
return extra2d::Compression::Zstd;
|
|
}
|
|
|
|
/**
|
|
* @brief 解析加密类型
|
|
*/
|
|
extra2d::Decryptor::Type parseEncryptType(const std::string& name) {
|
|
if (name == "xor" || name == "XOR") {
|
|
return extra2d::Decryptor::Type::XOR;
|
|
} else if (name == "aes" || name == "AES" || name == "aes256") {
|
|
return extra2d::Decryptor::Type::AES256;
|
|
}
|
|
return extra2d::Decryptor::Type::XOR;
|
|
}
|
|
|
|
/**
|
|
* @brief 解析命令行参数
|
|
*/
|
|
bool parseArgs(int argc, char* argv[], Options& opts) {
|
|
if (argc < 2) {
|
|
opts.showHelp = true;
|
|
return false;
|
|
}
|
|
|
|
opts.command = argv[1];
|
|
|
|
if (opts.command == "-h" || opts.command == "--help") {
|
|
opts.showHelp = true;
|
|
return true;
|
|
}
|
|
|
|
for (int i = 2; i < argc; ++i) {
|
|
std::string arg = argv[i];
|
|
|
|
if (arg == "-h" || arg == "--help") {
|
|
opts.showHelp = true;
|
|
return true;
|
|
} else if (arg == "-v" || arg == "--verbose") {
|
|
opts.verbose = true;
|
|
} else if (arg == "-c" || arg == "--compression") {
|
|
if (i + 1 < argc) {
|
|
opts.compression = parseCompression(argv[++i]);
|
|
}
|
|
} else if (arg == "-l" || arg == "--level") {
|
|
if (i + 1 < argc) {
|
|
opts.compressionLevel = std::stoi(argv[++i]);
|
|
}
|
|
} else if (arg == "-e" || arg == "--encrypt") {
|
|
if (i + 1 < argc) {
|
|
opts.encryptKey = argv[++i];
|
|
}
|
|
} else if (arg == "-t" || arg == "--encrypt-type") {
|
|
if (i + 1 < argc) {
|
|
opts.encryptType = parseEncryptType(argv[++i]);
|
|
}
|
|
} else if (arg[0] != '-') {
|
|
if (opts.command == "create") {
|
|
if (opts.outputFile.empty()) {
|
|
opts.outputFile = arg;
|
|
} else {
|
|
opts.inputs.push_back(arg);
|
|
}
|
|
} else if (opts.command == "list") {
|
|
if (opts.packFile.empty()) {
|
|
opts.packFile = arg;
|
|
}
|
|
} else if (opts.command == "extract") {
|
|
if (opts.packFile.empty()) {
|
|
opts.packFile = arg;
|
|
} else if (opts.outputDir.empty()) {
|
|
opts.outputDir = arg;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 创建资源包
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @brief 格式化文件大小
|
|
*/
|
|
std::string formatSize(size_t bytes) {
|
|
const char* units[] = {"B", "KB", "MB", "GB"};
|
|
int unitIndex = 0;
|
|
double size = static_cast<double>(bytes);
|
|
|
|
while (size >= 1024.0 && unitIndex < 3) {
|
|
size /= 1024.0;
|
|
++unitIndex;
|
|
}
|
|
|
|
std::ostringstream ss;
|
|
ss.precision(2);
|
|
ss << size << " " << units[unitIndex];
|
|
return ss.str();
|
|
}
|
|
|
|
/**
|
|
* @brief 创建资源包
|
|
*/
|
|
int createPack(const Options& opts) {
|
|
if (opts.outputFile.empty()) {
|
|
std::cerr << "错误: 未指定输出文件\n";
|
|
return 1;
|
|
}
|
|
|
|
if (opts.inputs.empty()) {
|
|
std::cerr << "错误: 未指定输入文件或目录\n";
|
|
return 1;
|
|
}
|
|
|
|
auto startTime = std::chrono::high_resolution_clock::now();
|
|
|
|
extra2d::AssetPackBuilder builder(opts.compression, opts.compressionLevel);
|
|
|
|
if (!opts.encryptKey.empty()) {
|
|
builder.setEncryption(opts.encryptKey, opts.encryptType);
|
|
}
|
|
|
|
size_t totalFiles = 0;
|
|
size_t totalSize = 0;
|
|
|
|
for (const auto& input : opts.inputs) {
|
|
fs::path inputPath(input);
|
|
|
|
if (!fs::exists(inputPath)) {
|
|
std::cerr << "警告: 跳过不存在的路径: " << input << "\n";
|
|
continue;
|
|
}
|
|
|
|
if (fs::is_directory(inputPath)) {
|
|
if (opts.verbose) {
|
|
std::cout << "扫描目录: " << input << "\n";
|
|
}
|
|
|
|
for (const auto& entry : fs::recursive_directory_iterator(inputPath)) {
|
|
if (entry.is_regular_file()) {
|
|
fs::path relativePath = fs::relative(entry.path(), inputPath);
|
|
std::string packPath = relativePath.generic_string();
|
|
|
|
if (opts.verbose) {
|
|
std::cout << " 添加: " << packPath << "\n";
|
|
}
|
|
|
|
if (builder.addFile(entry.path().string(), packPath)) {
|
|
++totalFiles;
|
|
totalSize += fs::file_size(entry.path());
|
|
}
|
|
}
|
|
}
|
|
} else if (fs::is_regular_file(inputPath)) {
|
|
std::string packPath = inputPath.filename().string();
|
|
|
|
if (opts.verbose) {
|
|
std::cout << "添加文件: " << packPath << "\n";
|
|
}
|
|
|
|
if (builder.addFile(inputPath.string(), packPath)) {
|
|
++totalFiles;
|
|
totalSize += fs::file_size(inputPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (totalFiles == 0) {
|
|
std::cerr << "错误: 没有文件被添加到资源包\n";
|
|
return 1;
|
|
}
|
|
|
|
if (opts.verbose) {
|
|
std::cout << "\n构建资源包: " << opts.outputFile << "\n";
|
|
}
|
|
|
|
if (!builder.build(opts.outputFile)) {
|
|
std::cerr << "错误: 构建资源包失败\n";
|
|
return 1;
|
|
}
|
|
|
|
auto endTime = std::chrono::high_resolution_clock::now();
|
|
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
|
|
|
|
size_t packSize = fs::file_size(opts.outputFile);
|
|
double ratio = totalSize > 0 ? (100.0 * packSize / totalSize) : 0.0;
|
|
|
|
std::cout << "\n完成!\n";
|
|
std::cout << " 文件数量: " << totalFiles << "\n";
|
|
std::cout << " 原始大小: " << formatSize(totalSize) << "\n";
|
|
std::cout << " 压缩大小: " << formatSize(packSize) << "\n";
|
|
std::cout << " 压缩比率: " << std::fixed << std::setprecision(1) << ratio << "%\n";
|
|
std::cout << " 耗时: " << duration.count() << "ms\n";
|
|
|
|
return 0;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 列出资源包内容
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @brief 列出资源包内容
|
|
*/
|
|
int listPack(const Options& opts) {
|
|
if (opts.packFile.empty()) {
|
|
std::cerr << "错误: 未指定资源包文件\n";
|
|
return 1;
|
|
}
|
|
|
|
if (!fs::exists(opts.packFile)) {
|
|
std::cerr << "错误: 资源包文件不存在: " << opts.packFile << "\n";
|
|
return 1;
|
|
}
|
|
|
|
extra2d::AssetPack pack;
|
|
if (!pack.open(opts.packFile)) {
|
|
std::cerr << "错误: 无法打开资源包\n";
|
|
return 1;
|
|
}
|
|
|
|
const auto& header = pack.header();
|
|
|
|
std::cout << "资源包: " << opts.packFile << "\n";
|
|
std::cout << "----------------------------------------\n";
|
|
std::cout << "版本: " << header.version << "\n";
|
|
std::cout << "条目数: " << pack.count() << "\n";
|
|
std::cout << "----------------------------------------\n\n";
|
|
|
|
auto assets = pack.assets();
|
|
|
|
std::cout << std::left << std::setw(40) << "路径"
|
|
<< std::right << std::setw(12) << "大小" << "\n";
|
|
std::cout << std::string(52, '-') << "\n";
|
|
|
|
size_t totalSize = 0;
|
|
for (const auto& id : assets) {
|
|
auto* entry = pack.getEntry(id);
|
|
if (entry) {
|
|
std::cout << std::left << std::setw(40) << id.path
|
|
<< std::right << std::setw(12) << formatSize(entry->size) << "\n";
|
|
totalSize += entry->size;
|
|
}
|
|
}
|
|
|
|
std::cout << std::string(52, '-') << "\n";
|
|
std::cout << std::left << std::setw(40) << "总计"
|
|
<< std::right << std::setw(12) << formatSize(totalSize) << "\n";
|
|
|
|
pack.close();
|
|
return 0;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 提取资源包
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @brief 提取资源包内容
|
|
*/
|
|
int extractPack(const Options& opts) {
|
|
if (opts.packFile.empty()) {
|
|
std::cerr << "错误: 未指定资源包文件\n";
|
|
return 1;
|
|
}
|
|
|
|
if (opts.outputDir.empty()) {
|
|
std::cerr << "错误: 未指定输出目录\n";
|
|
return 1;
|
|
}
|
|
|
|
if (!fs::exists(opts.packFile)) {
|
|
std::cerr << "错误: 资源包文件不存在: " << opts.packFile << "\n";
|
|
return 1;
|
|
}
|
|
|
|
extra2d::AssetPack pack;
|
|
if (!pack.open(opts.packFile)) {
|
|
std::cerr << "错误: 无法打开资源包\n";
|
|
return 1;
|
|
}
|
|
|
|
fs::path outputDir(opts.outputDir);
|
|
|
|
if (!fs::exists(outputDir)) {
|
|
fs::create_directories(outputDir);
|
|
}
|
|
|
|
auto assets = pack.assets();
|
|
size_t extracted = 0;
|
|
size_t failed = 0;
|
|
|
|
std::cout << "提取资源包到: " << opts.outputDir << "\n\n";
|
|
|
|
for (const auto& id : assets) {
|
|
fs::path outputPath = outputDir / id.path;
|
|
fs::path parentDir = outputPath.parent_path();
|
|
|
|
if (!parentDir.empty() && !fs::exists(parentDir)) {
|
|
fs::create_directories(parentDir);
|
|
}
|
|
|
|
auto data = pack.read(id);
|
|
|
|
if (data.empty()) {
|
|
std::cerr << "警告: 无法读取: " << id.path << "\n";
|
|
++failed;
|
|
continue;
|
|
}
|
|
|
|
std::ofstream outFile(outputPath, std::ios::binary);
|
|
if (!outFile) {
|
|
std::cerr << "警告: 无法创建文件: " << outputPath.string() << "\n";
|
|
++failed;
|
|
continue;
|
|
}
|
|
|
|
outFile.write(reinterpret_cast<const char*>(data.data()), data.size());
|
|
|
|
if (opts.verbose) {
|
|
std::cout << " 提取: " << id.path << " (" << formatSize(data.size()) << ")\n";
|
|
}
|
|
|
|
++extracted;
|
|
}
|
|
|
|
pack.close();
|
|
|
|
std::cout << "\n完成! 提取 " << extracted << " 个文件";
|
|
if (failed > 0) {
|
|
std::cout << ", " << failed << " 个失败";
|
|
}
|
|
std::cout << "\n";
|
|
|
|
return failed > 0 ? 1 : 0;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 主函数
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#include <iomanip>
|
|
|
|
int main(int argc, char* argv[]) {
|
|
Options opts;
|
|
|
|
if (!parseArgs(argc, argv, opts)) {
|
|
printHelp(argv[0]);
|
|
return 1;
|
|
}
|
|
|
|
if (opts.showHelp) {
|
|
printHelp(argv[0]);
|
|
return 0;
|
|
}
|
|
|
|
if (opts.command == "create") {
|
|
return createPack(opts);
|
|
} else if (opts.command == "list") {
|
|
return listPack(opts);
|
|
} else if (opts.command == "extract") {
|
|
return extractPack(opts);
|
|
} else {
|
|
std::cerr << "错误: 未知命令: " << opts.command << "\n";
|
|
printHelp(argv[0]);
|
|
return 1;
|
|
}
|
|
}
|