From 5f372211792ce496c822bb9ac9ca0b3e02d0bd48 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 04:14:32 -0700 Subject: [PATCH] refactor(editor): extract data-tree audit/migration into cli_data_tree.cpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the seven proprietary-data-tree handlers out of main.cpp: --migrate-data-tree --bench-migrate-data-tree --list-data-tree-largest --export-data-tree-md --info-data-tree --strip-data-tree --audit-data-tree All operate on a Blizzard-format extracted Data tree (the .m2/ .skin/.wmo/.blp/.dbc files) — they audit, migrate, or strip proprietary-format files in support of the open-format migration story. Original placement spanned two sub-blocks (12546-12892 and 13093-13417 in main.cpp) interrupted by --gen-texture and --add-texture-to-zone in the middle. Extraction collapses both sub-blocks into one cohesive translation unit. main.cpp drops 16,321 → 15,653 lines (-668). Behavior verified by re-running --info-data-tree against a missing directory. --- CMakeLists.txt | 1 + tools/editor/cli_data_tree.cpp | 740 +++++++++++++++++++++++++++++++++ tools/editor/cli_data_tree.hpp | 21 + tools/editor/main.cpp | 676 +----------------------------- 4 files changed, 766 insertions(+), 672 deletions(-) create mode 100644 tools/editor/cli_data_tree.cpp create mode 100644 tools/editor/cli_data_tree.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 07710d07..e8cce99a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1316,6 +1316,7 @@ add_executable(wowee_editor tools/editor/cli_pack.cpp tools/editor/cli_content_info.cpp tools/editor/cli_zone_info.cpp + tools/editor/cli_data_tree.cpp tools/editor/editor_app.cpp tools/editor/editor_camera.cpp tools/editor/editor_viewport.cpp diff --git a/tools/editor/cli_data_tree.cpp b/tools/editor/cli_data_tree.cpp new file mode 100644 index 00000000..0376c141 --- /dev/null +++ b/tools/editor/cli_data_tree.cpp @@ -0,0 +1,740 @@ +#include "cli_data_tree.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +int handleMigrateDataTree(int& i, int argc, char** argv) { + // End-to-end open-format migration. Runs all four bulk + // converters (m2/wmo/blp/dbc → wom/wob/png/json) in order + // on a single extracted Data tree. Each step's full + // output streams through; aggregate exit code is failure + // if any sub-converter fails. + // + // Idempotent: re-running on a partially-converted tree + // re-attempts the originals (which still produce the + // same sidecar) without removing any prior outputs. + std::string srcDir = argv[++i]; + namespace fs = std::filesystem; + if (!fs::exists(srcDir) || !fs::is_directory(srcDir)) { + std::fprintf(stderr, + "migrate-data-tree: %s is not a directory\n", + srcDir.c_str()); + return 1; + } + std::string self = argv[0]; + struct Step { const char* name; const char* flag; int rc; }; + std::vector steps = { + {"M2 → WOM ", "--convert-m2-batch", 0}, + {"WMO → WOB ", "--convert-wmo-batch", 0}, + {"BLP → PNG ", "--convert-blp-batch", 0}, + {"DBC → JSON", "--convert-dbc-batch", 0}, + }; + int totalFailed = 0; + std::printf("migrate-data-tree: %s\n", srcDir.c_str()); + for (auto& s : steps) { + std::printf("\n=== %s (%s) ===\n", s.name, s.flag); + std::fflush(stdout); + std::string cmd = "\"" + self + "\" " + s.flag + " \"" + srcDir + "\""; + s.rc = std::system(cmd.c_str()); + if (s.rc != 0) totalFailed++; + } + std::printf("\n=== migrate-data-tree summary ===\n"); + for (const auto& s : steps) { + std::printf(" [%s] %s (rc=%d)\n", + s.rc == 0 ? "PASS" : "FAIL", s.name, s.rc); + } + if (totalFailed == 0) { + std::printf("\n ALL FOUR PASSED — open-format migration complete\n"); + return 0; + } + std::printf("\n %d step(s) reported failures (re-run individually for detail)\n", + totalFailed); + return 1; +} + +int handleBenchMigrateDataTree(int& i, int argc, char** argv) { + // Time each --migrate-data-tree step end-to-end. Useful + // for capacity planning ("how long will the full extracted + // Data tree take?") and regression detection (a recent + // change shouldn't make M2 conversion 2x slower). + // + // Sub-batches are dispatched the same way --migrate-data- + // tree dispatches them — so the timings here are exactly + // what the user will experience running the migration. + std::string srcDir = argv[++i]; + bool jsonOut = (i + 1 < argc && + std::strcmp(argv[i + 1], "--json") == 0); + if (jsonOut) i++; + namespace fs = std::filesystem; + if (!fs::exists(srcDir) || !fs::is_directory(srcDir)) { + std::fprintf(stderr, + "bench-migrate-data-tree: %s is not a directory\n", + srcDir.c_str()); + return 1; + } + std::string self = argv[0]; + struct Step { + const char* name; + const char* flag; + double ms = 0; + int rc = 0; + }; + std::vector steps = { + {"M2 → WOM ", "--convert-m2-batch", 0, 0}, + {"WMO → WOB ", "--convert-wmo-batch", 0, 0}, + {"BLP → PNG ", "--convert-blp-batch", 0, 0}, + {"DBC → JSON", "--convert-dbc-batch", 0, 0}, + }; + double totalMs = 0; + for (auto& s : steps) { + std::string cmd = "\"" + self + "\" " + s.flag + " \"" + srcDir + "\""; + cmd += " >/dev/null 2>&1"; + auto t0 = std::chrono::steady_clock::now(); + s.rc = std::system(cmd.c_str()); + auto t1 = std::chrono::steady_clock::now(); + s.ms = std::chrono::duration(t1 - t0).count(); + totalMs += s.ms; + } + if (jsonOut) { + nlohmann::json j; + j["srcDir"] = srcDir; + j["totalMs"] = totalMs; + nlohmann::json arr = nlohmann::json::array(); + for (const auto& s : steps) { + double share = totalMs > 0 ? 100.0 * s.ms / totalMs : 0.0; + arr.push_back({{"name", s.name}, + {"flag", s.flag}, + {"ms", s.ms}, + {"share", share}, + {"rc", s.rc}}); + } + j["steps"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("bench-migrate-data-tree: %s\n", srcDir.c_str()); + std::printf(" total : %.1f ms (%.2f s)\n", totalMs, totalMs / 1000.0); + std::printf("\n step wall-clock share status\n"); + for (const auto& s : steps) { + double share = totalMs > 0 ? 100.0 * s.ms / totalMs : 0.0; + std::printf(" %-15s %8.1f ms %5.1f%% %s (rc=%d)\n", + s.name, s.ms, share, + s.rc == 0 ? "ok" : "FAIL", s.rc); + } + return 0; +} + +int handleListDataTreeLargest(int& i, int argc, char** argv) { + // Top-N largest proprietary files (.m2/.wmo/.blp/.dbc). + // Helps prioritize migration: convert the biggest files + // first to free the most disk space sooner. Annotates + // each file with whether an open sidecar already exists, + // so users can see at a glance which heavy hitters are + // already migrated vs still pending. + // + // Default N = 20. Sized for a terminal page; use --json + // (or pass a larger N) for full lists. + std::string srcDir = argv[++i]; + int N = 20; + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { N = std::stoi(argv[++i]); } catch (...) {} + if (N < 1) N = 20; + } + namespace fs = std::filesystem; + if (!fs::exists(srcDir) || !fs::is_directory(srcDir)) { + std::fprintf(stderr, + "list-data-tree-largest: %s is not a directory\n", + srcDir.c_str()); + return 1; + } + static const std::vector> + kPairs = { + {".m2", ".wom"}, + {".wmo", ".wob"}, + {".blp", ".png"}, + {".dbc", ".json"}, + }; + // Open sidecar set for the migration-status annotation. + std::map>> + openSets; + std::error_code ec; + for (const auto& e : fs::recursive_directory_iterator(srcDir, ec)) { + if (!e.is_regular_file()) continue; + std::string ext = e.path().extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { return std::tolower(c); }); + for (const auto& [_, openExt] : kPairs) { + if (ext == openExt) { + openSets[openExt].insert( + {e.path().parent_path().string(), + e.path().stem().string()}); + break; + } + } + } + struct Entry { + std::string path; + uint64_t bytes; + std::string ext; + bool migrated; + }; + std::vector entries; + uint64_t totalBytes = 0; + for (const auto& e : fs::recursive_directory_iterator(srcDir, ec)) { + if (!e.is_regular_file()) continue; + std::string ext = e.path().extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { return std::tolower(c); }); + std::string openExt; + for (const auto& [propExt, oExt] : kPairs) { + if (ext == propExt) { openExt = oExt; break; } + } + if (openExt.empty()) continue; + uint64_t sz = e.file_size(ec); + if (ec) sz = 0; + std::pair key{ + e.path().parent_path().string(), + e.path().stem().string()}; + bool migrated = openSets[openExt].count(key) > 0; + entries.push_back({e.path().string(), sz, ext, migrated}); + totalBytes += sz; + } + std::sort(entries.begin(), entries.end(), + [](const Entry& a, const Entry& b) { + return a.bytes > b.bytes; + }); + int shown = std::min(static_cast(entries.size()), N); + uint64_t shownBytes = 0; + for (int k = 0; k < shown; ++k) shownBytes += entries[k].bytes; + std::printf("list-data-tree-largest: %s\n", srcDir.c_str()); + std::printf(" proprietary files : %zu (total %.1f MB)\n", + entries.size(), totalBytes / (1024.0 * 1024.0)); + std::printf(" showing top : %d (%.1f MB, %.1f%% of total)\n", + shown, shownBytes / (1024.0 * 1024.0), + totalBytes ? 100.0 * shownBytes / totalBytes : 0.0); + if (entries.empty()) { + std::printf("\n (no proprietary files found)\n"); + return 0; + } + std::printf("\n rank ext bytes status path\n"); + for (int k = 0; k < shown; ++k) { + const auto& e = entries[k]; + std::printf(" %4d %-4s %10llu %-7s %s\n", + k + 1, e.ext.c_str(), + static_cast(e.bytes), + e.migrated ? "migrate" : "pending", + e.path.c_str()); + } + return 0; +} + +int handleExportDataTreeMd(int& i, int argc, char** argv) { + // Markdown migration-progress report. Drops cleanly into + // PR descriptions, CI artifacts, or status pages on + // GitHub Pages. Same numbers as --info-data-tree but + // formatted as a Markdown table with a status badge, + // bytes summary, and recommended next steps so a reader + // can act on the report without consulting the CLI help. + std::string srcDir = argv[++i]; + std::string outPath; + if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; + namespace fs = std::filesystem; + if (!fs::exists(srcDir) || !fs::is_directory(srcDir)) { + std::fprintf(stderr, + "export-data-tree-md: %s is not a directory\n", + srcDir.c_str()); + return 1; + } + if (outPath.empty()) outPath = srcDir + "/MIGRATION.md"; + static const std::vector> + kPairs = { + {".m2", ".wom"}, + {".wmo", ".wob"}, + {".blp", ".png"}, + {".dbc", ".json"}, + }; + // Same scan as --info-data-tree. + std::map>> + byExt; + std::map bytesByExt; + std::error_code ec; + for (const auto& e : fs::recursive_directory_iterator(srcDir, ec)) { + if (!e.is_regular_file()) continue; + std::string ext = e.path().extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { return std::tolower(c); }); + byExt[ext].insert({e.path().parent_path().string(), + e.path().stem().string()}); + uint64_t sz = e.file_size(ec); + if (!ec) bytesByExt[ext] += sz; + } + struct Row { + std::string prop, open; + int propCount, sidecarCount, orphanOpenCount; + uint64_t propBytes; + double share; + }; + std::vector rows; + int totalProp = 0, totalSidecar = 0, totalOrphan = 0; + uint64_t totalPropBytes = 0; + for (const auto& [propExt, openExt] : kPairs) { + Row r{propExt, openExt, 0, 0, 0, 0, 0.0}; + const auto& propSet = byExt[propExt]; + const auto& openSet = byExt[openExt]; + r.propCount = static_cast(propSet.size()); + for (const auto& key : openSet) { + if (propSet.count(key)) r.sidecarCount++; + else r.orphanOpenCount++; + } + r.propBytes = bytesByExt[propExt]; + r.share = r.propCount > 0 + ? 100.0 * r.sidecarCount / r.propCount + : 100.0; + totalProp += r.propCount; + totalSidecar += r.sidecarCount; + totalOrphan += r.orphanOpenCount; + totalPropBytes += r.propBytes; + rows.push_back(r); + } + double overallShare = totalProp > 0 + ? 100.0 * totalSidecar / totalProp + : 100.0; + const char* badge = + overallShare >= 100.0 ? "**100% migrated**" : + overallShare >= 75.0 ? "**Mostly migrated**" : + overallShare >= 25.0 ? "*Partially migrated*" : + "*Migration pending*"; + std::ofstream out(outPath); + if (!out) { + std::fprintf(stderr, + "export-data-tree-md: cannot write %s\n", outPath.c_str()); + return 1; + } + out << "# Data Tree Migration Report\n\n"; + out << "Source: `" << srcDir << "`\n\n"; + out << "Status: " << badge << " (" << std::fixed; + out.precision(1); + out << overallShare << "% sidecar coverage)\n\n"; + out << "## Summary\n\n"; + out << "- Proprietary files: **" << totalProp << "** (" + << std::fixed; + out.precision(2); + out << (totalPropBytes / (1024.0 * 1024.0)) << " MB)\n"; + out << "- Open sidecars present: **" << totalSidecar << "**\n"; + out << "- Orphan open files (no proprietary source): **" + << totalOrphan << "**\n\n"; + out << "## Per-format pairs\n\n"; + out << "| Pair | Proprietary | Sidecars | Orphan open | Prop bytes | Share |\n"; + out << "|------|------------:|---------:|------------:|-----------:|------:|\n"; + for (const auto& r : rows) { + out << "| " << r.prop << " → " << r.open << " | " + << r.propCount << " | " + << r.sidecarCount << " | " + << r.orphanOpenCount << " | " + << r.propBytes << " | " + << std::fixed; + out.precision(1); + out << r.share << "% |\n"; + } + out << "\n## Recommended next steps\n\n"; + if (overallShare < 100.0) { + out << "1. Run `wowee_editor --migrate-data-tree " << srcDir + << "` to fill in the missing sidecars.\n"; + out << "2. Run `wowee_editor --audit-data-tree " << srcDir + << "` to confirm 100% coverage.\n"; + out << "3. Run `wowee_editor --strip-data-tree " << srcDir + << "` to delete the proprietary originals.\n"; + } else { + out << "All proprietary files are migrated. Run " + << "`wowee_editor --strip-data-tree " << srcDir + << "` to delete the originals and ship the open-only tree.\n"; + } + out.close(); + std::printf("Wrote %s\n", outPath.c_str()); + std::printf(" status : %s\n", badge); + std::printf(" share : %.1f%%\n", overallShare); + std::printf(" proprietary : %d files, %.2f MB\n", + totalProp, totalPropBytes / (1024.0 * 1024.0)); + return 0; +} + +int handleInfoDataTree(int& i, int argc, char** argv) { + // Non-destructive companion to --migrate-data-tree. Walks + // recursively, counts files per format pair + // (proprietary vs open replacement), and reports per-pair + // counts plus an overall "migration share" — the fraction + // of source files that already have an open sidecar + // present. + // + // Designed to drop into CI dashboards: a 100% share + // means every proprietary asset has a deterministic open + // counterpart on disk and you can drop the originals. + std::string srcDir = argv[++i]; + bool jsonOut = (i + 1 < argc && + std::strcmp(argv[i + 1], "--json") == 0); + if (jsonOut) i++; + namespace fs = std::filesystem; + if (!fs::exists(srcDir) || !fs::is_directory(srcDir)) { + std::fprintf(stderr, + "info-data-tree: %s is not a directory\n", + srcDir.c_str()); + return 1; + } + // Each pair: proprietary extension + open extension. The + // open file is considered a "sidecar" when it sits next + // to the proprietary file with the same stem. + struct Pair { + const char* prop; // ".m2" + const char* open; // ".wom" + int propCount = 0; + int sidecarCount = 0; // .wom next to a .m2 + int orphanOpenCount = 0; // .wom with no matching .m2 + }; + std::vector pairs = { + {".m2", ".wom"}, + {".wmo", ".wob"}, + {".blp", ".png"}, + {".dbc", ".json"}, + }; + // First pass: collect filenames by extension. Use a set + // of (parent, stem) for the sidecar lookup so the test is + // O(log n) per file rather than O(n). + std::map>> byExt; + std::error_code ec; + for (const auto& e : fs::recursive_directory_iterator(srcDir, ec)) { + if (!e.is_regular_file()) continue; + std::string ext = e.path().extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { return std::tolower(c); }); + byExt[ext].insert({e.path().parent_path().string(), + e.path().stem().string()}); + } + for (auto& p : pairs) { + const auto& propSet = byExt[p.prop]; + const auto& openSet = byExt[p.open]; + p.propCount = static_cast(propSet.size()); + for (const auto& key : openSet) { + if (propSet.count(key)) p.sidecarCount++; + else p.orphanOpenCount++; + } + } + int totalProp = 0, totalSidecar = 0, totalOrphanOpen = 0; + for (const auto& p : pairs) { + totalProp += p.propCount; + totalSidecar += p.sidecarCount; + totalOrphanOpen += p.orphanOpenCount; + } + double overallShare = totalProp > 0 + ? 100.0 * totalSidecar / totalProp + : 100.0; + if (jsonOut) { + nlohmann::json j; + j["srcDir"] = srcDir; + j["totalProprietary"] = totalProp; + j["totalSidecars"] = totalSidecar; + j["totalOrphanOpen"] = totalOrphanOpen; + j["migrationShare"] = overallShare; + nlohmann::json arr = nlohmann::json::array(); + for (const auto& p : pairs) { + double share = p.propCount > 0 + ? 100.0 * p.sidecarCount / p.propCount + : 100.0; + arr.push_back({{"proprietary", p.prop}, + {"open", p.open}, + {"propCount", p.propCount}, + {"sidecarCount", p.sidecarCount}, + {"orphanOpenCount", p.orphanOpenCount}, + {"share", share}}); + } + j["pairs"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("info-data-tree: %s\n", srcDir.c_str()); + std::printf(" total proprietary : %d\n", totalProp); + std::printf(" total sidecars : %d (open files matched to a proprietary)\n", + totalSidecar); + std::printf(" orphan open files : %d (no matching proprietary — already-stripped)\n", + totalOrphanOpen); + std::printf(" migration share : %.1f%% (sidecars / proprietary)\n", + overallShare); + std::printf("\n pair prop open-side orphan share\n"); + for (const auto& p : pairs) { + double share = p.propCount > 0 + ? 100.0 * p.sidecarCount / p.propCount + : 100.0; + char label[32]; + std::snprintf(label, sizeof(label), "%-4s → %-5s", p.prop, p.open); + std::printf(" %-14s %5d %9d %6d %5.1f%%\n", + label, p.propCount, p.sidecarCount, + p.orphanOpenCount, share); + } + return 0; +} + +int handleStripDataTree(int& i, int argc, char** argv) { + // Destructive cleanup. Walks , finds every + // proprietary file (.m2/.wmo/.blp/.dbc) that already has + // a matching open sidecar at the same (parent, stem), + // and deletes the proprietary file. Sidecar match uses + // case-insensitive extension comparison. + // + // Honors --dry-run for safe previews. Mirrors the + // --strip-zone convention (defaults to actually delete). + // + // Recommended workflow: --info-data-tree to see the + // share, --migrate-data-tree to fill in missing sidecars, + // --strip-data-tree --dry-run to confirm the kill list, + // then --strip-data-tree to apply. + std::string srcDir = argv[++i]; + bool dryRun = false; + if (i + 1 < argc && std::strcmp(argv[i + 1], "--dry-run") == 0) { + dryRun = true; i++; + } + namespace fs = std::filesystem; + if (!fs::exists(srcDir) || !fs::is_directory(srcDir)) { + std::fprintf(stderr, + "strip-data-tree: %s is not a directory\n", + srcDir.c_str()); + return 1; + } + // Build the (parent, stem) set of every open file first. + // The proprietary→open ext map serves both as the strip + // target list and as the per-pair routing table. + static const std::vector> + kPairs = { + {".m2", ".wom"}, + {".wmo", ".wob"}, + {".blp", ".png"}, + {".dbc", ".json"}, + }; + std::map>> + openSets; // open ext -> set of (parent, stem) + std::error_code ec; + for (const auto& e : fs::recursive_directory_iterator(srcDir, ec)) { + if (!e.is_regular_file()) continue; + std::string ext = e.path().extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { return std::tolower(c); }); + for (const auto& [_, openExt] : kPairs) { + if (ext == openExt) { + openSets[openExt].insert( + {e.path().parent_path().string(), + e.path().stem().string()}); + break; + } + } + } + // Walk again, this time deleting (or previewing) each + // proprietary file whose key appears in its pair's open + // set. + int removed = 0, failed = 0; + uint64_t freedBytes = 0; + std::map perExtRemoved; + for (const auto& [propExt, openExt] : kPairs) { + const auto& openSet = openSets[openExt]; + if (openSet.empty()) continue; + for (const auto& e : fs::recursive_directory_iterator(srcDir, ec)) { + if (!e.is_regular_file()) continue; + std::string ext = e.path().extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { return std::tolower(c); }); + if (ext != propExt) continue; + std::pair key{ + e.path().parent_path().string(), + e.path().stem().string()}; + if (!openSet.count(key)) continue; // no sidecar — keep + uint64_t sz = e.file_size(ec); + if (ec) sz = 0; + if (dryRun) { + std::printf(" would remove: %s (%llu bytes)\n", + e.path().c_str(), + static_cast(sz)); + removed++; + perExtRemoved[propExt]++; + freedBytes += sz; + } else { + if (fs::remove(e.path(), ec)) { + std::printf(" removed: %s (%llu bytes)\n", + e.path().c_str(), + static_cast(sz)); + removed++; + perExtRemoved[propExt]++; + freedBytes += sz; + } else { + std::fprintf(stderr, + " WARN: failed to remove %s (%s)\n", + e.path().c_str(), ec.message().c_str()); + failed++; + } + } + } + } + std::printf("\nstrip-data-tree: %s%s\n", + srcDir.c_str(), dryRun ? " (dry-run)" : ""); + std::printf(" %s : %d file(s)\n", + dryRun ? "would remove" : "removed ", removed); + std::printf(" freed : %.1f KB\n", freedBytes / 1024.0); + if (!perExtRemoved.empty()) { + std::printf("\n Per-extension:\n"); + for (const auto& [ext, count] : perExtRemoved) { + std::printf(" %-5s : %d\n", ext.c_str(), count); + } + } + if (failed > 0) { + std::printf("\n FAILED : %d (see stderr)\n", failed); + } + if (dryRun && removed > 0) { + std::printf("\n re-run without --dry-run to apply\n"); + } + return failed == 0 ? 0 : 1; +} + +int handleAuditDataTree(int& i, int argc, char** argv) { + // Non-destructive CI gate. Walks and exits 1 if + // any proprietary file (.m2/.wmo/.blp/.dbc) lacks a + // matching open sidecar at the same (parent, stem). The + // pre-strip safety check: don't run --strip-data-tree + // until this returns exit 0. + // + // Lists missing sidecars (capped at 50) so the user can + // re-run --migrate-data-tree to fill them in. + std::string srcDir = argv[++i]; + namespace fs = std::filesystem; + if (!fs::exists(srcDir) || !fs::is_directory(srcDir)) { + std::fprintf(stderr, + "audit-data-tree: %s is not a directory\n", + srcDir.c_str()); + return 1; + } + static const std::vector> + kPairs = { + {".m2", ".wom"}, + {".wmo", ".wob"}, + {".blp", ".png"}, + {".dbc", ".json"}, + }; + // Build (parent, stem) sets per open ext for fast lookup. + std::map>> + openSets; + std::map> propByExt; + std::error_code ec; + for (const auto& e : fs::recursive_directory_iterator(srcDir, ec)) { + if (!e.is_regular_file()) continue; + std::string ext = e.path().extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { return std::tolower(c); }); + bool isOpen = false; + for (const auto& [propExt, openExt] : kPairs) { + if (ext == openExt) { + openSets[openExt].insert( + {e.path().parent_path().string(), + e.path().stem().string()}); + isOpen = true; + break; + } + } + if (isOpen) continue; + for (const auto& [propExt, _] : kPairs) { + if (ext == propExt) { + propByExt[propExt].push_back(e.path().string()); + break; + } + } + } + // Check each proprietary file for its sidecar. + int totalProp = 0, totalMissing = 0; + std::vector missing; + std::map missingPerExt; + for (const auto& [propExt, openExt] : kPairs) { + const auto& openSet = openSets[openExt]; + for (const auto& fullPath : propByExt[propExt]) { + totalProp++; + fs::path p(fullPath); + std::pair key{ + p.parent_path().string(), p.stem().string()}; + if (openSet.count(key)) continue; + totalMissing++; + missingPerExt[propExt]++; + missing.push_back(fullPath); + } + } + std::sort(missing.begin(), missing.end()); + std::printf("audit-data-tree: %s\n", srcDir.c_str()); + std::printf(" proprietary files : %d\n", totalProp); + std::printf(" missing sidecars : %d\n", totalMissing); + if (totalMissing == 0) { + if (totalProp > 0) { + std::printf("\n PASSED — every proprietary file has an open sidecar\n"); + } else { + std::printf("\n PASSED — no proprietary files present\n"); + } + return 0; + } + std::printf("\n FAILED — re-run --migrate-data-tree to fill the gaps\n"); + std::printf("\n Per-extension missing:\n"); + for (const auto& [ext, count] : missingPerExt) { + std::printf(" %-5s : %d\n", ext.c_str(), count); + } + std::printf("\n Missing sidecars (sorted):\n"); + size_t shown = 0; + for (const auto& m : missing) { + if (shown >= 50) { + std::printf(" ... and %zu more\n", missing.size() - shown); + break; + } + std::printf(" - %s\n", m.c_str()); + shown++; + } + return 1; +} + + +} // namespace + +bool handleDataTree(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--migrate-data-tree") == 0 && i + 1 < argc) { + outRc = handleMigrateDataTree(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--bench-migrate-data-tree") == 0 && i + 1 < argc) { + outRc = handleBenchMigrateDataTree(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--list-data-tree-largest") == 0 && i + 1 < argc) { + outRc = handleListDataTreeLargest(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--export-data-tree-md") == 0 && i + 1 < argc) { + outRc = handleExportDataTreeMd(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-data-tree") == 0 && i + 1 < argc) { + outRc = handleInfoDataTree(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--strip-data-tree") == 0 && i + 1 < argc) { + outRc = handleStripDataTree(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--audit-data-tree") == 0 && i + 1 < argc) { + outRc = handleAuditDataTree(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_data_tree.hpp b/tools/editor/cli_data_tree.hpp new file mode 100644 index 00000000..47f1dfdf --- /dev/null +++ b/tools/editor/cli_data_tree.hpp @@ -0,0 +1,21 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +// Dispatch the proprietary-data-tree audit + migration handlers: +// --migrate-data-tree --bench-migrate-data-tree +// --list-data-tree-largest --export-data-tree-md +// --info-data-tree --strip-data-tree +// --audit-data-tree +// +// All operate on a Blizzard-format extracted Data tree +// (the .m2/.skin/.wmo/.blp/.dbc files). +// +// Returns true if matched; outRc holds the exit code. +bool handleDataTree(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 1fc6b2e5..ad65ed6a 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -17,6 +17,7 @@ #include "cli_pack.hpp" #include "cli_content_info.hpp" #include "cli_zone_info.hpp" +#include "cli_data_tree.hpp" #include "content_pack.hpp" #include "npc_spawner.hpp" #include "object_placer.hpp" @@ -478,6 +479,9 @@ int main(int argc, char* argv[]) { if (wowee::editor::cli::handleZoneInfo(i, argc, argv, outRc)) { return outRc; } + if (wowee::editor::cli::handleDataTree(i, argc, argv, outRc)) { + return outRc; + } } if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) { dataPath = argv[++i]; @@ -12539,353 +12543,6 @@ int main(int argc, char* argv[]) { std::printf(" pass --dry-run off to actually delete\n"); } return totalFailed == 0 ? 0 : 1; - } else if (std::strcmp(argv[i], "--migrate-data-tree") == 0 && i + 1 < argc) { - // End-to-end open-format migration. Runs all four bulk - // converters (m2/wmo/blp/dbc → wom/wob/png/json) in order - // on a single extracted Data tree. Each step's full - // output streams through; aggregate exit code is failure - // if any sub-converter fails. - // - // Idempotent: re-running on a partially-converted tree - // re-attempts the originals (which still produce the - // same sidecar) without removing any prior outputs. - std::string srcDir = argv[++i]; - namespace fs = std::filesystem; - if (!fs::exists(srcDir) || !fs::is_directory(srcDir)) { - std::fprintf(stderr, - "migrate-data-tree: %s is not a directory\n", - srcDir.c_str()); - return 1; - } - std::string self = argv[0]; - struct Step { const char* name; const char* flag; int rc; }; - std::vector steps = { - {"M2 → WOM ", "--convert-m2-batch", 0}, - {"WMO → WOB ", "--convert-wmo-batch", 0}, - {"BLP → PNG ", "--convert-blp-batch", 0}, - {"DBC → JSON", "--convert-dbc-batch", 0}, - }; - int totalFailed = 0; - std::printf("migrate-data-tree: %s\n", srcDir.c_str()); - for (auto& s : steps) { - std::printf("\n=== %s (%s) ===\n", s.name, s.flag); - std::fflush(stdout); - std::string cmd = "\"" + self + "\" " + s.flag + " \"" + srcDir + "\""; - s.rc = std::system(cmd.c_str()); - if (s.rc != 0) totalFailed++; - } - std::printf("\n=== migrate-data-tree summary ===\n"); - for (const auto& s : steps) { - std::printf(" [%s] %s (rc=%d)\n", - s.rc == 0 ? "PASS" : "FAIL", s.name, s.rc); - } - if (totalFailed == 0) { - std::printf("\n ALL FOUR PASSED — open-format migration complete\n"); - return 0; - } - std::printf("\n %d step(s) reported failures (re-run individually for detail)\n", - totalFailed); - return 1; - } else if (std::strcmp(argv[i], "--bench-migrate-data-tree") == 0 && i + 1 < argc) { - // Time each --migrate-data-tree step end-to-end. Useful - // for capacity planning ("how long will the full extracted - // Data tree take?") and regression detection (a recent - // change shouldn't make M2 conversion 2x slower). - // - // Sub-batches are dispatched the same way --migrate-data- - // tree dispatches them — so the timings here are exactly - // what the user will experience running the migration. - std::string srcDir = argv[++i]; - bool jsonOut = (i + 1 < argc && - std::strcmp(argv[i + 1], "--json") == 0); - if (jsonOut) i++; - namespace fs = std::filesystem; - if (!fs::exists(srcDir) || !fs::is_directory(srcDir)) { - std::fprintf(stderr, - "bench-migrate-data-tree: %s is not a directory\n", - srcDir.c_str()); - return 1; - } - std::string self = argv[0]; - struct Step { - const char* name; - const char* flag; - double ms = 0; - int rc = 0; - }; - std::vector steps = { - {"M2 → WOM ", "--convert-m2-batch", 0, 0}, - {"WMO → WOB ", "--convert-wmo-batch", 0, 0}, - {"BLP → PNG ", "--convert-blp-batch", 0, 0}, - {"DBC → JSON", "--convert-dbc-batch", 0, 0}, - }; - double totalMs = 0; - for (auto& s : steps) { - std::string cmd = "\"" + self + "\" " + s.flag + " \"" + srcDir + "\""; - cmd += " >/dev/null 2>&1"; - auto t0 = std::chrono::steady_clock::now(); - s.rc = std::system(cmd.c_str()); - auto t1 = std::chrono::steady_clock::now(); - s.ms = std::chrono::duration(t1 - t0).count(); - totalMs += s.ms; - } - if (jsonOut) { - nlohmann::json j; - j["srcDir"] = srcDir; - j["totalMs"] = totalMs; - nlohmann::json arr = nlohmann::json::array(); - for (const auto& s : steps) { - double share = totalMs > 0 ? 100.0 * s.ms / totalMs : 0.0; - arr.push_back({{"name", s.name}, - {"flag", s.flag}, - {"ms", s.ms}, - {"share", share}, - {"rc", s.rc}}); - } - j["steps"] = arr; - std::printf("%s\n", j.dump(2).c_str()); - return 0; - } - std::printf("bench-migrate-data-tree: %s\n", srcDir.c_str()); - std::printf(" total : %.1f ms (%.2f s)\n", totalMs, totalMs / 1000.0); - std::printf("\n step wall-clock share status\n"); - for (const auto& s : steps) { - double share = totalMs > 0 ? 100.0 * s.ms / totalMs : 0.0; - std::printf(" %-15s %8.1f ms %5.1f%% %s (rc=%d)\n", - s.name, s.ms, share, - s.rc == 0 ? "ok" : "FAIL", s.rc); - } - return 0; - } else if (std::strcmp(argv[i], "--list-data-tree-largest") == 0 && i + 1 < argc) { - // Top-N largest proprietary files (.m2/.wmo/.blp/.dbc). - // Helps prioritize migration: convert the biggest files - // first to free the most disk space sooner. Annotates - // each file with whether an open sidecar already exists, - // so users can see at a glance which heavy hitters are - // already migrated vs still pending. - // - // Default N = 20. Sized for a terminal page; use --json - // (or pass a larger N) for full lists. - std::string srcDir = argv[++i]; - int N = 20; - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { N = std::stoi(argv[++i]); } catch (...) {} - if (N < 1) N = 20; - } - namespace fs = std::filesystem; - if (!fs::exists(srcDir) || !fs::is_directory(srcDir)) { - std::fprintf(stderr, - "list-data-tree-largest: %s is not a directory\n", - srcDir.c_str()); - return 1; - } - static const std::vector> - kPairs = { - {".m2", ".wom"}, - {".wmo", ".wob"}, - {".blp", ".png"}, - {".dbc", ".json"}, - }; - // Open sidecar set for the migration-status annotation. - std::map>> - openSets; - std::error_code ec; - for (const auto& e : fs::recursive_directory_iterator(srcDir, ec)) { - if (!e.is_regular_file()) continue; - std::string ext = e.path().extension().string(); - std::transform(ext.begin(), ext.end(), ext.begin(), - [](unsigned char c) { return std::tolower(c); }); - for (const auto& [_, openExt] : kPairs) { - if (ext == openExt) { - openSets[openExt].insert( - {e.path().parent_path().string(), - e.path().stem().string()}); - break; - } - } - } - struct Entry { - std::string path; - uint64_t bytes; - std::string ext; - bool migrated; - }; - std::vector entries; - uint64_t totalBytes = 0; - for (const auto& e : fs::recursive_directory_iterator(srcDir, ec)) { - if (!e.is_regular_file()) continue; - std::string ext = e.path().extension().string(); - std::transform(ext.begin(), ext.end(), ext.begin(), - [](unsigned char c) { return std::tolower(c); }); - std::string openExt; - for (const auto& [propExt, oExt] : kPairs) { - if (ext == propExt) { openExt = oExt; break; } - } - if (openExt.empty()) continue; - uint64_t sz = e.file_size(ec); - if (ec) sz = 0; - std::pair key{ - e.path().parent_path().string(), - e.path().stem().string()}; - bool migrated = openSets[openExt].count(key) > 0; - entries.push_back({e.path().string(), sz, ext, migrated}); - totalBytes += sz; - } - std::sort(entries.begin(), entries.end(), - [](const Entry& a, const Entry& b) { - return a.bytes > b.bytes; - }); - int shown = std::min(static_cast(entries.size()), N); - uint64_t shownBytes = 0; - for (int k = 0; k < shown; ++k) shownBytes += entries[k].bytes; - std::printf("list-data-tree-largest: %s\n", srcDir.c_str()); - std::printf(" proprietary files : %zu (total %.1f MB)\n", - entries.size(), totalBytes / (1024.0 * 1024.0)); - std::printf(" showing top : %d (%.1f MB, %.1f%% of total)\n", - shown, shownBytes / (1024.0 * 1024.0), - totalBytes ? 100.0 * shownBytes / totalBytes : 0.0); - if (entries.empty()) { - std::printf("\n (no proprietary files found)\n"); - return 0; - } - std::printf("\n rank ext bytes status path\n"); - for (int k = 0; k < shown; ++k) { - const auto& e = entries[k]; - std::printf(" %4d %-4s %10llu %-7s %s\n", - k + 1, e.ext.c_str(), - static_cast(e.bytes), - e.migrated ? "migrate" : "pending", - e.path.c_str()); - } - return 0; - } else if (std::strcmp(argv[i], "--export-data-tree-md") == 0 && i + 1 < argc) { - // Markdown migration-progress report. Drops cleanly into - // PR descriptions, CI artifacts, or status pages on - // GitHub Pages. Same numbers as --info-data-tree but - // formatted as a Markdown table with a status badge, - // bytes summary, and recommended next steps so a reader - // can act on the report without consulting the CLI help. - std::string srcDir = argv[++i]; - std::string outPath; - if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; - namespace fs = std::filesystem; - if (!fs::exists(srcDir) || !fs::is_directory(srcDir)) { - std::fprintf(stderr, - "export-data-tree-md: %s is not a directory\n", - srcDir.c_str()); - return 1; - } - if (outPath.empty()) outPath = srcDir + "/MIGRATION.md"; - static const std::vector> - kPairs = { - {".m2", ".wom"}, - {".wmo", ".wob"}, - {".blp", ".png"}, - {".dbc", ".json"}, - }; - // Same scan as --info-data-tree. - std::map>> - byExt; - std::map bytesByExt; - std::error_code ec; - for (const auto& e : fs::recursive_directory_iterator(srcDir, ec)) { - if (!e.is_regular_file()) continue; - std::string ext = e.path().extension().string(); - std::transform(ext.begin(), ext.end(), ext.begin(), - [](unsigned char c) { return std::tolower(c); }); - byExt[ext].insert({e.path().parent_path().string(), - e.path().stem().string()}); - uint64_t sz = e.file_size(ec); - if (!ec) bytesByExt[ext] += sz; - } - struct Row { - std::string prop, open; - int propCount, sidecarCount, orphanOpenCount; - uint64_t propBytes; - double share; - }; - std::vector rows; - int totalProp = 0, totalSidecar = 0, totalOrphan = 0; - uint64_t totalPropBytes = 0; - for (const auto& [propExt, openExt] : kPairs) { - Row r{propExt, openExt, 0, 0, 0, 0, 0.0}; - const auto& propSet = byExt[propExt]; - const auto& openSet = byExt[openExt]; - r.propCount = static_cast(propSet.size()); - for (const auto& key : openSet) { - if (propSet.count(key)) r.sidecarCount++; - else r.orphanOpenCount++; - } - r.propBytes = bytesByExt[propExt]; - r.share = r.propCount > 0 - ? 100.0 * r.sidecarCount / r.propCount - : 100.0; - totalProp += r.propCount; - totalSidecar += r.sidecarCount; - totalOrphan += r.orphanOpenCount; - totalPropBytes += r.propBytes; - rows.push_back(r); - } - double overallShare = totalProp > 0 - ? 100.0 * totalSidecar / totalProp - : 100.0; - const char* badge = - overallShare >= 100.0 ? "**100% migrated**" : - overallShare >= 75.0 ? "**Mostly migrated**" : - overallShare >= 25.0 ? "*Partially migrated*" : - "*Migration pending*"; - std::ofstream out(outPath); - if (!out) { - std::fprintf(stderr, - "export-data-tree-md: cannot write %s\n", outPath.c_str()); - return 1; - } - out << "# Data Tree Migration Report\n\n"; - out << "Source: `" << srcDir << "`\n\n"; - out << "Status: " << badge << " (" << std::fixed; - out.precision(1); - out << overallShare << "% sidecar coverage)\n\n"; - out << "## Summary\n\n"; - out << "- Proprietary files: **" << totalProp << "** (" - << std::fixed; - out.precision(2); - out << (totalPropBytes / (1024.0 * 1024.0)) << " MB)\n"; - out << "- Open sidecars present: **" << totalSidecar << "**\n"; - out << "- Orphan open files (no proprietary source): **" - << totalOrphan << "**\n\n"; - out << "## Per-format pairs\n\n"; - out << "| Pair | Proprietary | Sidecars | Orphan open | Prop bytes | Share |\n"; - out << "|------|------------:|---------:|------------:|-----------:|------:|\n"; - for (const auto& r : rows) { - out << "| " << r.prop << " → " << r.open << " | " - << r.propCount << " | " - << r.sidecarCount << " | " - << r.orphanOpenCount << " | " - << r.propBytes << " | " - << std::fixed; - out.precision(1); - out << r.share << "% |\n"; - } - out << "\n## Recommended next steps\n\n"; - if (overallShare < 100.0) { - out << "1. Run `wowee_editor --migrate-data-tree " << srcDir - << "` to fill in the missing sidecars.\n"; - out << "2. Run `wowee_editor --audit-data-tree " << srcDir - << "` to confirm 100% coverage.\n"; - out << "3. Run `wowee_editor --strip-data-tree " << srcDir - << "` to delete the proprietary originals.\n"; - } else { - out << "All proprietary files are migrated. Run " - << "`wowee_editor --strip-data-tree " << srcDir - << "` to delete the originals and ship the open-only tree.\n"; - } - out.close(); - std::printf("Wrote %s\n", outPath.c_str()); - std::printf(" status : %s\n", badge); - std::printf(" share : %.1f%%\n", overallShare); - std::printf(" proprietary : %d files, %.2f MB\n", - totalProp, totalPropBytes / (1024.0 * 1024.0)); - return 0; } else if (std::strcmp(argv[i], "--gen-texture") == 0 && i + 2 < argc) { // Synthesize a placeholder PNG texture. Lets users add a // working texture to their project without an external @@ -13086,331 +12743,6 @@ int main(int argc, char* argv[]) { std::printf(" next : --add-texture-to-mesh %s\n", destPath.c_str()); return 0; - } else if (std::strcmp(argv[i], "--info-data-tree") == 0 && i + 1 < argc) { - // Non-destructive companion to --migrate-data-tree. Walks - // recursively, counts files per format pair - // (proprietary vs open replacement), and reports per-pair - // counts plus an overall "migration share" — the fraction - // of source files that already have an open sidecar - // present. - // - // Designed to drop into CI dashboards: a 100% share - // means every proprietary asset has a deterministic open - // counterpart on disk and you can drop the originals. - std::string srcDir = argv[++i]; - bool jsonOut = (i + 1 < argc && - std::strcmp(argv[i + 1], "--json") == 0); - if (jsonOut) i++; - namespace fs = std::filesystem; - if (!fs::exists(srcDir) || !fs::is_directory(srcDir)) { - std::fprintf(stderr, - "info-data-tree: %s is not a directory\n", - srcDir.c_str()); - return 1; - } - // Each pair: proprietary extension + open extension. The - // open file is considered a "sidecar" when it sits next - // to the proprietary file with the same stem. - struct Pair { - const char* prop; // ".m2" - const char* open; // ".wom" - int propCount = 0; - int sidecarCount = 0; // .wom next to a .m2 - int orphanOpenCount = 0; // .wom with no matching .m2 - }; - std::vector pairs = { - {".m2", ".wom"}, - {".wmo", ".wob"}, - {".blp", ".png"}, - {".dbc", ".json"}, - }; - // First pass: collect filenames by extension. Use a set - // of (parent, stem) for the sidecar lookup so the test is - // O(log n) per file rather than O(n). - std::map>> byExt; - std::error_code ec; - for (const auto& e : fs::recursive_directory_iterator(srcDir, ec)) { - if (!e.is_regular_file()) continue; - std::string ext = e.path().extension().string(); - std::transform(ext.begin(), ext.end(), ext.begin(), - [](unsigned char c) { return std::tolower(c); }); - byExt[ext].insert({e.path().parent_path().string(), - e.path().stem().string()}); - } - for (auto& p : pairs) { - const auto& propSet = byExt[p.prop]; - const auto& openSet = byExt[p.open]; - p.propCount = static_cast(propSet.size()); - for (const auto& key : openSet) { - if (propSet.count(key)) p.sidecarCount++; - else p.orphanOpenCount++; - } - } - int totalProp = 0, totalSidecar = 0, totalOrphanOpen = 0; - for (const auto& p : pairs) { - totalProp += p.propCount; - totalSidecar += p.sidecarCount; - totalOrphanOpen += p.orphanOpenCount; - } - double overallShare = totalProp > 0 - ? 100.0 * totalSidecar / totalProp - : 100.0; - if (jsonOut) { - nlohmann::json j; - j["srcDir"] = srcDir; - j["totalProprietary"] = totalProp; - j["totalSidecars"] = totalSidecar; - j["totalOrphanOpen"] = totalOrphanOpen; - j["migrationShare"] = overallShare; - nlohmann::json arr = nlohmann::json::array(); - for (const auto& p : pairs) { - double share = p.propCount > 0 - ? 100.0 * p.sidecarCount / p.propCount - : 100.0; - arr.push_back({{"proprietary", p.prop}, - {"open", p.open}, - {"propCount", p.propCount}, - {"sidecarCount", p.sidecarCount}, - {"orphanOpenCount", p.orphanOpenCount}, - {"share", share}}); - } - j["pairs"] = arr; - std::printf("%s\n", j.dump(2).c_str()); - return 0; - } - std::printf("info-data-tree: %s\n", srcDir.c_str()); - std::printf(" total proprietary : %d\n", totalProp); - std::printf(" total sidecars : %d (open files matched to a proprietary)\n", - totalSidecar); - std::printf(" orphan open files : %d (no matching proprietary — already-stripped)\n", - totalOrphanOpen); - std::printf(" migration share : %.1f%% (sidecars / proprietary)\n", - overallShare); - std::printf("\n pair prop open-side orphan share\n"); - for (const auto& p : pairs) { - double share = p.propCount > 0 - ? 100.0 * p.sidecarCount / p.propCount - : 100.0; - char label[32]; - std::snprintf(label, sizeof(label), "%-4s → %-5s", p.prop, p.open); - std::printf(" %-14s %5d %9d %6d %5.1f%%\n", - label, p.propCount, p.sidecarCount, - p.orphanOpenCount, share); - } - return 0; - } else if (std::strcmp(argv[i], "--strip-data-tree") == 0 && i + 1 < argc) { - // Destructive cleanup. Walks , finds every - // proprietary file (.m2/.wmo/.blp/.dbc) that already has - // a matching open sidecar at the same (parent, stem), - // and deletes the proprietary file. Sidecar match uses - // case-insensitive extension comparison. - // - // Honors --dry-run for safe previews. Mirrors the - // --strip-zone convention (defaults to actually delete). - // - // Recommended workflow: --info-data-tree to see the - // share, --migrate-data-tree to fill in missing sidecars, - // --strip-data-tree --dry-run to confirm the kill list, - // then --strip-data-tree to apply. - std::string srcDir = argv[++i]; - bool dryRun = false; - if (i + 1 < argc && std::strcmp(argv[i + 1], "--dry-run") == 0) { - dryRun = true; i++; - } - namespace fs = std::filesystem; - if (!fs::exists(srcDir) || !fs::is_directory(srcDir)) { - std::fprintf(stderr, - "strip-data-tree: %s is not a directory\n", - srcDir.c_str()); - return 1; - } - // Build the (parent, stem) set of every open file first. - // The proprietary→open ext map serves both as the strip - // target list and as the per-pair routing table. - static const std::vector> - kPairs = { - {".m2", ".wom"}, - {".wmo", ".wob"}, - {".blp", ".png"}, - {".dbc", ".json"}, - }; - std::map>> - openSets; // open ext -> set of (parent, stem) - std::error_code ec; - for (const auto& e : fs::recursive_directory_iterator(srcDir, ec)) { - if (!e.is_regular_file()) continue; - std::string ext = e.path().extension().string(); - std::transform(ext.begin(), ext.end(), ext.begin(), - [](unsigned char c) { return std::tolower(c); }); - for (const auto& [_, openExt] : kPairs) { - if (ext == openExt) { - openSets[openExt].insert( - {e.path().parent_path().string(), - e.path().stem().string()}); - break; - } - } - } - // Walk again, this time deleting (or previewing) each - // proprietary file whose key appears in its pair's open - // set. - int removed = 0, failed = 0; - uint64_t freedBytes = 0; - std::map perExtRemoved; - for (const auto& [propExt, openExt] : kPairs) { - const auto& openSet = openSets[openExt]; - if (openSet.empty()) continue; - for (const auto& e : fs::recursive_directory_iterator(srcDir, ec)) { - if (!e.is_regular_file()) continue; - std::string ext = e.path().extension().string(); - std::transform(ext.begin(), ext.end(), ext.begin(), - [](unsigned char c) { return std::tolower(c); }); - if (ext != propExt) continue; - std::pair key{ - e.path().parent_path().string(), - e.path().stem().string()}; - if (!openSet.count(key)) continue; // no sidecar — keep - uint64_t sz = e.file_size(ec); - if (ec) sz = 0; - if (dryRun) { - std::printf(" would remove: %s (%llu bytes)\n", - e.path().c_str(), - static_cast(sz)); - removed++; - perExtRemoved[propExt]++; - freedBytes += sz; - } else { - if (fs::remove(e.path(), ec)) { - std::printf(" removed: %s (%llu bytes)\n", - e.path().c_str(), - static_cast(sz)); - removed++; - perExtRemoved[propExt]++; - freedBytes += sz; - } else { - std::fprintf(stderr, - " WARN: failed to remove %s (%s)\n", - e.path().c_str(), ec.message().c_str()); - failed++; - } - } - } - } - std::printf("\nstrip-data-tree: %s%s\n", - srcDir.c_str(), dryRun ? " (dry-run)" : ""); - std::printf(" %s : %d file(s)\n", - dryRun ? "would remove" : "removed ", removed); - std::printf(" freed : %.1f KB\n", freedBytes / 1024.0); - if (!perExtRemoved.empty()) { - std::printf("\n Per-extension:\n"); - for (const auto& [ext, count] : perExtRemoved) { - std::printf(" %-5s : %d\n", ext.c_str(), count); - } - } - if (failed > 0) { - std::printf("\n FAILED : %d (see stderr)\n", failed); - } - if (dryRun && removed > 0) { - std::printf("\n re-run without --dry-run to apply\n"); - } - return failed == 0 ? 0 : 1; - } else if (std::strcmp(argv[i], "--audit-data-tree") == 0 && i + 1 < argc) { - // Non-destructive CI gate. Walks and exits 1 if - // any proprietary file (.m2/.wmo/.blp/.dbc) lacks a - // matching open sidecar at the same (parent, stem). The - // pre-strip safety check: don't run --strip-data-tree - // until this returns exit 0. - // - // Lists missing sidecars (capped at 50) so the user can - // re-run --migrate-data-tree to fill them in. - std::string srcDir = argv[++i]; - namespace fs = std::filesystem; - if (!fs::exists(srcDir) || !fs::is_directory(srcDir)) { - std::fprintf(stderr, - "audit-data-tree: %s is not a directory\n", - srcDir.c_str()); - return 1; - } - static const std::vector> - kPairs = { - {".m2", ".wom"}, - {".wmo", ".wob"}, - {".blp", ".png"}, - {".dbc", ".json"}, - }; - // Build (parent, stem) sets per open ext for fast lookup. - std::map>> - openSets; - std::map> propByExt; - std::error_code ec; - for (const auto& e : fs::recursive_directory_iterator(srcDir, ec)) { - if (!e.is_regular_file()) continue; - std::string ext = e.path().extension().string(); - std::transform(ext.begin(), ext.end(), ext.begin(), - [](unsigned char c) { return std::tolower(c); }); - bool isOpen = false; - for (const auto& [propExt, openExt] : kPairs) { - if (ext == openExt) { - openSets[openExt].insert( - {e.path().parent_path().string(), - e.path().stem().string()}); - isOpen = true; - break; - } - } - if (isOpen) continue; - for (const auto& [propExt, _] : kPairs) { - if (ext == propExt) { - propByExt[propExt].push_back(e.path().string()); - break; - } - } - } - // Check each proprietary file for its sidecar. - int totalProp = 0, totalMissing = 0; - std::vector missing; - std::map missingPerExt; - for (const auto& [propExt, openExt] : kPairs) { - const auto& openSet = openSets[openExt]; - for (const auto& fullPath : propByExt[propExt]) { - totalProp++; - fs::path p(fullPath); - std::pair key{ - p.parent_path().string(), p.stem().string()}; - if (openSet.count(key)) continue; - totalMissing++; - missingPerExt[propExt]++; - missing.push_back(fullPath); - } - } - std::sort(missing.begin(), missing.end()); - std::printf("audit-data-tree: %s\n", srcDir.c_str()); - std::printf(" proprietary files : %d\n", totalProp); - std::printf(" missing sidecars : %d\n", totalMissing); - if (totalMissing == 0) { - if (totalProp > 0) { - std::printf("\n PASSED — every proprietary file has an open sidecar\n"); - } else { - std::printf("\n PASSED — no proprietary files present\n"); - } - return 0; - } - std::printf("\n FAILED — re-run --migrate-data-tree to fill the gaps\n"); - std::printf("\n Per-extension missing:\n"); - for (const auto& [ext, count] : missingPerExt) { - std::printf(" %-5s : %d\n", ext.c_str(), count); - } - std::printf("\n Missing sidecars (sorted):\n"); - size_t shown = 0; - for (const auto& m : missing) { - if (shown >= 50) { - std::printf(" ... and %zu more\n", missing.size() - shown); - break; - } - std::printf(" - %s\n", m.c_str()); - shown++; - } - return 1; } else if (std::strcmp(argv[i], "--repair-zone") == 0 && i + 1 < argc) { // Auto-fix the common manifest-vs-disk drift issues that // accumulate when a zone is hand-edited or partially copied: