feat(editor): add --info-pack-budget for per-extension WCP byte breakdown

Where --info-wcp shows file counts per category, this drills into
per-extension byte costs so users can spot what's bloating an
archive before shipping:

  wowee_editor --info-pack-budget custom_zones/MyZone.wcp

  WCP budget: custom_zones/MyZone.wcp
    total: 47 file(s), 2.34 MB

    ext           count        bytes      KB    share
    .whm              4      1683456  1644.0   70.3%
    .wob              3       451200   440.6   18.8%
    .wom             12       163840   160.0    6.8%
    .json             8        85120    83.1    3.6%
    .woc              1        12672    12.4    0.5%

Sorted by bytes descending so the heaviest contributors surface
first. Useful for:
- Spotting accidental .glb/.obj inclusion in shipping packs
  (`--pack-wcp` should run after `--strip-zone` to keep
  derived outputs out)
- Capacity budgeting when targeting a max-pack-size
- Comparing pre/post compression ratios

Pairs with --info-wcp (counts), --list-wcp (full file list),
--diff-wcp (compare two packs), --info-pack-budget (this one,
byte costs).

Verified on a freshly-mvp-zone packed WCP: 6 files / 0.17 MB
correctly broken down (whm 84%, wot 14.9%, json 1.1%).
This commit is contained in:
Kelsi 2026-05-06 17:38:28 -07:00
parent baf54d5e47
commit 1797ffd280

View file

@ -740,6 +740,8 @@ static void printUsage(const char* argv0) {
std::printf(" Print every field for one object placement (type, path, transform)\n");
std::printf(" --info-wcp <wcp-path> [--json]\n");
std::printf(" Print WCP archive metadata (name, files) and exit\n");
std::printf(" --info-pack-budget <wcp-path> [--json]\n");
std::printf(" Per-extension byte breakdown of a WCP archive (sized largest-first)\n");
std::printf(" --list-wcp <wcp-path> Print every file inside a WCP archive (sorted by path) and exit\n");
std::printf(" --diff-wcp <a> <b> [--json]\n");
std::printf(" Compare two WCPs file-by-file; exit 0 if identical, 1 otherwise\n");
@ -791,7 +793,7 @@ int main(int argc, char* argv[]) {
"--info-wob", "--info-woc", "--info-wot",
"--info-creatures", "--info-objects", "--info-quests",
"--info-extract", "--info-extract-tree", "--list-missing-sidecars",
"--info-png", "--info-jsondbc", "--info-blp",
"--info-png", "--info-jsondbc", "--info-blp", "--info-pack-budget",
"--info-m2", "--info-wmo", "--info-adt",
"--info-zone", "--info-wcp", "--list-wcp",
"--list-creatures", "--list-objects", "--list-quests",
@ -4205,6 +4207,71 @@ int main(int argc, char* argv[]) {
}
std::printf(" total bytes : %.2f MB\n", totalSize / (1024.0 * 1024.0));
return 0;
} else if (std::strcmp(argv[i], "--info-pack-budget") == 0 && i + 1 < argc) {
// Per-extension byte breakdown of a WCP archive. --info-wcp
// gives counts per category; this gives bytes per extension
// so users can spot what's bloating an archive before
// shipping. ('Why is my pack 80MB? Oh, the .glb baked
// outputs got included.')
std::string path = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
wowee::editor::ContentPackInfo info;
if (!wowee::editor::ContentPacker::readInfo(path, info)) {
std::fprintf(stderr,
"info-pack-budget: failed to read %s\n", path.c_str());
return 1;
}
// Sum bytes per extension (lower-cased).
std::map<std::string, std::pair<int, uint64_t>> byExt;
uint64_t totalBytes = 0;
for (const auto& f : info.files) {
std::string ext;
auto dot = f.path.find_last_of('.');
if (dot != std::string::npos) ext = f.path.substr(dot);
else ext = "(no-ext)";
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return std::tolower(c); });
byExt[ext].first++;
byExt[ext].second += f.size;
totalBytes += f.size;
}
// Sort by bytes descending.
std::vector<std::pair<std::string, std::pair<int, uint64_t>>> sorted(
byExt.begin(), byExt.end());
std::sort(sorted.begin(), sorted.end(),
[](const auto& a, const auto& b) {
return a.second.second > b.second.second;
});
if (jsonOut) {
nlohmann::json j;
j["wcp"] = path;
j["totalFiles"] = info.files.size();
j["totalBytes"] = totalBytes;
nlohmann::json arr = nlohmann::json::array();
for (const auto& [ext, cb] : sorted) {
arr.push_back({{"ext", ext},
{"count", cb.first},
{"bytes", cb.second}});
}
j["byExtension"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("WCP budget: %s\n", path.c_str());
std::printf(" total: %zu file(s), %.2f MB\n",
info.files.size(), totalBytes / (1024.0 * 1024.0));
std::printf("\n ext count bytes KB share\n");
for (const auto& [ext, cb] : sorted) {
double pct = totalBytes > 0
? 100.0 * cb.second / totalBytes : 0.0;
std::printf(" %-12s %6d %11llu %6.1f %5.1f%%\n",
ext.c_str(), cb.first,
static_cast<unsigned long long>(cb.second),
cb.second / 1024.0, pct);
}
return 0;
} else if (std::strcmp(argv[i], "--info-wot") == 0 && i + 1 < argc) {
std::string base = argv[++i];
bool jsonOut = (i + 1 < argc &&