From e547b4b82b8fde33838d82db4afe15939219536a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 11:05:10 -0700 Subject: [PATCH] feat(extract): add --purge-proprietary mode to free disk after open conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- tools/asset_extract/main.cpp | 98 ++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/tools/asset_extract/main.cpp b/tools/asset_extract/main.cpp index f23edbe3..aa0e59c2 100644 --- a/tools/asset_extract/main.cpp +++ b/tools/asset_extract/main.cpp @@ -35,6 +35,10 @@ static void printUsage(const char* prog) { << " --upgrade-extract \n" << " Standalone post-extract pass on an existing tree —\n" << " writes open-format sidecars without re-running MPQ extract\n" + << " --purge-proprietary \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 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 . 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(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)) {