diff --git a/.vcore_c819_v2_manifest.json b/.vcore_c819_v2_manifest.json new file mode 100644 index 0000000000..1422aa2940 --- /dev/null +++ b/.vcore_c819_v2_manifest.json @@ -0,0 +1,12 @@ +{ + "original": { + "graph": "5d632279d7e25748f3bd8e7ffe954482821334bef12088aad885e6a3ef79c6ac", + "executor": "016cdfb51387ab58d3e15a36b91e8fd69381b66962edd62214e4c54868340468", + "header": "61ccbca3b666016a465d8a9a9049dfa5420177e66aa073fdfe927d749ad2b53b" + }, + "patched": { + "graph": "a9da61755df58666cfa2c6deb7f5d886760036936bc51b51b91ad4ef0ff76ecd", + "executor": "cbb79474640192c5c853174e219deade3694231060d90163362385db2fd6fb6c", + "header": "64bd460621aa63c3cf7e341173249a5b945091e955e2673523478b0762e89dde" + } +} \ No newline at end of file diff --git a/src/server/game/World/ClientlessActionExecutorMgr.cpp b/src/server/game/World/ClientlessActionExecutorMgr.cpp index d56b2706a2..59ca741dab 100644 --- a/src/server/game/World/ClientlessActionExecutorMgr.cpp +++ b/src/server/game/World/ClientlessActionExecutorMgr.cpp @@ -12,6 +12,7 @@ #include "Opcodes.h" #include "Player.h" #include "MotionMaster.h" +#include "PathGenerator.h" // VCORE_C819_DEFERRED_START_NODES_V2 #include #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 = diff --git a/src/server/game/World/ClientlessActionExecutorMgr.h b/src/server/game/World/ClientlessActionExecutorMgr.h index eb95e5b9ab..4c119689cf 100644 --- a/src/server/game/World/ClientlessActionExecutorMgr.h +++ b/src/server/game/World/ClientlessActionExecutorMgr.h @@ -52,6 +52,8 @@ private: std::vector Route; std::size_t RouteStep = 0; + bool DeferredStartActive = false; // VCORE_C819_DEFERRED_START_NODES_V2 + uint32 DeferredStartAttempts = 0; std::vector EdgePath; diff --git a/src/server/game/World/ClientlessTravelGraphMgr.cpp b/src/server/game/World/ClientlessTravelGraphMgr.cpp index a9e74527c7..2b1346e35e 100644 --- a/src/server/game/World/ClientlessTravelGraphMgr.cpp +++ b/src/server/game/World/ClientlessTravelGraphMgr.cpp @@ -389,6 +389,10 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( std::unordered_set const* blockedEdgeIds, std::unordered_set 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), 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 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(