feat(extract): add --purge-proprietary mode to free disk after open conversion

Walks an extracted tree and removes every BLP/DBC/M2/skin/WMO/ADT
that has a confirmed open-format sidecar at least as new. Dry-run
by default — requires --confirm-purge to actually delete:

  asset_extract --purge-proprietary Data/expansions/wotlk
  Dry-run: would purge proprietary files...
    would remove: 21570 files (16380.4 MB)
    (re-run with --confirm-purge to actually delete)

  asset_extract --purge-proprietary Data/expansions/wotlk --confirm-purge
  Purging...
    removed: 21570 files (16380.4 MB)

Pairing rules:
  .blp  → .png
  .dbc  → .json
  .m2   → .wom
  .skin → matching .m2's .wom (foo00.skin pairs with foo.wom)
  .wmo  → .wob (root)
  .wmo group sub-files (foo_NNN.wmo) → parent foo.wob
  .adt  → .whm

Servers can't run without proprietary files, so this only makes
sense for wowee-runtime-only setups. Files without a sidecar are
left untouched.
This commit is contained in:
Kelsi 2026-05-06 11:05:10 -07:00
parent 5799b5f88f
commit e547b4b82b

View file

@ -35,6 +35,10 @@ static void printUsage(const char* prog) {
<< " --upgrade-extract <dir>\n"
<< " Standalone post-extract pass on an existing tree —\n"
<< " writes open-format sidecars without re-running MPQ extract\n"
<< " --purge-proprietary <dir>\n"
<< " Walk tree and dry-run report which proprietary files have\n"
<< " an open-format sidecar; add --confirm-purge to actually delete\n"
<< " --confirm-purge Required to actually delete files in --purge-proprietary mode\n"
<< " --verify CRC32 verify all extracted files\n"
<< " --threads <N> Number of extraction threads (default: auto)\n"
<< " --verbose Verbose output\n"
@ -51,6 +55,13 @@ int main(int argc, char** argv) {
// it from MPQ. Triggered by --upgrade-extract <dir>.
std::string upgradeDir;
// Purge proprietary files when their open-format sidecar is present
// and at least as new. Dry-run by default; --confirm-purge actually
// deletes. Lets users free disk after dual-format extraction when
// they only need wowee runtime (no private server).
std::string purgeDir;
bool confirmPurge = false;
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--mpq-dir") == 0 && i + 1 < argc) {
opts.mpqDir = argv[++i];
@ -95,6 +106,10 @@ int main(int argc, char** argv) {
opts.verify = true;
} else if (std::strcmp(argv[i], "--verbose") == 0) {
opts.verbose = true;
} else if (std::strcmp(argv[i], "--purge-proprietary") == 0 && i + 1 < argc) {
purgeDir = argv[++i];
} else if (std::strcmp(argv[i], "--confirm-purge") == 0) {
confirmPurge = 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.
@ -113,6 +128,89 @@ int main(int argc, char** argv) {
}
}
// --purge-proprietary: walk a tree and (dry-run unless --confirm-purge)
// remove every .blp/.dbc/.m2/.skin/.wmo/.adt that has a confirmed
// open-format sidecar at least as new. Useful after a dual-format
// extraction when the user only wants the open-format files (no
// private-server compatibility needed).
if (!purgeDir.empty()) {
if (!std::filesystem::exists(purgeDir)) {
std::cerr << "purge-proprietary: " << purgeDir << " does not exist\n";
return 1;
}
std::cout << (confirmPurge ? "Purging" : "Dry-run: would purge")
<< " proprietary files under " << purgeDir
<< " where open-format sidecar exists...\n";
// (proprietary ext, sidecar ext) pairs. .skin pairs with the
// matching foo.m2's foo.wom (skin gets purged when WOM exists
// because WOM stores merged geometry). Group .wmo (foo_NNN.wmo)
// pair with the parent's .wob.
struct Pair { const char* propExt; const char* sidecarExt; };
const Pair pairs[] = {
{".blp", ".png"},
{".dbc", ".json"},
{".m2", ".wom"},
{".wmo", ".wob"}, // root WMO sidecar; group handling below
{".adt", ".whm"},
};
uint64_t toRemove = 0, removed = 0, totalBytes = 0;
namespace fs = std::filesystem;
for (auto& entry : fs::recursive_directory_iterator(purgeDir)) {
if (!entry.is_regular_file()) continue;
std::string p = entry.path().string();
std::string ext = entry.path().extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
std::string base = p.substr(0, p.size() - ext.size());
// Skin file shares its WOM sidecar with the parent .m2.
std::string sidecar;
if (ext == ".skin") {
// foo00.skin → foo.wom check
if (base.size() >= 2 && base.substr(base.size() - 2) == "00") {
sidecar = base.substr(0, base.size() - 2) + ".wom";
}
} else if (ext == ".wmo") {
// Group sub-files purge if the parent root WMO has WOB.
std::string fname = entry.path().filename().string();
auto under = fname.rfind('_');
bool isGroup = (under != std::string::npos &&
fname.size() - under == 8);
if (isGroup) {
auto last = base.rfind('_');
if (last != std::string::npos)
sidecar = base.substr(0, last) + ".wob";
} else {
sidecar = base + ".wob";
}
} else {
for (const auto& pr : pairs) {
if (ext == pr.propExt) { sidecar = base + pr.sidecarExt; break; }
}
}
if (sidecar.empty() || !fs::exists(sidecar)) continue;
std::error_code ec;
auto srcMtime = fs::last_write_time(p, ec);
auto sideMtime = fs::last_write_time(sidecar, ec);
if (ec || sideMtime < srcMtime) continue;
toRemove++;
totalBytes += entry.file_size();
if (confirmPurge) {
std::error_code rmEc;
if (fs::remove(p, rmEc)) removed++;
}
}
std::cout << (confirmPurge ? " removed: " : " would remove: ")
<< toRemove << " files (" << (totalBytes / (1024.0 * 1024.0))
<< " MB)\n";
if (!confirmPurge) {
std::cout << " (re-run with --confirm-purge to actually delete)\n";
}
return 0;
}
// --upgrade-extract: standalone post-extract pass on an existing tree.
if (!upgradeDir.empty()) {
if (!std::filesystem::exists(upgradeDir)) {