2178 lines
92 KiB
C++
2178 lines
92 KiB
C++
#include "AdventureAreaMgr.h"
|
|
#include "ConditionMgr.h"
|
|
#include "ClientlessTravelGraphMgr.h"
|
|
#include "ClientlessTransportMgr.h"
|
|
#include "ClientlessFlightMgr.h"
|
|
|
|
#include "DatabaseEnv.h"
|
|
#include "Log.h"
|
|
#include "Player.h"
|
|
#include "ClientlessTravelMgr.h"
|
|
#include "PathGenerator.h" // VCORE_C818_PREDICTIVE_GRAPH
|
|
#include "ObjectMgr.h"
|
|
#include "SpellMgr.h"
|
|
#include "SpellInfo.h"
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <map>
|
|
#include <cmath>
|
|
#include <limits>
|
|
#include <queue>
|
|
#include <utility>
|
|
|
|
ClientlessTravelGraphMgr* ClientlessTravelGraphMgr::instance()
|
|
{
|
|
static ClientlessTravelGraphMgr instance;
|
|
return &instance;
|
|
}
|
|
|
|
bool ClientlessTravelGraphMgr::EnsureLoaded()
|
|
{
|
|
if (_loaded)
|
|
return true;
|
|
|
|
return Reload();
|
|
}
|
|
|
|
bool ClientlessTravelGraphMgr::ResolvePortalUse(uint32 spawnId,
|
|
ClientlessPortalUse& portal, std::string& error)
|
|
{
|
|
portal = {};
|
|
auto const* spawn = sObjectMgr->GetGameObjectData(spawnId);
|
|
auto const* info = spawn ? sObjectMgr->GetGameObjectTemplate(spawn->id) : nullptr;
|
|
if (!info || info->type != GAMEOBJECT_TYPE_SPELLCASTER)
|
|
{
|
|
error = "spawn must be a static SPELLCASTER GameObject (type 22)";
|
|
return false;
|
|
}
|
|
SpellInfo const* spell = sSpellMgr->GetSpellInfo(info->spellcaster.spell);
|
|
SpellTargetPosition const* destination = nullptr;
|
|
if (spell)
|
|
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
|
|
{
|
|
auto const& effect = spell->Effects[i];
|
|
if (effect.Effect != SPELL_EFFECT_TELEPORT_UNITS)
|
|
continue;
|
|
// Only explicit database destinations are unambiguous. Homebind,
|
|
// script-selected destinations and triggered spell chains need
|
|
// their own interaction definitions, not guessed target nodes.
|
|
if (destination || (effect.TargetA.GetTarget() != TARGET_DEST_DB &&
|
|
effect.TargetB.GetTarget() != TARGET_DEST_DB))
|
|
{
|
|
error = "portal spell has an ambiguous or non-DB destination";
|
|
return false;
|
|
}
|
|
destination = sSpellMgr->GetSpellTargetPosition(spell->Id, SpellEffIndex(i));
|
|
if (!destination)
|
|
{
|
|
error = "portal teleport effect has no native spell_target_position";
|
|
return false;
|
|
}
|
|
}
|
|
if (!destination)
|
|
{
|
|
error = "portal requires a direct teleport spell with a fixed DB destination";
|
|
return false;
|
|
}
|
|
portal.SpawnId = spawnId;
|
|
portal.Entry = spawn->id;
|
|
portal.SpellId = spell->Id;
|
|
portal.Source.Key = "portal_go_" + std::to_string(spawnId) + "_source";
|
|
portal.Source.Type = "portal";
|
|
portal.Source.WorldPointKey = portal.Source.Key;
|
|
portal.Source.MapId = spawn->mapId;
|
|
portal.Source.X = spawn->spawnPoint.GetPositionX();
|
|
portal.Source.Y = spawn->spawnPoint.GetPositionY();
|
|
portal.Source.Z = spawn->spawnPoint.GetPositionZ();
|
|
portal.Source.O = spawn->spawnPoint.GetOrientation();
|
|
portal.Target.Key = "portal_go_" + std::to_string(spawnId) + "_target";
|
|
portal.Target.Type = "portal";
|
|
portal.Target.WorldPointKey = portal.Target.Key;
|
|
portal.Target.MapId = destination->target_mapId;
|
|
portal.Target.X = destination->target_X;
|
|
portal.Target.Y = destination->target_Y;
|
|
portal.Target.Z = destination->target_Z;
|
|
portal.Target.O = destination->target_Orientation;
|
|
return true;
|
|
}
|
|
|
|
ClientlessPortalUse const* ClientlessTravelGraphMgr::GetPortalUse(uint32 edgeDatabaseId) const
|
|
{
|
|
auto itr = _portalUses.find(edgeDatabaseId);
|
|
return itr != _portalUses.end() ? &itr->second : nullptr;
|
|
}
|
|
|
|
bool ClientlessTravelGraphMgr::PortalPrerequisites(Player const* player,uint32 edgeDatabaseId) const
|
|
{
|
|
auto portal=GetPortalUse(edgeDatabaseId);
|
|
auto info=portal?sObjectMgr->GetGameObjectTemplate(portal->Entry):nullptr;
|
|
if(!info || info->type!=GAMEOBJECT_TYPE_SPELLCASTER)return false;
|
|
// Native Spell::CheckCast sees caster,target. Public portals either let the
|
|
// player cast on themself or let the object cast on the visiting player.
|
|
uint8 playerTargets=info->spellcaster.playerCast?3:2;
|
|
return sConditionMgr->IsPlayerMeetingPrerequisiteConditions(CONDITION_SOURCE_TYPE_SPELL,
|
|
portal->SpellId,player,playerTargets);
|
|
}
|
|
|
|
bool ClientlessTravelGraphMgr::Reload()
|
|
{
|
|
_positionProbes.Clear();
|
|
_bridgeNeighborhoods.clear();
|
|
_nodes.clear();
|
|
_edges.clear();
|
|
_edgePoints.clear();
|
|
_portalUses.clear();
|
|
_edgeCount = 0;
|
|
|
|
QueryResult nodeResult = WorldDatabase.Query(
|
|
"SELECT n.id, n.node_key, n.node_type, n.world_point_key, "
|
|
"w.map, w.position_x, w.position_y, w.position_z, w.orientation "
|
|
"FROM vcore_clientless_travel_node n "
|
|
"INNER JOIN vcore_clientless_world_point w "
|
|
"ON w.point_key = n.world_point_key "
|
|
"WHERE n.enabled = 1 AND w.enabled = 1 "
|
|
"ORDER BY n.id");
|
|
|
|
if (nodeResult)
|
|
{
|
|
do
|
|
{
|
|
Field* fields = nodeResult->Fetch();
|
|
|
|
ClientlessTravelNode node;
|
|
node.DatabaseId = fields[0].GetUInt32();
|
|
node.Key = fields[1].GetString();
|
|
node.Type = fields[2].GetString();
|
|
node.WorldPointKey = fields[3].GetString();
|
|
node.MapId = fields[4].GetUInt16();
|
|
node.X = fields[5].GetFloat();
|
|
node.Y = fields[6].GetFloat();
|
|
node.Z = fields[7].GetFloat();
|
|
node.O = fields[8].GetFloat();
|
|
|
|
if (!node.Key.empty())
|
|
_nodes[node.Key] = std::move(node);
|
|
}
|
|
while (nodeResult->NextRow());
|
|
}
|
|
|
|
std::unordered_map<uint32, ClientlessPortalUse> portalBindings;
|
|
// The schema is opt-in, so existing installations retain their graph even
|
|
// before applying the interactive-portal migration.
|
|
if (WorldDatabase.Query("SHOW TABLES LIKE 'vcore_clientless_portal_use'"))
|
|
if (QueryResult bindings = WorldDatabase.Query(
|
|
"SELECT edge_id,gameobject_spawn_id,gameobject_entry,spell_id FROM vcore_clientless_portal_use"))
|
|
do
|
|
{
|
|
Field* fields = bindings->Fetch();
|
|
ClientlessPortalUse portal;
|
|
std::string error;
|
|
if (!ResolvePortalUse(fields[1].GetUInt32(), portal, error) ||
|
|
portal.Entry != fields[2].GetUInt32() || portal.SpellId != fields[3].GetUInt32())
|
|
{
|
|
TC_LOG_WARN("server.adventurebot.navigation", "PORTAL_USE_BINDING_INVALID edge=%u reason='%s'",
|
|
fields[0].GetUInt32(), error.c_str());
|
|
continue;
|
|
}
|
|
portalBindings.emplace(fields[0].GetUInt32(), std::move(portal));
|
|
} while (bindings->NextRow());
|
|
|
|
QueryResult edgeResult = WorldDatabase.Query(
|
|
"SELECT id, from_node_key, to_node_key, edge_type, cost, bidirectional "
|
|
"FROM vcore_clientless_travel_edge "
|
|
"WHERE enabled = 1 "
|
|
"ORDER BY id");
|
|
|
|
if (edgeResult)
|
|
{
|
|
do
|
|
{
|
|
Field* fields = edgeResult->Fetch();
|
|
|
|
ClientlessTravelEdge edge;
|
|
edge.DatabaseId = fields[0].GetUInt32();
|
|
edge.FromNode = fields[1].GetString();
|
|
edge.ToNode = fields[2].GetString();
|
|
edge.Type = fields[3].GetString();
|
|
edge.Cost = fields[4].GetFloat();
|
|
bool const bidirectional = fields[5].GetBool();
|
|
|
|
if (_nodes.find(edge.FromNode) == _nodes.end() ||
|
|
_nodes.find(edge.ToNode) == _nodes.end())
|
|
{
|
|
TC_LOG_WARN("server.adventurebot",
|
|
"[AdventureBot] ignoring travel edge id=%u from='%s' to='%s': node missing",
|
|
edge.DatabaseId, edge.FromNode.c_str(), edge.ToNode.c_str());
|
|
continue;
|
|
}
|
|
|
|
if (edge.Type.empty())
|
|
edge.Type = "ground";
|
|
|
|
if (edge.Cost <= 0.0f)
|
|
edge.Cost = 1.0f;
|
|
|
|
if (edge.Type == "portal_use")
|
|
{
|
|
auto binding = portalBindings.find(edge.DatabaseId);
|
|
auto const* portal = binding != portalBindings.end() ? &binding->second : nullptr;
|
|
auto const& target = _nodes.at(edge.ToNode);
|
|
if (!portal || bidirectional || edge.FromNode != portal->Source.Key ||
|
|
edge.ToNode != portal->Target.Key ||
|
|
_nodes.at(edge.FromNode).MapId != portal->Source.MapId ||
|
|
target.MapId != portal->Target.MapId ||
|
|
std::hypot(std::hypot(target.X - portal->Target.X, target.Y - portal->Target.Y),
|
|
target.Z - portal->Target.Z) > 1.0f)
|
|
{
|
|
TC_LOG_WARN("server.adventurebot.navigation", "PORTAL_USE_EDGE_UNAVAILABLE edge=%u", edge.DatabaseId);
|
|
continue;
|
|
}
|
|
_portalUses.emplace(edge.DatabaseId, *portal);
|
|
}
|
|
|
|
if (ClientlessTransportMgr::IsTransportEdge(edge.Type) &&
|
|
!sClientlessTransportMgr->HasConnection(edge.FromNode, edge.ToNode))
|
|
{
|
|
TC_LOG_WARN("server.adventurebot.navigation", "TRANSPORT_EDGE_UNAVAILABLE edge=%u from='%s' to='%s'",
|
|
edge.DatabaseId, edge.FromNode.c_str(), edge.ToNode.c_str());
|
|
continue;
|
|
}
|
|
_edges[edge.FromNode].push_back(edge);
|
|
++_edgeCount;
|
|
|
|
if (bidirectional)
|
|
{
|
|
ClientlessTravelEdge reverse = edge;
|
|
reverse.FromNode = edge.ToNode;
|
|
reverse.ToNode = edge.FromNode;
|
|
reverse.Reverse = true;
|
|
_edges[reverse.FromNode].push_back(std::move(reverse));
|
|
++_edgeCount;
|
|
}
|
|
}
|
|
while (edgeResult->NextRow());
|
|
}
|
|
|
|
QueryResult pointResult = WorldDatabase.Query(
|
|
"SELECT edge_id, point_index, map, position_x, position_y, position_z, orientation "
|
|
"FROM vcore_clientless_travel_edge_point "
|
|
"ORDER BY edge_id, point_index");
|
|
|
|
if (pointResult)
|
|
{
|
|
do
|
|
{
|
|
Field* fields = pointResult->Fetch();
|
|
|
|
ClientlessTravelEdgePoint point;
|
|
uint32 edgeId = fields[0].GetUInt32();
|
|
point.MapId = fields[2].GetUInt16();
|
|
point.X = fields[3].GetFloat();
|
|
point.Y = fields[4].GetFloat();
|
|
point.Z = fields[5].GetFloat();
|
|
point.O = fields[6].GetFloat();
|
|
|
|
_edgePoints[edgeId].push_back(point);
|
|
}
|
|
while (pointResult->NextRow());
|
|
}
|
|
|
|
_loaded = true;
|
|
|
|
TC_LOG_INFO("server.adventurebot",
|
|
"[AdventureBot] loaded travel graph: %u node(s), %u directed edge(s).",
|
|
uint32(_nodes.size()), _edgeCount);
|
|
|
|
return !_nodes.empty();
|
|
}
|
|
|
|
std::string ClientlessTravelGraphMgr::FindNearestStartNode(
|
|
Player const* player,
|
|
float maxDistance) const
|
|
{
|
|
if (!player || !player->IsInWorld())
|
|
return {};
|
|
|
|
std::string bestKey;
|
|
float bestDistance = std::numeric_limits<float>::max();
|
|
|
|
for (auto const& pair : _nodes)
|
|
{
|
|
ClientlessTravelNode const& node = pair.second;
|
|
if (node.MapId != player->GetMapId())
|
|
continue;
|
|
if ((!RescueNodeAllowed(node) || !sAdventureAreaMgr->AllowsNode(player,node))) continue;
|
|
|
|
float const dx = node.X - player->GetPositionX();
|
|
float const dy = node.Y - player->GetPositionY();
|
|
float const dz = node.Z - player->GetPositionZ();
|
|
float const distance = std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
|
|
if (distance > maxDistance || distance >= bestDistance)
|
|
continue;
|
|
|
|
bestDistance = distance;
|
|
bestKey = pair.first;
|
|
}
|
|
|
|
return bestKey;
|
|
}
|
|
|
|
bool ClientlessTravelGraphMgr::PlanRoute(
|
|
Player const* player,
|
|
std::string const& destinationNode,
|
|
float startRadius,
|
|
std::vector<ClientlessTravelRouteStep>& route,
|
|
std::string& error,
|
|
std::unordered_set<uint32> const* blockedEdgeIds)
|
|
{
|
|
route.clear();
|
|
error.clear();
|
|
|
|
EnsureLoaded();
|
|
|
|
if (!player || !player->IsInWorld())
|
|
{
|
|
error = "player is not in world";
|
|
return false;
|
|
}
|
|
|
|
if (_nodes.find(destinationNode) == _nodes.end())
|
|
{
|
|
error = "unknown destination node: " + destinationNode;
|
|
return false;
|
|
}
|
|
|
|
if ((!RescueNodeAllowed(_nodes.at(destinationNode)) || !sAdventureAreaMgr->AllowsNode(player,_nodes.at(destinationNode))))
|
|
{ error="destination node denied by area/node catalog"; return false; }
|
|
|
|
if (startRadius <= 0.0f)
|
|
startRadius = 500.0f;
|
|
|
|
std::string const startNode = FindNearestStartNode(player, startRadius);
|
|
if (startNode.empty())
|
|
{
|
|
error = "no travel node near current position";
|
|
return false;
|
|
}
|
|
|
|
struct QueueItem
|
|
{
|
|
float Cost;
|
|
std::string Node;
|
|
|
|
bool operator>(QueueItem const& other) const
|
|
{
|
|
return Cost > other.Cost;
|
|
}
|
|
};
|
|
|
|
struct Previous
|
|
{
|
|
std::string Node;
|
|
std::string EdgeType;
|
|
uint32 EdgeDatabaseId = 0;
|
|
bool EdgeReverse = false;
|
|
};
|
|
|
|
std::priority_queue<QueueItem, std::vector<QueueItem>, std::greater<QueueItem>> open;
|
|
std::unordered_map<std::string, float> distance;
|
|
std::unordered_map<std::string, Previous> previous;
|
|
|
|
for (auto const& pair : _nodes)
|
|
distance[pair.first] = std::numeric_limits<float>::infinity();
|
|
|
|
distance[startNode] = 0.0f;
|
|
open.push({ 0.0f, startNode });
|
|
|
|
while (!open.empty())
|
|
{
|
|
QueueItem current = open.top();
|
|
open.pop();
|
|
|
|
if (current.Cost != distance[current.Node])
|
|
continue;
|
|
|
|
if (current.Node == destinationNode)
|
|
break;
|
|
|
|
auto edgeItr = _edges.find(current.Node);
|
|
if (edgeItr == _edges.end())
|
|
continue;
|
|
|
|
for (ClientlessTravelEdge const& edge : edgeItr->second)
|
|
{
|
|
if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge))) continue;
|
|
if (blockedEdgeIds &&
|
|
edge.DatabaseId &&
|
|
blockedEdgeIds->find(edge.DatabaseId) != blockedEdgeIds->end())
|
|
continue;
|
|
|
|
float const candidate = current.Cost + edge.Cost;
|
|
if (candidate >= distance[edge.ToNode])
|
|
continue;
|
|
|
|
distance[edge.ToNode] = candidate;
|
|
previous[edge.ToNode] = {
|
|
current.Node, edge.Type, edge.DatabaseId, edge.Reverse
|
|
};
|
|
open.push({ candidate, edge.ToNode });
|
|
}
|
|
}
|
|
|
|
if (!std::isfinite(distance[destinationNode]))
|
|
{
|
|
error = "no known route from " + startNode + " to " + destinationNode;
|
|
return false;
|
|
}
|
|
|
|
std::vector<std::string> nodeKeys;
|
|
std::string cursor = destinationNode;
|
|
nodeKeys.push_back(cursor);
|
|
|
|
while (cursor != startNode)
|
|
{
|
|
auto previousItr = previous.find(cursor);
|
|
if (previousItr == previous.end())
|
|
{
|
|
error = "route reconstruction failed";
|
|
return false;
|
|
}
|
|
|
|
cursor = previousItr->second.Node;
|
|
nodeKeys.push_back(cursor);
|
|
}
|
|
|
|
std::reverse(nodeKeys.begin(), nodeKeys.end());
|
|
|
|
route.reserve(nodeKeys.size());
|
|
|
|
for (std::size_t i = 0; i < nodeKeys.size(); ++i)
|
|
{
|
|
ClientlessTravelNode const& node = _nodes.at(nodeKeys[i]);
|
|
|
|
ClientlessTravelRouteStep step;
|
|
step.NodeKey = node.Key;
|
|
step.NodeType = node.Type;
|
|
step.WorldPointKey = node.WorldPointKey;
|
|
step.MapId = node.MapId;
|
|
step.X = node.X;
|
|
step.Y = node.Y;
|
|
step.Z = node.Z;
|
|
step.O = node.O;
|
|
|
|
if (i == 0)
|
|
step.EdgeType = "start";
|
|
else
|
|
{
|
|
Previous const& prev = previous.at(nodeKeys[i]);
|
|
step.EdgeType = prev.EdgeType;
|
|
step.EdgeDatabaseId = prev.EdgeDatabaseId;
|
|
step.EdgeReverse = prev.EdgeReverse;
|
|
}
|
|
|
|
route.push_back(std::move(step));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
namespace
|
|
{
|
|
std::string MakeLocalBridgeKey(std::string const& from, std::string const& to)
|
|
{
|
|
return from + "->" + to;
|
|
}
|
|
}
|
|
|
|
bool ClientlessTravelGraphMgr::FindNearestNodeToPosition(
|
|
uint32 mapId,
|
|
float x,
|
|
float y,
|
|
float z,
|
|
std::string& nodeKey,
|
|
float& resultDistance) const
|
|
{
|
|
nodeKey.clear();
|
|
resultDistance = std::numeric_limits<float>::infinity();
|
|
|
|
for (auto const& pair : _nodes)
|
|
{
|
|
ClientlessTravelNode const& node = pair.second;
|
|
if (node.MapId != mapId || !RescueNodeAllowed(node))
|
|
continue;
|
|
|
|
float const dx = node.X - x;
|
|
float const dy = node.Y - y;
|
|
float const dz = node.Z - z;
|
|
float const distance = std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
if (distance >= resultDistance)
|
|
continue;
|
|
|
|
resultDistance = distance;
|
|
nodeKey = node.Key;
|
|
}
|
|
|
|
return !nodeKey.empty();
|
|
}
|
|
|
|
ClientlessTravelNode const* ClientlessTravelGraphMgr::FindNode(std::string const& key) const
|
|
{
|
|
auto const it = _nodes.find(key);
|
|
return it == _nodes.end() ? nullptr : &it->second;
|
|
}
|
|
|
|
bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
|
|
Player const* player,
|
|
std::string const& destinationNode,
|
|
float startRadius,
|
|
float bridgeRadius,
|
|
std::vector<ClientlessTravelRouteStep>& route,
|
|
bool& complete,
|
|
std::string& reachedNode,
|
|
std::string& error,
|
|
std::unordered_set<uint32> const* blockedEdgeIds,
|
|
std::unordered_set<std::string> const* blockedBridgeKeys,
|
|
ClientlessTravelLocationGoal const* locationGoal)
|
|
{
|
|
struct Trace
|
|
{
|
|
ClientlessGraphProbeCache& Cache;std::string const& Target;
|
|
std::chrono::steady_clock::time_point Start=std::chrono::steady_clock::now();
|
|
std::uint64_t Hits=Cache.Hits,Calls=0,NavUs=0,StartUs=0,StartCalls=0,FlightUs=0,FlightCalls=0;
|
|
~Trace()
|
|
{
|
|
auto us=std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now()-Start).count();
|
|
if(us>=50000)TC_LOG_INFO("server.worldserver","CPC_ROUTE target='%s' totalMs=%.3f navMs=%.3f probes=%llu cacheHits=%llu cacheSize=%u startMs=%.3f startCalls=%llu flightMs=%.3f flightCalls=%llu",Target.c_str(),us/1000.0,NavUs/1000.0,static_cast<unsigned long long>(Calls),static_cast<unsigned long long>(Cache.Hits-Hits),uint32(Cache.Size()),StartUs/1000.0,static_cast<unsigned long long>(StartCalls),FlightUs/1000.0,static_cast<unsigned long long>(FlightCalls));
|
|
}
|
|
} trace{_positionProbes,destinationNode};
|
|
|
|
// Reuse all start-point observations only WITHIN this immutable planning
|
|
// call, including its two search tiers. Never cache live Player paths globally.
|
|
std::map<ClientlessGraphProbeCache::Key,ClientlessPathDiagnostics> startProbes;
|
|
auto analyzeStart=[&](Position const& target,ClientlessPathDiagnostics& result)
|
|
{
|
|
auto key=ClientlessGraphProbeCache::MakeKey(player->GetMapId(),player->GetInstanceId(),player->GetPositionX(),player->GetPositionY(),player->GetPositionZ(),target.GetPositionX(),target.GetPositionY(),target.GetPositionZ());
|
|
auto found=startProbes.find(key);if(found!=startProbes.end()){result=found->second;return;}
|
|
auto begin=std::chrono::steady_clock::now();++trace.StartCalls;
|
|
sClientlessTravelMgr->AnalyzePath(const_cast<Player*>(player),target,result);
|
|
trace.StartUs+=std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now()-begin).count();
|
|
startProbes.emplace(key,result);
|
|
};
|
|
sClientlessFlightMgr->BeginPlanning(player);
|
|
struct FlightPlanningScope
|
|
{
|
|
~FlightPlanningScope() { sClientlessFlightMgr->BeginPlanning(); }
|
|
} flightPlanningScope;
|
|
// VCORE_C819_DEFERRED_START_NODES: run the unchanged C8.18 search in two tiers.
|
|
// A partial ordinary route must not suppress a complete route via a deferred root.
|
|
auto runSearch = [&](bool allowDeferred) -> bool
|
|
{
|
|
route.clear();
|
|
reachedNode.clear();
|
|
error.clear();
|
|
complete = false;
|
|
|
|
EnsureLoaded();
|
|
|
|
if (!player || !player->IsInWorld())
|
|
{
|
|
error = "player is not in world";
|
|
return false;
|
|
}
|
|
|
|
auto destinationItr = _nodes.find(destinationNode);
|
|
if (destinationItr == _nodes.end())
|
|
{
|
|
error = "unknown destination node: " + destinationNode;
|
|
return false;
|
|
}
|
|
|
|
if (!locationGoal && (!RescueNodeAllowed(destinationItr->second) || !sAdventureAreaMgr->AllowsNode(player,destinationItr->second)))
|
|
{ error="destination node denied by area/node catalog"; return false; }
|
|
if (locationGoal)
|
|
{
|
|
Position goal;goal.Relocate(locationGoal->X,locationGoal->Y,locationGoal->Z);
|
|
if (!sAdventureAreaMgr->AllowsPosition(player,locationGoal->MapId,goal))
|
|
{ error="destination location denied by area catalog"; return false; }
|
|
}
|
|
bool const flightEnabled = sClientlessFlightMgr->Enabled(player);
|
|
auto flightCost = [&](ClientlessTravelNode const& from, uint32 map, float x, float y, float z, bool access = false)
|
|
{
|
|
if (!flightEnabled || from.MapId != map ||
|
|
(blockedBridgeKeys && blockedBridgeKeys->count("flight_origin:"+from.Key)))
|
|
return std::numeric_limits<float>::infinity();
|
|
Position a,b; a.Relocate(from.X,from.Y,from.Z,from.O); b.Relocate(x,y,z);
|
|
auto begin=std::chrono::steady_clock::now();++trace.FlightCalls;
|
|
float result=sClientlessFlightMgr->Estimate(player,map,a,b,access);
|
|
if(access && std::isfinite(result))
|
|
{
|
|
float distance=a.GetExactDist(&b);
|
|
if(distance<20)result=std::numeric_limits<float>::infinity();
|
|
else result=(sClientlessFlightMgr->Preferred(player,map,a) || sClientlessFlightMgr->Preferred(player,map,b))?
|
|
40+distance*0.4f:180+distance*1.5f;
|
|
}
|
|
trace.FlightUs+=std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now()-begin).count();
|
|
return result;
|
|
};
|
|
if (startRadius <= 0.0f)
|
|
startRadius = 500.0f;
|
|
// C8.18: this cap applies ONLY to virtual node-to-node bridges.
|
|
// Start access, destination location access and virtual ground entries retain
|
|
// their distinct travel/search limits.
|
|
constexpr float MaxVirtualNodeBridge = 200.0f; // C8.20 empirical cap
|
|
if (bridgeRadius <= 0.0f)
|
|
bridgeRadius = MaxVirtualNodeBridge;
|
|
bridgeRadius = std::min(bridgeRadius, MaxVirtualNodeBridge);
|
|
|
|
struct State
|
|
{
|
|
std::string Node;
|
|
bool LastWasBridge = false;
|
|
// Preserve each validated start entry until same-edge exits are scored.
|
|
std::string EntryKey;
|
|
};
|
|
|
|
auto stateKey = [](State const& state)
|
|
{
|
|
return state.Node + (state.LastWasBridge ? "\x1f" "B" : "\x1f" "R") + state.EntryKey;
|
|
};
|
|
|
|
struct QueueItem
|
|
{
|
|
float Cost = 0.0f;
|
|
State Value;
|
|
bool operator>(QueueItem const& other) const { return Cost > other.Cost; }
|
|
};
|
|
|
|
struct Previous
|
|
{
|
|
std::string StateKey;
|
|
std::string Node;
|
|
std::string EdgeType;
|
|
uint32 EdgeDatabaseId = 0;
|
|
bool EdgeReverse = false;
|
|
// An intermediate virtual entry is represented by the landing waypoint
|
|
// followed by the remaining portion of its REAL recorded ground edge.
|
|
bool VirtualGroundEntry = false;
|
|
uint32 GroundStartIndex = 0;
|
|
ClientlessTravelEdgePoint EntryPoint;
|
|
};
|
|
|
|
std::priority_queue<QueueItem, std::vector<QueueItem>, std::greater<QueueItem>> open;
|
|
std::unordered_map<std::string, float> distance;
|
|
std::unordered_map<std::string, Previous> previous;
|
|
std::unordered_map<std::string, State> states;
|
|
std::unordered_map<std::string, uint32> persistentHops;
|
|
|
|
// VCORE_VIRTUAL_GROUND_EDGE_ENTRY_V1: a root may be a virtual recorded-road entry.
|
|
struct VirtualEntry
|
|
{
|
|
uint32 EdgeId = 0;
|
|
uint32 PointIndex = 0; // original order of _edgePoints
|
|
bool Reverse = false;
|
|
ClientlessTravelEdgePoint Point;
|
|
float AccessCost = 0.0f;
|
|
};
|
|
std::unordered_map<std::string, VirtualEntry> virtualRoots;
|
|
|
|
// Exact road-section cost from recorded arclength; handles reverse
|
|
// travel as well as virtual entry + exit within the SAME edge.
|
|
auto roadSectionCost = [&](uint32 edgeId, bool reverse, uint32 begin,
|
|
uint32 end, float fullCost) -> float
|
|
{
|
|
auto itr = _edgePoints.find(edgeId);
|
|
if (itr == _edgePoints.end() || end >= itr->second.size() || begin > end)
|
|
return std::numeric_limits<float>::infinity();
|
|
std::vector<ClientlessTravelEdgePoint> ordered = itr->second;
|
|
if (reverse)
|
|
std::reverse(ordered.begin(), ordered.end());
|
|
float total = 0.0f, section = 0.0f;
|
|
for (uint32 i = 1; i < ordered.size(); ++i)
|
|
{
|
|
float dx = ordered[i].X - ordered[i - 1].X;
|
|
float dy = ordered[i].Y - ordered[i - 1].Y;
|
|
float dz = ordered[i].Z - ordered[i - 1].Z;
|
|
float const segment = std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
total += segment;
|
|
if (i > begin && i <= end)
|
|
section += segment;
|
|
}
|
|
return total > 0.1f ? fullCost * section / total
|
|
: std::numeric_limits<float>::infinity();
|
|
};
|
|
|
|
std::unordered_set<std::string> flightRoots;
|
|
uint32 startCount = 0;
|
|
for (auto const& pair : _nodes)
|
|
{
|
|
ClientlessTravelNode const& node = pair.second;
|
|
if (node.MapId != player->GetMapId())
|
|
continue;
|
|
if ((!RescueNodeAllowed(node) || !sAdventureAreaMgr->AllowsNode(player,node))) continue;
|
|
|
|
float const dx = node.X - player->GetPositionX();
|
|
float const dy = node.Y - player->GetPositionY();
|
|
float const dz = node.Z - player->GetPositionZ();
|
|
float const localDistance = std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
if (localDistance > startRadius)
|
|
continue;
|
|
float startCost = localDistance;
|
|
|
|
// VCORE_VIRTUAL_EDGE_V11_PORTAL_APPROACH: never seed an unreachable node.
|
|
if (blockedBridgeKeys && blockedBridgeKeys->count("start:" + node.Key))
|
|
continue;
|
|
if (localDistance > 1.5f)
|
|
{
|
|
Position entryTarget;
|
|
entryTarget.Relocate(node.X, node.Y, node.Z, node.O);
|
|
ClientlessPathDiagnostics entryNav;
|
|
analyzeStart(entryTarget,entryNav);
|
|
bool const reachable = entryNav.Calculated && entryNav.Complete &&
|
|
entryNav.FinalDistance <= 1.5f && entryNav.MaxSegmentLength <= 8.0f &&
|
|
entryNav.MaxZDelta <= 5.0f && entryNav.PathLength <= 500.0f;
|
|
ClientlessTravelNode departure;
|
|
departure.Key="flight_departure";
|
|
departure.MapId=player->GetMapId();departure.X=player->GetPositionX();departure.Y=player->GetPositionY();departure.Z=player->GetPositionZ();
|
|
float air=blockedBridgeKeys && blockedBridgeKeys->count("flight:flight_departure->"+node.Key)?
|
|
std::numeric_limits<float>::infinity():flightCost(departure,node.MapId,node.X,node.Y,node.Z,!reachable);
|
|
if(std::isfinite(air) && (!reachable || air<entryNav.PathLength))
|
|
{flightRoots.insert(node.Key);startCost=air;}
|
|
else
|
|
{
|
|
// VCORE_C819_DEFERRED_START_NODES: incomplete is not the same as unsafe.
|
|
// The endpoint is handled by the executor, not counted as arrival at node.
|
|
bool const partial = entryNav.Calculated &&
|
|
(entryNav.PathType & uint32(PATHFIND_INCOMPLETE)) != 0 &&
|
|
(entryNav.PathType & uint32(PATHFIND_NOPATH)) == 0 &&
|
|
entryNav.PointCount >= 2 && entryNav.PathLength >= 2.0f &&
|
|
entryNav.PathLength <= 500.0f && entryNav.MaxSegmentLength <= 8.0f &&
|
|
entryNav.MaxZDelta <= 5.0f &&
|
|
entryNav.FinalDistance + 1.0f < entryNav.DirectDistance;
|
|
if (!reachable && !(allowDeferred && partial))
|
|
{
|
|
if (partial && !allowDeferred)
|
|
TC_LOG_INFO("server.adventurebot.navigation",
|
|
"C819_START_NODE_DEFER node='%s' path=%.2f final=%.2f",
|
|
node.Key.c_str(), entryNav.PathLength, entryNav.FinalDistance);
|
|
else if (!partial)
|
|
TC_LOG_INFO("server.adventurebot.navigation",
|
|
"START_NODE_REJECT node='%s' distance=%.2f reason='%s'",
|
|
node.Key.c_str(), localDistance, entryNav.Reason.c_str());
|
|
continue;
|
|
}
|
|
startCost = entryNav.PathLength;
|
|
if (!reachable)
|
|
startCost += 100000.0f; // Tier 2 only: deferred roots rank after reachable roots
|
|
}
|
|
}
|
|
State state{ node.Key, false };
|
|
std::string const key = stateKey(state);
|
|
auto found = distance.find(key);
|
|
if (found != distance.end() && found->second <= startCost)
|
|
continue;
|
|
|
|
distance[key] = startCost;
|
|
states[key] = state;
|
|
persistentHops[key] = 0;
|
|
open.push({ startCost, state });
|
|
++startCount;
|
|
}
|
|
|
|
// Use the nearest ten nodes merely to shortlist real recorded ground edges.
|
|
// All distances below are on the player's current map; no database mutation.
|
|
std::vector<std::pair<float, std::string>> nearestNodes;
|
|
for (auto const& pair : _nodes)
|
|
{
|
|
ClientlessTravelNode const& node = pair.second;
|
|
if (node.MapId != player->GetMapId())
|
|
continue;
|
|
if ((!RescueNodeAllowed(node) || !sAdventureAreaMgr->AllowsNode(player,node))) continue;
|
|
float dx = node.X - player->GetPositionX();
|
|
float dy = node.Y - player->GetPositionY();
|
|
float dz = node.Z - player->GetPositionZ();
|
|
nearestNodes.emplace_back(dx * dx + dy * dy + dz * dz, pair.first);
|
|
}
|
|
std::sort(nearestNodes.begin(), nearestNodes.end());
|
|
if (nearestNodes.size() > 10)
|
|
nearestNodes.resize(10);
|
|
|
|
struct EntryCandidate
|
|
{
|
|
float DistanceSq;
|
|
ClientlessTravelEdge Edge;
|
|
uint32 Index;
|
|
ClientlessTravelEdgePoint Point;
|
|
uint32 Count;
|
|
};
|
|
std::vector<EntryCandidate> entryCandidates;
|
|
std::unordered_set<std::string> visitedDirectedEdges;
|
|
for (auto const& near : nearestNodes)
|
|
{
|
|
// Both outgoing and incoming records are considered for spatial discovery.
|
|
// A directed entry still follows only the direction present in _edges.
|
|
for (auto const& edgePair : _edges)
|
|
{
|
|
for (ClientlessTravelEdge const& edge : edgePair.second)
|
|
{
|
|
if (edge.Type != "ground" || !edge.DatabaseId ||
|
|
(edge.FromNode != near.second && edge.ToNode != near.second))
|
|
continue;
|
|
if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge))) continue;
|
|
if (blockedEdgeIds && blockedEdgeIds->count(edge.DatabaseId))
|
|
continue;
|
|
std::string key = std::to_string(edge.DatabaseId) + (edge.Reverse ? "R" : "F");
|
|
if (!visitedDirectedEdges.insert(key).second)
|
|
continue;
|
|
auto pointIt = _edgePoints.find(edge.DatabaseId);
|
|
if (pointIt == _edgePoints.end() || pointIt->second.size() < 3)
|
|
continue;
|
|
std::vector<ClientlessTravelEdgePoint> const& points = pointIt->second;
|
|
// Candidates are stored in original point order, even for reverse travel.
|
|
for (uint32 i = 0; i < points.size(); ++i)
|
|
{
|
|
if ((!edge.Reverse && i + 1 >= points.size()) || (edge.Reverse && i == 0))
|
|
continue;
|
|
ClientlessTravelEdgePoint const& point = points[i];
|
|
if (point.MapId != player->GetMapId())
|
|
continue;
|
|
float dx = point.X - player->GetPositionX();
|
|
float dy = point.Y - player->GetPositionY();
|
|
float dz = point.Z - player->GetPositionZ();
|
|
float dsq = dx * dx + dy * dy + dz * dz;
|
|
// Keep local MMAP checks bounded; startRadius is a planning limit.
|
|
if (dsq > startRadius * startRadius)
|
|
continue;
|
|
entryCandidates.push_back({ dsq, edge, i, point, uint32(points.size()) });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
std::sort(entryCandidates.begin(), entryCandidates.end(),
|
|
[](EntryCandidate const& a, EntryCandidate const& b) { return a.DistanceSq < b.DistanceSq; });
|
|
|
|
uint32 validated = 0;
|
|
uint32 accepted = 0;
|
|
for (EntryCandidate const& candidate : entryCandidates)
|
|
{
|
|
if (validated >= 24)
|
|
break;
|
|
++validated;
|
|
Position target;
|
|
target.Relocate(candidate.Point.X, candidate.Point.Y, candidate.Point.Z, candidate.Point.O);
|
|
ClientlessPathDiagnostics nav;
|
|
// A point on top of the player needs no pathfinder round trip.
|
|
bool const alreadyThere = candidate.DistanceSq <= 2.25f;
|
|
if (!alreadyThere)
|
|
{
|
|
analyzeStart(target,nav);
|
|
if (!nav.Calculated || !nav.Complete || nav.Suspicious ||
|
|
nav.FinalDistance > 1.5f || nav.PathLength > 500.0f)
|
|
continue;
|
|
}
|
|
|
|
State state{ candidate.Edge.ToNode, false,
|
|
":entry:" + std::to_string(candidate.Edge.DatabaseId) +
|
|
(candidate.Edge.Reverse ? "R:" : "F:") + std::to_string(candidate.Index) };
|
|
std::string key = stateKey(state);
|
|
if (blockedBridgeKeys && blockedBridgeKeys->count("start:" + candidate.Edge.ToNode))
|
|
continue;
|
|
uint32 const startIndex = candidate.Edge.Reverse
|
|
? candidate.Count - 1 - candidate.Index : candidate.Index;
|
|
float newCost = (alreadyThere ? 0.0f : nav.PathLength) +
|
|
roadSectionCost(candidate.Edge.DatabaseId, candidate.Edge.Reverse,
|
|
startIndex, candidate.Count - 1, candidate.Edge.Cost);
|
|
auto old = distance.find(key);
|
|
if (old != distance.end() && old->second <= newCost)
|
|
continue;
|
|
distance[key] = newCost;
|
|
states[key] = state;
|
|
persistentHops[key] = 1;
|
|
previous.erase(key);
|
|
virtualRoots[key] = { candidate.Edge.DatabaseId, candidate.Index,
|
|
candidate.Edge.Reverse, candidate.Point,
|
|
alreadyThere ? 0.0f : nav.PathLength };
|
|
open.push({ newCost, state });
|
|
++accepted;
|
|
}
|
|
if (validated)
|
|
TC_LOG_INFO("server.adventurebot.navigation",
|
|
"VIRTUAL_EDGE_CANDIDATES map=%u tested=%u accepted=%u",
|
|
player->GetMapId(), validated, accepted);
|
|
|
|
if (!startCount && !accepted)
|
|
{
|
|
error = "no reachable travel node or recorded ground edge near current position";
|
|
return false;
|
|
}
|
|
|
|
// C8.20: virtual node bridges are equal-cost ground alternatives and
|
|
// participate even when stored graph routes also exist.
|
|
bool const repairRequired = true;
|
|
TC_LOG_INFO("server.adventurebot.navigation",
|
|
"C818_ROUTE_MODE destination='%s' mode='%s' roots=%u",
|
|
destinationNode.c_str(), locationGoal ? "location" : "all-edges",
|
|
uint32(startCount + accepted));
|
|
|
|
// Dynamic candidates may be examined many times during Dijkstra. Avoid
|
|
// duplicating costly Detour queries for identical directed connections.
|
|
std::unordered_map<std::string, float> validBridgeLengths;
|
|
std::unordered_set<std::string> rejectedBridges;
|
|
uint32 bridgeTests = 0, bridgeAccepted = 0, bridgeRejected = 0;
|
|
uint32 bridgeDistanceRejected = 0;
|
|
uint32 forwardEntryTests = 0, forwardEntryAccepted = 0;
|
|
|
|
auto analyze = [&](ClientlessTravelNode const& from,
|
|
uint32 targetMap, float x, float y, float z)
|
|
{
|
|
Position a, b;
|
|
a.Relocate(from.X, from.Y, from.Z, from.O);
|
|
b.Relocate(x, y, z, 0.0f);
|
|
// The map is deliberately taken from the future graph node, never
|
|
// from Arthoran's current position. No Player/WorldObject is required.
|
|
if (targetMap != from.MapId)
|
|
return VcorePositionPathResult{};
|
|
auto allowedPath=[&](std::vector<std::array<float,3>> const& points)
|
|
{
|
|
if(!sAdventureAreaMgr->Applies(player))return true;
|
|
if(points.empty())return false;
|
|
for(std::size_t i=1;i<points.size();++i)
|
|
{
|
|
Position p,q;p.Relocate(points[i-1][0],points[i-1][1],points[i-1][2]);q.Relocate(points[i][0],points[i][1],points[i][2]);
|
|
if(!sAdventureAreaMgr->AllowsSegment(player,from.MapId,p,q))return false;
|
|
}
|
|
return true;
|
|
};
|
|
auto key=ClientlessGraphProbeCache::MakeKey(from.MapId,player->GetInstanceId(),from.X,from.Y,from.Z,x,y,z);
|
|
if(auto cached=_positionProbes.Find(key))
|
|
{
|
|
if(!allowedPath(cached->Points))return VcorePositionPathResult{};
|
|
VcorePositionPathResult result;result.Result=VcorePositionPathResult::Status::Complete;
|
|
result.PathLength=cached->Length;result.FinalDistance=cached->Final;result.DirectDistance=cached->Direct;
|
|
return result;
|
|
}
|
|
auto started=std::chrono::steady_clock::now();++trace.Calls;
|
|
auto result=PathGenerator::AnalyzePositions(from.MapId, from.MapId,player->GetInstanceId(),a,b,NAV_GROUND);
|
|
trace.NavUs+=std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now()-started).count();
|
|
std::vector<std::array<float,3>> points;
|
|
for(auto const& p:result.Points)points.push_back({p.x,p.y,p.z});
|
|
_positionProbes.Record(key,result.IsComplete(),result.PathLength,result.FinalDistance,result.DirectDistance,points);
|
|
if(!allowedPath(points))return VcorePositionPathResult{};
|
|
return result;
|
|
};
|
|
|
|
// Select the closest eligible waypoint per directed road geometrically.
|
|
// Future destination tiles need not be loaded: local access is estimated
|
|
// here and validated by the executor only after reaching the exit.
|
|
struct ExitCandidate
|
|
{
|
|
std::string FromNode;
|
|
std::string ToNode;
|
|
uint32 EdgeId = 0;
|
|
bool Reverse = false;
|
|
uint32 EndIndex = 0; // in execution-direction original road points
|
|
float RoadCost = 0.0f;
|
|
float LocalCost = 0.0f;
|
|
float DistanceSq = 0.0f;
|
|
ClientlessTravelEdgePoint Point;
|
|
bool Flight = false;
|
|
};
|
|
float const locationAccessRadius = locationGoal
|
|
? std::min(300.0f, locationGoal->AnchorRadius) : 0.0f;
|
|
std::vector<ExitCandidate> rawExitCandidates;
|
|
std::unordered_map<std::string, std::vector<ExitCandidate>> exitsByFrom;
|
|
if (locationGoal || flightEnabled)
|
|
{
|
|
auto const& targetNode = destinationItr->second;
|
|
uint32 const goalMap = locationGoal ? locationGoal->MapId : targetNode.MapId;
|
|
float const goalX = locationGoal ? locationGoal->X : targetNode.X;
|
|
float const goalY = locationGoal ? locationGoal->Y : targetNode.Y;
|
|
float const goalZ = locationGoal ? locationGoal->Z : targetNode.Z;
|
|
float const radius = flightEnabled ? 20000.0f : locationAccessRadius;
|
|
std::unordered_set<std::string> scannedDirected;
|
|
for (auto const& edgePair : _edges)
|
|
{
|
|
for (ClientlessTravelEdge const& edge : edgePair.second)
|
|
{
|
|
if (edge.Type != "ground" || !edge.DatabaseId ||
|
|
(blockedEdgeIds && blockedEdgeIds->count(edge.DatabaseId)))
|
|
continue;
|
|
if (_nodes.at(edge.FromNode).MapId!=goalMap || _nodes.at(edge.ToNode).MapId!=goalMap) continue;
|
|
if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge))) continue;
|
|
std::string const identity = std::to_string(edge.DatabaseId) +
|
|
(edge.Reverse ? "R" : "F");
|
|
if (!scannedDirected.insert(identity).second)
|
|
continue;
|
|
auto pointsItr = _edgePoints.find(edge.DatabaseId);
|
|
if (pointsItr == _edgePoints.end() || pointsItr->second.size() < 3)
|
|
continue;
|
|
std::vector<ClientlessTravelEdgePoint> ordered = pointsItr->second;
|
|
if (edge.Reverse)
|
|
std::reverse(ordered.begin(), ordered.end());
|
|
float totalLength = 0.0f;
|
|
for (std::size_t i = 1; i < ordered.size(); ++i)
|
|
{
|
|
float dx = ordered[i].X - ordered[i - 1].X;
|
|
float dy = ordered[i].Y - ordered[i - 1].Y;
|
|
float dz = ordered[i].Z - ordered[i - 1].Z;
|
|
totalLength += std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
}
|
|
if (totalLength < 0.1f)
|
|
continue;
|
|
float travelled = 0.0f;
|
|
ExitCandidate nearest;
|
|
nearest.DistanceSq = std::numeric_limits<float>::infinity();
|
|
for (uint32 i = 1; i + 1 < ordered.size(); ++i)
|
|
{
|
|
float dx = ordered[i].X - ordered[i - 1].X;
|
|
float dy = ordered[i].Y - ordered[i - 1].Y;
|
|
float dz = ordered[i].Z - ordered[i - 1].Z;
|
|
travelled += std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
ClientlessTravelEdgePoint const& point = ordered[i];
|
|
if (point.MapId != goalMap)
|
|
continue;
|
|
dx = point.X - goalX;
|
|
dy = point.Y - goalY;
|
|
dz = point.Z - goalZ;
|
|
float const dsq = dx * dx + dy * dy + dz * dz;
|
|
if (dsq > radius * radius)
|
|
continue;
|
|
std::string const exitKey = "exit:" + std::to_string(edge.DatabaseId) +
|
|
":" + std::to_string(i) + (edge.Reverse ? "R" : "F");
|
|
if (blockedBridgeKeys && blockedBridgeKeys->count(exitKey))
|
|
continue;
|
|
ExitCandidate candidate;
|
|
candidate.FromNode = edge.FromNode;
|
|
candidate.ToNode = edge.ToNode;
|
|
candidate.EdgeId = edge.DatabaseId;
|
|
candidate.Reverse = edge.Reverse;
|
|
candidate.EndIndex = i;
|
|
candidate.RoadCost = edge.Cost * (travelled / totalLength);
|
|
candidate.DistanceSq = dsq;
|
|
candidate.Point = point;
|
|
bool const groundExitExhausted = blockedBridgeKeys &&
|
|
blockedBridgeKeys->count("ground_exit_edge:" + identity);
|
|
candidate.LocalCost = locationGoal && !groundExitExhausted && dsq <= locationAccessRadius*locationAccessRadius
|
|
? std::sqrt(dsq) : std::numeric_limits<float>::infinity();
|
|
ClientlessTravelNode exitNode;
|
|
exitNode.MapId=point.MapId;exitNode.X=point.X;exitNode.Y=point.Y;exitNode.Z=point.Z;
|
|
float const air = blockedBridgeKeys && blockedBridgeKeys->count("flight_exit:"+exitKey)
|
|
? std::numeric_limits<float>::infinity() : flightCost(exitNode,goalMap,goalX,goalY,goalZ);
|
|
if (air < candidate.LocalCost) { candidate.LocalCost=air;candidate.Flight=true; }
|
|
if (!std::isfinite(candidate.LocalCost)) continue;
|
|
// Never replace a node goal with an unvalidated ground exit.
|
|
if (!locationGoal && !candidate.Flight) continue;
|
|
if (std::isfinite(nearest.DistanceSq) &&
|
|
candidate.RoadCost+candidate.LocalCost >= nearest.RoadCost+nearest.LocalCost) continue;
|
|
nearest = std::move(candidate);
|
|
}
|
|
if (std::isfinite(nearest.DistanceSq))
|
|
rawExitCandidates.push_back(std::move(nearest));
|
|
}
|
|
}
|
|
std::sort(rawExitCandidates.begin(), rawExitCandidates.end(),
|
|
[](ExitCandidate const& a, ExitCandidate const& b)
|
|
{ return a.DistanceSq < b.DistanceSq; });
|
|
for (ExitCandidate const& candidate : rawExitCandidates)
|
|
exitsByFrom[candidate.FromNode].push_back(candidate);
|
|
TC_LOG_INFO("server.adventurebot.navigation",
|
|
"C821_EXIT_CANDIDATES directedEdges=%u radius=%.1f localCost=air-distance validation=on-arrival",
|
|
uint32(rawExitCandidates.size()), radius);
|
|
}
|
|
struct ExitSelection
|
|
{
|
|
bool Active = false;
|
|
bool RootPartial = false;
|
|
ExitCandidate Candidate;
|
|
bool ForwardPartial = false;
|
|
uint32 StartIndex = 0;
|
|
ClientlessTravelEdgePoint EntryPoint;
|
|
} bestExit;
|
|
float bestGoalCost = std::numeric_limits<float>::infinity();
|
|
std::string bestGoalState;
|
|
bool bestFinalFlight = false;
|
|
// C8.20: a virtual entry is represented in the queue by its FAR
|
|
// endpoint. Check exits BEFORE that endpoint independently: otherwise
|
|
// an exit halfway along the road would pay for the entire remaining edge.
|
|
if (locationGoal || flightEnabled)
|
|
{
|
|
for (auto const& root : virtualRoots)
|
|
{
|
|
if (previous.find(root.first) != previous.end())
|
|
continue;
|
|
for (auto const& exitGroup : exitsByFrom)
|
|
{
|
|
for (ExitCandidate const& exit : exitGroup.second)
|
|
{
|
|
if (exit.EdgeId != root.second.EdgeId ||
|
|
exit.Reverse != root.second.Reverse)
|
|
continue;
|
|
auto pointsItr = _edgePoints.find(exit.EdgeId);
|
|
if (pointsItr == _edgePoints.end())
|
|
continue;
|
|
uint32 const startIndex = root.second.Reverse ?
|
|
uint32(pointsItr->second.size() - 1 - root.second.PointIndex) :
|
|
root.second.PointIndex;
|
|
if (exit.EndIndex <= startIndex)
|
|
continue;
|
|
float fullEdgeCost = 0.0f;
|
|
auto fromNodeItr = _edges.find(exit.FromNode);
|
|
if (fromNodeItr != _edges.end())
|
|
for (ClientlessTravelEdge const& edge : fromNodeItr->second)
|
|
if (edge.DatabaseId == exit.EdgeId && edge.Reverse == exit.Reverse)
|
|
fullEdgeCost = edge.Cost;
|
|
if (fullEdgeCost <= 0.0f)
|
|
continue;
|
|
float const partialCost = roadSectionCost(exit.EdgeId,
|
|
exit.Reverse, startIndex, exit.EndIndex, fullEdgeCost);
|
|
float const score = root.second.AccessCost + partialCost + exit.LocalCost;
|
|
if (!std::isfinite(score) || score >= bestGoalCost)
|
|
continue;
|
|
bestGoalCost = score;
|
|
bestGoalState = root.first;
|
|
bestExit = { true, true, exit };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
while (!open.empty())
|
|
{
|
|
QueueItem current = open.top();
|
|
open.pop();
|
|
|
|
std::string const currentKey = stateKey(current.Value);
|
|
auto distanceItr = distance.find(currentKey);
|
|
if (distanceItr == distance.end() || current.Cost != distanceItr->second)
|
|
continue;
|
|
|
|
if (current.Cost >= bestGoalCost) break;
|
|
if (!locationGoal && current.Value.Node == destinationNode)
|
|
{
|
|
bestGoalState = currentKey;
|
|
bestGoalCost = current.Cost;
|
|
bestExit = {};
|
|
bestFinalFlight = false;
|
|
break;
|
|
}
|
|
|
|
// C8.20: location goal is a first-class search terminator.
|
|
// Node endpoints and road exits compete by the SAME full route cost.
|
|
if (locationGoal)
|
|
{
|
|
if (current.Cost >= bestGoalCost)
|
|
break; // Dijkstra lower bound; all remaining edges are nonnegative
|
|
ClientlessTravelNode const& candidateNode = _nodes.at(current.Value.Node);
|
|
bool const sourcePortal = candidateNode.Key.rfind("portal_at_", 0) == 0 &&
|
|
candidateNode.Key.size() >= 7 &&
|
|
candidateNode.Key.compare(candidateNode.Key.size() - 7, 7, "_source") == 0;
|
|
if (!sourcePortal && candidateNode.MapId == locationGoal->MapId)
|
|
{
|
|
float dx = candidateNode.X - locationGoal->X;
|
|
float dy = candidateNode.Y - locationGoal->Y;
|
|
float dz = candidateNode.Z - locationGoal->Z;
|
|
float const direct = std::sqrt(dx*dx + dy*dy + dz*dz);
|
|
// Use the same local access limit for nodes and road exits.
|
|
// Otherwise a distant node can bypass a closer usable exit.
|
|
float const air = blockedBridgeKeys && blockedBridgeKeys->count("flight_finish:"+candidateNode.Key)
|
|
? std::numeric_limits<float>::infinity() : flightCost(candidateNode,locationGoal->MapId,locationGoal->X,locationGoal->Y,locationGoal->Z);
|
|
if (direct <= locationAccessRadius || std::isfinite(air))
|
|
{
|
|
// Node anchors and virtual exits use the same estimate.
|
|
// Neither is rejected because a remote tile is unloaded.
|
|
std::string const finishKey = "finish:" + candidateNode.Key;
|
|
float const finalCost = blockedBridgeKeys && blockedBridgeKeys->count(finishKey)
|
|
? std::numeric_limits<float>::infinity() :
|
|
std::min(direct<=locationAccessRadius ? direct : std::numeric_limits<float>::infinity(),air);
|
|
if (current.Cost + finalCost < bestGoalCost)
|
|
{
|
|
bestGoalCost = current.Cost + finalCost;
|
|
bestGoalState = currentKey;
|
|
bestExit = {};
|
|
bestFinalFlight = air <= finalCost;
|
|
}
|
|
}
|
|
}
|
|
if (!sourcePortal)
|
|
{
|
|
auto exitsItr = exitsByFrom.find(current.Value.Node);
|
|
if (exitsItr != exitsByFrom.end())
|
|
{
|
|
for (ExitCandidate const& exit : exitsItr->second)
|
|
{
|
|
float const score = current.Cost + exit.RoadCost + exit.LocalCost;
|
|
if (score >= bestGoalCost)
|
|
continue;
|
|
bestGoalCost = score;
|
|
bestGoalState = currentKey;
|
|
bestExit = { true, false, exit };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!locationGoal && flightEnabled)
|
|
{
|
|
auto exits=exitsByFrom.find(current.Value.Node);
|
|
if (exits!=exitsByFrom.end())
|
|
for (auto const& exit:exits->second)
|
|
if (current.Cost+exit.RoadCost+exit.LocalCost<bestGoalCost)
|
|
{
|
|
bestGoalCost=current.Cost+exit.RoadCost+exit.LocalCost;
|
|
bestGoalState=currentKey;bestExit={true,false,exit};
|
|
}
|
|
}
|
|
|
|
// VCORE_C816_PORTAL_MOUNT_GUARD: a source node is not ordinary
|
|
// ground transit. Once reached during a planned route, its only legal
|
|
// continuation is the portal edge itself. Physical AreaTriggers may
|
|
// fire even before the route executor advances to that edge.
|
|
bool const isPortalSource =
|
|
current.Value.Node.rfind("portal_at_", 0) == 0 &&
|
|
current.Value.Node.size() >= 7 &&
|
|
current.Value.Node.compare(current.Value.Node.size() - 7, 7, "_source") == 0;
|
|
bool const portalArrival = isPortalSource && previous.find(currentKey) != previous.end();
|
|
|
|
// Persistent TravelGraph edges always reset the bridge guard.
|
|
auto edgeItr = _edges.find(current.Value.Node);
|
|
if (edgeItr != _edges.end())
|
|
{
|
|
for (ClientlessTravelEdge const& edge : edgeItr->second)
|
|
{
|
|
if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge))) continue;
|
|
if (portalArrival && edge.Type != "portal")
|
|
continue;
|
|
if (blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->count(edge.DatabaseId))
|
|
continue;
|
|
|
|
State next{ edge.ToNode, false };
|
|
std::string const nextKey = stateKey(next);
|
|
float const candidate = current.Cost + edge.Cost;
|
|
|
|
auto old = distance.find(nextKey);
|
|
if (old != distance.end() && candidate >= old->second)
|
|
continue;
|
|
|
|
distance[nextKey] = candidate;
|
|
states[nextKey] = next;
|
|
persistentHops[nextKey] = persistentHops[currentKey] + 1;
|
|
previous[nextKey] = {
|
|
currentKey, current.Value.Node, edge.Type,
|
|
edge.DatabaseId, edge.Reverse
|
|
};
|
|
open.push({ candidate, next });
|
|
}
|
|
}
|
|
|
|
// Dynamic flights connect real nodes; never the synthetic entry waypoints.
|
|
// Include them before the local-bridge guard: a flight is a separate
|
|
// macro travel mode with its own native departure and landing checks.
|
|
if (flightEnabled && !portalArrival)
|
|
{
|
|
auto const& origin=_nodes.at(current.Value.Node);
|
|
for (auto const& pair:_nodes)
|
|
{
|
|
auto const& target=pair.second;
|
|
if (target.Key==origin.Key || target.MapId!=origin.MapId) continue;
|
|
State next{target.Key,false};std::string key=stateKey(next);
|
|
auto old=distance.find(key);
|
|
float dx=target.X-origin.X,dy=target.Y-origin.Y,dz=target.Z-origin.Z;
|
|
// Even a preferred legal flight costs at least this much.
|
|
// Skip expensive terrain sampling when it cannot improve the
|
|
// route already found; this preserves the full graph search.
|
|
float lowerBound=40.0f+std::sqrt(dx*dx+dy*dy+dz*dz)*0.4f;
|
|
if(old!=distance.end() && old->second<=current.Cost+lowerBound)continue;
|
|
if ((!RescueNodeAllowed(target) || !sAdventureAreaMgr->AllowsNode(player,target))) continue;
|
|
if (blockedBridgeKeys && blockedBridgeKeys->count("flight:"+origin.Key+"->"+target.Key)) continue;
|
|
float cost=flightCost(origin,target.MapId,target.X,target.Y,target.Z);
|
|
if (!std::isfinite(cost)) continue;
|
|
float score=current.Cost+cost;
|
|
if(old!=distance.end()&&old->second<=score)continue;
|
|
distance[key]=score;states[key]=next;persistentHops[key]=persistentHops[currentKey]+1;
|
|
previous[key]={currentKey,current.Value.Node,"free_flight",0,false};
|
|
open.push({score,next});
|
|
}
|
|
}
|
|
|
|
// A local bridge is deliberately impossible immediately after another
|
|
// local bridge. This prevents MMAP from silently becoming a long-range
|
|
// planner while still allowing unlimited bridges over a long journey
|
|
// when real transport/ground edges lie between them.
|
|
if (!repairRequired || current.Value.LastWasBridge || portalArrival)
|
|
continue;
|
|
|
|
ClientlessTravelNode const& from = _nodes.at(current.Value.Node);
|
|
|
|
// C8.18: if a future node is disconnected, attach it to a recorded
|
|
// road at an MMAP-reachable waypoint. Only remaining portions of
|
|
// stored, directed ground edges are admitted; no off-graph shortcut.
|
|
// A portal source may not be treated as an ordinary intermediate node.
|
|
bool const futurePortalSource =
|
|
from.Key.rfind("portal_at_", 0) == 0 &&
|
|
from.Key.size() >= 7 &&
|
|
from.Key.compare(from.Key.size() - 7, 7, "_source") == 0;
|
|
if (!futurePortalSource && persistentHops[currentKey] > 0 &&
|
|
!(blockedBridgeKeys &&
|
|
blockedBridgeKeys->count(MakeLocalBridgeKey(from.Key, "virtual_ground_entry"))))
|
|
{
|
|
// Check a bounded set of the geographically closest waypoint
|
|
// candidates first to avoid O(all-points * Detour) work.
|
|
struct ForwardCandidate
|
|
{
|
|
float DistanceSq;
|
|
ClientlessTravelEdge const* Edge;
|
|
uint32 Index;
|
|
ClientlessTravelEdgePoint const* Point;
|
|
uint32 PointCount;
|
|
};
|
|
std::vector<ForwardCandidate> candidates;
|
|
constexpr float ForwardEntryRadius = 200.0f;
|
|
for (auto const& edgePair : _edges)
|
|
{
|
|
for (ClientlessTravelEdge const& edge : edgePair.second)
|
|
{
|
|
if (edge.Type != "ground" || !edge.DatabaseId ||
|
|
(blockedEdgeIds && blockedEdgeIds->count(edge.DatabaseId)))
|
|
continue;
|
|
if(_nodes.at(edge.FromNode).MapId!=from.MapId || _nodes.at(edge.ToNode).MapId!=from.MapId)continue;
|
|
if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge))) continue;
|
|
auto pts = _edgePoints.find(edge.DatabaseId);
|
|
if (pts == _edgePoints.end() || pts->second.size() < 3)
|
|
continue;
|
|
for (uint32 i = 0; i < pts->second.size(); ++i)
|
|
{
|
|
if ((!edge.Reverse && i + 1 >= pts->second.size()) ||
|
|
(edge.Reverse && i == 0))
|
|
continue;
|
|
ClientlessTravelEdgePoint const& pt = pts->second[i];
|
|
if (pt.MapId != from.MapId)
|
|
continue;
|
|
float dx = pt.X - from.X, dy = pt.Y - from.Y, dz = pt.Z - from.Z;
|
|
float dsq = dx * dx + dy * dy + dz * dz;
|
|
if (dsq <= ForwardEntryRadius * ForwardEntryRadius)
|
|
candidates.push_back({ dsq, &edge, i, &pt, uint32(pts->second.size()) });
|
|
}
|
|
}
|
|
}
|
|
std::sort(candidates.begin(), candidates.end(),
|
|
[](ForwardCandidate const& a, ForwardCandidate const& b)
|
|
{ return a.DistanceSq < b.DistanceSq; });
|
|
// One candidate per directed recorded edge is enough for a first
|
|
// robust repair pass; use the closest admissible point on it.
|
|
std::unordered_set<std::string> testedDirectedEdges;
|
|
uint32 localTests = 0;
|
|
for (ForwardCandidate const& entry : candidates)
|
|
{
|
|
if (localTests >= 24)
|
|
break;
|
|
std::string directionKey = std::to_string(entry.Edge->DatabaseId) +
|
|
(entry.Edge->Reverse ? "R" : "F");
|
|
if (!testedDirectedEdges.insert(directionKey).second)
|
|
continue;
|
|
++localTests;
|
|
++forwardEntryTests;
|
|
VcorePositionPathResult const probe = analyze(from, entry.Point->MapId,
|
|
entry.Point->X, entry.Point->Y, entry.Point->Z);
|
|
if (!probe.IsComplete() || probe.FinalDistance > 1.5f ||
|
|
probe.PathLength > 500.0f)
|
|
continue;
|
|
|
|
if (entry.Edge->ToNode == from.Key)
|
|
continue;
|
|
uint32 const entryIndex = entry.Edge->Reverse
|
|
? entry.PointCount - 1 - entry.Index : entry.Index;
|
|
if (locationGoal)
|
|
{
|
|
auto exitGroup = exitsByFrom.find(entry.Edge->FromNode);
|
|
if (exitGroup != exitsByFrom.end())
|
|
for (ExitCandidate const& exit : exitGroup->second)
|
|
{
|
|
if (exit.EdgeId != entry.Edge->DatabaseId ||
|
|
exit.Reverse != entry.Edge->Reverse || exit.EndIndex <= entryIndex)
|
|
continue;
|
|
float const score = current.Cost + probe.PathLength + exit.LocalCost +
|
|
roadSectionCost(exit.EdgeId, exit.Reverse, entryIndex,
|
|
exit.EndIndex, entry.Edge->Cost);
|
|
if (score >= bestGoalCost)
|
|
continue;
|
|
bestGoalCost = score;
|
|
bestGoalState = currentKey;
|
|
bestExit = { true, false, exit, true, entryIndex, *entry.Point };
|
|
}
|
|
}
|
|
State next{ entry.Edge->ToNode, false };
|
|
std::string const nextKey = stateKey(next);
|
|
uint32 const begin = entry.Edge->Reverse
|
|
? entry.PointCount - 1 - entry.Index : entry.Index;
|
|
float const cost = current.Cost + probe.PathLength +
|
|
roadSectionCost(entry.Edge->DatabaseId, entry.Edge->Reverse,
|
|
begin, entry.PointCount - 1, entry.Edge->Cost);
|
|
auto old = distance.find(nextKey);
|
|
if (old != distance.end() && cost >= old->second)
|
|
continue;
|
|
distance[nextKey] = cost;
|
|
states[nextKey] = next;
|
|
persistentHops[nextKey] = persistentHops[currentKey] + 1;
|
|
Previous link;
|
|
link.StateKey = currentKey;
|
|
link.Node = current.Value.Node;
|
|
link.EdgeType = "ground";
|
|
link.EdgeDatabaseId = entry.Edge->DatabaseId;
|
|
link.EdgeReverse = entry.Edge->Reverse;
|
|
link.VirtualGroundEntry = true;
|
|
link.GroundStartIndex = entry.Edge->Reverse
|
|
? entry.PointCount - 1 - entry.Index : entry.Index;
|
|
link.EntryPoint = *entry.Point;
|
|
previous[nextKey] = link;
|
|
open.push({ cost, next });
|
|
++forwardEntryAccepted;
|
|
TC_LOG_INFO("server.adventurebot.navigation",
|
|
"C818_FORWARD_ENTRY_PLAN from='%s' viaEdge=%u point=%u reverse=%u to='%s' path=%.1f",
|
|
from.Key.c_str(), entry.Edge->DatabaseId, entry.Index,
|
|
entry.Edge->Reverse ? 1u : 0u, entry.Edge->ToNode.c_str(), probe.PathLength);
|
|
}
|
|
}
|
|
|
|
// Fixed graph-node neighborhoods are shared across plans. Keep a cube
|
|
// superset: the original exact sphere/path/block-list checks still run.
|
|
uint32 radiusBits=0;std::memcpy(&radiusBits,&bridgeRadius,sizeof(float));
|
|
std::string neighborhoodKey=from.Key+":"+std::to_string(radiusBits);
|
|
auto neighborhood=_bridgeNeighborhoods.find(neighborhoodKey);
|
|
if(neighborhood==_bridgeNeighborhoods.end())
|
|
{
|
|
if(_bridgeNeighborhoods.size()>=4096)_bridgeNeighborhoods.clear();
|
|
std::vector<std::string> candidates;
|
|
for(auto const& entry:_nodes)
|
|
{
|
|
auto const& to=entry.second;
|
|
if(to.MapId==from.MapId && std::fabs(to.X-from.X)<=bridgeRadius &&
|
|
std::fabs(to.Y-from.Y)<=bridgeRadius && std::fabs(to.Z-from.Z)<=bridgeRadius)candidates.push_back(entry.first);
|
|
}
|
|
neighborhood=_bridgeNeighborhoods.emplace(neighborhoodKey,std::move(candidates)).first;
|
|
}
|
|
for (auto const& key : neighborhood->second)
|
|
{
|
|
ClientlessTravelNode const& to = _nodes.at(key);
|
|
if ((!RescueNodeAllowed(to) || !sAdventureAreaMgr->AllowsNode(player,to))) continue;
|
|
if (to.Key == from.Key || to.MapId != from.MapId)
|
|
continue;
|
|
|
|
float const dx = to.X - from.X;
|
|
float const dy = to.Y - from.Y;
|
|
float const dz = to.Z - from.Z;
|
|
float const direct = std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
if (direct > bridgeRadius)
|
|
continue;
|
|
|
|
std::string const bridgeKey = MakeLocalBridgeKey(from.Key, to.Key);
|
|
if (blockedBridgeKeys && blockedBridgeKeys->count(bridgeKey))
|
|
continue;
|
|
|
|
// C8.18: even in repair mode, a virtual node bridge is not
|
|
// eligible until the future-node -> future-node MMAP is complete.
|
|
// The geometry cap above is a cheap first-stage filter.
|
|
float measuredLength = 0.0f;
|
|
auto cached = validBridgeLengths.find(bridgeKey);
|
|
if (cached != validBridgeLengths.end())
|
|
measuredLength = cached->second;
|
|
else
|
|
{
|
|
if (rejectedBridges.count(bridgeKey))
|
|
continue;
|
|
++bridgeTests;
|
|
VcorePositionPathResult const probe = analyze(from, to.MapId, to.X, to.Y, to.Z);
|
|
if (!probe.IsComplete() || probe.FinalDistance > 1.5f ||
|
|
!std::isfinite(probe.PathLength) || probe.PathLength > bridgeRadius * 2.0f)
|
|
{
|
|
rejectedBridges.insert(bridgeKey);
|
|
++bridgeRejected;
|
|
TC_LOG_DEBUG("server.adventurebot.navigation",
|
|
"C818_BRIDGE_REJECT from='%s' to='%s' direct=%.1f status=%u final=%.1f",
|
|
from.Key.c_str(), to.Key.c_str(), direct,
|
|
unsigned(probe.Result), probe.FinalDistance);
|
|
continue;
|
|
}
|
|
measuredLength = probe.PathLength;
|
|
validBridgeLengths.emplace(bridgeKey, measuredLength);
|
|
++bridgeAccepted;
|
|
}
|
|
|
|
// A bridge must lead back into real TravelGraph infrastructure.
|
|
// The sole exception is the actual requested destination, where a
|
|
// local bridge may be the final leg of the graph journey.
|
|
bool hasPersistentContinuation = to.Key == destinationNode;
|
|
auto continuationItr = _edges.find(to.Key);
|
|
if (!hasPersistentContinuation && continuationItr != _edges.end())
|
|
{
|
|
for (ClientlessTravelEdge const& continuation : continuationItr->second)
|
|
{
|
|
if (blockedEdgeIds && continuation.DatabaseId && blockedEdgeIds->count(continuation.DatabaseId))
|
|
continue;
|
|
hasPersistentContinuation = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!hasPersistentContinuation)
|
|
continue;
|
|
|
|
State next{ to.Key, true };
|
|
std::string const nextKey = stateKey(next);
|
|
|
|
// C8.20: dynamic node-to-node MMAP bridges are ordinary local
|
|
// ground connections, not penalized fallback routes.
|
|
float const candidate = current.Cost + measuredLength;
|
|
auto old = distance.find(nextKey);
|
|
if (old != distance.end() && candidate >= old->second)
|
|
continue;
|
|
|
|
distance[nextKey] = candidate;
|
|
states[nextKey] = next;
|
|
persistentHops[nextKey] = persistentHops[currentKey];
|
|
previous[nextKey] = {
|
|
currentKey, current.Value.Node, "local_bridge", 0, false
|
|
};
|
|
open.push({ candidate, next });
|
|
}
|
|
}
|
|
|
|
if ((locationGoal || flightEnabled) && bestExit.Active && bestExit.RootPartial &&
|
|
!bestGoalState.empty())
|
|
previous.erase(bestGoalState);
|
|
std::string selectedState = bestGoalState;
|
|
if (!selectedState.empty())
|
|
complete = true;
|
|
|
|
if (selectedState.empty())
|
|
{
|
|
ClientlessTravelNode const& destination = destinationItr->second;
|
|
float bestSameMapDistance = std::numeric_limits<float>::infinity();
|
|
uint32 bestPersistentHops = 0;
|
|
float bestCost = std::numeric_limits<float>::infinity();
|
|
|
|
// Prefer any reachable point on the destination map. This gives a
|
|
// meaningful geographic partial route (e.g. Ironforge when Kharanos
|
|
// itself is disconnected). If the destination map was never reached,
|
|
// fall back to the state that made the most progress over real edges.
|
|
for (auto const& pair : states)
|
|
{
|
|
auto distItr = distance.find(pair.first);
|
|
if (distItr == distance.end() || !std::isfinite(distItr->second))
|
|
continue;
|
|
|
|
ClientlessTravelNode const& node = _nodes.at(pair.second.Node);
|
|
// Never terminate a partial journey on an unintended trigger source.
|
|
bool const stopAtPortalSource = node.Key.rfind("portal_at_", 0) == 0 &&
|
|
node.Key.size() >= 7 &&
|
|
node.Key.compare(node.Key.size() - 7, 7, "_source") == 0 &&
|
|
node.Key != destinationNode;
|
|
if (stopAtPortalSource)
|
|
continue;
|
|
if (node.MapId != destination.MapId)
|
|
continue;
|
|
|
|
float const dx = node.X - destination.X;
|
|
float const dy = node.Y - destination.Y;
|
|
float const dz = node.Z - destination.Z;
|
|
float const localDistance = std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
|
|
if (localDistance < bestSameMapDistance ||
|
|
(std::fabs(localDistance - bestSameMapDistance) < 0.5f && distItr->second < bestCost))
|
|
{
|
|
bestSameMapDistance = localDistance;
|
|
bestCost = distItr->second;
|
|
selectedState = pair.first;
|
|
}
|
|
}
|
|
|
|
if (selectedState.empty())
|
|
{
|
|
for (auto const& pair : states)
|
|
{
|
|
auto distItr = distance.find(pair.first);
|
|
if (distItr == distance.end() || !std::isfinite(distItr->second))
|
|
continue;
|
|
|
|
ClientlessTravelNode const& candidateNode = _nodes.at(pair.second.Node);
|
|
if (candidateNode.Key != destinationNode &&
|
|
candidateNode.Key.rfind("portal_at_", 0) == 0 &&
|
|
candidateNode.Key.size() >= 7 &&
|
|
candidateNode.Key.compare(candidateNode.Key.size()-7, 7, "_source") == 0)
|
|
continue;
|
|
uint32 const hops = persistentHops[pair.first];
|
|
if (hops > bestPersistentHops ||
|
|
(hops == bestPersistentHops && distItr->second < bestCost))
|
|
{
|
|
bestPersistentHops = hops;
|
|
bestCost = distItr->second;
|
|
selectedState = pair.first;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (selectedState.empty())
|
|
{
|
|
error = "no adaptive route candidate";
|
|
return false;
|
|
}
|
|
|
|
reachedNode = states.at(selectedState).Node;
|
|
|
|
struct Reconstructed
|
|
{
|
|
State Value;
|
|
std::string IncomingEdgeType;
|
|
uint32 EdgeDatabaseId = 0;
|
|
bool EdgeReverse = false;
|
|
bool VirtualGroundEntry = false;
|
|
uint32 GroundStartIndex = 0;
|
|
ClientlessTravelEdgePoint EntryPoint;
|
|
};
|
|
|
|
std::vector<Reconstructed> reversed;
|
|
std::string cursor = selectedState;
|
|
while (true)
|
|
{
|
|
State const& state = states.at(cursor);
|
|
auto prevItr = previous.find(cursor);
|
|
if (prevItr == previous.end())
|
|
{
|
|
reversed.push_back({ state, "start", 0, false });
|
|
break;
|
|
}
|
|
|
|
reversed.push_back({
|
|
state,
|
|
prevItr->second.EdgeType,
|
|
prevItr->second.EdgeDatabaseId,
|
|
prevItr->second.EdgeReverse,
|
|
prevItr->second.VirtualGroundEntry,
|
|
prevItr->second.GroundStartIndex,
|
|
prevItr->second.EntryPoint
|
|
});
|
|
cursor = prevItr->second.StateKey;
|
|
}
|
|
|
|
std::reverse(reversed.begin(), reversed.end());
|
|
route.reserve(reversed.size() + 1);
|
|
// If the shortest root entered a recorded edge mid-way, precede the actual
|
|
// endpoint with an MMAP-reachable virtual waypoint; do NOT rewrite the DB edge.
|
|
if (!reversed.empty())
|
|
{
|
|
std::string rootKey = stateKey(reversed.front().Value);
|
|
auto root = virtualRoots.find(rootKey);
|
|
if (root != virtualRoots.end() && previous.find(rootKey) == previous.end())
|
|
{
|
|
VirtualEntry const& entry = root->second;
|
|
ClientlessTravelRouteStep initial;
|
|
initial.NodeKey = "virtual_ground_entry";
|
|
initial.NodeType = "road";
|
|
initial.EdgeType = "start";
|
|
initial.MapId = entry.Point.MapId;
|
|
initial.X = entry.Point.X;
|
|
initial.Y = entry.Point.Y;
|
|
initial.Z = entry.Point.Z;
|
|
initial.O = entry.Point.O;
|
|
route.push_back(initial);
|
|
TC_LOG_INFO("server.adventurebot.navigation",
|
|
"VIRTUAL_EDGE_SELECTED edgeDbId=%u pointIndex=%u reverse=%u",
|
|
entry.EdgeId, entry.PointIndex, entry.Reverse ? 1u : 0u);
|
|
}
|
|
}
|
|
|
|
for (Reconstructed const& item : reversed)
|
|
{
|
|
if (item.VirtualGroundEntry)
|
|
{
|
|
ClientlessTravelRouteStep connector;
|
|
connector.NodeKey = "virtual_ground_entry";
|
|
connector.NodeType = "road";
|
|
connector.EdgeType = "local_bridge";
|
|
connector.MapId = item.EntryPoint.MapId;
|
|
connector.X = item.EntryPoint.X;
|
|
connector.Y = item.EntryPoint.Y;
|
|
connector.Z = item.EntryPoint.Z;
|
|
connector.O = item.EntryPoint.O;
|
|
route.push_back(std::move(connector));
|
|
TC_LOG_INFO("server.adventurebot.navigation",
|
|
"C818_FORWARD_ENTRY_SELECTED waypoint=(%.2f %.2f %.2f) edgeDbId=%u offset=%u",
|
|
item.EntryPoint.X, item.EntryPoint.Y, item.EntryPoint.Z,
|
|
item.EdgeDatabaseId, item.GroundStartIndex);
|
|
}
|
|
if(route.empty() && item.IncomingEdgeType=="start" && flightRoots.count(item.Value.Node))
|
|
{
|
|
ClientlessTravelRouteStep departure;departure.NodeKey="flight_departure";departure.NodeType="ground";departure.EdgeType="start";
|
|
departure.MapId=player->GetMapId();departure.X=player->GetPositionX();departure.Y=player->GetPositionY();departure.Z=player->GetPositionZ();
|
|
route.push_back(departure);
|
|
}
|
|
ClientlessTravelNode const& node = _nodes.at(item.Value.Node);
|
|
ClientlessTravelRouteStep step;
|
|
step.NodeKey = node.Key;
|
|
step.NodeType = node.Type;
|
|
step.WorldPointKey = node.WorldPointKey;
|
|
step.EdgeType = item.IncomingEdgeType=="start" && flightRoots.count(item.Value.Node)?"free_flight":item.IncomingEdgeType;
|
|
step.EdgeDatabaseId = item.EdgeDatabaseId;
|
|
step.EdgeReverse = item.EdgeReverse;
|
|
if (item.VirtualGroundEntry)
|
|
step.GroundStartIndex = int32(item.GroundStartIndex);
|
|
if (route.size() == 1 && route.front().NodeKey == "virtual_ground_entry" &&
|
|
item.IncomingEdgeType == "start")
|
|
{
|
|
auto root = virtualRoots.find(stateKey(item.Value));
|
|
if (root != virtualRoots.end())
|
|
{
|
|
step.EdgeType = "ground";
|
|
step.EdgeDatabaseId = root->second.EdgeId;
|
|
step.EdgeReverse = root->second.Reverse;
|
|
step.GroundStartIndex = root->second.Reverse
|
|
? int32(_edgePoints.at(root->second.EdgeId).size() - 1 - root->second.PointIndex)
|
|
: int32(root->second.PointIndex);
|
|
}
|
|
}
|
|
step.MapId = node.MapId;
|
|
step.X = node.X;
|
|
step.Y = node.Y;
|
|
step.Z = node.Z;
|
|
step.O = node.O;
|
|
route.push_back(std::move(step));
|
|
}
|
|
|
|
if ((locationGoal || flightEnabled) && complete && bestExit.Active)
|
|
{
|
|
ExitCandidate const& exit = bestExit.Candidate;
|
|
if (bestExit.RootPartial)
|
|
{
|
|
// The reconstructed root already contains the virtual entry
|
|
// waypoint and the matching road. Stop that SAME edge early.
|
|
if (route.empty() || route.back().EdgeType != "ground" ||
|
|
route.back().EdgeDatabaseId != exit.EdgeId)
|
|
{
|
|
error = "virtual entry/exit reconstruction mismatch";
|
|
route.clear();
|
|
return false;
|
|
}
|
|
route.back().GroundContinuationNode = route.back().NodeKey;
|
|
route.back().GroundEndIndex = int32(exit.EndIndex);
|
|
route.back().NodeKey = "virtual_ground_exit";
|
|
route.back().MapId = exit.Point.MapId;
|
|
route.back().X = exit.Point.X;
|
|
route.back().Y = exit.Point.Y;
|
|
route.back().Z = exit.Point.Z;
|
|
route.back().O = exit.Point.O;
|
|
}
|
|
else
|
|
{
|
|
if (bestExit.ForwardPartial)
|
|
{
|
|
ClientlessTravelRouteStep connector;
|
|
connector.NodeKey = "virtual_ground_entry";
|
|
connector.NodeType = "road";
|
|
connector.EdgeType = "local_bridge";
|
|
connector.MapId = bestExit.EntryPoint.MapId;
|
|
connector.X = bestExit.EntryPoint.X;
|
|
connector.Y = bestExit.EntryPoint.Y;
|
|
connector.Z = bestExit.EntryPoint.Z;
|
|
connector.O = bestExit.EntryPoint.O;
|
|
route.push_back(std::move(connector));
|
|
}
|
|
ClientlessTravelRouteStep end;
|
|
if (bestExit.ForwardPartial)
|
|
end.GroundStartIndex = int32(bestExit.StartIndex);
|
|
end.NodeKey = "virtual_ground_exit";
|
|
end.NodeType = "road";
|
|
end.EdgeType = "ground";
|
|
end.EdgeDatabaseId = exit.EdgeId;
|
|
end.EdgeReverse = exit.Reverse;
|
|
end.GroundEndIndex = int32(exit.EndIndex);
|
|
end.GroundContinuationNode = exit.ToNode;
|
|
end.MapId = exit.Point.MapId;
|
|
end.X = exit.Point.X;
|
|
end.Y = exit.Point.Y;
|
|
end.Z = exit.Point.Z;
|
|
end.O = exit.Point.O;
|
|
route.push_back(std::move(end));
|
|
}
|
|
reachedNode = "virtual_ground_exit";
|
|
TC_LOG_INFO("server.adventurebot.navigation",
|
|
"C820_EXIT_SELECTED edgeDbId=%u reverse=%u endIndex=%u "
|
|
"road=%.1f local=%.1f total=%.1f rootPartial=%u",
|
|
exit.EdgeId, exit.Reverse ? 1u : 0u, exit.EndIndex,
|
|
exit.RoadCost, exit.LocalCost, bestGoalCost,
|
|
bestExit.RootPartial ? 1u : 0u);
|
|
}
|
|
else if (locationGoal && complete)
|
|
TC_LOG_INFO("server.adventurebot.navigation",
|
|
"C820_NODE_FINISH node='%s' fullCost=%.1f",
|
|
reachedNode.c_str(), bestGoalCost);
|
|
|
|
if (complete && (bestExit.Active ? bestExit.Candidate.Flight : bestFinalFlight))
|
|
{
|
|
ClientlessTravelRouteStep flight;
|
|
auto const& target=destinationItr->second;
|
|
flight.NodeKey=locationGoal ? "flight_location" : target.Key;
|
|
flight.NodeType=locationGoal ? "location" : target.Type;
|
|
flight.EdgeType="free_flight";
|
|
flight.MapId=locationGoal ? locationGoal->MapId : target.MapId;
|
|
flight.X=locationGoal ? locationGoal->X : target.X;
|
|
flight.Y=locationGoal ? locationGoal->Y : target.Y;
|
|
flight.Z=locationGoal ? locationGoal->Z : target.Z;
|
|
flight.O=locationGoal ? 0 : target.O;
|
|
route.push_back(std::move(flight));
|
|
reachedNode=locationGoal ? "flight_location" : target.Key;
|
|
}
|
|
|
|
TC_LOG_INFO("server.adventurebot.navigation",
|
|
"C818_PREFLIGHT complete=%u bridgeTests=%u bridgeAccepted=%u bridgeRejected=%u "
|
|
"distanceRejected=%u forwardTests=%u forwardAccepted=%u steps=%u",
|
|
complete ? 1u : 0u, bridgeTests, bridgeAccepted, bridgeRejected,
|
|
bridgeDistanceRejected, forwardEntryTests, forwardEntryAccepted, uint32(route.size()));
|
|
if (!complete)
|
|
error = "destination not fully reachable; returning best-effort partial route";
|
|
|
|
return !route.empty();
|
|
|
|
};
|
|
|
|
bool const firstOk = runSearch(false);
|
|
if (firstOk && complete)
|
|
{
|
|
TC_LOG_INFO("server.adventurebot.navigation", "C819_ROUTE_TIER tier=reachable complete=1");
|
|
return true;
|
|
}
|
|
std::vector<ClientlessTravelRouteStep> firstRoute = route;
|
|
bool const firstComplete = complete;
|
|
std::string firstReached = reachedNode;
|
|
std::string firstError = error;
|
|
bool const secondOk = runSearch(true);
|
|
if (secondOk && complete)
|
|
{
|
|
TC_LOG_INFO("server.adventurebot.navigation", "C819_ROUTE_TIER tier=deferred complete=1");
|
|
return true;
|
|
}
|
|
// If no complete route exists, a selected deferred root is still a useful
|
|
// approach to real graph infrastructure, provided that its execution is safe.
|
|
if (secondOk && !complete && !route.empty() &&
|
|
route.front().EdgeType == "start" &&
|
|
route.front().NodeKey != "virtual_ground_entry" &&
|
|
firstOk && !firstRoute.empty() &&
|
|
route.front().NodeKey != firstRoute.front().NodeKey)
|
|
{
|
|
TC_LOG_INFO("server.adventurebot.navigation",
|
|
"C819_ROUTE_TIER tier=deferred-approach complete=0");
|
|
return true;
|
|
}
|
|
if (firstOk)
|
|
{
|
|
route = std::move(firstRoute);
|
|
complete = firstComplete;
|
|
reachedNode = std::move(firstReached);
|
|
error = std::move(firstError);
|
|
TC_LOG_INFO("server.adventurebot.navigation", "C819_ROUTE_TIER tier=reachable-partial complete=0");
|
|
return true;
|
|
}
|
|
TC_LOG_INFO("server.adventurebot.navigation", "C819_ROUTE_TIER tier=deferred-partial complete=%u", complete ? 1u : 0u);
|
|
return secondOk;
|
|
}
|
|
|
|
bool ClientlessTravelGraphMgr::PlanRouteToPosition(
|
|
Player const* player,
|
|
uint32 destinationMapId,
|
|
float destinationX,
|
|
float destinationY,
|
|
float destinationZ,
|
|
float startRadius,
|
|
float anchorRadius,
|
|
std::vector<ClientlessTravelRouteStep>& route,
|
|
std::string& anchorNode,
|
|
std::string& error,
|
|
std::unordered_set<uint32> const* blockedEdgeIds)
|
|
{
|
|
route.clear();
|
|
anchorNode.clear();
|
|
error.clear();
|
|
|
|
EnsureLoaded();
|
|
|
|
if (!player || !player->IsInWorld())
|
|
{
|
|
error = "player is not in world";
|
|
return false;
|
|
}
|
|
|
|
Position goal;goal.Relocate(destinationX,destinationY,destinationZ);
|
|
if (!sAdventureAreaMgr->AllowsPosition(player,destinationMapId,goal))
|
|
{ error="destination location denied by area catalog"; return false; }
|
|
|
|
if (startRadius <= 0.0f)
|
|
startRadius = 500.0f;
|
|
|
|
if (anchorRadius <= 0.0f)
|
|
anchorRadius = 1000.0f;
|
|
|
|
struct QueueItem
|
|
{
|
|
float Cost;
|
|
std::string Node;
|
|
|
|
bool operator>(QueueItem const& other) const
|
|
{
|
|
return Cost > other.Cost;
|
|
}
|
|
};
|
|
|
|
struct Previous
|
|
{
|
|
std::string Node;
|
|
std::string EdgeType;
|
|
uint32 EdgeDatabaseId = 0;
|
|
bool EdgeReverse = false;
|
|
};
|
|
|
|
std::priority_queue<QueueItem, std::vector<QueueItem>, std::greater<QueueItem>> open;
|
|
std::unordered_map<std::string, float> distance;
|
|
std::unordered_map<std::string, Previous> previous;
|
|
|
|
for (auto const& pair : _nodes)
|
|
distance[pair.first] = std::numeric_limits<float>::infinity();
|
|
|
|
// Multi-source Dijkstra: every graph node close enough to the player's
|
|
// actual position is a possible entry point. Seed cost includes the local
|
|
// distance needed to reach that node, so nearer usable entries are still
|
|
// preferred without letting an isolated node block the route entirely.
|
|
uint32 startCandidateCount = 0;
|
|
for (auto const& pair : _nodes)
|
|
{
|
|
ClientlessTravelNode const& node = pair.second;
|
|
if (node.MapId != player->GetMapId())
|
|
continue;
|
|
if ((!RescueNodeAllowed(node) || !sAdventureAreaMgr->AllowsNode(player,node))) continue;
|
|
|
|
float const dx = node.X - player->GetPositionX();
|
|
float const dy = node.Y - player->GetPositionY();
|
|
float const dz = node.Z - player->GetPositionZ();
|
|
float const localStartDistance = std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
|
|
if (localStartDistance > startRadius)
|
|
continue;
|
|
|
|
distance[node.Key] = localStartDistance;
|
|
open.push({ localStartDistance, node.Key });
|
|
++startCandidateCount;
|
|
}
|
|
|
|
if (startCandidateCount == 0)
|
|
{
|
|
error = "no travel node near current position";
|
|
return false;
|
|
}
|
|
|
|
while (!open.empty())
|
|
{
|
|
QueueItem current = open.top();
|
|
open.pop();
|
|
|
|
if (current.Cost != distance[current.Node])
|
|
continue;
|
|
|
|
auto edgeItr = _edges.find(current.Node);
|
|
if (edgeItr == _edges.end())
|
|
continue;
|
|
|
|
for (ClientlessTravelEdge const& edge : edgeItr->second)
|
|
{
|
|
if ((!RescueEdgeAllowed(edge) || !sAdventureAreaMgr->AllowsEdge(player,edge))) continue;
|
|
if (blockedEdgeIds &&
|
|
edge.DatabaseId &&
|
|
blockedEdgeIds->find(edge.DatabaseId) != blockedEdgeIds->end())
|
|
continue;
|
|
|
|
float const candidate = current.Cost + edge.Cost;
|
|
if (candidate >= distance[edge.ToNode])
|
|
continue;
|
|
|
|
distance[edge.ToNode] = candidate;
|
|
previous[edge.ToNode] = {
|
|
current.Node, edge.Type, edge.DatabaseId, edge.Reverse
|
|
};
|
|
open.push({ candidate, edge.ToNode });
|
|
}
|
|
}
|
|
|
|
// The destination anchor's primary job is to get the bot close to the
|
|
// requested Location. Do not combine macro-route cost and final local
|
|
// distance into one score: that can make the current/start node win simply
|
|
// because reaching it costs zero, leaving MMAP with a very long final leg.
|
|
//
|
|
// Instead choose the reachable node closest to the destination first.
|
|
// Graph cost is only a tie-breaker between equally suitable anchors.
|
|
float bestDestinationDistance = std::numeric_limits<float>::infinity();
|
|
float bestGraphCost = std::numeric_limits<float>::infinity();
|
|
|
|
for (auto const& pair : _nodes)
|
|
{
|
|
ClientlessTravelNode const& node = pair.second;
|
|
if ((!RescueNodeAllowed(node) || !sAdventureAreaMgr->AllowsNode(player,node))) continue;
|
|
if (node.MapId != destinationMapId)
|
|
continue;
|
|
|
|
auto distanceItr = distance.find(node.Key);
|
|
if (distanceItr == distance.end() || !std::isfinite(distanceItr->second))
|
|
continue;
|
|
|
|
float const dx = node.X - destinationX;
|
|
float const dy = node.Y - destinationY;
|
|
float const dz = node.Z - destinationZ;
|
|
float const localDestinationDistance = std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
|
|
if (localDestinationDistance > anchorRadius)
|
|
continue;
|
|
|
|
float const graphCost = distanceItr->second;
|
|
|
|
constexpr float DistanceTieEpsilon = 0.5f;
|
|
bool const closerToDestination =
|
|
localDestinationDistance + DistanceTieEpsilon < bestDestinationDistance;
|
|
|
|
bool const sameDestinationDistance =
|
|
std::fabs(localDestinationDistance - bestDestinationDistance) <= DistanceTieEpsilon;
|
|
|
|
if (!closerToDestination &&
|
|
!(sameDestinationDistance && graphCost < bestGraphCost))
|
|
continue;
|
|
|
|
bestDestinationDistance = localDestinationDistance;
|
|
bestGraphCost = graphCost;
|
|
anchorNode = node.Key;
|
|
}
|
|
|
|
if (anchorNode.empty())
|
|
{
|
|
error = "no reachable travel node near destination";
|
|
return false;
|
|
}
|
|
|
|
// Reconstruct until we reach whichever seeded start candidate actually won.
|
|
// Seed roots deliberately have no Previous entry.
|
|
std::vector<std::string> nodeKeys;
|
|
std::string cursor = anchorNode;
|
|
nodeKeys.push_back(cursor);
|
|
|
|
while (true)
|
|
{
|
|
auto previousItr = previous.find(cursor);
|
|
if (previousItr == previous.end())
|
|
break;
|
|
|
|
cursor = previousItr->second.Node;
|
|
nodeKeys.push_back(cursor);
|
|
}
|
|
|
|
std::reverse(nodeKeys.begin(), nodeKeys.end());
|
|
route.reserve(nodeKeys.size());
|
|
|
|
for (std::size_t i = 0; i < nodeKeys.size(); ++i)
|
|
{
|
|
ClientlessTravelNode const& node = _nodes.at(nodeKeys[i]);
|
|
|
|
ClientlessTravelRouteStep step;
|
|
step.NodeKey = node.Key;
|
|
step.NodeType = node.Type;
|
|
step.WorldPointKey = node.WorldPointKey;
|
|
step.MapId = node.MapId;
|
|
step.X = node.X;
|
|
step.Y = node.Y;
|
|
step.Z = node.Z;
|
|
step.O = node.O;
|
|
|
|
if (i == 0)
|
|
step.EdgeType = "start";
|
|
else
|
|
{
|
|
Previous const& prev = previous.at(nodeKeys[i]);
|
|
step.EdgeType = prev.EdgeType;
|
|
step.EdgeDatabaseId = prev.EdgeDatabaseId;
|
|
step.EdgeReverse = prev.EdgeReverse;
|
|
}
|
|
|
|
route.push_back(std::move(step));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
std::vector<ClientlessTravelEdgePoint> const*
|
|
ClientlessTravelGraphMgr::GetEdgePoints(uint32 edgeDatabaseId) const
|
|
{
|
|
auto itr = _edgePoints.find(edgeDatabaseId);
|
|
return itr != _edgePoints.end() ? &itr->second : nullptr;
|
|
}
|
|
|
|
// Rescue nodes stay visible to administration but are never normal graph roots,
|
|
// anchors, bridge endpoints or shortcuts. Only this bounded planning scope can
|
|
// enter the recorded escape network. Native access policy remains mandatory.
|
|
bool ClientlessTravelGraphMgr::PlanRescueRoute(Player* player,
|
|
std::vector<ClientlessTravelRouteStep>& route, std::string& teleportNode,
|
|
bool forceTeleport, std::unordered_set<uint32> const* blockedEdgeIds)
|
|
{
|
|
route.clear();teleportNode.clear();EnsureLoaded();
|
|
if(!player || !player->IsInWorld())return false;
|
|
struct Scope {bool& Flag;bool Previous;Scope(bool& f):Flag(f),Previous(f){Flag=true;}~Scope(){Flag=Previous;}} scope(_rescuePlanning);
|
|
std::vector<ClientlessTravelNode const*> roots;
|
|
for(auto const& entry:_nodes)
|
|
if(entry.second.Type=="rescue" && entry.second.MapId==player->GetMapId() &&
|
|
sAdventureAreaMgr->AllowsNode(player,entry.second))roots.push_back(&entry.second);
|
|
auto distance=[&](ClientlessTravelNode const* n){return player->GetExactDist(n->X,n->Y,n->Z);};
|
|
std::sort(roots.begin(),roots.end(),[&](auto a,auto b){float da=distance(a),db=distance(b);return da==db?a->Key<b->Key:da<db;});
|
|
std::vector<ClientlessTravelRouteStep> fallback;
|
|
std::string fallbackNode;
|
|
auto stepOf=[](ClientlessTravelNode const& node,ClientlessTravelEdge const* edge)
|
|
{
|
|
ClientlessTravelRouteStep step;step.NodeKey=node.Key;step.NodeType=node.Type;step.WorldPointKey=node.WorldPointKey;
|
|
step.MapId=node.MapId;step.X=node.X;step.Y=node.Y;step.Z=node.Z;step.O=node.O;
|
|
step.EdgeType=edge?edge->Type:"start";step.EdgeDatabaseId=edge?edge->DatabaseId:0;
|
|
step.EdgeReverse=edge && edge->Reverse;return step;
|
|
};
|
|
for(auto root:roots)
|
|
{
|
|
// Follow only explicitly recorded same-map ground edges until the first
|
|
// ordinary node. No virtual bridge or flight may invent an escape edge.
|
|
std::vector<std::string> queue{root->Key};
|
|
std::unordered_map<std::string,ClientlessTravelEdge const*> previous;
|
|
previous[root->Key]=nullptr;std::string exit;
|
|
for(size_t i=0;i<queue.size() && exit.empty();++i)
|
|
{
|
|
auto outgoing=_edges.find(queue[i]);if(outgoing==_edges.end())continue;
|
|
for(auto const& edge:outgoing->second)
|
|
{
|
|
auto const& target=_nodes.at(edge.ToNode);
|
|
auto points=GetEdgePoints(edge.DatabaseId);
|
|
if(edge.Type!="ground" || !points || points->size()<2 || target.MapId!=root->MapId ||
|
|
previous.count(target.Key) || (blockedEdgeIds && blockedEdgeIds->count(edge.DatabaseId)) ||
|
|
!sAdventureAreaMgr->AllowsNode(player,target) || !sAdventureAreaMgr->AllowsEdge(player,edge))continue;
|
|
previous[target.Key]=&edge;
|
|
if(target.Type!="rescue"){exit=target.Key;break;}
|
|
queue.push_back(target.Key);
|
|
}
|
|
}
|
|
if(exit.empty())continue;
|
|
std::vector<ClientlessTravelRouteStep> escape;
|
|
for(std::string key=exit;key!=root->Key;)
|
|
{auto edge=previous.at(key);escape.push_back(stepOf(_nodes.at(key),edge));key=edge->FromNode;}
|
|
std::reverse(escape.begin(),escape.end());
|
|
std::vector<ClientlessTravelRouteStep> approach;bool complete=false;std::string reached,error;
|
|
if(!forceTeleport && PlanAdaptiveRoute(player,root->Key,4000,200,approach,complete,reached,error,blockedEdgeIds) && complete)
|
|
{
|
|
route=std::move(approach);route.insert(route.end(),escape.begin(),escape.end());return true;
|
|
}
|
|
if(fallback.empty())
|
|
{fallback.push_back(stepOf(*root,nullptr));fallback.insert(fallback.end(),escape.begin(),escape.end());fallbackNode=root->Key;}
|
|
}
|
|
// The special transfer permission applies to stranded Deeprun Tram bots
|
|
// only. Other maps may use a reachable escape network, never teleport to it.
|
|
if(player->GetMapId()!=369 || fallback.empty())return false;
|
|
route=std::move(fallback);teleportNode=fallbackNode;return true;
|
|
}
|