diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp
index 9260e184..e0605833 100644
--- a/tools/editor/main.cpp
+++ b/tools/editor/main.cpp
@@ -714,6 +714,8 @@ static void printUsage(const char* argv0) {
std::printf(" List every objective on a quest (for --remove-quest-objective)\n");
std::printf(" --list-quest-rewards
[--json]\n");
std::printf(" List XP/coin/item rewards on a quest\n");
+ std::printf(" --info-quest-graph-stats [--json]\n");
+ std::printf(" Analyze quest chain graph (roots, leaves, depths, cycles, orphans)\n");
std::printf(" --info-creature
[--json]\n");
std::printf(" Print every field for one creature spawn (stats, behavior, AI, flags)\n");
std::printf(" --info-quest [--json]\n");
@@ -775,6 +777,7 @@ int main(int argc, char* argv[]) {
"--list-creatures", "--list-objects", "--list-quests",
"--list-quest-objectives", "--list-quest-rewards",
"--info-creature", "--info-quest", "--info-object",
+ "--info-quest-graph-stats",
"--unpack-wcp", "--pack-wcp",
"--validate", "--validate-wom", "--validate-wob", "--validate-woc",
"--validate-whm", "--validate-all", "--validate-glb", "--info-glb",
@@ -2755,6 +2758,93 @@ int main(int argc, char* argv[]) {
std::printf(" [%zu] %s\n", k, r.itemRewards[k].c_str());
}
return 0;
+ } else if (std::strcmp(argv[i], "--info-quest-graph-stats") == 0 && i + 1 < argc) {
+ // Topology analysis of the quest dependency graph. Where
+ // --export-quest-graph visualizes it, this quantifies it:
+ // roots = quests no one chains TO (entry points)
+ // leaves = quests with no nextQuestId (terminal)
+ // orphans = roots that are also leaves (one-shot quests)
+ // cycles = circular chain detected
+ // maxDepth = longest path from any root
+ // avgDepth = mean path length across all roots
+ std::string path = argv[++i];
+ bool jsonOut = (i + 1 < argc &&
+ std::strcmp(argv[i + 1], "--json") == 0);
+ if (jsonOut) i++;
+ wowee::editor::QuestEditor qe;
+ if (!qe.loadFromFile(path)) {
+ std::fprintf(stderr,
+ "info-quest-graph-stats: failed to load %s\n", path.c_str());
+ return 1;
+ }
+ const auto& quests = qe.getQuests();
+ // Build id -> nextId and reverse adjacency.
+ std::unordered_map nextOf;
+ std::unordered_set hasInbound;
+ std::unordered_set validIds;
+ for (const auto& q : quests) {
+ validIds.insert(q.id);
+ nextOf[q.id] = q.nextQuestId;
+ }
+ for (const auto& q : quests) {
+ if (q.nextQuestId != 0 && validIds.count(q.nextQuestId)) {
+ hasInbound.insert(q.nextQuestId);
+ }
+ }
+ int roots = 0, leaves = 0, orphans = 0;
+ int cycles = 0;
+ int maxDepth = 0;
+ int sumDepths = 0;
+ for (const auto& q : quests) {
+ bool isRoot = (hasInbound.count(q.id) == 0);
+ bool isLeaf = (q.nextQuestId == 0 ||
+ validIds.count(q.nextQuestId) == 0);
+ if (isRoot) roots++;
+ if (isLeaf) leaves++;
+ if (isRoot && isLeaf) orphans++;
+ if (isRoot) {
+ // Walk the chain forward, counting depth + cycle-guarding.
+ std::unordered_set visited;
+ int depth = 1;
+ uint32_t current = q.id;
+ while (current != 0 && validIds.count(current)) {
+ if (!visited.insert(current).second) {
+ cycles++;
+ break;
+ }
+ auto it = nextOf.find(current);
+ if (it == nextOf.end() || it->second == 0) break;
+ current = it->second;
+ depth++;
+ }
+ if (depth > maxDepth) maxDepth = depth;
+ sumDepths += depth;
+ }
+ }
+ double avgDepth = (roots > 0) ? double(sumDepths) / roots : 0.0;
+ if (jsonOut) {
+ nlohmann::json j;
+ j["file"] = path;
+ j["totalQuests"] = quests.size();
+ j["roots"] = roots;
+ j["leaves"] = leaves;
+ j["orphans"] = orphans;
+ j["cycles"] = cycles;
+ j["maxDepth"] = maxDepth;
+ j["avgDepth"] = avgDepth;
+ std::printf("%s\n", j.dump(2).c_str());
+ return cycles == 0 ? 0 : 1;
+ }
+ std::printf("Quest graph: %s\n", path.c_str());
+ std::printf(" total quests : %zu\n", quests.size());
+ std::printf(" roots : %d (no inbound chain — entry points)\n", roots);
+ std::printf(" leaves : %d (no outbound chain — terminal)\n", leaves);
+ std::printf(" orphans : %d (root AND leaf — one-shot)\n", orphans);
+ std::printf(" cycles : %d %s\n", cycles,
+ cycles == 0 ? "" : "(BROKEN — chains loop back)");
+ std::printf(" max depth : %d\n", maxDepth);
+ std::printf(" avg depth : %.2f (chain length per root)\n", avgDepth);
+ return cycles == 0 ? 0 : 1;
} else if (std::strcmp(argv[i], "--info-creature") == 0 && i + 2 < argc) {
// Single-creature deep dive — every CreatureSpawn field for
// one entry. Companion to --list-creatures (which is a