diff --git a/src/server/game/World/ClientlessActionExecutorMgr.cpp b/src/server/game/World/ClientlessActionExecutorMgr.cpp index 8a36e6d226..d56b2706a2 100644 --- a/src/server/game/World/ClientlessActionExecutorMgr.cpp +++ b/src/server/game/World/ClientlessActionExecutorMgr.cpp @@ -19,7 +19,9 @@ #include "WorldSession.h" #include +#include #include +#include #include #include #include @@ -1745,6 +1747,50 @@ void ClientlessActionExecutorMgr::UpdateTravelToNode( { ClientlessTravelEdgePoint const& destination = record.EdgePath.back(); + // VCORE_C816_PORTAL_MOUNT_GUARD: when a long recorded-road spline starts + // in a no-mount zone, reconsider riding after the player has moved. + // Never attempt another mount while already mounted, and never touch + // portal/taxi/local steps. A per-character clock limits failed attempts. + if (player && player->IsInWorld() && player->IsSplineEnabled() && !player->IsMounted()) + { + using Clock = std::chrono::steady_clock; + static std::unordered_map nextMountCheck; + Clock::time_point const now = Clock::now(); + auto const check = nextMountCheck.find(characterGuid); + if (check == nextMountCheck.end() || now >= check->second) + { + nextMountCheck[characterGuid] = now + std::chrono::seconds(6); + // Use the remaining recorded route, not merely the straight-line + // distance to its endpoint (winding roads could look short). + float remainingRoad = 0.0f; + float x = player->GetPositionX(), y = player->GetPositionY(), z = player->GetPositionZ(); + std::size_t nearIndex = 0; + float nearestSq = std::numeric_limits::infinity(); + for (std::size_t i = 0; i < record.EdgePath.size(); ++i) + { + auto const& p = record.EdgePath[i]; + float const dx = p.X - x, dy = p.Y - y, dz = p.Z - z; + float const dsq = dx*dx + dy*dy + dz*dz; + if (dsq < nearestSq) { nearestSq = dsq; nearIndex = i; } + } + for (std::size_t i = nearIndex + 1; i < record.EdgePath.size(); ++i) + { + auto const& a = record.EdgePath[i-1]; + auto const& b = record.EdgePath[i]; + float const dx = b.X-a.X, dy=b.Y-a.Y, dz=b.Z-a.Z; + remainingRoad += std::sqrt(dx*dx + dy*dy + dz*dz); + } + if (remainingRoad >= 50.0f) + { + sClientlessLuaEngine->PrepareGroundMount(player, remainingRoad, + "recorded-ground-recheck"); + TC_LOG_INFO("server.adventurebot.navigation", + "GROUND_MOUNT_RECHECK guid=%u remaining=%.1f mounted=%u", + characterGuid, remainingRoad, player->IsMounted() ? 1u : 0u); + } + } + } + if (!player || player->GetMapId() != destination.MapId) { record.Status.State = ClientlessActionExecutionState::Failed; diff --git a/src/server/game/World/ClientlessTravelGraphMgr.cpp b/src/server/game/World/ClientlessTravelGraphMgr.cpp index 3a8b05f382..cdba556f21 100644 --- a/src/server/game/World/ClientlessTravelGraphMgr.cpp +++ b/src/server/game/World/ClientlessTravelGraphMgr.cpp @@ -641,12 +641,24 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( break; } + // 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 (portalArrival && edge.Type != "portal") + continue; if (blockedEdgeIds && edge.DatabaseId && blockedEdgeIds->count(edge.DatabaseId)) continue; @@ -673,7 +685,7 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( // 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 (current.Value.LastWasBridge) + if (current.Value.LastWasBridge || portalArrival) continue; ClientlessTravelNode const& from = _nodes.at(current.Value.Node); @@ -754,6 +766,13 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( 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; @@ -779,6 +798,12 @@ bool ClientlessTravelGraphMgr::PlanAdaptiveRoute( 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))