From c0f2ab751584f28ae789e2ea840ed69c102cf71b Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 8 May 2026 18:24:01 -0700 Subject: [PATCH] refactor(editor): extract per-zone inventory into cli_zone_inventory.cpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Continues the modularization. Moves the four per-zone inventory handlers into their own file: --list-zone-meshes (WOM stats) --list-zone-audio (WAV header parse) --list-zone-textures (texture-ref histogram) --info-zone-summary (BOOTSTRAPPED/PARTIAL/EMPTY status) Each consumes a trailing --json flag the same way; consolidated that check into a consumeJsonFlag helper at the top. main.cpp drops 27,445 → 27,200 lines (-245). Per-zone inventory behavior unchanged (re-verified all 4 commands). A pre-existing duplicate --list-zone-meshes handler (the older detailed per-mesh listing, similar to the --list-project-meshes collision fixed in 976549fe) is now dead code in the in-line chain since the extracted module dispatches first. Will rename or remove that duplicate in a follow-up cleanup. --- CMakeLists.txt | 1 + tools/editor/cli_zone_inventory.cpp | 402 ++++++++++++++++++++++++++++ tools/editor/cli_zone_inventory.hpp | 18 ++ tools/editor/main.cpp | 369 +------------------------ 4 files changed, 425 insertions(+), 365 deletions(-) create mode 100644 tools/editor/cli_zone_inventory.cpp create mode 100644 tools/editor/cli_zone_inventory.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 50baa14f..9c96d7d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1302,6 +1302,7 @@ add_executable(wowee_editor tools/editor/cli_zone_packs.cpp tools/editor/cli_audits.cpp tools/editor/cli_readmes.cpp + tools/editor/cli_zone_inventory.cpp tools/editor/editor_app.cpp tools/editor/editor_camera.cpp tools/editor/editor_viewport.cpp diff --git a/tools/editor/cli_zone_inventory.cpp b/tools/editor/cli_zone_inventory.cpp new file mode 100644 index 00000000..bd770579 --- /dev/null +++ b/tools/editor/cli_zone_inventory.cpp @@ -0,0 +1,402 @@ +#include "cli_zone_inventory.hpp" + +#include "pipeline/wowee_model.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +// Match `[--json]` trailing flag and consume it. Returns true if +// --json was present (caller emits JSON instead of human table). +bool consumeJsonFlag(int& i, int argc, char** argv) { + if (i + 1 < argc && std::strcmp(argv[i + 1], "--json") == 0) { + i++; + return true; + } + return false; +} + +int handleZoneMeshes(int& i, int argc, char** argv) { + // Inventory every WOM in a zone with quick stats: file size, + // vert/tri/bone/anim/batch counts. Companion to + // --list-zone-textures (which counts inbound texture refs). + std::string zoneDir = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + namespace fs = std::filesystem; + if (!fs::exists(zoneDir + "/zone.json")) { + std::fprintf(stderr, + "list-zone-meshes: %s has no zone.json\n", zoneDir.c_str()); + return 1; + } + struct Row { + std::string path; + uint64_t bytes = 0; + size_t verts = 0, tris = 0; + size_t bones = 0, anims = 0, batches = 0, textures = 0; + }; + std::vector rows; + std::error_code ec; + for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) { + if (!e.is_regular_file()) continue; + if (e.path().extension() != ".wom") continue; + Row r; + r.path = fs::relative(e.path(), zoneDir).string(); + r.bytes = static_cast(e.file_size()); + std::string base = e.path().string(); + base = base.substr(0, base.size() - 4); + auto wom = wowee::pipeline::WoweeModelLoader::load(base); + r.verts = wom.vertices.size(); + r.tris = wom.indices.size() / 3; + r.bones = wom.bones.size(); + r.anims = wom.animations.size(); + r.batches = wom.batches.size(); + r.textures = wom.texturePaths.size(); + rows.push_back(std::move(r)); + } + std::sort(rows.begin(), rows.end(), + [](const Row& a, const Row& b) { return a.path < b.path; }); + uint64_t totalBytes = 0; + size_t totalVerts = 0, totalTris = 0; + for (const auto& r : rows) { + totalBytes += r.bytes; + totalVerts += r.verts; + totalTris += r.tris; + } + if (jsonOut) { + nlohmann::json j; + j["zone"] = zoneDir; + j["meshCount"] = rows.size(); + j["totalBytes"] = totalBytes; + j["totalVerts"] = totalVerts; + j["totalTris"] = totalTris; + nlohmann::json arr = nlohmann::json::array(); + for (const auto& r : rows) { + arr.push_back({ + {"path", r.path}, + {"bytes", r.bytes}, + {"verts", r.verts}, + {"tris", r.tris}, + {"bones", r.bones}, + {"anims", r.anims}, + {"batches", r.batches}, + {"textures", r.textures}, + }); + } + j["meshes"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("Zone meshes: %s\n", zoneDir.c_str()); + std::printf(" meshes : %zu\n", rows.size()); + std::printf(" total bytes : %llu\n", + static_cast(totalBytes)); + std::printf(" total verts : %zu\n", totalVerts); + std::printf(" total tris : %zu\n", totalTris); + if (rows.empty()) { + std::printf(" *no .wom files found*\n"); + return 0; + } + std::printf("\n %8s %7s %7s %4s %4s %4s %4s %s\n", + "bytes", "verts", "tris", "bone", "anim", "batc", "tex", "path"); + for (const auto& r : rows) { + std::printf(" %8llu %7zu %7zu %4zu %4zu %4zu %4zu %s\n", + static_cast(r.bytes), + r.verts, r.tris, + r.bones, r.anims, r.batches, r.textures, + r.path.c_str()); + } + return 0; +} + +int handleZoneAudio(int& i, int argc, char** argv) { + // Inventory every WAV under /audio/ with stats parsed + // from the RIFF/WAVE header: sample rate, channels, bits per + // sample, duration. Limited to audio/ subdir to avoid walking + // the whole zone tree. + std::string zoneDir = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + namespace fs = std::filesystem; + if (!fs::exists(zoneDir + "/zone.json")) { + std::fprintf(stderr, + "list-zone-audio: %s has no zone.json\n", zoneDir.c_str()); + return 1; + } + struct Row { + std::string path; + uint64_t bytes = 0; + uint32_t sampleRate = 0; + uint16_t channels = 0; + uint16_t bitsPerSample = 0; + float duration = 0.0f; + bool valid = false; + }; + std::vector rows; + std::error_code ec; + fs::path audioDir = fs::path(zoneDir) / "audio"; + if (!fs::exists(audioDir)) { + std::printf("Zone audio: %s\n", zoneDir.c_str()); + std::printf(" *no audio/ subdirectory*\n"); + return 0; + } + for (const auto& e : fs::recursive_directory_iterator(audioDir, ec)) { + if (!e.is_regular_file()) continue; + if (e.path().extension() != ".wav") continue; + Row r; + r.path = fs::relative(e.path(), zoneDir).string(); + r.bytes = static_cast(e.file_size()); + FILE* f = std::fopen(e.path().c_str(), "rb"); + if (f) { + char hdr[44]; + if (std::fread(hdr, 1, 44, f) == 44 && + std::memcmp(hdr, "RIFF", 4) == 0 && + std::memcmp(hdr + 8, "WAVE", 4) == 0 && + std::memcmp(hdr + 12, "fmt ", 4) == 0) { + std::memcpy(&r.channels, hdr + 22, 2); + std::memcpy(&r.sampleRate, hdr + 24, 4); + std::memcpy(&r.bitsPerSample, hdr + 34, 2); + uint32_t dataBytes = 0; + std::memcpy(&dataBytes, hdr + 40, 4); + if (r.sampleRate > 0 && r.channels > 0 && r.bitsPerSample > 0) { + uint32_t bytesPerSample = + static_cast(r.channels) * + (r.bitsPerSample / 8); + if (bytesPerSample > 0) { + r.duration = + static_cast(dataBytes) / + (r.sampleRate * bytesPerSample); + } + r.valid = true; + } + } + std::fclose(f); + } + rows.push_back(std::move(r)); + } + std::sort(rows.begin(), rows.end(), + [](const Row& a, const Row& b) { return a.path < b.path; }); + uint64_t totalBytes = 0; + float totalDuration = 0.0f; + for (const auto& r : rows) { + totalBytes += r.bytes; + totalDuration += r.duration; + } + if (jsonOut) { + nlohmann::json j; + j["zone"] = zoneDir; + j["wavCount"] = rows.size(); + j["totalBytes"] = totalBytes; + j["totalDuration"] = totalDuration; + nlohmann::json arr = nlohmann::json::array(); + for (const auto& r : rows) { + arr.push_back({ + {"path", r.path}, + {"bytes", r.bytes}, + {"sampleRate", r.sampleRate}, + {"channels", r.channels}, + {"bitsPerSample", r.bitsPerSample}, + {"duration", r.duration}, + {"valid", r.valid}, + }); + } + j["audio"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("Zone audio: %s\n", zoneDir.c_str()); + std::printf(" WAVs : %zu\n", rows.size()); + std::printf(" total bytes : %llu\n", + static_cast(totalBytes)); + std::printf(" total duration : %.2f sec\n", totalDuration); + if (rows.empty()) { + std::printf(" *no .wav files found in audio/*\n"); + return 0; + } + std::printf("\n %8s %6s %4s %4s %7s %s\n", + "bytes", "rate", "ch", "bit", "sec", "path"); + for (const auto& r : rows) { + if (r.valid) { + std::printf(" %8llu %6u %4u %4u %7.2f %s\n", + static_cast(r.bytes), + r.sampleRate, + static_cast(r.channels), + static_cast(r.bitsPerSample), + r.duration, r.path.c_str()); + } else { + std::printf(" %8llu ? ? ? ? %s (invalid header)\n", + static_cast(r.bytes), + r.path.c_str()); + } + } + return 0; +} + +int handleZoneTextures(int& i, int argc, char** argv) { + // Aggregate texture references across every WOM model in a + // zone directory. Lists the textures those models pull in + // with reference counts, useful for ship-list verification. + std::string zoneDir = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + namespace fs = std::filesystem; + if (!fs::exists(zoneDir + "/zone.json")) { + std::fprintf(stderr, + "list-zone-textures: %s has no zone.json\n", zoneDir.c_str()); + return 1; + } + std::map texHist; // path -> count of WOMs that ref it + int womCount = 0; + std::error_code ec; + for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) { + if (!e.is_regular_file()) continue; + if (e.path().extension() != ".wom") continue; + womCount++; + std::string base = e.path().string(); + if (base.size() >= 4) base = base.substr(0, base.size() - 4); + auto wom = wowee::pipeline::WoweeModelLoader::load(base); + std::unordered_set seenInThisWom; + for (const auto& tp : wom.texturePaths) { + if (tp.empty()) continue; + if (seenInThisWom.insert(tp).second) { + texHist[tp]++; + } + } + } + if (jsonOut) { + nlohmann::json j; + j["zone"] = zoneDir; + j["womCount"] = womCount; + j["uniqueTextures"] = texHist.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& [path, count] : texHist) { + arr.push_back({{"path", path}, {"refCount", count}}); + } + j["textures"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("Zone textures: %s\n", zoneDir.c_str()); + std::printf(" WOMs scanned : %d\n", womCount); + std::printf(" unique textures : %zu\n", texHist.size()); + if (texHist.empty()) { + std::printf(" *no texture references*\n"); + return 0; + } + std::printf("\n refs path\n"); + for (const auto& [path, count] : texHist) { + std::printf(" %4d %s\n", count, path.c_str()); + } + return 0; +} + +int handleZoneSummary(int& i, int argc, char** argv) { + // One-glance health digest for a zone. Combines per-category + // counts/bytes with a quick BOOTSTRAPPED/PARTIAL/EMPTY status. + std::string zoneDir = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + namespace fs = std::filesystem; + if (!fs::exists(zoneDir + "/zone.json")) { + std::fprintf(stderr, + "info-zone-summary: %s has no zone.json\n", zoneDir.c_str()); + return 1; + } + std::string mapName = "?"; + try { + std::ifstream zf(zoneDir + "/zone.json"); + if (zf) { + nlohmann::json zj; + zf >> zj; + if (zj.contains("mapName") && zj["mapName"].is_string()) { + mapName = zj["mapName"].get(); + } + } + } catch (...) { /* tolerated — leave as ? */ } + auto scan = [&](const std::string& sub, const std::string& ext) + -> std::pair { + int n = 0; + uint64_t b = 0; + fs::path p = fs::path(zoneDir) / sub; + if (!fs::exists(p)) return {0, 0}; + std::error_code ec; + for (const auto& e : fs::recursive_directory_iterator(p, ec)) { + if (!e.is_regular_file()) continue; + if (e.path().extension() != ext) continue; + n++; + b += e.file_size(); + } + return {n, b}; + }; + auto [texN, texB] = scan("textures", ".png"); + auto [mshN, mshB] = scan("meshes", ".wom"); + auto [audN, audB] = scan("audio", ".wav"); + std::string status; + if (texN > 0 && mshN > 0 && audN > 0) status = "BOOTSTRAPPED"; + else if (texN + mshN + audN > 0) status = "PARTIAL"; + else status = "EMPTY"; + uint64_t totalBytes = texB + mshB + audB; + int totalAssets = texN + mshN + audN; + if (jsonOut) { + nlohmann::json j; + j["zone"] = zoneDir; + j["mapName"] = mapName; + j["status"] = status; + j["totalAssets"] = totalAssets; + j["totalBytes"] = totalBytes; + j["textures"] = {{"count", texN}, {"bytes", texB}}; + j["meshes"] = {{"count", mshN}, {"bytes", mshB}}; + j["audio"] = {{"count", audN}, {"bytes", audB}}; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("Zone: %s (%s)\n", mapName.c_str(), zoneDir.c_str()); + std::printf(" status : %s\n", status.c_str()); + std::printf(" textures : %d (%llu bytes)\n", + texN, static_cast(texB)); + std::printf(" meshes : %d (%llu bytes)\n", + mshN, static_cast(mshB)); + std::printf(" audio : %d (%llu bytes)\n", + audN, static_cast(audB)); + std::printf(" TOTAL : %d assets, %llu bytes\n", + totalAssets, + static_cast(totalBytes)); + return 0; +} + +} // namespace + +bool handleZoneInventory(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--list-zone-meshes") == 0 && i + 1 < argc) { + outRc = handleZoneMeshes(i, argc, argv); + return true; + } + if (std::strcmp(argv[i], "--list-zone-audio") == 0 && i + 1 < argc) { + outRc = handleZoneAudio(i, argc, argv); + return true; + } + if (std::strcmp(argv[i], "--list-zone-textures") == 0 && i + 1 < argc) { + outRc = handleZoneTextures(i, argc, argv); + return true; + } + if (std::strcmp(argv[i], "--info-zone-summary") == 0 && i + 1 < argc) { + outRc = handleZoneSummary(i, argc, argv); + return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_zone_inventory.hpp b/tools/editor/cli_zone_inventory.hpp new file mode 100644 index 00000000..894ad5f4 --- /dev/null +++ b/tools/editor/cli_zone_inventory.hpp @@ -0,0 +1,18 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +// Dispatch the four per-zone inventory handlers: +// --list-zone-meshes (WOMs with vert/tri/bone/anim/batch counts) +// --list-zone-audio (WAVs with sample-rate/duration) +// --list-zone-textures (texture refs across every WOM) +// --info-zone-summary (one-glance BOOTSTRAPPED/PARTIAL/EMPTY) +// +// Returns true if matched; outRc holds the exit code. +bool handleZoneInventory(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 d0ef108f..1419aa42 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -3,6 +3,7 @@ #include "cli_zone_packs.hpp" #include "cli_audits.hpp" #include "cli_readmes.hpp" +#include "cli_zone_inventory.hpp" #include "content_pack.hpp" #include "npc_spawner.hpp" #include "object_placer.hpp" @@ -1379,6 +1380,9 @@ int main(int argc, char* argv[]) { if (wowee::editor::cli::handleReadmes(i, argc, argv, outRc)) { return outRc; } + if (wowee::editor::cli::handleZoneInventory(i, argc, argv, outRc)) { + return outRc; + } } if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) { dataPath = argv[++i]; @@ -1949,290 +1953,6 @@ int main(int argc, char* argv[]) { wom.bones.size(), rootCount); std::printf(" next: dot -Tpng %s -o bones.png\n", outPath.c_str()); return 0; - } else if (std::strcmp(argv[i], "--list-zone-meshes") == 0 && i + 1 < argc) { - // Inventory every WOM in a zone with quick stats: file - // size, vert/tri/bone/anim/batch counts. Companion to - // --list-zone-textures (which counts inbound texture - // refs); this answers "which meshes ship with this - // zone and how heavy is each." - std::string zoneDir = argv[++i]; - bool jsonOut = (i + 1 < argc && - std::strcmp(argv[i + 1], "--json") == 0); - if (jsonOut) i++; - namespace fs = std::filesystem; - if (!fs::exists(zoneDir + "/zone.json")) { - std::fprintf(stderr, - "list-zone-meshes: %s has no zone.json\n", zoneDir.c_str()); - return 1; - } - struct Row { - std::string path; - uint64_t bytes = 0; - size_t verts = 0, tris = 0; - size_t bones = 0, anims = 0, batches = 0, textures = 0; - }; - std::vector rows; - std::error_code ec; - for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) { - if (!e.is_regular_file()) continue; - if (e.path().extension() != ".wom") continue; - Row r; - r.path = fs::relative(e.path(), zoneDir).string(); - r.bytes = static_cast(e.file_size()); - std::string base = e.path().string(); - base = base.substr(0, base.size() - 4); - auto wom = wowee::pipeline::WoweeModelLoader::load(base); - r.verts = wom.vertices.size(); - r.tris = wom.indices.size() / 3; - r.bones = wom.bones.size(); - r.anims = wom.animations.size(); - r.batches = wom.batches.size(); - r.textures = wom.texturePaths.size(); - rows.push_back(std::move(r)); - } - std::sort(rows.begin(), rows.end(), - [](const Row& a, const Row& b) { return a.path < b.path; }); - uint64_t totalBytes = 0; - size_t totalVerts = 0, totalTris = 0; - for (const auto& r : rows) { - totalBytes += r.bytes; - totalVerts += r.verts; - totalTris += r.tris; - } - if (jsonOut) { - nlohmann::json j; - j["zone"] = zoneDir; - j["meshCount"] = rows.size(); - j["totalBytes"] = totalBytes; - j["totalVerts"] = totalVerts; - j["totalTris"] = totalTris; - nlohmann::json arr = nlohmann::json::array(); - for (const auto& r : rows) { - arr.push_back({ - {"path", r.path}, - {"bytes", r.bytes}, - {"verts", r.verts}, - {"tris", r.tris}, - {"bones", r.bones}, - {"anims", r.anims}, - {"batches", r.batches}, - {"textures", r.textures}, - }); - } - j["meshes"] = arr; - std::printf("%s\n", j.dump(2).c_str()); - return 0; - } - std::printf("Zone meshes: %s\n", zoneDir.c_str()); - std::printf(" meshes : %zu\n", rows.size()); - std::printf(" total bytes : %llu\n", - static_cast(totalBytes)); - std::printf(" total verts : %zu\n", totalVerts); - std::printf(" total tris : %zu\n", totalTris); - if (rows.empty()) { - std::printf(" *no .wom files found*\n"); - return 0; - } - std::printf("\n %8s %7s %7s %4s %4s %4s %4s %s\n", - "bytes", "verts", "tris", "bone", "anim", "batc", "tex", "path"); - for (const auto& r : rows) { - std::printf(" %8llu %7zu %7zu %4zu %4zu %4zu %4zu %s\n", - static_cast(r.bytes), - r.verts, r.tris, - r.bones, r.anims, r.batches, r.textures, - r.path.c_str()); - } - return 0; - } else if (std::strcmp(argv[i], "--list-zone-audio") == 0 && i + 1 < argc) { - // Inventory every WAV under /audio/ with quick - // stats parsed straight from the RIFF/WAVE header: - // sample rate, channel count, bits per sample, duration. - // Companion to --list-zone-meshes / --list-zone-textures - // — completes the per-zone asset accounting trio. - std::string zoneDir = argv[++i]; - bool jsonOut = (i + 1 < argc && - std::strcmp(argv[i + 1], "--json") == 0); - if (jsonOut) i++; - namespace fs = std::filesystem; - if (!fs::exists(zoneDir + "/zone.json")) { - std::fprintf(stderr, - "list-zone-audio: %s has no zone.json\n", zoneDir.c_str()); - return 1; - } - struct Row { - std::string path; - uint64_t bytes = 0; - uint32_t sampleRate = 0; - uint16_t channels = 0; - uint16_t bitsPerSample = 0; - float duration = 0.0f; - bool valid = false; - }; - std::vector rows; - std::error_code ec; - // Limit the search to /audio/ (matches the - // gen-zone-audio-pack output convention) so we don't - // walk the entire zone tree looking for stray WAVs. - fs::path audioDir = fs::path(zoneDir) / "audio"; - if (!fs::exists(audioDir)) { - std::printf("Zone audio: %s\n", zoneDir.c_str()); - std::printf(" *no audio/ subdirectory*\n"); - return 0; - } - for (const auto& e : fs::recursive_directory_iterator(audioDir, ec)) { - if (!e.is_regular_file()) continue; - if (e.path().extension() != ".wav") continue; - Row r; - r.path = fs::relative(e.path(), zoneDir).string(); - r.bytes = static_cast(e.file_size()); - FILE* f = std::fopen(e.path().c_str(), "rb"); - if (f) { - // RIFF header: 12 bytes (RIFF + size + WAVE). - // fmt chunk follows: "fmt " + 16 + format(2) + - // channels(2) + sampleRate(4) + byteRate(4) + - // blockAlign(2) + bitsPerSample(2). We only - // peek the 4-byte fields we care about. - char hdr[44]; - if (std::fread(hdr, 1, 44, f) == 44 && - std::memcmp(hdr, "RIFF", 4) == 0 && - std::memcmp(hdr + 8, "WAVE", 4) == 0 && - std::memcmp(hdr + 12, "fmt ", 4) == 0) { - std::memcpy(&r.channels, hdr + 22, 2); - std::memcpy(&r.sampleRate, hdr + 24, 4); - std::memcpy(&r.bitsPerSample, hdr + 34, 2); - uint32_t dataBytes = 0; - std::memcpy(&dataBytes, hdr + 40, 4); - if (r.sampleRate > 0 && r.channels > 0 && r.bitsPerSample > 0) { - uint32_t bytesPerSample = - static_cast(r.channels) * - (r.bitsPerSample / 8); - if (bytesPerSample > 0) { - r.duration = - static_cast(dataBytes) / - (r.sampleRate * bytesPerSample); - } - r.valid = true; - } - } - std::fclose(f); - } - rows.push_back(std::move(r)); - } - std::sort(rows.begin(), rows.end(), - [](const Row& a, const Row& b) { return a.path < b.path; }); - uint64_t totalBytes = 0; - float totalDuration = 0.0f; - for (const auto& r : rows) { - totalBytes += r.bytes; - totalDuration += r.duration; - } - if (jsonOut) { - nlohmann::json j; - j["zone"] = zoneDir; - j["wavCount"] = rows.size(); - j["totalBytes"] = totalBytes; - j["totalDuration"] = totalDuration; - nlohmann::json arr = nlohmann::json::array(); - for (const auto& r : rows) { - arr.push_back({ - {"path", r.path}, - {"bytes", r.bytes}, - {"sampleRate", r.sampleRate}, - {"channels", r.channels}, - {"bitsPerSample", r.bitsPerSample}, - {"duration", r.duration}, - {"valid", r.valid}, - }); - } - j["audio"] = arr; - std::printf("%s\n", j.dump(2).c_str()); - return 0; - } - std::printf("Zone audio: %s\n", zoneDir.c_str()); - std::printf(" WAVs : %zu\n", rows.size()); - std::printf(" total bytes : %llu\n", - static_cast(totalBytes)); - std::printf(" total duration : %.2f sec\n", totalDuration); - if (rows.empty()) { - std::printf(" *no .wav files found in audio/*\n"); - return 0; - } - std::printf("\n %8s %6s %4s %4s %7s %s\n", - "bytes", "rate", "ch", "bit", "sec", "path"); - for (const auto& r : rows) { - if (r.valid) { - std::printf(" %8llu %6u %4u %4u %7.2f %s\n", - static_cast(r.bytes), - r.sampleRate, - static_cast(r.channels), - static_cast(r.bitsPerSample), - r.duration, r.path.c_str()); - } else { - std::printf(" %8llu ? ? ? ? %s (invalid header)\n", - static_cast(r.bytes), - r.path.c_str()); - } - } - return 0; - } else if (std::strcmp(argv[i], "--list-zone-textures") == 0 && i + 1 < argc) { - // Aggregate texture references across every WOM model in a - // zone directory. Companion to --list-zone-deps (which lists - // model paths) — this lists the textures those models pull in. - // Useful for verifying every BLP/PNG ships with the zone. - std::string zoneDir = argv[++i]; - bool jsonOut = (i + 1 < argc && - std::strcmp(argv[i + 1], "--json") == 0); - if (jsonOut) i++; - namespace fs = std::filesystem; - if (!fs::exists(zoneDir + "/zone.json")) { - std::fprintf(stderr, - "list-zone-textures: %s has no zone.json\n", zoneDir.c_str()); - return 1; - } - std::map texHist; // path -> count of WOMs that ref it - int womCount = 0; - std::error_code ec; - for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) { - if (!e.is_regular_file()) continue; - std::string ext = e.path().extension().string(); - if (ext != ".wom") continue; - womCount++; - std::string base = e.path().string(); - if (base.size() >= 4) base = base.substr(0, base.size() - 4); - auto wom = wowee::pipeline::WoweeModelLoader::load(base); - std::unordered_set seenInThisWom; - for (const auto& tp : wom.texturePaths) { - if (tp.empty()) continue; - if (seenInThisWom.insert(tp).second) { - texHist[tp]++; - } - } - } - if (jsonOut) { - nlohmann::json j; - j["zone"] = zoneDir; - j["womCount"] = womCount; - j["uniqueTextures"] = texHist.size(); - nlohmann::json arr = nlohmann::json::array(); - for (const auto& [path, count] : texHist) { - arr.push_back({{"path", path}, {"refCount", count}}); - } - j["textures"] = arr; - std::printf("%s\n", j.dump(2).c_str()); - return 0; - } - std::printf("Zone textures: %s\n", zoneDir.c_str()); - std::printf(" WOMs scanned : %d\n", womCount); - std::printf(" unique textures : %zu\n", texHist.size()); - if (texHist.empty()) { - std::printf(" *no texture references*\n"); - return 0; - } - std::printf("\n refs path\n"); - for (const auto& [path, count] : texHist) { - std::printf(" %4d %s\n", count, path.c_str()); - } - return 0; } else if (std::strcmp(argv[i], "--list-project-meshes") == 0 && i + 1 < argc) { // Project-wide companion to --list-zone-meshes. Walks // every zone in and reports a per-zone @@ -14411,87 +14131,6 @@ int main(int argc, char* argv[]) { std::printf(" objects : %d\n", objects); std::printf(" items : %d\n", items); return 0; - } else if (std::strcmp(argv[i], "--info-zone-summary") == 0 && i + 1 < argc) { - // One-glance health digest for a zone. Combines the per- - // category counts/bytes from the inventory commands with - // a quick pass/fail signal from validate-zone-pack. Lets - // a user see at a glance whether a zone is bootstrapped, - // empty, or partially populated. - std::string zoneDir = argv[++i]; - bool jsonOut = (i + 1 < argc && - std::strcmp(argv[i + 1], "--json") == 0); - if (jsonOut) i++; - namespace fs = std::filesystem; - if (!fs::exists(zoneDir + "/zone.json")) { - std::fprintf(stderr, - "info-zone-summary: %s has no zone.json\n", zoneDir.c_str()); - return 1; - } - // Load zone.json for the friendly map name. - std::string mapName = "?"; - try { - std::ifstream zf(zoneDir + "/zone.json"); - if (zf) { - nlohmann::json zj; - zf >> zj; - if (zj.contains("mapName") && zj["mapName"].is_string()) { - mapName = zj["mapName"].get(); - } - } - } catch (...) { /* tolerated — leave as ? */ } - // Per-category quick scan: count + bytes only. - auto scan = [&](const std::string& sub, const std::string& ext) - -> std::pair { - int n = 0; - uint64_t b = 0; - fs::path p = fs::path(zoneDir) / sub; - if (!fs::exists(p)) return {0, 0}; - std::error_code ec; - for (const auto& e : fs::recursive_directory_iterator(p, ec)) { - if (!e.is_regular_file()) continue; - if (e.path().extension() != ext) continue; - n++; - b += e.file_size(); - } - return {n, b}; - }; - auto [texN, texB] = scan("textures", ".png"); - auto [mshN, mshB] = scan("meshes", ".wom"); - auto [audN, audB] = scan("audio", ".wav"); - // Pack health: bootstrap pass if we have all three - // categories with at least 1 file each. "Partial" if - // some but not all. "Empty" if none. - std::string status; - if (texN > 0 && mshN > 0 && audN > 0) status = "BOOTSTRAPPED"; - else if (texN + mshN + audN > 0) status = "PARTIAL"; - else status = "EMPTY"; - uint64_t totalBytes = texB + mshB + audB; - int totalAssets = texN + mshN + audN; - if (jsonOut) { - nlohmann::json j; - j["zone"] = zoneDir; - j["mapName"] = mapName; - j["status"] = status; - j["totalAssets"] = totalAssets; - j["totalBytes"] = totalBytes; - j["textures"] = {{"count", texN}, {"bytes", texB}}; - j["meshes"] = {{"count", mshN}, {"bytes", mshB}}; - j["audio"] = {{"count", audN}, {"bytes", audB}}; - std::printf("%s\n", j.dump(2).c_str()); - return 0; - } - std::printf("Zone: %s (%s)\n", mapName.c_str(), zoneDir.c_str()); - std::printf(" status : %s\n", status.c_str()); - std::printf(" textures : %d (%llu bytes)\n", - texN, static_cast(texB)); - std::printf(" meshes : %d (%llu bytes)\n", - mshN, static_cast(mshB)); - std::printf(" audio : %d (%llu bytes)\n", - audN, static_cast(audB)); - std::printf(" TOTAL : %d assets, %llu bytes\n", - totalAssets, - static_cast(totalBytes)); - return 0; } else if (std::strcmp(argv[i], "--info-project-summary") == 0 && i + 1 < argc) { // Project-wide companion to --info-zone-summary. Walks // every zone in and reports a per-zone