From 81cc146d58d7a9a62eb479a479935870d017f9d7 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 11:14:41 -0700 Subject: [PATCH] feat(editor): --zone-summary --json for unified machine-readable report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds --json output to the one-shot zone-summary aggregator. Refactor also moves creature/object/quest data reads to a shared step before either branch so both human and JSON outputs use the same numbers. Schema: { "zone": "custom_zones/Foo", "score": 3, "maxScore": 7, "formats": "WOT WHM zone.json ", "counts": { "wot":1, "whm":1, "wom":0, "wob":0, "woc":0, "png":0 }, "creatures": { "total":N, "hostile":N, "questgiver":N, "vendor":N }, "objects": { "total":N, "m2":N, "wmo":N }, "quests": { "total":N, "chainWarnings":N } } Now CI can gate on any combination — open-format coverage, NPC counts, quest chain health — from a single command. Fourth and last commonly-CI'd inspector to gain --json mode (after --info-extract, --validate, --info-wcp). --- tools/editor/main.cpp | 122 +++++++++++++++++++++++++++++------------- 1 file changed, 84 insertions(+), 38 deletions(-) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index a8e295bf..2b30897d 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -38,7 +38,8 @@ static void printUsage(const char* argv0) { std::printf(" --export-png Render heightmap, normal-map, and zone-map PNG previews\n"); std::printf(" --validate [--json]\n"); std::printf(" Score zone open-format completeness and exit\n"); - std::printf(" --zone-summary One-shot validate + creature/object/quest counts and exit\n"); + std::printf(" --zone-summary [--json]\n"); + std::printf(" One-shot validate + creature/object/quest counts and exit\n"); std::printf(" --info Print WOM file metadata (version, counts) and exit\n"); std::printf(" --info-wob Print WOB building metadata (groups, portals, doodads) and exit\n"); std::printf(" --info-woc Print WOC collision metadata (triangle counts, bounds) and exit\n"); @@ -590,57 +591,102 @@ int main(int argc, char* argv[]) { // Collapses the most common multi-step inspection into a single // command; useful for CI reports and quick sanity checks. std::string zoneDir = argv[++i]; + // Optional --json after the dir for machine-readable output. + bool jsonOut = (i + 1 < argc && + std::strcmp(argv[i + 1], "--json") == 0); + if (jsonOut) i++; namespace fs = std::filesystem; if (!fs::exists(zoneDir)) { std::fprintf(stderr, "zone-summary: %s does not exist\n", zoneDir.c_str()); return 1; } auto v = wowee::editor::ContentPacker::validateZone(zoneDir); + + // Read creature/object/quest data once so both human and JSON + // outputs share the same numbers. + int creatureTotal = 0, hostile = 0, qg = 0, vendor = 0; + int objectTotal = 0, m2Count = 0, wmoCount = 0; + int questTotal = 0, chainWarnings = 0; + std::string creaturesPath = zoneDir + "/creatures.json"; + if (fs::exists(creaturesPath)) { + wowee::editor::NpcSpawner sp; + if (sp.loadFromFile(creaturesPath)) { + creatureTotal = static_cast(sp.getSpawns().size()); + for (const auto& s : sp.getSpawns()) { + if (s.hostile) hostile++; + if (s.questgiver) qg++; + if (s.vendor) vendor++; + } + } + } + std::string objectsPath = zoneDir + "/objects.json"; + if (fs::exists(objectsPath)) { + wowee::editor::ObjectPlacer op; + if (op.loadFromFile(objectsPath)) { + objectTotal = static_cast(op.getObjects().size()); + for (const auto& o : op.getObjects()) { + if (o.type == wowee::editor::PlaceableType::M2) m2Count++; + else wmoCount++; + } + } + } + std::string questsPath = zoneDir + "/quests.json"; + if (fs::exists(questsPath)) { + wowee::editor::QuestEditor qe; + if (qe.loadFromFile(questsPath)) { + questTotal = static_cast(qe.getQuests().size()); + std::vector errors; + qe.validateChains(errors); + chainWarnings = static_cast(errors.size()); + } + } + + if (jsonOut) { + nlohmann::json j; + j["zone"] = zoneDir; + j["score"] = v.openFormatScore(); + j["maxScore"] = 7; + j["formats"] = v.summary(); + j["counts"] = { + {"wot", v.wotCount}, {"whm", v.whmCount}, + {"wom", v.womCount}, {"wob", v.wobCount}, + {"woc", v.wocCount}, {"png", v.pngCount}, + }; + j["creatures"] = { + {"total", creatureTotal}, + {"hostile", hostile}, + {"questgiver", qg}, + {"vendor", vendor}, + }; + j["objects"] = { + {"total", objectTotal}, + {"m2", m2Count}, + {"wmo", wmoCount}, + }; + j["quests"] = { + {"total", questTotal}, + {"chainWarnings", chainWarnings}, + }; + std::printf("%s\n", j.dump(2).c_str()); + return v.openFormatScore() == 7 ? 0 : 1; + } std::printf("Zone: %s\n", zoneDir.c_str()); std::printf(" open formats : %d/7 (%s)\n", v.openFormatScore(), v.summary().c_str()); std::printf(" WOT/WHM : %d/%d WOM: %d WOB: %d WOC: %d PNG: %d\n", v.wotCount, v.whmCount, v.womCount, v.wobCount, v.wocCount, v.pngCount); - // Creature stats - std::string creaturesPath = zoneDir + "/creatures.json"; - if (fs::exists(creaturesPath)) { - wowee::editor::NpcSpawner sp; - if (sp.loadFromFile(creaturesPath)) { - int hostile = 0, qg = 0, vendor = 0; - for (const auto& s : sp.getSpawns()) { - if (s.hostile) hostile++; - if (s.questgiver) qg++; - if (s.vendor) vendor++; - } - std::printf(" creatures : %zu (%d hostile, %d quest, %d vendor)\n", - sp.getSpawns().size(), hostile, qg, vendor); - } + if (creatureTotal > 0) { + std::printf(" creatures : %d (%d hostile, %d quest, %d vendor)\n", + creatureTotal, hostile, qg, vendor); } - // Object stats - std::string objectsPath = zoneDir + "/objects.json"; - if (fs::exists(objectsPath)) { - wowee::editor::ObjectPlacer op; - if (op.loadFromFile(objectsPath)) { - int m2 = 0, wmo = 0; - for (const auto& o : op.getObjects()) { - if (o.type == wowee::editor::PlaceableType::M2) m2++; - else wmo++; - } - std::printf(" objects : %zu (%d M2, %d WMO)\n", - op.getObjects().size(), m2, wmo); - } + if (objectTotal > 0) { + std::printf(" objects : %d (%d M2, %d WMO)\n", + objectTotal, m2Count, wmoCount); } - // Quest stats - std::string questsPath = zoneDir + "/quests.json"; - if (fs::exists(questsPath)) { - wowee::editor::QuestEditor qe; - if (qe.loadFromFile(questsPath)) { - std::vector errors; - qe.validateChains(errors); - std::printf(" quests : %zu (%zu chain warnings)\n", - qe.getQuests().size(), errors.size()); - } + if (questTotal > 0) { + std::printf(" quests : %d (%d chain warnings)\n", + questTotal, chainWarnings); } return v.openFormatScore() == 7 ? 0 : 1; } else if (std::strcmp(argv[i], "--validate") == 0 && i + 1 < argc) {