feat(editor): add --info-project-water for cross-zone water rollup

Project-wide companion to --info-zone-water. Walks every zone in
<projectDir>, sums water chunks/layers and liquid type counts per
zone, and emits a per-zone breakdown table plus project-wide totals
broken down by liquid type (water/ocean/magma/slime).

Useful for "do my coastal zones actually carry ocean data" sanity
checks and for budget planning when many zones share liquid types
(e.g. an archipelago where every zone has 256 water chunks per tile).

Brings command count to 179.
This commit is contained in:
Kelsi 2026-05-06 22:06:22 -07:00
parent 439cd943e4
commit e54c33a792

View file

@ -688,6 +688,8 @@ static void printUsage(const char* argv0) {
std::printf(" Combined spatial bounding box across every zone (per-zone table + project union)\n");
std::printf(" --info-zone-extents <zoneDir> [--json]\n");
std::printf(" Compute the zone's bounding box (XY tile range, Z height min/max)\n");
std::printf(" --info-project-water <projectDir> [--json]\n");
std::printf(" Aggregate water-layer stats across every zone (per-zone breakdown + project totals)\n");
std::printf(" --info-zone-water <zoneDir> [--json]\n");
std::printf(" Aggregate water-layer stats across all tiles (layer count, types, area)\n");
std::printf(" --info-zone-density <zoneDir> [--json]\n");
@ -882,7 +884,7 @@ int main(int argc, char* argv[]) {
"--zone-summary", "--info-zone-tree", "--info-project-tree",
"--info-zone-bytes", "--info-project-bytes",
"--info-zone-extents", "--info-project-extents",
"--info-zone-water",
"--info-zone-water", "--info-project-water",
"--info-zone-density",
"--export-zone-summary-md", "--export-quest-graph",
"--export-zone-csv", "--export-zone-html", "--export-project-html",
@ -6098,6 +6100,133 @@ int main(int argc, char* argv[]) {
std::printf(" (no water in this zone)\n");
}
return 0;
} else if (std::strcmp(argv[i], "--info-project-water") == 0 && i + 1 < argc) {
// Project-wide water rollup. Walks every zone in projectDir,
// sums water chunks/layers/types per zone, then totals
// across the project. Useful for "do my coastal zones
// actually carry ocean data" sanity checks and for budget
// planning when many zones share liquid types.
std::string projectDir = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
namespace fs = std::filesystem;
if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) {
std::fprintf(stderr,
"info-project-water: %s is not a directory\n",
projectDir.c_str());
return 1;
}
std::vector<std::string> zones;
for (const auto& entry : fs::directory_iterator(projectDir)) {
if (!entry.is_directory()) continue;
if (!fs::exists(entry.path() / "zone.json")) continue;
zones.push_back(entry.path().string());
}
std::sort(zones.begin(), zones.end());
auto typeName = [](uint16_t t) {
switch (t) {
case 0: return "water";
case 1: return "ocean";
case 2: return "magma";
case 3: return "slime";
}
return "?";
};
struct ZRow {
std::string name;
int loadedTiles = 0, waterChunks = 0, totalLayers = 0;
std::map<uint16_t, int> typeHist;
};
std::vector<ZRow> rows;
int gLoadedTiles = 0, gWaterChunks = 0, gTotalLayers = 0;
std::map<uint16_t, int> gTypeHist;
float gMinH = 1e30f, gMaxH = -1e30f;
for (const auto& zoneDir : zones) {
ZRow r;
r.name = fs::path(zoneDir).filename().string();
wowee::editor::ZoneManifest zm;
if (!zm.load(zoneDir + "/zone.json")) {
rows.push_back(r);
continue;
}
for (const auto& [tx, ty] : zm.tiles) {
std::string tileBase = zoneDir + "/" + zm.mapName + "_" +
std::to_string(tx) + "_" + std::to_string(ty);
if (!wowee::pipeline::WoweeTerrainLoader::exists(tileBase)) continue;
wowee::pipeline::ADTTerrain terrain;
wowee::pipeline::WoweeTerrainLoader::load(tileBase, terrain);
r.loadedTiles++;
for (const auto& w : terrain.waterData) {
if (!w.hasWater()) continue;
r.waterChunks++;
r.totalLayers += static_cast<int>(w.layers.size());
for (const auto& layer : w.layers) {
r.typeHist[layer.liquidType]++;
gMinH = std::min(gMinH, layer.minHeight);
gMaxH = std::max(gMaxH, layer.maxHeight);
}
}
}
gLoadedTiles += r.loadedTiles;
gWaterChunks += r.waterChunks;
gTotalLayers += r.totalLayers;
for (const auto& [t, c] : r.typeHist) gTypeHist[t] += c;
rows.push_back(r);
}
if (gWaterChunks == 0) { gMinH = 0; gMaxH = 0; }
if (jsonOut) {
nlohmann::json j;
j["project"] = projectDir;
j["zoneCount"] = zones.size();
j["loadedTiles"] = gLoadedTiles;
j["waterChunks"] = gWaterChunks;
j["totalLayers"] = gTotalLayers;
j["heightRange"] = {gMinH, gMaxH};
nlohmann::json zarr = nlohmann::json::array();
for (const auto& r : rows) {
nlohmann::json types = nlohmann::json::array();
for (const auto& [t, c] : r.typeHist) {
types.push_back({{"type", t}, {"name", typeName(t)},
{"layerCount", c}});
}
zarr.push_back({{"name", r.name},
{"loadedTiles", r.loadedTiles},
{"waterChunks", r.waterChunks},
{"totalLayers", r.totalLayers},
{"types", types}});
}
j["zones"] = zarr;
nlohmann::json gtypes = nlohmann::json::array();
for (const auto& [t, c] : gTypeHist) {
gtypes.push_back({{"type", t}, {"name", typeName(t)},
{"layerCount", c}});
}
j["types"] = gtypes;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("Project water: %s\n", projectDir.c_str());
std::printf(" zones : %zu\n", zones.size());
std::printf(" loaded tiles : %d\n", gLoadedTiles);
std::printf(" water chunks : %d (out of %d possible)\n",
gWaterChunks, gLoadedTiles * 256);
std::printf(" total layers : %d\n", gTotalLayers);
if (gWaterChunks > 0) {
std::printf(" height range : %.2f to %.2f\n", gMinH, gMaxH);
std::printf("\n By liquid type (project-wide):\n");
for (const auto& [t, c] : gTypeHist) {
std::printf(" %s (%u): %d layer(s)\n",
typeName(t), t, c);
}
}
std::printf("\n zone tiles water-chunks layers\n");
for (const auto& r : rows) {
std::printf(" %-20s %5d %12d %6d\n",
r.name.substr(0, 20).c_str(),
r.loadedTiles, r.waterChunks, r.totalLayers);
}
return 0;
} else if (std::strcmp(argv[i], "--info-zone-density") == 0 && i + 1 < argc) {
// Per-tile content density. Catches sparse zones (5 mobs
// across 16 tiles → boring) and over-stuffed ones (200 mobs