From 2152b230c831b3265e8e8f3fdc3401b0ea0a96ff Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 16:48:46 -0700 Subject: [PATCH] feat(editor): add --export-project-md for GitHub-renderable project README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Markdown counterpart to --export-project-html. Generates a README.md indexing every zone with counts + bake/viewer/doc artifact status. GitHub renders it natively at the project root: wowee_editor --export-project-md custom_zones # Wowee Project — Zone Index *Auto-generated. 2 zone(s) discovered in `custom_zones`.* ## Summary | Metric | Total | |---|---:| | Zones | 2 | | Tiles | 2 | | Creatures | 2 | | ... ## Zones | Zone | Tiles | Creatures | Objects | Quests | Bake | Viewer | Docs | |---|---:|---:|---:|---:|:---:|:---:|:---:| | Desert | 1 | 1 | 1 | 1 | — | — | — | | [Forest](Forest/ZONE.md) | 1 | 1 | 1 | 1 | ✓ | [view](Forest/Forest.html) | [md](Forest/ZONE.md) | Per-zone row links to its ZONE.md (if --export-zone-summary-md was run) and its HTML viewer (if --export-zone-html was run). The Bake column shows ✓ if .glb exists. Status columns make it instantly visible which zones are bake-ready vs documentation-only. Pairs with --export-project-html (interactive viewer index) — same data, different presentation: HTML for browsers, Markdown for GitHub Pages READMEs and PR descriptions. Verified on a 2-zone project where one zone had been baked + viewer-exported + doc-exported and the other hadn't: README.md correctly shows ✓/links for the baked zone, em-dashes for the unbaked one. --- tools/editor/main.cpp | 99 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 94d75614..5e5df06a 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -656,6 +656,8 @@ static void printUsage(const char* argv0) { std::printf(" Emit a single-file HTML viewer next to the zone .glb (model-viewer based)\n"); std::printf(" --export-project-html [out.html]\n"); std::printf(" Generate an index.html linking to every zone's HTML viewer in \n"); + std::printf(" --export-project-md [out.md]\n"); + std::printf(" Generate a README.md indexing every zone with counts + viewer/bake status\n"); std::printf(" --export-quest-graph [out.dot]\n"); std::printf(" Render quest-chain DAG as Graphviz DOT (pipe to `dot -Tpng -o quests.png`)\n"); std::printf(" --info [--json]\n"); @@ -794,7 +796,7 @@ int main(int argc, char* argv[]) { "--info-zone-extents", "--export-zone-summary-md", "--export-quest-graph", "--export-zone-csv", "--export-zone-html", "--export-project-html", - "--export-zone-checksum", + "--export-project-md", "--export-zone-checksum", "--scaffold-zone", "--mvp-zone", "--add-tile", "--remove-tile", "--list-tiles", "--for-each-zone", "--zone-stats", "--info-tilemap", "--list-zone-deps", "--check-zone-refs", "--check-zone-content", @@ -5182,6 +5184,101 @@ int main(int argc, char* argv[]) { std::printf(" %zu zone(s) listed, %d with viewable HTML\n", entries.size(), withViewer); return 0; + } else if (std::strcmp(argv[i], "--export-project-md") == 0 && i + 1 < argc) { + // Markdown counterpart to --export-project-html. Generates a + // README.md indexing every zone with counts + bake/viewer + // status. GitHub renders it natively at the project root. + // Pairs with --export-zone-summary-md (per-zone) — the project + // README links to each zone's per-zone .md. + std::string projectDir = argv[++i]; + std::string outPath; + if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; + namespace fs = std::filesystem; + if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) { + std::fprintf(stderr, + "export-project-md: %s is not a directory\n", + projectDir.c_str()); + return 1; + } + if (outPath.empty()) outPath = projectDir + "/README.md"; + // Per-zone collection: name + counts + which artifacts exist. + struct Row { + std::string name, dirRel, mapName; + int tiles = 0, creatures = 0, objects = 0, quests = 0; + bool hasGlb = false, hasHtml = false, hasZoneMd = false; + }; + std::vector rows; + for (const auto& entry : fs::directory_iterator(projectDir)) { + if (!entry.is_directory()) continue; + if (!fs::exists(entry.path() / "zone.json")) continue; + wowee::editor::ZoneManifest zm; + if (!zm.load((entry.path() / "zone.json").string())) continue; + Row r; + r.name = zm.displayName.empty() ? zm.mapName : zm.displayName; + r.dirRel = entry.path().filename().string(); + r.mapName = zm.mapName; + r.tiles = static_cast(zm.tiles.size()); + wowee::editor::NpcSpawner sp; + if (sp.loadFromFile((entry.path() / "creatures.json").string())) { + r.creatures = static_cast(sp.spawnCount()); + } + wowee::editor::ObjectPlacer op; + if (op.loadFromFile((entry.path() / "objects.json").string())) { + r.objects = static_cast(op.getObjects().size()); + } + wowee::editor::QuestEditor qe; + if (qe.loadFromFile((entry.path() / "quests.json").string())) { + r.quests = static_cast(qe.questCount()); + } + r.hasGlb = fs::exists(entry.path() / (zm.mapName + ".glb")); + r.hasHtml = fs::exists(entry.path() / (zm.mapName + ".html")); + r.hasZoneMd = fs::exists(entry.path() / "ZONE.md"); + rows.push_back(std::move(r)); + } + std::sort(rows.begin(), rows.end(), + [](const Row& a, const Row& b) { return a.name < b.name; }); + int totalT = 0, totalC = 0, totalO = 0, totalQ = 0; + for (const auto& r : rows) { + totalT += r.tiles; totalC += r.creatures; + totalO += r.objects; totalQ += r.quests; + } + std::ofstream out(outPath); + if (!out) { + std::fprintf(stderr, + "export-project-md: cannot write %s\n", outPath.c_str()); + return 1; + } + out << "# Wowee Project — Zone Index\n\n"; + out << "*Auto-generated. " << rows.size() + << " zone(s) discovered in `" << projectDir << "`.*\n\n"; + out << "## Summary\n\n"; + out << "| Metric | Total |\n|---|---:|\n"; + out << "| Zones | " << rows.size() << " |\n"; + out << "| Tiles | " << totalT << " |\n"; + out << "| Creatures | " << totalC << " |\n"; + out << "| Objects | " << totalO << " |\n"; + out << "| Quests | " << totalQ << " |\n\n"; + out << "## Zones\n\n"; + out << "| Zone | Tiles | Creatures | Objects | Quests | Bake | Viewer | Docs |\n"; + out << "|---|---:|---:|---:|---:|:---:|:---:|:---:|\n"; + for (const auto& r : rows) { + out << "| "; + if (r.hasZoneMd) { + out << "[" << r.name << "](" << r.dirRel << "/ZONE.md)"; + } else { + out << r.name; + } + out << " | " << r.tiles << " | " << r.creatures << " | " + << r.objects << " | " << r.quests << " | " + << (r.hasGlb ? "✓" : "—") << " | " + << (r.hasHtml ? "[view](" + r.dirRel + "/" + r.mapName + ".html)" : "—") << " | " + << (r.hasZoneMd ? "[md](" + r.dirRel + "/ZONE.md)" : "—") << " |\n"; + } + out.close(); + std::printf("Wrote %s\n", outPath.c_str()); + std::printf(" %zu zone(s) indexed (%d tiles, %d creatures, %d objects, %d quests)\n", + rows.size(), totalT, totalC, totalO, totalQ); + return 0; } else if (std::strcmp(argv[i], "--export-quest-graph") == 0 && i + 1 < argc) { // Render quest chains as a Graphviz DOT graph. Visualizing // quest dependencies in plain text rapidly becomes unreadable