Patch for the portalnode approach and selection
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-10-19 23:15:11 +02:00
1 parent b37a0ddcc4
commit 56da0d60cf
4 files changed
+222 -8

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
{
"original": {
"graph": "5d632279d7e25748f3bd8e7ffe954482821334bef12088aad885e6a3ef79c6ac",
"executor": "016cdfb51387ab58d3e15a36b91e8fd69381b66962edd62214e4c54868340468",
"header": "61ccbca3b666016a465d8a9a9049dfa5420177e66aa073fdfe927d749ad2b53b"
},
"patched": {
"graph": "a9da61755df58666cfa2c6deb7f5d886760036936bc51b51b91ad4ef0ff76ecd",
"executor": "cbb79474640192c5c853174e219deade3694231060d90163362385db2fd6fb6c",
"header": "64bd460621aa63c3cf7e341173249a5b945091e955e2673523478b0762e89dde"
}
}
@@ -12,6 +12,7 @@
#include "Opcodes.h"
#include "Player.h"
#include "MotionMaster.h"
#include "PathGenerator.h" // VCORE_C819_DEFERRED_START_NODES_V2
#include <algorithm>
#include "Position.h"
@@ -190,6 +191,8 @@ bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction c
record.PlannedAnchorRadius = 1000.0f;
record.PlannedDestinationNode.clear();
record.FailedBridgeKeys.clear();
record.DeferredStartActive = false; // VCORE_C819_DEFERRED_START_NODES_V2
record.DeferredStartAttempts = 0;
record.RouteComplete = true;
record.HardDestination = false;
record.LocalBridgeRadius = 300.0f;
@@ -1279,6 +1282,95 @@ bool ClientlessActionExecutorMgr::StartRouteStep(
bool const portalSourceApproach =
step.NodeKey.rfind("portal_at_", 0) == 0 &&
step.NodeKey.find("_source") != std::string::npos;
// VCORE_C819_DEFERRED_START_NODES_V2: a deferred START root is only a
// graph candidate. Move along its safe MMAP portion before entering the graph.
// A partial-path endpoint is never considered successful arrival at the node.
if (step.EdgeType == "start" && record.RouteStep == 0)
{
// If Arthoran is already physically inside the source trigger, the
// graph start is reached even if the navmesh ends before its center.
if (portalSourceApproach)
{
uint32 triggerId = 0;
if (ParsePortalSourceNodeKey(step.NodeKey, triggerId))
{
AreaTriggerEntry const* trigger = sAreaTriggerStore.LookupEntry(triggerId);
if (trigger && player->GetMapId() == uint32(trigger->ContinentID) &&
player->IsInAreaTriggerRadius(trigger))
{
TC_LOG_INFO("server.adventurebot.navigation",
"C819_START_PORTAL_READY node='%s' trigger=%u",
step.NodeKey.c_str(), triggerId);
++record.RouteStep;
return StartRouteStep(player, characterGuid, record);
}
}
}
ClientlessPathDiagnostics entryNav;
sClientlessTravelMgr->AnalyzePath(player, destination, entryNav);
bool const partial = entryNav.Calculated && !entryNav.Complete &&
(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;
// All safe deferred roots execute only the MMAP portion. For portals,
// the real trigger-radius check below decides whether this was enough.
// No off-mesh straight-line movement or artificial teleport is added.
if (partial)
{
constexpr uint32 MaxDeferredApproaches = 4;
if (record.DeferredStartAttempts >= MaxDeferredApproaches)
{
record.FailedBridgeKeys.insert("start:" + step.NodeKey);
return RecoverTravel(player, characterGuid, record, 0,
"deferred-start-attempts-exhausted");
}
PathGenerator entryPath(player);
if (entryPath.CalculatePath(step.X, step.Y, step.Z) &&
(uint32(entryPath.GetPathType()) & uint32(PATHFIND_INCOMPLETE)) != 0 &&
(uint32(entryPath.GetPathType()) & uint32(PATHFIND_NOPATH)) == 0 &&
entryPath.GetPath().size() >= 2)
{
auto const& endpoint = entryPath.GetPath().back();
Position safeEnd;
safeEnd.Relocate(endpoint.x, endpoint.y, endpoint.z, player->GetOrientation());
float const dx = endpoint.x - player->GetPositionX();
float const dy = endpoint.y - player->GetPositionY();
float const dz = endpoint.z - player->GetPositionZ();
float const progress = std::sqrt(dx * dx + dy * dy + dz * dz);
float const tx = endpoint.x - step.X;
float const ty = endpoint.y - step.Y;
float const tz = endpoint.z - step.Z;
float const remaining = std::sqrt(tx * tx + ty * ty + tz * tz);
// Prevent a second path query from silently giving us a different,
// non-progressing or unsafe endpoint. StartTravel validates MMAP again.
if (progress >= 2.0f && remaining + 1.0f < entryNav.DirectDistance &&
std::fabs(remaining - entryNav.FinalDistance) <= 2.0f &&
sClientlessTravelMgr->StartTravel(player, safeEnd, true, false))
{
ClientlessTravelStatus const* started =
sClientlessTravelMgr->GetStatus(characterGuid);
if (started && started->State != ClientlessTravelState::PathRejected)
{
record.DeferredStartActive = true;
++record.DeferredStartAttempts;
TC_LOG_INFO("server.adventurebot.navigation",
"C819_START_APPROACH node='%s' segment=%.2f remaining=%.2f attempt=%u",
step.NodeKey.c_str(), progress, remaining,
record.DeferredStartAttempts);
return true;
}
}
}
record.FailedBridgeKeys.insert("start:" + step.NodeKey);
return RecoverTravel(player, characterGuid, record, 0,
"deferred-start-no-progress");
}
}
if (!sClientlessTravelMgr->StartTravel(player, destination, !portalSourceApproach,
portalSourceApproach))
{
@@ -1499,9 +1591,10 @@ bool ClientlessActionExecutorMgr::RecoverTravel(
return false;
}
if (!bridgeFailure)
if (!bridgeFailure && reason != "deferred-start-endpoint-replan")
++record.RecoveryCount;
sClientlessTravelMgr->CancelTravel(characterGuid);
record.DeferredStartActive = false; // VCORE_C819_DEFERRED_START_NODES_V2
record.EdgePath.clear();
record.TaxiActive = false;
record.TaxiPathId = 0;
@@ -1850,6 +1943,50 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode(
if (travel->State == ClientlessTravelState::Moving)
return;
// VCORE_C819_DEFERRED_START_NODES_V2: local endpoint != graph arrival.
if (record.DeferredStartActive)
{
record.DeferredStartActive = false;
if (travel->State != ClientlessTravelState::Arrived)
{
RecoverTravel(player, characterGuid, record, 0,
"deferred-start-movement-failed");
return;
}
if (record.RouteStep >= record.Route.size())
{
record.Status.State = ClientlessActionExecutionState::Failed;
record.Status.Detail = "deferred start route step disappeared";
record.Active = false;
return;
}
ClientlessTravelRouteStep const& start = record.Route[record.RouteStep];
TC_LOG_INFO("server.adventurebot.navigation",
"C819_START_ENDPOINT guid=%u node='%s' pos=(%.3f %.3f %.3f)",
characterGuid, start.NodeKey.c_str(),
player->GetPositionX(), player->GetPositionY(), player->GetPositionZ());
uint32 triggerId = 0;
if (ParsePortalSourceNodeKey(start.NodeKey, triggerId))
{
AreaTriggerEntry const* trigger = sAreaTriggerStore.LookupEntry(triggerId);
if (trigger && player->GetMapId() == uint32(trigger->ContinentID) &&
player->IsInAreaTriggerRadius(trigger))
{
// Only the real in-radius check authorizes the portal edge.
TC_LOG_INFO("server.adventurebot.navigation",
"C819_START_PORTAL_READY node='%s' trigger=%u",
start.NodeKey.c_str(), triggerId);
++record.RouteStep;
StartRouteStep(player, characterGuid, record);
return;
}
}
// Real replan from the new physical position, never EDGE_COMPLETE.
RecoverTravel(player, characterGuid, record, 0,
"deferred-start-endpoint-replan");
return;
}
if (travel->State != ClientlessTravelState::Arrived)
{
uint32 const failedEdgeId =
@@ -52,6 +52,8 @@ private:
std::vector<ClientlessTravelRouteStep> Route;
std::size_t RouteStep = 0;
bool DeferredStartActive = false; // VCORE_C819_DEFERRED_START_NODES_V2
uint32 DeferredStartAttempts = 0;
std::vector<ClientlessTravelEdgePoint> EdgePath;
@@ -389,6 +389,10 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
std::unordered_set<uint32> const* blockedEdgeIds,
std::unordered_set<std::string> const* blockedBridgeKeys)
{
// 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();
@@ -491,16 +495,33 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
entryTarget.Relocate(node.X, node.Y, node.Z, node.O);
ClientlessPathDiagnostics entryNav;
sClientlessTravelMgr->AnalyzePath(const_cast<Player*>(player), entryTarget, entryNav);
if (!entryNav.Calculated || !entryNav.Complete || entryNav.FinalDistance > 1.5f ||
entryNav.MaxSegmentLength > 8.0f || entryNav.MaxZDelta > 5.0f ||
entryNav.PathLength > 500.0f)
bool const reachable = entryNav.Calculated && entryNav.Complete &&
entryNav.FinalDistance <= 1.5f && entryNav.MaxSegmentLength <= 8.0f &&
entryNav.MaxZDelta <= 5.0f && entryNav.PathLength <= 500.0f;
// 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))
{
TC_LOG_INFO("server.adventurebot.navigation",
"START_NODE_REJECT node='%s' distance=%.2f reason='%s'",
node.Key.c_str(), localDistance, entryNav.Reason.c_str());
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);
@@ -511,7 +532,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
distance[key] = startCost;
states[key] = state;
persistentHops[key] = 0;
open.push({ localDistance, state });
open.push({ startCost, state });
++startCount;
}
@@ -1158,6 +1179,48 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute(
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(