From a25961e2dd1c32df1f5aedb42f55e5a82b3ebc0e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 07:28:15 -0700 Subject: [PATCH] refactor(editor): extract --info-{zone,project}-water into cli_info_water.cpp Moves the two water-layer audit handlers (--info-zone-water, --info-project-water) out of main.cpp into a new cli_info_water.{hpp,cpp} module. Both aggregate liquid data (water/ocean/magma/slime) across MH2O chunks; the project variant rolls per-zone counts into a project-wide total with height range and per-type histogram. main.cpp shrinks by 209 lines (7,463 to 7,254). Both --json output modes preserved for capacity-planning pipelines. --- CMakeLists.txt | 1 + tools/editor/cli_info_water.cpp | 254 ++++++++++++++++++++++++++++++++ tools/editor/cli_info_water.hpp | 22 +++ tools/editor/main.cpp | 217 +-------------------------- 4 files changed, 281 insertions(+), 213 deletions(-) create mode 100644 tools/editor/cli_info_water.cpp create mode 100644 tools/editor/cli_info_water.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 309f82aa..af303a02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1332,6 +1332,7 @@ add_executable(wowee_editor tools/editor/cli_info_tree.cpp tools/editor/cli_info_bytes.cpp tools/editor/cli_info_extents.cpp + tools/editor/cli_info_water.cpp tools/editor/editor_app.cpp tools/editor/editor_camera.cpp tools/editor/editor_viewport.cpp diff --git a/tools/editor/cli_info_water.cpp b/tools/editor/cli_info_water.cpp new file mode 100644 index 00000000..811f175a --- /dev/null +++ b/tools/editor/cli_info_water.cpp @@ -0,0 +1,254 @@ +#include "cli_info_water.hpp" + +#include "zone_manifest.hpp" +#include "pipeline/wowee_terrain_loader.hpp" +#include "pipeline/adt_loader.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +int handleInfoZoneWater(int& i, int argc, char** argv) { + // Aggregate water-layer stats across all tiles in a zone. + // Useful for confirming a 'lake zone' actually has water, + // or for budget planning ('how many MH2O cells does my + // archipelago zone carry?'). Liquid types: 0=water, + // 1=ocean, 2=magma, 3=slime. + std::string zoneDir = argv[++i]; + bool jsonOut = (i + 1 < argc && + std::strcmp(argv[i + 1], "--json") == 0); + if (jsonOut) i++; + namespace fs = std::filesystem; + std::string manifestPath = zoneDir + "/zone.json"; + if (!fs::exists(manifestPath)) { + std::fprintf(stderr, + "info-zone-water: %s has no zone.json\n", zoneDir.c_str()); + return 1; + } + wowee::editor::ZoneManifest zm; + if (!zm.load(manifestPath)) { + std::fprintf(stderr, "info-zone-water: parse failed\n"); + return 1; + } + int waterChunks = 0, totalLayers = 0; + std::map typeHist; // liquidType -> chunk count + float minH = 1e30f, maxH = -1e30f; + int loadedTiles = 0; + 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); + loadedTiles++; + for (size_t c = 0; c < terrain.waterData.size(); ++c) { + const auto& w = terrain.waterData[c]; + if (!w.hasWater()) continue; + waterChunks++; + totalLayers += static_cast(w.layers.size()); + for (const auto& layer : w.layers) { + typeHist[layer.liquidType]++; + minH = std::min(minH, layer.minHeight); + maxH = std::max(maxH, layer.maxHeight); + } + } + } + if (waterChunks == 0) { minH = 0; maxH = 0; } + auto typeName = [](uint16_t t) { + switch (t) { + case 0: return "water"; + case 1: return "ocean"; + case 2: return "magma"; + case 3: return "slime"; + } + return "?"; + }; + if (jsonOut) { + nlohmann::json j; + j["zone"] = zoneDir; + j["loadedTiles"] = loadedTiles; + j["waterChunks"] = waterChunks; + j["totalLayers"] = totalLayers; + j["heightRange"] = {minH, maxH}; + nlohmann::json types = nlohmann::json::array(); + for (const auto& [t, c] : typeHist) { + types.push_back({{"type", t}, {"name", typeName(t)}, {"layerCount", c}}); + } + j["types"] = types; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("Zone water: %s\n", zoneDir.c_str()); + std::printf(" loaded tiles : %d\n", loadedTiles); + std::printf(" water chunks : %d (out of %d possible)\n", + waterChunks, loadedTiles * 256); + std::printf(" total layers : %d\n", totalLayers); + if (waterChunks > 0) { + std::printf(" height range : %.2f to %.2f\n", minH, maxH); + std::printf("\n By liquid type:\n"); + for (const auto& [t, c] : typeHist) { + std::printf(" %s (%u): %d layer(s)\n", + typeName(t), t, c); + } + } else { + std::printf(" (no water in this zone)\n"); + } + return 0; +} + +int handleInfoProjectWater(int& i, int argc, char** argv) { + // 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 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 typeHist; + }; + std::vector rows; + int gLoadedTiles = 0, gWaterChunks = 0, gTotalLayers = 0; + std::map 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(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; +} + +} // namespace + +bool handleInfoWater(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--info-zone-water") == 0 && i + 1 < argc) { + outRc = handleInfoZoneWater(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-project-water") == 0 && i + 1 < argc) { + outRc = handleInfoProjectWater(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_info_water.hpp b/tools/editor/cli_info_water.hpp new file mode 100644 index 00000000..f6591bb0 --- /dev/null +++ b/tools/editor/cli_info_water.hpp @@ -0,0 +1,22 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +// Dispatch the water-layer audit handlers — aggregate liquid +// data (water/ocean/magma/slime) across a zone or project. +// Useful for confirming a 'lake zone' actually carries water, +// or for budget planning when many zones share liquid types. +// --info-zone-water drill into one zone's MH2O layers +// --info-project-water project-wide rollup with per-zone rows +// +// Both support an optional trailing `--json` flag for +// machine-readable reports. +// +// Returns true if matched; outRc holds the exit code. +bool handleInfoWater(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 1289df43..86a851a9 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -33,6 +33,7 @@ #include "cli_info_tree.hpp" #include "cli_info_bytes.hpp" #include "cli_info_extents.hpp" +#include "cli_info_water.hpp" #include "content_pack.hpp" #include "npc_spawner.hpp" #include "object_placer.hpp" @@ -463,6 +464,9 @@ int main(int argc, char* argv[]) { if (wowee::editor::cli::handleInfoExtents(i, argc, argv, outRc)) { return outRc; } + if (wowee::editor::cli::handleInfoWater(i, argc, argv, outRc)) { + return outRc; + } } if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) { dataPath = argv[++i]; @@ -1304,219 +1308,6 @@ int main(int argc, char* argv[]) { questTotal, chainWarnings); } return v.openFormatScore() == 7 ? 0 : 1; - } else if (std::strcmp(argv[i], "--info-zone-water") == 0 && i + 1 < argc) { - // Aggregate water-layer stats across all tiles in a zone. - // Useful for confirming a 'lake zone' actually has water, - // or for budget planning ('how many MH2O cells does my - // archipelago zone carry?'). Liquid types: 0=water, - // 1=ocean, 2=magma, 3=slime. - std::string zoneDir = argv[++i]; - bool jsonOut = (i + 1 < argc && - std::strcmp(argv[i + 1], "--json") == 0); - if (jsonOut) i++; - namespace fs = std::filesystem; - std::string manifestPath = zoneDir + "/zone.json"; - if (!fs::exists(manifestPath)) { - std::fprintf(stderr, - "info-zone-water: %s has no zone.json\n", zoneDir.c_str()); - return 1; - } - wowee::editor::ZoneManifest zm; - if (!zm.load(manifestPath)) { - std::fprintf(stderr, "info-zone-water: parse failed\n"); - return 1; - } - int waterChunks = 0, totalLayers = 0; - std::map typeHist; // liquidType -> chunk count - float minH = 1e30f, maxH = -1e30f; - int loadedTiles = 0; - 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); - loadedTiles++; - for (size_t c = 0; c < terrain.waterData.size(); ++c) { - const auto& w = terrain.waterData[c]; - if (!w.hasWater()) continue; - waterChunks++; - totalLayers += static_cast(w.layers.size()); - for (const auto& layer : w.layers) { - typeHist[layer.liquidType]++; - minH = std::min(minH, layer.minHeight); - maxH = std::max(maxH, layer.maxHeight); - } - } - } - if (waterChunks == 0) { minH = 0; maxH = 0; } - auto typeName = [](uint16_t t) { - switch (t) { - case 0: return "water"; - case 1: return "ocean"; - case 2: return "magma"; - case 3: return "slime"; - } - return "?"; - }; - if (jsonOut) { - nlohmann::json j; - j["zone"] = zoneDir; - j["loadedTiles"] = loadedTiles; - j["waterChunks"] = waterChunks; - j["totalLayers"] = totalLayers; - j["heightRange"] = {minH, maxH}; - nlohmann::json types = nlohmann::json::array(); - for (const auto& [t, c] : typeHist) { - types.push_back({{"type", t}, {"name", typeName(t)}, {"layerCount", c}}); - } - j["types"] = types; - std::printf("%s\n", j.dump(2).c_str()); - return 0; - } - std::printf("Zone water: %s\n", zoneDir.c_str()); - std::printf(" loaded tiles : %d\n", loadedTiles); - std::printf(" water chunks : %d (out of %d possible)\n", - waterChunks, loadedTiles * 256); - std::printf(" total layers : %d\n", totalLayers); - if (waterChunks > 0) { - std::printf(" height range : %.2f to %.2f\n", minH, maxH); - std::printf("\n By liquid type:\n"); - for (const auto& [t, c] : typeHist) { - std::printf(" %s (%u): %d layer(s)\n", - typeName(t), t, c); - } - } else { - 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 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 typeHist; - }; - std::vector rows; - int gLoadedTiles = 0, gWaterChunks = 0, gTotalLayers = 0; - std::map 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(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