From 6b06bd07f9f0fce86f84fd96e7ac7b75f665749a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 07:33:31 -0700 Subject: [PATCH] feat(quest): detect orphan quests + speed up chain validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit validateChains now also flags quests with no questgiver and no turn-in NPC — those are unreachable in-game and a common authoring mistake. Also replaced the O(n²) inner lookup with an O(1) unordered_map of id → nextId so circular detection scales. --- tools/editor/quest_editor.cpp | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/tools/editor/quest_editor.cpp b/tools/editor/quest_editor.cpp index e8066640..1558ed87 100644 --- a/tools/editor/quest_editor.cpp +++ b/tools/editor/quest_editor.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace wowee { namespace editor { @@ -156,7 +157,11 @@ bool QuestEditor::loadFromFile(const std::string& path) { bool QuestEditor::validateChains(std::vector& errors) const { errors.clear(); std::unordered_set validIds; - for (const auto& q : quests_) validIds.insert(q.id); + std::unordered_map nextById; // id -> nextId + for (const auto& q : quests_) { + validIds.insert(q.id); + nextById[q.id] = q.nextQuestId; + } for (const auto& q : quests_) { if (q.nextQuestId != 0 && validIds.find(q.nextQuestId) == validIds.end()) { @@ -164,7 +169,16 @@ bool QuestEditor::validateChains(std::vector& errors) const { "\" chains to non-existent quest " + std::to_string(q.nextQuestId)); } - // Circular chain detection + // Quest with no questgiver and no turn-in is unreachable in-game. + // Common authoring mistake — flag it so the player isn't stuck + // wondering why a quest never appears. + if (q.questGiverNpcId == 0 && q.turnInNpcId == 0) { + errors.push_back("Quest [" + std::to_string(q.id) + "] \"" + q.title + + "\" has no questgiver or turn-in NPC (unreachable)"); + } + + // Circular chain detection. Use the precomputed map so the inner + // lookup is O(1) instead of O(n) — was O(n²) per starting quest. if (q.nextQuestId != 0) { std::unordered_set visited; uint32_t current = q.id; @@ -174,11 +188,8 @@ bool QuestEditor::validateChains(std::vector& errors) const { std::to_string(q.id) + "] \"" + q.title + "\""); break; } - uint32_t next = 0; - for (const auto& other : quests_) { - if (other.id == current) { next = other.nextQuestId; break; } - } - current = next; + auto it = nextById.find(current); + current = (it != nextById.end()) ? it->second : 0; } } }