Implement clientless transports, Lua reload and same-map portals

Add native transport recording and execution with grid lifecycle handling, clientless teleport acknowledgements, and regression tests. Support targeted same-map portal imports and preserve disabled taxi nodes. Document live transport and portal verification and the remaining portal inventory.
This commit is contained in:
Nico committed 2026-10-26 11:27:56 +01:00
1 parent 01a9ba4b0b
commit 1164a72ea3
40 files changed
+4533 -163

No files matched your search

@@ -1,4 +1,5 @@
#include "ClientlessTravelGraphMgr.h"
#include "ClientlessTransportMgr.h"
#include "DatabaseEnv.h"
#include "Log.h"
@@ -100,6 +101,13 @@ bool ClientlessTravelGraphMgr::Reload()
if (edge.Cost <= 0.0f)
edge.Cost = 1.0f;
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;
@@ -428,11 +436,13 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
{
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");
return state.Node + (state.LastWasBridge ? "\x1f" "B" : "\x1f" "R") + state.EntryKey;
};
struct QueueItem
@@ -473,6 +483,32 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
};
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();
};
uint32 startCount = 0;
for (auto const& pair : _nodes)
{
@@ -628,14 +664,17 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
continue;
}
State state{ candidate.Edge.ToNode, false };
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;
float remainingFraction = candidate.Edge.Reverse
? float(candidate.Index) / float(candidate.Count - 1)
: float(candidate.Count - candidate.Index - 1) / float(candidate.Count - 1);
float newCost = (alreadyThere ? 0.0f : nav.PathLength) + candidate.Edge.Cost * remainingFraction;
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;
@@ -690,10 +729,9 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
player->GetInstanceId(), a, b, NAV_GROUND);
};
// VCORE_C820_VIRTUAL_EXIT_NODES: preflight a bounded, geographical
// shortlist of directed recorded-road exits. Candidate generation does
// not mutate nodes, edges or stored path points. Costs use travelled
// arclength, not a flat per-bridge penalty or waypoint index fraction.
// 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;
@@ -705,12 +743,13 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
float DistanceSq = 0.0f;
ClientlessTravelEdgePoint Point;
};
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;
uint32 exitTests = 0, exitAccepted = 0;
if (locationGoal)
{
float const radius = std::min(300.0f, locationGoal->AnchorRadius);
float const radius = locationAccessRadius;
std::unordered_set<std::string> scannedDirected;
for (auto const& edgePair : _edges)
{
@@ -740,6 +779,8 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
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;
@@ -753,7 +794,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
dy = point.Y - locationGoal->Y;
dz = point.Z - locationGoal->Z;
float const dsq = dx * dx + dy * dy + dz * dz;
if (dsq > radius * radius)
if (dsq > radius * radius || dsq >= nearest.DistanceSq)
continue;
std::string const exitKey = "exit:" + std::to_string(edge.DatabaseId) +
":" + std::to_string(i) + (edge.Reverse ? "R" : "F");
@@ -767,74 +808,30 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
candidate.RoadCost = edge.Cost * (travelled / totalLength);
candidate.DistanceSq = dsq;
candidate.Point = point;
rawExitCandidates.push_back(std::move(candidate));
candidate.LocalCost = std::sqrt(dsq);
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; });
// Bound Detour preflight work, not the number of ground edges.
constexpr uint32 MaxExitPreflights = 96;
for (ExitCandidate candidate : rawExitCandidates)
{
if (exitTests >= MaxExitPreflights)
break;
++exitTests;
Position fromPosition, goalPosition;
fromPosition.Relocate(candidate.Point.X, candidate.Point.Y,
candidate.Point.Z, candidate.Point.O);
goalPosition.Relocate(locationGoal->X, locationGoal->Y,
locationGoal->Z, 0.0f);
if (candidate.DistanceSq <= 2.25f)
candidate.LocalCost = 0.0f;
else
{
VcorePositionPathResult const probe = PathGenerator::AnalyzePositions(
locationGoal->MapId, locationGoal->MapId,
player->GetInstanceId(), fromPosition, goalPosition, NAV_GROUND);
if (!probe.IsComplete() || probe.FinalDistance > 1.5f ||
!std::isfinite(probe.PathLength) || probe.PathLength > 500.0f)
continue;
candidate.LocalCost = probe.PathLength;
}
exitsByFrom[candidate.FromNode].push_back(std::move(candidate));
++exitAccepted;
}
for (ExitCandidate const& candidate : rawExitCandidates)
exitsByFrom[candidate.FromNode].push_back(candidate);
TC_LOG_INFO("server.adventurebot.navigation",
"C820_EXIT_PREFLIGHT candidates=%u tested=%u accepted=%u radius=%.1f",
uint32(rawExitCandidates.size()), exitTests, exitAccepted, radius);
"C821_EXIT_CANDIDATES directedEdges=%u radius=%.1f localCost=air-distance validation=on-arrival",
uint32(rawExitCandidates.size()), radius);
}
// 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();
};
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;
@@ -915,23 +912,15 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
float dy = candidateNode.Y - locationGoal->Y;
float dz = candidateNode.Z - locationGoal->Z;
float const direct = std::sqrt(dx*dx + dy*dy + dz*dz);
if (direct <= locationGoal->AnchorRadius)
// Use the same local access limit for nodes and road exits.
// Otherwise a distant node can bypass a closer usable exit.
if (direct <= locationAccessRadius)
{
Position a, b;
a.Relocate(candidateNode.X, candidateNode.Y, candidateNode.Z, candidateNode.O);
b.Relocate(locationGoal->X, locationGoal->Y, locationGoal->Z, 0.0f);
float finalCost = std::numeric_limits<float>::infinity();
if (direct <= 1.5f)
finalCost = 0.0f;
else
{
VcorePositionPathResult const probe = PathGenerator::AnalyzePositions(
candidateNode.MapId, candidateNode.MapId,
player->GetInstanceId(), a, b, NAV_GROUND);
if (probe.IsComplete() && probe.FinalDistance <= 1.5f &&
std::isfinite(probe.PathLength) && probe.PathLength <= 500.0f)
finalCost = probe.PathLength;
}
// 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() : direct;
if (current.Cost + finalCost < bestGoalCost)
{
bestGoalCost = current.Cost + finalCost;
@@ -1081,13 +1070,34 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
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);
float remainingFraction = entry.Edge->Reverse
? float(entry.Index) / float(entry.PointCount - 1)
: float(entry.PointCount - entry.Index - 1) / float(entry.PointCount - 1);
float const cost = current.Cost + 1000.0f + probe.PathLength +
entry.Edge->Cost * remainingFraction;
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;
@@ -1418,7 +1428,22 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
}
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";