From 49be0f279a56d4c080d5095c906007dfd9978ab0 Mon Sep 17 00:00:00 2001 From: bash Date: Sat, 9 May 2026 13:19:01 +0200 Subject: [PATCH] fix(Core/Travel): Port cmangos path-cheating guards to BuildPath and runtime refine --- src/Ai/Base/Actions/MovementActions.cpp | 9 ++++++ src/Mgr/Travel/TravelNode.cpp | 41 +++++++++++++++++++++++++ src/Mgr/Travel/TravelNode.h | 13 ++++++++ 3 files changed, 63 insertions(+) diff --git a/src/Ai/Base/Actions/MovementActions.cpp b/src/Ai/Base/Actions/MovementActions.cpp index 453b3ffc6..fb60493b4 100644 --- a/src/Ai/Base/Actions/MovementActions.cpp +++ b/src/Ai/Base/Actions/MovementActions.cpp @@ -3237,6 +3237,15 @@ bool MovementAction::RefineWalkPoints(std::vector& walkPoints) return false; } + // Reject "pathfinder cheating" — same checks the offline gen + // applies to BuildPath. Catches cached segments where the + // live navmesh still produces a near-vertical hop or a + // 2-point straight line through geometry. + if (TravelPath::IsPathCheating(segPath, aPos.distance(bPos))) + { + return false; + } + // First segment: include its start point so the spline // begins from the original A. Later segments: skip the first // point — it duplicates the previous segment's tail. diff --git a/src/Mgr/Travel/TravelNode.cpp b/src/Mgr/Travel/TravelNode.cpp index 1386a9259..df77344ac 100644 --- a/src/Mgr/Travel/TravelNode.cpp +++ b/src/Mgr/Travel/TravelNode.cpp @@ -226,6 +226,14 @@ TravelNodePath* TravelNode::BuildPath(TravelNode* endNode, Unit* bot, bool postP bool canPath = endPos->isPathTo(path); // Check if we reached our destination. + // Reject "pathfinder cheating" — too-short or too-steep results + // that mmap accepts but a player can't actually walk. Without this, + // the segment gets cached + saved to playerbots_travelnode_path + // and dispatched at runtime as straight-line spline through whatever + // mountain/cliff sat between A and B (cmangos parity). + if (canPath && TravelPath::IsPathCheating(path, getPosition()->distance(endNode->getPosition()))) + canPath = false; + if (!canPath && endNode->hasLinkTo(this)) // Unable to find a path? See if the reverse is possible. { TravelNodePath backNodePath = *endNode->getPathTo(this); @@ -678,6 +686,39 @@ void TravelNode::print([[maybe_unused]] bool printFailed) } // Attempts to move ahead of the path. +bool TravelPath::IsPathCheating(std::vector const& path, float endpointDistance) +{ + if (path.empty()) + return false; + + // Guard 1: 2-point path for >5y is navmesh "gave up" — straight + // line through whatever's between A and B. + if (path.size() == 2 && endpointDistance > 5.0f) + return true; + + // Guard 2: steep slope at start or end suggests the pathfinder + // hopped through a near-vertical step. >10y drop with >2:1 slope + // is too steep to walk. + if (path.size() > 2) + { + WorldPosition const& a = path.front(); + WorldPosition const& b = path[1]; + float vDist = std::fabs(a.GetPositionZ() - b.GetPositionZ()); + float hDist = a.GetExactDist2d(b.GetPositionX(), b.GetPositionY()); + if (vDist > 10.0f && (hDist == 0.0f || vDist / hDist > 2.0f)) + return true; + + WorldPosition const& c = path.back(); + WorldPosition const& d = path[path.size() - 2]; + float vDist2 = std::fabs(c.GetPositionZ() - d.GetPositionZ()); + float hDist2 = c.GetExactDist2d(d.GetPositionX(), d.GetPositionY()); + if (vDist2 > 10.0f && (hDist2 == 0.0f || vDist2 / hDist2 > 2.0f)) + return true; + } + + return false; +} + bool TravelPath::makeShortCut(WorldPosition startPos, float maxDist, Unit* bot) { if (GetPath().empty()) diff --git a/src/Mgr/Travel/TravelNode.h b/src/Mgr/Travel/TravelNode.h index 86e08137e..780898a51 100644 --- a/src/Mgr/Travel/TravelNode.h +++ b/src/Mgr/Travel/TravelNode.h @@ -493,6 +493,19 @@ public: bool makeShortCut(WorldPosition startPos, float maxDist, Unit* bot = nullptr); + // Detect "pathfinder cheating" — paths that PathGenerator accepts + // but a player can't actually walk: + // * a 2-point path for an endpoint distance > 5y means navmesh + // gave up and returned the straight A->B line. + // * a vertical drop > 10y combined with a slope steeper than + // 2:1 at either start or end means the pathfinder hopped + // through a near-vertical step the navmesh permits but a + // player wouldn't survive. + // cmangos applies the same two checks in TravelNode::buildPath + // before caching a node-to-node segment. + static bool IsPathCheating(std::vector const& path, + float endpointDistance); + std::ostringstream const print(); private: