diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 032246dd..8dd3bafc 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -480,6 +480,8 @@ static void printUsage(const char* argv0) { std::printf(" Recursively run all per-format validators on every file\n"); std::printf(" --zone-summary [--json]\n"); std::printf(" One-shot validate + creature/object/quest counts and exit\n"); + std::printf(" --export-zone-summary-md [out.md]\n"); + std::printf(" Render a markdown documentation page for a zone (manifest + content)\n"); std::printf(" --info [--json]\n"); std::printf(" Print WOM file metadata (version, counts) and exit\n"); std::printf(" --info-wob [--json]\n"); @@ -555,6 +557,7 @@ int main(int argc, char* argv[]) { "--unpack-wcp", "--pack-wcp", "--validate", "--validate-wom", "--validate-wob", "--validate-woc", "--validate-whm", "--validate-all", "--zone-summary", + "--export-zone-summary-md", "--scaffold-zone", "--add-tile", "--remove-tile", "--list-tiles", "--for-each-zone", "--add-creature", "--add-object", "--add-quest", @@ -2357,6 +2360,158 @@ int main(int argc, char* argv[]) { questTotal, chainWarnings); } return v.openFormatScore() == 7 ? 0 : 1; + } else if (std::strcmp(argv[i], "--export-zone-summary-md") == 0 && i + 1 < argc) { + // Render a Markdown documentation page for a zone. Useful for + // designers tracking changes between versions, generating + // GitHub Pages docs, or reviewing zones in PRs without + // round-tripping through the GUI. + std::string zoneDir = argv[++i]; + std::string outPath; + if (i + 1 < argc && argv[i + 1][0] != '-') { + outPath = argv[++i]; + } + namespace fs = std::filesystem; + std::string manifestPath = zoneDir + "/zone.json"; + if (!fs::exists(manifestPath)) { + std::fprintf(stderr, + "export-zone-summary-md: %s has no zone.json\n", zoneDir.c_str()); + return 1; + } + wowee::editor::ZoneManifest zm; + if (!zm.load(manifestPath)) { + std::fprintf(stderr, + "export-zone-summary-md: failed to parse %s\n", manifestPath.c_str()); + return 1; + } + // Default output: ZONE.md sitting next to zone.json. + if (outPath.empty()) outPath = zoneDir + "/ZONE.md"; + // Load content sub-files; missing ones contribute 0 entries. + wowee::editor::NpcSpawner sp; + sp.loadFromFile(zoneDir + "/creatures.json"); + wowee::editor::ObjectPlacer op; + op.loadFromFile(zoneDir + "/objects.json"); + wowee::editor::QuestEditor qe; + qe.loadFromFile(zoneDir + "/quests.json"); + std::ofstream md(outPath); + if (!md) { + std::fprintf(stderr, + "export-zone-summary-md: cannot write %s\n", outPath.c_str()); + return 1; + } + md << "# " << (zm.displayName.empty() ? zm.mapName : zm.displayName) << "\n\n"; + md << "*Auto-generated by `wowee_editor --export-zone-summary-md`. " + "Do not edit by hand.*\n\n"; + md << "## Manifest\n\n"; + md << "| Field | Value |\n"; + md << "|---|---|\n"; + md << "| Map name | `" << zm.mapName << "` |\n"; + md << "| Display name | " << zm.displayName << " |\n"; + md << "| Map ID | " << zm.mapId << " |\n"; + if (!zm.biome.empty()) md << "| Biome | " << zm.biome << " |\n"; + md << "| Base height | " << zm.baseHeight << " |\n"; + md << "| Tile count | " << zm.tiles.size() << " |\n"; + md << "| Allow flying | " << (zm.allowFlying ? "yes" : "no") << " |\n"; + md << "| PvP enabled | " << (zm.pvpEnabled ? "yes" : "no") << " |\n"; + md << "| Indoor | " << (zm.isIndoor ? "yes" : "no") << " |\n"; + md << "| Sanctuary | " << (zm.isSanctuary ? "yes" : "no") << " |\n"; + if (!zm.musicTrack.empty()) md << "| Music | `" << zm.musicTrack << "` |\n"; + if (!zm.ambienceDay.empty()) md << "| Ambient (day) | `" << zm.ambienceDay << "` |\n"; + if (!zm.ambienceNight.empty())md << "| Ambient (night) | `" << zm.ambienceNight << "` |\n"; + if (!zm.description.empty()) { + md << "\n### Description\n\n" << zm.description << "\n"; + } + md << "\n## Tiles\n\n"; + md << "| tx | ty |\n|---|---|\n"; + for (const auto& [tx, ty] : zm.tiles) { + md << "| " << tx << " | " << ty << " |\n"; + } + md << "\n## Creatures (" << sp.spawnCount() << ")\n\n"; + if (sp.spawnCount() == 0) { + md << "*No creature spawns.*\n"; + } else { + md << "| # | Name | Lvl | DisplayId | Pos (x, y, z) | Flags |\n"; + md << "|---|---|---|---|---|---|\n"; + for (size_t k = 0; k < sp.spawnCount(); ++k) { + const auto& s = sp.getSpawns()[k]; + md << "| " << k << " | " << s.name << " | " << s.level << " | " + << s.displayId << " | (" + << s.position.x << ", " << s.position.y << ", " << s.position.z + << ") |"; + if (s.hostile) md << " hostile"; + if (s.questgiver) md << " quest"; + if (s.vendor) md << " vendor"; + if (s.trainer) md << " trainer"; + md << " |\n"; + } + } + md << "\n## Objects (" << op.getObjects().size() << ")\n\n"; + if (op.getObjects().empty()) { + md << "*No object placements.*\n"; + } else { + md << "| # | Type | Path | Pos | Scale |\n"; + md << "|---|---|---|---|---|\n"; + for (size_t k = 0; k < op.getObjects().size(); ++k) { + const auto& o = op.getObjects()[k]; + md << "| " << k << " | " + << (o.type == wowee::editor::PlaceableType::M2 ? "m2" : "wmo") + << " | `" << o.path << "` | (" + << o.position.x << ", " << o.position.y << ", " << o.position.z + << ") | " << o.scale << " |\n"; + } + } + md << "\n## Quests (" << qe.questCount() << ")\n\n"; + if (qe.questCount() == 0) { + md << "*No quests.*\n"; + } else { + using OT = wowee::editor::QuestObjectiveType; + auto typeName = [](OT t) { + switch (t) { + case OT::KillCreature: return "kill"; + case OT::CollectItem: return "collect"; + case OT::TalkToNPC: return "talk"; + case OT::ExploreArea: return "explore"; + case OT::EscortNPC: return "escort"; + case OT::UseObject: return "use"; + } + return "?"; + }; + for (size_t k = 0; k < qe.questCount(); ++k) { + const auto& q = qe.getQuests()[k]; + md << "### " << k << ". " << q.title << "\n\n"; + md << "- Required level: " << q.requiredLevel << "\n"; + md << "- Quest giver NPC ID: " << q.questGiverNpcId << "\n"; + md << "- Turn-in NPC ID: " << q.turnInNpcId << "\n"; + md << "- XP: " << q.reward.xp << "\n"; + if (q.reward.gold || q.reward.silver || q.reward.copper) { + md << "- Coin: " << q.reward.gold << "g " + << q.reward.silver << "s " << q.reward.copper << "c\n"; + } + if (!q.objectives.empty()) { + md << "- Objectives:\n"; + for (const auto& obj : q.objectives) { + md << " - **" << typeName(obj.type) << "** " + << obj.targetName << " ×" << obj.targetCount; + if (!obj.description.empty()) { + md << " — *" << obj.description << "*"; + } + md << "\n"; + } + } + if (!q.reward.itemRewards.empty()) { + md << "- Item rewards:\n"; + for (const auto& it : q.reward.itemRewards) { + md << " - `" << it << "`\n"; + } + } + md << "\n"; + } + } + md.close(); + std::printf("Wrote %s\n", outPath.c_str()); + std::printf(" zone=%s, %zu tiles, %zu creatures, %zu objects, %zu quests\n", + zm.mapName.c_str(), zm.tiles.size(), sp.spawnCount(), + op.getObjects().size(), qe.questCount()); + return 0; } else if (std::strcmp(argv[i], "--validate") == 0 && i + 1 < argc) { std::string zoneDir = argv[++i]; // Optional --json after the dir for machine-readable output