mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-07 17:43:51 +00:00
feat(editor): add --info-quest-graph-stats for chain topology analysis
Where --export-quest-graph visualizes the quest dependency graph,
this quantifies it. Useful for spotting authoring issues (orphan
quests that only appear as one-offs, broken chains via cycles)
and getting a sense of quest density:
wowee_editor --info-quest-graph-stats $Z/quests.json
Quest graph: $Z/quests.json
total quests : 4
roots : 2 (no inbound chain — entry points)
leaves : 2 (no outbound chain — terminal)
orphans : 1 (root AND leaf — one-shot)
cycles : 0
max depth : 3
avg depth : 2.00 (chain length per root)
Definitions:
roots = quests no other quest chains TO (player entry points)
leaves = quests with no nextQuestId or nextQuestId pointing to
a missing quest (terminal — chain ends here)
orphans = root AND leaf (one-shot quests with no neighbors)
cycles = number of roots whose forward walk hits a node twice
maxDepth = longest path from any root forward through the chain
avgDepth = mean path length across all roots
Cycle-guarded forward walk uses a visited-set per root, so the
cycle count is bounded even on intentionally-broken inputs.
Exit 1 if cycles > 0 so CI can gate before shipping a broken
chain. JSON mode emits all six stats for dashboard consumption.
Verified on 4-quest zone (Q1→Q2→Q3 chain + Loner orphan):
correctly reports 2 roots, 2 leaves, 1 orphan, 0 cycles, max
depth 3, avg depth 2.00.
This commit is contained in:
parent
cc91a1146f
commit
9c7b6aebfc
1 changed files with 90 additions and 0 deletions
|
|
@ -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 <p> <questIdx> [--json]\n");
|
||||
std::printf(" List XP/coin/item rewards on a quest\n");
|
||||
std::printf(" --info-quest-graph-stats <p> [--json]\n");
|
||||
std::printf(" Analyze quest chain graph (roots, leaves, depths, cycles, orphans)\n");
|
||||
std::printf(" --info-creature <p> <idx> [--json]\n");
|
||||
std::printf(" Print every field for one creature spawn (stats, behavior, AI, flags)\n");
|
||||
std::printf(" --info-quest <p> <idx> [--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<uint32_t, uint32_t> nextOf;
|
||||
std::unordered_set<uint32_t> hasInbound;
|
||||
std::unordered_set<uint32_t> 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<uint32_t> 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue