From 2904fa0560467906918ee19acfb21d0d434b6862 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 14:44:53 -0700 Subject: [PATCH] feat(editor): add --export-zone-deps-md for shareable dependency tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Markdown counterpart to --list-zone-deps. PR reviewers see at a glance whether every referenced model exists in either open or proprietary form across the conventional asset roots: wowee_editor --export-zone-deps-md custom_zones/MyZone # -> custom_zones/MyZone/DEPS.md # Dependencies — MyZone *Auto-generated. Status is best-effort — checks zone-local, output/, custom_zones/, Data/ roots in that order.* ## Direct M2 placements (12) | Refs | Path | Status | |---:|---|---| | 8 | `World/Tree.m2` | open + proprietary | | 3 | `World/Lamp.m2` | open only | | 1 | `World/Banner.m2` | MISSING | Status column resolves each path against zone-local + output/ + custom_zones/ + Data/ roots, trying both .wob/.wmo for buildings and .wom/.m2 for models. Catches missing assets BEFORE pack-wcp would silently include broken refs. GitHub-Flavored Markdown — sortable by Refs column once rendered, backtick-wrapped paths so URLs/spaces don't confuse the viewer. Verified: scaffolded zone with 2 M2 placements (one duplicated) + 1 WMO placement → DEPS.md has 3 sections (one per category) with correct ref counts (Tree.m2 ×2) and MISSING status for paths that don't resolve in any root. --- tools/editor/main.cpp | 120 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index b579a15b..564fa26d 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -428,6 +428,8 @@ static void printUsage(const char* argv0) { std::printf(" Aggregate counts across every zone in \n"); std::printf(" --list-zone-deps [--json]\n"); std::printf(" List external M2/WMO model paths a zone references (objects + WOB doodads)\n"); + std::printf(" --export-zone-deps-md [out.md]\n"); + std::printf(" Markdown dep table for a zone (with on-disk presence column)\n"); std::printf(" --check-zone-refs [--json]\n"); std::printf(" Verify every referenced model/quest NPC actually exists; exit 1 on missing refs\n"); std::printf(" --for-each-zone -- \n"); @@ -662,7 +664,7 @@ int main(int argc, char* argv[]) { "--export-zone-csv", "--export-zone-html", "--scaffold-zone", "--add-tile", "--remove-tile", "--list-tiles", "--for-each-zone", "--zone-stats", "--list-zone-deps", - "--check-zone-refs", + "--check-zone-refs", "--export-zone-deps-md", "--add-creature", "--add-object", "--add-quest", "--add-quest-objective", "--add-quest-reward-item", "--set-quest-reward", "--remove-quest-objective", "--clone-quest", "--clone-creature", @@ -9155,6 +9157,122 @@ int main(int argc, char* argv[]) { emit("Direct WMO placements", directWMO); emit("WOB doodad M2 refs", doodadM2); return 0; + } else if (std::strcmp(argv[i], "--export-zone-deps-md") == 0 && i + 1 < argc) { + // Markdown counterpart to --list-zone-deps. Writes a sortable + // GitHub-rendered table of every external model the zone + // references plus on-disk presence (so PR reviewers see at a + // glance whether dependencies are accounted for in the + // accompanying asset bundle). + std::string zoneDir = argv[++i]; + std::string outPath; + if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; + namespace fs = std::filesystem; + if (!fs::exists(zoneDir + "/zone.json")) { + std::fprintf(stderr, + "export-zone-deps-md: %s has no zone.json\n", zoneDir.c_str()); + return 1; + } + wowee::editor::ZoneManifest zm; + zm.load(zoneDir + "/zone.json"); + if (outPath.empty()) outPath = zoneDir + "/DEPS.md"; + // Same dep-collection pass as --list-zone-deps. + std::map directM2; + std::map directWMO; + std::map doodadM2; + wowee::editor::ObjectPlacer op; + if (op.loadFromFile(zoneDir + "/objects.json")) { + for (const auto& o : op.getObjects()) { + if (o.type == wowee::editor::PlaceableType::M2) directM2[o.path]++; + else if (o.type == wowee::editor::PlaceableType::WMO) directWMO[o.path]++; + } + } + int wobCount = 0; + std::error_code ec; + for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) { + if (!e.is_regular_file() || + e.path().extension() != ".wob") continue; + wobCount++; + std::string base = e.path().string(); + if (base.size() >= 4) base = base.substr(0, base.size() - 4); + auto bld = wowee::pipeline::WoweeBuildingLoader::load(base); + for (const auto& d : bld.doodads) { + if (!d.modelPath.empty()) doodadM2[d.modelPath]++; + } + } + // Resolve dep on disk. Same heuristic as --check-zone-refs: + // try both open + proprietary in conventional roots. + auto stripExt = [](const std::string& p, const char* ext) { + size_t n = std::strlen(ext); + if (p.size() >= n) { + std::string tail = p.substr(p.size() - n); + std::string lower = tail; + for (auto& c : lower) c = std::tolower(static_cast(c)); + if (lower == ext) return p.substr(0, p.size() - n); + } + return p; + }; + auto resolveStatus = [&](const std::string& path, bool isWMO) { + std::string base, openExt, propExt; + if (isWMO) { + base = stripExt(path, ".wmo"); + openExt = ".wob"; propExt = ".wmo"; + } else { + base = stripExt(path, ".m2"); + openExt = ".wom"; propExt = ".m2"; + } + std::vector roots = { + "", zoneDir + "/", "output/", "custom_zones/", "Data/" + }; + bool hasOpen = false, hasProp = false; + for (const auto& root : roots) { + if (fs::exists(root + base + openExt)) hasOpen = true; + if (fs::exists(root + base + propExt)) hasProp = true; + } + if (hasOpen && hasProp) return "open + proprietary"; + if (hasOpen) return "open only"; + if (hasProp) return "proprietary only"; + return "MISSING"; + }; + std::ofstream out(outPath); + if (!out) { + std::fprintf(stderr, + "export-zone-deps-md: cannot write %s\n", outPath.c_str()); + return 1; + } + out << "# Dependencies — " << + (zm.displayName.empty() ? zm.mapName : zm.displayName) << "\n\n"; + out << "*Auto-generated by `wowee_editor --export-zone-deps-md`. " + "Status is best-effort — checks zone-local, output/, " + "custom_zones/, Data/ roots in that order.*\n\n"; + auto emitTable = [&](const char* heading, + const std::map& m, + bool isWMO) { + out << "## " << heading << " (" << m.size() << ")\n\n"; + if (m.empty()) { + out << "*None.*\n\n"; + return; + } + out << "| Refs | Path | Status |\n"; + out << "|---:|---|---|\n"; + for (const auto& [path, count] : m) { + out << "| " << count << " | `" << path << "` | " + << resolveStatus(path, isWMO) << " |\n"; + } + out << "\n"; + }; + emitTable("Direct M2 placements", directM2, false); + emitTable("Direct WMO placements", directWMO, true); + emitTable("WOB doodad M2 refs", doodadM2, false); + out << "## Summary\n\n"; + out << "- Zone: `" << zm.mapName << "`\n"; + out << "- WOBs scanned: " << wobCount << "\n"; + out << "- Unique dependencies: " << + directM2.size() + directWMO.size() + doodadM2.size() << "\n"; + out.close(); + std::printf("Wrote %s\n", outPath.c_str()); + std::printf(" %zu M2 placements, %zu WMO placements, %zu WOB doodad refs\n", + directM2.size(), directWMO.size(), doodadM2.size()); + return 0; } else if (std::strcmp(argv[i], "--check-zone-refs") == 0 && i + 1 < argc) { // Cross-reference checker: every model path in objects.json // must resolve as either an open WOM/WOB sidecar or a