mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-07 17:43:51 +00:00
Compares the source file's mtime against the sidecar's; if the
sidecar is newer, the conversion is skipped and counted into
stats.skipped. Re-running --upgrade-extract on a fully-converted
tree is now nearly free (just an mtime check per file).
asset_extract --upgrade-extract Data/expansions/wotlk
Walking ... (first run)
JSON (DBC→JSON) : 240 ok
asset_extract --upgrade-extract Data/expansions/wotlk
Walking ... (second run, all sidecars up to date)
up-to-date (skip) : 240
JSON (DBC→JSON) : 0 ok
emitOpenFormats() takes a new optional 'incremental' flag (default
false to preserve the asset_extract main-loop's overwrite behavior
since fresh extraction always wants new sidecars).
Verified end-to-end with a hand-built DBC: first run converts,
second run reports 'up-to-date (skip): 1'.
231 lines
11 KiB
C++
231 lines
11 KiB
C++
#include "extractor.hpp"
|
|
#include "open_format_emitter.hpp"
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <iostream>
|
|
#include <string>
|
|
#include <cstring>
|
|
|
|
static void printUsage(const char* prog) {
|
|
std::cout << "Usage: " << prog << " --mpq-dir <path> --output <path> [options]\n"
|
|
<< "\n"
|
|
<< "Extract WoW MPQ archives to organized loose files with manifest.\n"
|
|
<< "\n"
|
|
<< "Required:\n"
|
|
<< " --mpq-dir <path> Path to WoW Data directory containing MPQ files\n"
|
|
<< " --output <path> Output directory for extracted assets\n"
|
|
<< "\n"
|
|
<< "Options:\n"
|
|
<< " --expansion <id> Expansion: classic, turtle, tbc, wotlk (default: auto-detect)\n"
|
|
<< " --locale <id> Locale: enUS, deDE, frFR, etc. (default: auto-detect)\n"
|
|
<< " --only-used-dbcs Extract only the DBCs wowee uses (no other assets)\n"
|
|
<< " --skip-dbc Do not extract DBFilesClient/*.dbc (visual assets only)\n"
|
|
<< " --dbc-csv Convert selected DBFilesClient/*.dbc to CSV under\n"
|
|
<< " <output>/expansions/<expansion>/db/*.csv (for committing)\n"
|
|
<< " --listfile <path> External listfile for MPQ file enumeration (auto-detected)\n"
|
|
<< " --reference-manifest <path>\n"
|
|
<< " Only extract files NOT in this manifest (delta extraction)\n"
|
|
<< " --dbc-csv-out <dir> Write CSV DBCs into <dir> (overrides default output path)\n"
|
|
<< " --emit-png Emit foo.png next to every extracted foo.blp\n"
|
|
<< " --emit-json-dbc Emit foo.json next to every extracted foo.dbc\n"
|
|
<< " --emit-wom Emit foo.wom next to every extracted foo.m2 (+skin)\n"
|
|
<< " --emit-wob Emit foo.wob next to every extracted foo.wmo (+groups)\n"
|
|
<< " --emit-terrain Emit foo.whm + foo.wot + foo.woc next to every foo.adt\n"
|
|
<< " --emit-open Shortcut: enable every open-format emitter (png+json+wom+wob+terrain)\n"
|
|
<< " --upgrade-extract <dir>\n"
|
|
<< " Standalone post-extract pass on an existing tree —\n"
|
|
<< " writes open-format sidecars without re-running MPQ extract\n"
|
|
<< " --verify CRC32 verify all extracted files\n"
|
|
<< " --threads <N> Number of extraction threads (default: auto)\n"
|
|
<< " --verbose Verbose output\n"
|
|
<< " --help Show this help\n";
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
wowee::tools::Extractor::Options opts;
|
|
std::string expansion;
|
|
std::string locale;
|
|
// Standalone open-format emit mode: skip MPQ enumeration entirely
|
|
// and just walk an existing extracted tree, writing sidecars in
|
|
// place. Useful for upgrading an old extraction without re-running
|
|
// it from MPQ. Triggered by --upgrade-extract <dir>.
|
|
std::string upgradeDir;
|
|
|
|
for (int i = 1; i < argc; ++i) {
|
|
if (std::strcmp(argv[i], "--mpq-dir") == 0 && i + 1 < argc) {
|
|
opts.mpqDir = argv[++i];
|
|
} else if (std::strcmp(argv[i], "--output") == 0 && i + 1 < argc) {
|
|
opts.outputDir = argv[++i];
|
|
} else if (std::strcmp(argv[i], "--expansion") == 0 && i + 1 < argc) {
|
|
expansion = argv[++i];
|
|
} else if (std::strcmp(argv[i], "--locale") == 0 && i + 1 < argc) {
|
|
locale = argv[++i];
|
|
} else if (std::strcmp(argv[i], "--threads") == 0 && i + 1 < argc) {
|
|
opts.threads = std::atoi(argv[++i]);
|
|
} else if (std::strcmp(argv[i], "--only-used-dbcs") == 0) {
|
|
opts.onlyUsedDbcs = true;
|
|
} else if (std::strcmp(argv[i], "--skip-dbc") == 0) {
|
|
opts.skipDbcExtraction = true;
|
|
} else if (std::strcmp(argv[i], "--dbc-csv") == 0) {
|
|
opts.generateDbcCsv = true;
|
|
} else if (std::strcmp(argv[i], "--emit-png") == 0) {
|
|
opts.emitPng = true;
|
|
} else if (std::strcmp(argv[i], "--emit-json-dbc") == 0) {
|
|
opts.emitJsonDbc = true;
|
|
} else if (std::strcmp(argv[i], "--emit-wom") == 0) {
|
|
opts.emitWom = true;
|
|
} else if (std::strcmp(argv[i], "--emit-wob") == 0) {
|
|
opts.emitWob = true;
|
|
} else if (std::strcmp(argv[i], "--emit-terrain") == 0) {
|
|
opts.emitTerrain = true;
|
|
} else if (std::strcmp(argv[i], "--emit-open") == 0) {
|
|
// Meta-flag: turn on every available open-format emitter.
|
|
opts.emitPng = true;
|
|
opts.emitJsonDbc = true;
|
|
opts.emitWom = true;
|
|
opts.emitWob = true;
|
|
opts.emitTerrain = true;
|
|
} else if (std::strcmp(argv[i], "--dbc-csv-out") == 0 && i + 1 < argc) {
|
|
opts.dbcCsvOutputDir = argv[++i];
|
|
} else if (std::strcmp(argv[i], "--listfile") == 0 && i + 1 < argc) {
|
|
opts.listFile = argv[++i];
|
|
} else if (std::strcmp(argv[i], "--reference-manifest") == 0 && i + 1 < argc) {
|
|
opts.referenceManifest = argv[++i];
|
|
} else if (std::strcmp(argv[i], "--verify") == 0) {
|
|
opts.verify = true;
|
|
} else if (std::strcmp(argv[i], "--verbose") == 0) {
|
|
opts.verbose = true;
|
|
} else if (std::strcmp(argv[i], "--upgrade-extract") == 0 && i + 1 < argc) {
|
|
upgradeDir = argv[++i];
|
|
// Implies --emit-open if no individual emit flag was set.
|
|
if (!opts.emitPng && !opts.emitJsonDbc && !opts.emitWom &&
|
|
!opts.emitWob && !opts.emitTerrain) {
|
|
opts.emitPng = opts.emitJsonDbc = opts.emitWom =
|
|
opts.emitWob = opts.emitTerrain = true;
|
|
}
|
|
} else if (std::strcmp(argv[i], "--help") == 0 || std::strcmp(argv[i], "-h") == 0) {
|
|
printUsage(argv[0]);
|
|
return 0;
|
|
} else {
|
|
std::cerr << "Unknown option: " << argv[i] << "\n";
|
|
printUsage(argv[0]);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// --upgrade-extract: standalone post-extract pass on an existing tree.
|
|
if (!upgradeDir.empty()) {
|
|
if (!std::filesystem::exists(upgradeDir)) {
|
|
std::cerr << "upgrade-extract: " << upgradeDir << " does not exist\n";
|
|
return 1;
|
|
}
|
|
std::cout << "Walking " << upgradeDir
|
|
<< " for open-format upgrades...\n";
|
|
auto t0 = std::chrono::steady_clock::now();
|
|
wowee::tools::OpenFormatStats stats;
|
|
// Pass 0 to auto-detect threads (or honor user --threads override).
|
|
unsigned int t = opts.threads > 0 ? static_cast<unsigned int>(opts.threads) : 0;
|
|
// upgrade-extract is always incremental — skip files whose sidecar
|
|
// is already up to date so re-runs are cheap.
|
|
wowee::tools::emitOpenFormats(upgradeDir,
|
|
opts.emitPng, opts.emitJsonDbc,
|
|
opts.emitWom, opts.emitWob,
|
|
opts.emitTerrain, stats, t,
|
|
/*incremental=*/true);
|
|
auto secs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() - t0).count() / 1000.0;
|
|
std::cout << " elapsed : " << secs << " s\n";
|
|
if (stats.skipped > 0)
|
|
std::cout << " up-to-date (skip) : " << stats.skipped << "\n";
|
|
std::cout << " PNG (BLP→PNG) : " << stats.pngOk << " ok"
|
|
<< (stats.pngFail ? ", " + std::to_string(stats.pngFail) + " failed" : "") << "\n";
|
|
std::cout << " JSON (DBC→JSON) : " << stats.jsonDbcOk << " ok"
|
|
<< (stats.jsonDbcFail ? ", " + std::to_string(stats.jsonDbcFail) + " failed" : "") << "\n";
|
|
std::cout << " WOM (M2→WOM) : " << stats.womOk << " ok"
|
|
<< (stats.womFail ? ", " + std::to_string(stats.womFail) + " failed" : "") << "\n";
|
|
std::cout << " WOB (WMO→WOB) : " << stats.wobOk << " ok"
|
|
<< (stats.wobFail ? ", " + std::to_string(stats.wobFail) + " failed" : "") << "\n";
|
|
std::cout << " WHM/WOT/WOC (ADT) : " << stats.whmOk << " ok"
|
|
<< (stats.whmFail ? ", " + std::to_string(stats.whmFail) + " failed" : "") << "\n";
|
|
return 0;
|
|
}
|
|
|
|
if (opts.mpqDir.empty() || opts.outputDir.empty()) {
|
|
std::cerr << "Error: --mpq-dir and --output are required\n\n";
|
|
printUsage(argv[0]);
|
|
return 1;
|
|
}
|
|
|
|
// Auto-detect expansion if not specified
|
|
if (expansion.empty() || expansion == "auto") {
|
|
expansion = wowee::tools::Extractor::detectExpansion(opts.mpqDir);
|
|
if (expansion.empty()) {
|
|
std::cerr << "Error: Could not auto-detect expansion. No known MPQ archives found in: "
|
|
<< opts.mpqDir << "\n"
|
|
<< "Specify manually with --expansion classic|tbc|wotlk\n";
|
|
return 1;
|
|
}
|
|
std::cout << "Auto-detected expansion: " << expansion << "\n";
|
|
}
|
|
opts.expansion = expansion;
|
|
|
|
// Auto-detect locale if not specified
|
|
if (locale.empty() || locale == "auto") {
|
|
locale = wowee::tools::Extractor::detectLocale(opts.mpqDir);
|
|
if (locale.empty()) {
|
|
std::cerr << "Warning: No locale directory found, skipping locale-specific archives\n";
|
|
} else {
|
|
std::cout << "Auto-detected locale: " << locale << "\n";
|
|
}
|
|
}
|
|
opts.locale = locale;
|
|
|
|
// Auto-detect external listfile if not specified
|
|
if (opts.listFile.empty()) {
|
|
// Look next to the binary, then in the source tree
|
|
namespace fs = std::filesystem;
|
|
std::string binDir = fs::path(argv[0]).parent_path().string();
|
|
for (const auto& candidate : {
|
|
binDir + "/listfile.txt",
|
|
binDir + "/../../../tools/asset_extract/listfile.txt",
|
|
opts.mpqDir + "/listfile.txt",
|
|
}) {
|
|
if (fs::exists(candidate)) {
|
|
opts.listFile = candidate;
|
|
std::cout << "Auto-detected listfile: " << candidate << "\n";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::cout << "=== Wowee Asset Extractor ===\n";
|
|
std::cout << "MPQ directory: " << opts.mpqDir << "\n";
|
|
std::cout << "Output: " << opts.outputDir << "\n";
|
|
std::cout << "Expansion: " << expansion << "\n";
|
|
if (!locale.empty()) {
|
|
std::cout << "Locale: " << locale << "\n";
|
|
}
|
|
if (opts.onlyUsedDbcs) {
|
|
std::cout << "Mode: only-used-dbcs\n";
|
|
}
|
|
if (opts.skipDbcExtraction) {
|
|
std::cout << "DBC extract: skipped\n";
|
|
}
|
|
if (opts.generateDbcCsv) {
|
|
std::cout << "DBC CSV: enabled\n";
|
|
if (!opts.dbcCsvOutputDir.empty()) {
|
|
std::cout << "DBC CSV out: " << opts.dbcCsvOutputDir << "\n";
|
|
}
|
|
}
|
|
|
|
if (!opts.referenceManifest.empty()) {
|
|
std::cout << "Reference: " << opts.referenceManifest << " (delta mode)\n";
|
|
}
|
|
|
|
if (!wowee::tools::Extractor::run(opts)) {
|
|
std::cerr << "Extraction failed!\n";
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|