diff --git a/src/Ai/World/Rpg/Action/NewRpgAction.cpp b/src/Ai/World/Rpg/Action/NewRpgAction.cpp index 15ec03b1d..59406274c 100644 --- a/src/Ai/World/Rpg/Action/NewRpgAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgAction.cpp @@ -287,14 +287,23 @@ bool NewRpgDoQuestAction::Execute(Event /*event*/) bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data) { - uint32 questId = data.questId; - if (data.pos != WorldPosition()) + uint32 const questId = data.questId; + + // === Spawn-index pipeline === + // Reference (cmangos) per-spawn pattern: walk to specific known + // spawns of the current objective one by one, advance through the + // candidate list on per-spawn timeout, refresh the list when the + // objective makes progress (so the list reflects what's still + // needed). No POI cluster roam, no random nudging. + + // 1. Detect objective completion. If the current objective is done, + // drop the cached spawn list so we re-fetch for the next + // incomplete objective on this tick. + if (!data.candidateSpawns.empty()) { - /// @TODO: extract to a new function - int32 currentObjective = data.objectiveIdx; - // check if the objective has completed + int32 const currentObjective = data.objectiveIdx; Quest const* quest = sObjectMgr->GetQuestTemplate(questId); - const QuestStatusData& q_status = bot->getQuestStatusMap().at(questId); + QuestStatusData const& q_status = bot->getQuestStatusMap().at(questId); bool completed = true; if (currentObjective < QUEST_OBJECTIVES_COUNT) { @@ -307,96 +316,103 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data) quest->RequiredItemCount[currentObjective - QUEST_OBJECTIVES_COUNT]) completed = false; } - // the current objective is completed, clear and find a new objective later if (completed) { + data.candidateSpawns.clear(); + data.currentSpawnIdx = 0; data.lastReachPOI = 0; - data.pos = WorldPosition(); data.objectiveIdx = 0; data.pursuedLootGO.Clear(); data.pursuedUseGO.Clear(); data.pursuedUseTarget.Clear(); } } - if (data.pos == WorldPosition()) + + // 2. Fetch spawn candidates if we don't have any. Abandon the + // quest if no spawns are indexed on the bot's current map (the + // quest is for another zone or our index is missing them). + if (data.candidateSpawns.empty()) { - std::vector poiInfo; - if (!GetQuestPOIPosAndObjectiveIdx(questId, poiInfo)) + std::vector spawns; + int32 objectiveIdx = 0; + if (!FetchQuestSpawnsForObjective(questId, spawns, objectiveIdx)) { - // can't find a poi pos to go, stop doing quest for now + botAI->lowPriorityQuest.insert(questId); + botAI->rpgStatistic.questAbandoned++; + LOG_DEBUG("playerbots", "[New RPG] {} abandoned quest {} — no spawns indexed", + bot->GetName(), questId); botAI->rpgInfo.ChangeToIdle(); return true; } - uint32 rndIdx = urand(0, poiInfo.size() - 1); - G3D::Vector2 nearestPoi = poiInfo[rndIdx].pos; - int32 objectiveIdx = poiInfo[rndIdx].objectiveIdx; - - float dx = nearestPoi.x, dy = nearestPoi.y; - - // z = MAX_HEIGHT as we do not know accurate z - float dz = std::max(bot->GetMap()->GetHeight(dx, dy, MAX_HEIGHT), bot->GetMap()->GetWaterLevel(dx, dy)); - - // double check for GetQuestPOIPosAndObjectiveIdx - if (dz == INVALID_HEIGHT || dz == VMAP_INVALID_HEIGHT_VALUE) - return false; - - WorldPosition pos(bot->GetMapId(), dx, dy, dz); + data.candidateSpawns = std::move(spawns); + data.currentSpawnIdx = 0; data.lastReachPOI = 0; - data.pos = pos; data.objectiveIdx = objectiveIdx; data.pursuedLootGO.Clear(); data.pursuedUseGO.Clear(); data.pursuedUseTarget.Clear(); } - if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI) + // 3. If we've exhausted the candidate list, abandon (the spawn + // list was sorted by distance and we tried each). + if (data.currentSpawnIdx >= data.candidateSpawns.size()) + { + botAI->lowPriorityQuest.insert(questId); + botAI->rpgStatistic.questAbandoned++; + LOG_DEBUG("playerbots", "[New RPG] {} abandoned quest {} — exhausted all {} candidate spawns", + bot->GetName(), questId, static_cast(data.candidateSpawns.size())); + botAI->rpgInfo.ChangeToIdle(); + return true; + } + + WorldPosition const& target = data.candidateSpawns[data.currentSpawnIdx]; + + // 4. Walk to the current target spawn. Yield to attack-anything + // only if a quest mob for this specific objective is adjacent + // (so we don't walk past the target we just spawned next to). + if (bot->GetDistance(target) > 10.0f && !data.lastReachPOI) { - // Yield to attack-anything ONLY if a mob needed by this exact - // quest+objective is right next to us. The broad variant (any - // quest in the log) yielded for every nearby mob and derailed - // turn-ins / cross-zone travel through other quests' clusters. if (HasNearbyQuestMobForObjective(15.0f, data.questId, data.objectiveIdx)) return false; - // Note: previously yielded ~10%/tick when any hostile was - // within 25y. That overrode the do-quest multiplier in - // practice (combined with bots getting aggroed on the way, - // which ALSO bypasses the multiplier via combat engine) and - // bots ended up grinding their way to POIs instead of - // travelling. Quest-mob exception above is kept so we don't - // walk past a quest target while gathering. Anything else - // hostile is the multiplier's job to throttle — and bots - // that DO get aggroed switch to combat engine where the - // class strategy handles it. - - if (MoveFarTo(data.pos)) + if (MoveFarTo(target)) { botAI->rpgInfo.moveRetryCount = 0; return true; } - // Retry counter (reference pattern): on N consecutive - // failures, drop this objective and go idle so the picker can - // try another quest / state. + // Retry counter: on N consecutive MoveFarTo failures, advance + // to the next candidate spawn rather than sit on an unreachable + // one. If that exhausts the list the abandon branch above + // catches it next tick. if (++botAI->rpgInfo.moveRetryCount >= NewRpgInfo::MAX_MOVE_RETRIES) - botAI->rpgInfo.ChangeToIdle(); + { + ++data.currentSpawnIdx; + data.lastReachPOI = 0; + botAI->rpgInfo.moveRetryCount = 0; + } return true; } - // Now we are near the quest objective - // kill mobs and looting quest should be done automatically by grind strategy + // 5. At the spawn. Stamp arrival on first reach so the per-spawn + // timeout below has a baseline. if (!data.lastReachPOI) { data.lastReachPOI = getMSTime(); return true; } - // stayed at this POI for more than 5 minutes - if (GetMSTimeDiffToNow(data.lastReachPOI) >= poiStayTime) + + // 6. Per-spawn timeout. The reference's TravelTarget expires after + // a configurable window; we use 30s — long enough to finish a + // melee pull, short enough to advance off an empty/dead spawn. + // On any progression since the list was fetched, refresh so we + // re-sort by distance and pick the next nearest live spawn. + constexpr uint32 perSpawnTimeoutMs = 30 * 1000; + if (GetMSTimeDiffToNow(data.lastReachPOI) >= perSpawnTimeoutMs) { bool hasProgression = false; - int32 currentObjective = data.objectiveIdx; - // check if the objective has progression + int32 const currentObjective = data.objectiveIdx; Quest const* quest = sObjectMgr->GetQuestTemplate(questId); - const QuestStatusData& q_status = bot->getQuestStatusMap().at(questId); + QuestStatusData const& q_status = bot->getQuestStatusMap().at(questId); if (currentObjective < QUEST_OBJECTIVES_COUNT) { if (q_status.CreatureOrGOCount[currentObjective] != 0 && quest->RequiredNpcOrGoCount[currentObjective]) @@ -408,28 +424,27 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data) quest->RequiredItemCount[currentObjective - QUEST_OBJECTIVES_COUNT]) hasProgression = true; } - if (!hasProgression) + if (hasProgression) { - // we has reach the poi for more than 5 mins but no progession - // may not be able to complete this quest, marked as abandoned - /// @TODO: It may be better to make lowPriorityQuest a global set shared by all bots (or saved in db) - botAI->lowPriorityQuest.insert(questId); - botAI->rpgStatistic.questAbandoned++; - LOG_DEBUG("playerbots", "[New RPG] {} marked as abandoned quest {}", bot->GetName(), questId); - botAI->rpgInfo.ChangeToIdle(); + // Refresh: re-fetch candidates so the list reflects what's + // still needed and is sorted from the bot's new position. + data.candidateSpawns.clear(); + data.currentSpawnIdx = 0; + data.lastReachPOI = 0; return true; } - // clear and select another poi later + // No progression at this spawn — advance to the next candidate. + ++data.currentSpawnIdx; data.lastReachPOI = 0; - data.pos = WorldPosition(); - data.objectiveIdx = 0; data.pursuedLootGO.Clear(); data.pursuedUseGO.Clear(); data.pursuedUseTarget.Clear(); return true; } - // at POI: drive toward specific objectives first + // 7. At spawn, within timeout: drive toward specific objectives. + // Combat strategy engages adjacent quest mobs; loot/use + // actions handle quest GOs and quest items. if (TryUseQuestItem(data.pursuedUseGO, data.pursuedUseTarget)) return true; if (TryLootQuestGO(data.pursuedLootGO)) @@ -437,96 +452,9 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data) if (TryUseQuestGO(data.pursuedUseGO)) return true; - // gather quests: roam for spawns. kill quests: yield to grind. - Quest const* quest = sObjectMgr->GetQuestTemplate(questId); - if (quest) - { - int32 obj = data.objectiveIdx; - bool isGatherObjective = false; - if (obj < QUEST_OBJECTIVES_COUNT) - { - int32 entry = quest->RequiredNpcOrGo[obj]; - if (entry < 0) // GO objective - isGatherObjective = true; - if (entry == 0 && obj < QUEST_ITEM_OBJECTIVES_COUNT && quest->RequiredItemId[obj]) - isGatherObjective = true; - } - else if (obj < QUEST_OBJECTIVES_COUNT + QUEST_ITEM_OBJECTIVES_COUNT) - { - isGatherObjective = true; - } - // source-item quest: need to find the target to use it on - if (quest->GetSrcItemId()) - isGatherObjective = true; - - if (isGatherObjective) - return MoveRandomNear(20.0f); - } - - // Kill-quest scout: at POI for 30s+ with no quest mob in sight - // means this cluster is empty. Switch to a different POI candidate - // (>50y away) if one exists; otherwise roam in place. - constexpr uint32 scoutTimeoutMs = 30 * 1000; - if (data.lastReachPOI && GetMSTimeDiffToNow(data.lastReachPOI) >= scoutTimeoutMs && - !HasNearbyQuestMob(30.0f)) - { - std::vector poiInfo; - if (GetQuestPOIPosAndObjectiveIdx(questId, poiInfo)) - { - std::vector alternatives; - for (size_t i = 0; i < poiInfo.size(); ++i) - { - float dx = poiInfo[i].pos.x - data.pos.GetPositionX(); - float dy = poiInfo[i].pos.y - data.pos.GetPositionY(); - if (dx * dx + dy * dy > 50.0f * 50.0f) - alternatives.push_back(i); - } - if (!alternatives.empty()) - { - size_t pickIdx = alternatives[urand(0, alternatives.size() - 1)]; - G3D::Vector2 newPoi = poiInfo[pickIdx].pos; - float dz = std::max(bot->GetMap()->GetHeight(newPoi.x, newPoi.y, MAX_HEIGHT), - bot->GetMap()->GetWaterLevel(newPoi.x, newPoi.y)); - if (dz != INVALID_HEIGHT && dz != VMAP_INVALID_HEIGHT_VALUE) - { - data.pos = WorldPosition(bot->GetMapId(), newPoi.x, newPoi.y, dz); - data.objectiveIdx = poiInfo[pickIdx].objectiveIdx; - data.lastReachPOI = 0; - data.pursuedLootGO.Clear(); - data.pursuedUseGO.Clear(); - data.pursuedUseTarget.Clear(); - return true; - } - } - } - return MoveRandomNear(20.0f); - } - - // kill quest: walk toward the marker before handing off to grind. - // lastReachPOI trips at ~10y so without this the bot fights on the - // edge and never reaches the dense cluster. Skip if a quest mob is - // in sight (might be the target) or a hostile is mid-pull. - if (bot->GetDistance(data.pos) > 5.0f) - { - if (HasNearbyQuestMob(30.0f)) - return false; - - GuidVector nearby = AI_VALUE(GuidVector, "possible targets"); - bool hostileClose = false; - for (ObjectGuid guid : nearby) - { - Unit* u = botAI->GetUnit(guid); - if (u && u->IsAlive() && bot->GetDistance(u) < 15.0f) - { - hostileClose = true; - break; - } - } - if (!hostileClose) - return MoveFarTo(data.pos); - } - - // yield to grind + // Yield this tick to combat/grind. No POI roam, no MoveRandomNear: + // bot stays at the spawn until either combat engages or the + // per-spawn timeout expires. return false; } diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp index 711d54513..98975dbfa 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp @@ -20,6 +20,7 @@ #include "Object.h" #include "ObjectAccessor.h" #include "OutdoorPvPMgr.h" +#include "QuestSpawnIndex.h" #include "ObjectDefines.h" #include "ObjectGuid.h" #include "ObjectMgr.h" @@ -1368,6 +1369,89 @@ bool NewRpgBaseAction::GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector return true; } +bool NewRpgBaseAction::FetchQuestSpawnsForObjective(uint32 questId, + std::vector& outSpawns, + int32& outObjectiveIdx) +{ + outSpawns.clear(); + + Quest const* quest = sObjectMgr->GetQuestTemplate(questId); + if (!quest) + return false; + + auto qStatusIt = bot->getQuestStatusMap().find(questId); + if (qStatusIt == bot->getQuestStatusMap().end()) + return false; + QuestStatusData const& q_status = qStatusIt->second; + + if (q_status.Status != QUEST_STATUS_INCOMPLETE) + return false; + + uint32 const botMapId = bot->GetMapId(); + + // Iterate creature/GO objectives first, then required-item drops. + // The first one with at least one indexed spawn on the bot's map + // wins. Subsequent objectives are picked next call after this one + // completes or the spawn list is exhausted. + auto tryFetch = [&](int32 entry, int32 idx) -> bool + { + if (!entry) + return false; + auto const& spawns = sQuestSpawnIndex->GetSpawns(botMapId, entry); + if (spawns.empty()) + return false; + outSpawns = spawns; + outObjectiveIdx = idx; + return true; + }; + + for (int i = 0; i < QUEST_OBJECTIVES_COUNT; ++i) + { + int32 npcOrGo = quest->RequiredNpcOrGo[i]; + if (!npcOrGo) + continue; + if (q_status.CreatureOrGOCount[i] >= quest->RequiredNpcOrGoCount[i]) + continue; // objective complete + if (tryFetch(npcOrGo, i)) + break; + } + + if (outSpawns.empty()) + { + // Required-item drops: source creature is encoded on the + // template (the loot-source mob). Use the creature spawn + // index for each ItemDrop entry. + for (int i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; ++i) + { + uint32 itemId = quest->RequiredItemId[i]; + if (!itemId) + continue; + if (q_status.ItemCount[i] >= quest->RequiredItemCount[i]) + continue; + // ItemDrop entries are encoded as the drop-source creature + // template id (positive). Fall back to scanning sObjectMgr + // creature-loot if/when we need stricter sourcing; for now + // this is best-effort. + int32 dropFrom = static_cast(quest->ItemDrop[i]); + if (dropFrom && tryFetch(dropFrom, QUEST_OBJECTIVES_COUNT + i)) + break; + } + } + + if (outSpawns.empty()) + return false; + + // Sort by distance from the bot so currentSpawnIdx=0 is the + // nearest. Reference's getNextDestination effectively does this + // each pick. + WorldPosition botPos(bot); + std::sort(outSpawns.begin(), outSpawns.end(), + [&botPos](WorldPosition const& a, WorldPosition const& b) + { return botPos.sqDistance(a) < botPos.sqDistance(b); }); + + return true; +} + WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot) { const std::vector& locs = sTravelMgr.GetLocsPerLevelCache(bot->GetLevel()); diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h index fb5ffa073..b0a508b30 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h @@ -76,6 +76,16 @@ protected: protected: bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector& poiInfo, bool toComplete = false); + // Reference per-spawn destination pattern: pick the first + // incomplete objective on `questId`, look up its spawns + // (creature OR gameobject — RequiredNpcOrGo encodes both) on + // the bot's current map, sort by distance from the bot, and + // return them in `outSpawns` with the resolved `outObjectiveIdx`. + // Returns false if no incomplete objective has spawns on the + // current map. + bool FetchQuestSpawnsForObjective(uint32 questId, + std::vector& outSpawns, + int32& outObjectiveIdx); static WorldPosition SelectRandomGrindPos(Player* bot); static WorldPosition SelectRandomCampPos(Player* bot); bool SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector& path); diff --git a/src/Ai/World/Rpg/NewRpgInfo.h b/src/Ai/World/Rpg/NewRpgInfo.h index f75a9b7df..521bcf185 100644 --- a/src/Ai/World/Rpg/NewRpgInfo.h +++ b/src/Ai/World/Rpg/NewRpgInfo.h @@ -46,7 +46,17 @@ struct NewRpgInfo const Quest* quest{nullptr}; uint32 questId{0}; int32 objectiveIdx{0}; + // Turn-in POI (DoCompletedQuest). Kept POI-based since this is + // the quest-giver location, not a mob spawn. WorldPosition pos{}; + // Reference (cmangos) per-spawn destination pattern for + // incomplete objectives: candidate spawn positions sorted by + // distance, walked one-by-one (current = + // candidateSpawns[currentSpawnIdx]) instead of POI-wander. + // Refreshed when the objective changes or the list is + // exhausted. + std::vector candidateSpawns; + uint32 currentSpawnIdx{0}; uint32 lastReachPOI{0}; // committed target per objective type. stops zig-zagging in // dense spawn clusters when "nearest" would flip each tick. diff --git a/src/Mgr/Quest/QuestSpawnIndex.cpp b/src/Mgr/Quest/QuestSpawnIndex.cpp new file mode 100644 index 000000000..8d111b183 --- /dev/null +++ b/src/Mgr/Quest/QuestSpawnIndex.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "QuestSpawnIndex.h" + +#include "CreatureData.h" +#include "GameObjectData.h" +#include "Log.h" +#include "ObjectMgr.h" + +QuestSpawnIndex* QuestSpawnIndex::instance() +{ + static QuestSpawnIndex inst; + return &inst; +} + +void QuestSpawnIndex::Init() +{ + if (_initialized) + return; + + uint32 creatures = 0; + uint32 gos = 0; + + for (auto const& kv : sObjectMgr->GetAllCreatureData()) + { + CreatureData const& cd = kv.second; + if (!cd.id1) + continue; + Key const key{cd.mapid, static_cast(cd.id1)}; + _index[key].emplace_back(cd.mapid, cd.posX, cd.posY, cd.posZ, cd.orientation); + ++creatures; + } + + for (auto const& kv : sObjectMgr->GetAllGOData()) + { + GameObjectData const& gd = kv.second; + if (!gd.id) + continue; + // Negative entry encodes GO (matches Quest::RequiredNpcOrGo + // convention used by the do-quest action callers). + Key const key{gd.mapid, -static_cast(gd.id)}; + _index[key].emplace_back(gd.mapid, gd.posX, gd.posY, gd.posZ, gd.orientation); + ++gos; + } + + _initialized = true; + + LOG_INFO("playerbots", + ">> QuestSpawnIndex: indexed {} creature spawns + {} GO spawns ({} unique keys).", + creatures, gos, static_cast(_index.size())); +} + +std::vector const& QuestSpawnIndex::GetSpawns(uint32 mapId, int32 entry) const +{ + auto it = _index.find(Key{mapId, entry}); + return (it != _index.end()) ? it->second : _empty; +} diff --git a/src/Mgr/Quest/QuestSpawnIndex.h b/src/Mgr/Quest/QuestSpawnIndex.h new file mode 100644 index 000000000..9af0dfe76 --- /dev/null +++ b/src/Mgr/Quest/QuestSpawnIndex.h @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_QUESTSPAWNINDEX_H +#define _PLAYERBOT_QUESTSPAWNINDEX_H + +#include +#include + +#include "Define.h" +#include "TravelMgr.h" + +// Maps `(mapId, RequiredNpcOrGo-style entry)` → list of spawn +// positions for that template on that map. The entry convention +// matches Quest::RequiredNpcOrGo: positive value = creature template +// id, negative value = gameobject template id (use the absolute +// value to look up in gameobject_template). +// +// Built once at module startup by scanning sObjectMgr's +// CreatureDataStore + GameObjectDataStore. Read-only thereafter. +// +// Used by the RPG do-quest action to walk directly to specific known +// spawns of a quest objective instead of wandering inside a POI +// cluster. Mirrors the reference's TravelMgr per-spawn destination +// indexing. +class QuestSpawnIndex +{ +public: + static QuestSpawnIndex* instance(); + + // Build the index from sObjectMgr's spawn data. Safe to call + // multiple times — second+ calls are no-ops. Call once after + // sObjectMgr->LoadCreatures / LoadGameObjects have populated + // their stores. + void Init(); + + // Returns spawns of `entry` on `mapId`. Empty list if none + // indexed. Stable reference for the lifetime of the index. + std::vector const& GetSpawns(uint32 mapId, int32 entry) const; + + [[nodiscard]] bool IsInitialized() const { return _initialized; } + +private: + QuestSpawnIndex() = default; + + bool _initialized{false}; + + struct Key + { + uint32 mapId; + int32 entry; + bool operator==(Key const& o) const { return mapId == o.mapId && entry == o.entry; } + }; + struct KeyHash + { + std::size_t operator()(Key const& k) const noexcept + { + return (std::size_t(k.mapId) << 32) ^ std::size_t(uint32(k.entry)); + } + }; + + std::unordered_map, KeyHash> _index; + std::vector _empty; +}; + +#define sQuestSpawnIndex QuestSpawnIndex::instance() + +#endif diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp index f4c609998..f5045c96e 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -20,6 +20,7 @@ #include "ModelIgnoreFlags.h" #include "PathGenerator.h" #include "Playerbots.h" +#include "QuestSpawnIndex.h" #include "RaceMgr.h" #include "TransportMgr.h" #include "VMapFactory.h" @@ -3584,6 +3585,7 @@ void TravelMgr::Init() PrepareDestinationCache(); } sTravelNodeMap.Init(); + sQuestSpawnIndex->Init(); } TravelMgr::FlightMasterInfo const* TravelMgr::GetNearestFlightMasterInfo(Player* bot) const