From 0b41ab391f49910b3ed7e731ac112f0f90b99361 Mon Sep 17 00:00:00 2001 From: bash Date: Sun, 31 May 2026 18:38:29 +0200 Subject: [PATCH] refactor(Core/Movement): Close walking-path divergences from reference (A-E) --- src/Ai/Base/Actions/MovementActions.cpp | 60 ++++++++++++++++++++----- src/Ai/Base/Actions/MovementActions.h | 10 ++++- src/Mgr/Travel/TravelMgr.cpp | 21 +++++++++ src/Mgr/Travel/TravelMgr.h | 3 ++ src/Mgr/Travel/TravelNode.cpp | 16 +++++++ src/Mgr/Travel/TravelNode.h | 8 ++++ 6 files changed, 105 insertions(+), 13 deletions(-) diff --git a/src/Ai/Base/Actions/MovementActions.cpp b/src/Ai/Base/Actions/MovementActions.cpp index 61c6e140c..47ca2b48f 100644 --- a/src/Ai/Base/Actions/MovementActions.cpp +++ b/src/Ai/Base/Actions/MovementActions.cpp @@ -31,7 +31,6 @@ #include "Position.h" #include "PositionValue.h" #include "Random.h" -#include "RandomPlayerbotMgr.h" #include "ServerFacade.h" #include "SharedDefines.h" #include "SpellAuraEffects.h" @@ -436,8 +435,11 @@ bool MovementAction::ReachCombatTo(Unit* target, float distance) // Combat callers pass ignoreEnemyTargets=true so ClipPath doesn't // halt the chase at an intermediate hostile when funnelling through // MoveTo2 — the chase target itself is the enemy we want to reach. - bool moved = MoveTo(target->GetMapId(), endPos.x, endPos.y, endPos.z, false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false, /*ignoreEnemyTargets*/true); + // react=true skips the end-of-dispatch WaitForReach so the bot keeps + // re-evaluating mid-chase instead of waiting for the spline to play + // out (which would suspend combat reactions for seconds at a time). + bool moved = MoveTo(target->GetMapId(), endPos.x, endPos.y, endPos.z, /*idle*/false, /*react*/true, false, false, + MovementPriority::MOVEMENT_COMBAT, /*lessDelay*/true, false, /*ignoreEnemyTargets*/true); // Only emit on a successful new commit — combat ticks call this // many times per second and MoveTo internally suppresses while a // prior spline is still playing. Emitting before the suppression @@ -2898,7 +2900,7 @@ bool MovementAction::BoardTransport(Transport* transport) } bool MovementAction::MoveTo2(WorldPosition endPos, - bool idle, [[maybe_unused]] bool react, + bool idle, bool react, [[maybe_unused]] bool noPath, bool ignoreEnemyTargets, MovementPriority priority, @@ -2918,6 +2920,25 @@ bool MovementAction::MoveTo2(WorldPosition endPos, WorldPosition botPos(bot); LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement"); + // Detailed-move throttle: if this bot is in low-activity mode + // (random/background) and a teleport cooldown is still in effect + // from a prior dispatch, postpone re-evaluation until the cooldown + // expires instead of re-resolving the path every tick. + bool const detailedMove = botAI->AllowActivity(DETAILED_MOVE_ACTIVITY, true); + if (!detailedMove && lastMove.nextTeleport) + { + time_t const now = time(nullptr); + if (lastMove.nextTeleport > now) + { + botAI->SetNextCheckDelay((uint32)((lastMove.nextTeleport - now) * 1000)); + return true; + } + } + else + { + lastMove.nextTeleport = 0; + } + // Short-stop: at destination — stop and clear the cached path. float const totalDistance = botPos.distance(endPos); if (totalDistance < sPlayerbotAIConfig.targetPosRecalcDistance) @@ -2976,6 +2997,10 @@ bool MovementAction::MoveTo2(WorldPosition endPos, if (path.empty()) return false; + // If destination is on land, snap any underwater waypoints to the + // water surface so the bot swims along the top instead of diving. + path.surfaceSnapWaypoints(endPos); + // Telemetry: show the path's actual tail coords vs bot + dest so we // can see whether the resolved path is heading toward the right place. if (botAI->HasStrategy("debug move", BOT_STATE_NON_COMBAT)) @@ -3001,7 +3026,7 @@ bool MovementAction::MoveTo2(WorldPosition endPos, botAI->DoSpecificAction("check mount state", Event(), true); bool const dispatched = - DispatchMovement(path, endPos, "walk", priority, lessDelay); + DispatchMovement(path, endPos, "walk", priority, lessDelay, react); if (dispatched && !idle) ClearIdleState(); @@ -3013,7 +3038,8 @@ bool MovementAction::DispatchMovement(TravelPath path, WorldPosition dest, char const* label, MovementPriority priority, - bool lessDelay) + bool lessDelay, + bool react) { // Build the PointsArray from the TravelPath. Done here (not at the // caller) so DispatchMovement can be invoked with a TravelPath @@ -3033,11 +3059,11 @@ bool MovementAction::DispatchMovement(TravelPath path, for (size_t i = 1; i < points.size(); ++i) totalDist += (points[i] - points[i - 1]).length(); - // Skip cosmetic walking for random bots with no nearby player — - // teleport to the path tail and schedule a cooldown instead. - // (Reference equivalent: long-distance teleport in MoveTo2 gated - // on !detailedMove && !HasPlayerNearby. Our gate is IsRandomBot.) - if (sRandomPlayerbotMgr.IsRandomBot(bot)) + // Skip cosmetic walking for low-activity bots with no nearby + // player — teleport to the path tail and schedule a cooldown + // instead. Matches the reference's MoveTo2 gate + // (`!detailedMove && !HasPlayerNearby`). + if (!botAI->AllowActivity(DETAILED_MOVE_ACTIVITY, true)) { WorldPosition tail(dest.GetMapId(), last.x, last.y, last.z); time_t now = time(nullptr); @@ -3072,7 +3098,11 @@ bool MovementAction::DispatchMovement(TravelPath path, } } - bool const generatePath = !bot->IsFlying() && !bot->isSwimming(); + // Reference: also gates on !IsInWater && !IsUnderWater so a bot + // wading through shallow water (no SWIMMING movement flag yet) + // doesn't trigger engine pathfinding mid-dispatch. + bool const generatePath = !bot->IsFlying() && !bot->isSwimming() && + !bot->IsInWater() && !bot->IsUnderWater(); // Pre-dispatch normalization: clear looping emote, stand, interrupt // non-melee cast. Reference does this at MoveTo2 level before @@ -3132,5 +3162,11 @@ bool MovementAction::DispatchMovement(TravelPath path, lastMove.Set(bot->GetMapId(), last.x, last.y, last.z, bot->GetOrientation(), (uint32)duration, priority); + // Reference: DispatchMovement ends with WaitForReach(size) to block + // the AI loop while the spline plays. Combat callers (react=true) + // opt out so they can keep re-evaluating mid-chase. + if (!react) + WaitForReach(points); + return true; } diff --git a/src/Ai/Base/Actions/MovementActions.h b/src/Ai/Base/Actions/MovementActions.h index 594d636cf..10e4f2813 100644 --- a/src/Ai/Base/Actions/MovementActions.h +++ b/src/Ai/Base/Actions/MovementActions.h @@ -69,6 +69,10 @@ protected: // MoveTo(mapId,...) delegates here unless an intentional bypass // (exact_waypoint / disableMoveSplinePath / flying / swimming / // backwards) routes the move straight to DoMovePoint. + // `react=true` opts the move out of the end-of-dispatch + // WaitForReach AI-loop block — combat callers should set this so the + // bot can keep re-evaluating mid-chase. Default false matches the + // reference's MoveTo2 default. bool MoveTo2(WorldPosition endPos, bool idle = false, bool react = false, bool noPath = false, bool ignoreEnemyTargets = false, @@ -91,11 +95,15 @@ protected: // re-evaluation for the full move duration. Until combat dispatch is // restructured to bypass MoveTo2, the WaitForReach is deliberately // omitted. + // `react=true` skips the end-of-dispatch WaitForReach so the AI + // loop isn't blocked while the spline plays — combat callers use + // this to keep re-evaluating mid-chase. bool DispatchMovement(TravelPath path, WorldPosition dest, char const* label, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, - bool lessDelay = false); + bool lessDelay = false, + bool react = false); bool MoveTo(WorldObject* target, float distance = 0.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL); bool MoveNear(WorldObject* target, float distance = sPlayerbotAIConfig.contactDistance, diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp index e482fc70f..f4c609998 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -274,6 +274,27 @@ bool WorldPosition::isUnderWater() : false; }; +bool WorldPosition::setAtWaterSurface() +{ + if (!isInWater() && !isUnderWater()) + return false; + Map* map = getMap(); + if (!map) + return false; + // Returns the water level when liquid is present; falls back to + // ground level otherwise. Our isInWater/isUnderWater preconditions + // ensure liquid exists, so the +0.5y nudge lands the point on top + // of the surface (matches the reference's surface snap). + float const level = map->GetWaterOrGroundLevel(PHASEMASK_NORMAL, + GetPositionX(), + GetPositionY(), + GetPositionZ()); + if (level <= INVALID_HEIGHT) + return false; + setZ(level + 0.5f); + return true; +} + bool WorldPosition::IsValid() { return !(GetMapId() == MAPID_INVALID && GetPositionX() == 0 && GetPositionY() == 0 && GetPositionZ() == 0); diff --git a/src/Mgr/Travel/TravelMgr.h b/src/Mgr/Travel/TravelMgr.h index 1ca8502f0..c580eaab7 100644 --- a/src/Mgr/Travel/TravelMgr.h +++ b/src/Mgr/Travel/TravelMgr.h @@ -138,6 +138,9 @@ public: bool isOverworld(); bool isInWater(); bool isUnderWater(); + // Snap Z to the water surface (level + 0.5y). Returns false if the + // point isn't in/under water or the water level can't be sampled. + bool setAtWaterSurface(); bool IsValid(); WorldPosition relPoint(WorldPosition* center); diff --git a/src/Mgr/Travel/TravelNode.cpp b/src/Mgr/Travel/TravelNode.cpp index f6207e579..7cec2b604 100644 --- a/src/Mgr/Travel/TravelNode.cpp +++ b/src/Mgr/Travel/TravelNode.cpp @@ -908,6 +908,22 @@ void TravelPath::ClipPath(PlayerbotAI* ai, Unit* mover, bool ignoreEnemyTargets) fullPath.erase(std::next(endP), fullPath.end()); } +void TravelPath::surfaceSnapWaypoints(WorldPosition endPos) +{ + if (fullPath.empty()) + return; + // Same map + dest is on land. If dest is itself underwater the bot + // wants to dive; leave waypoints alone. + if (fullPath.front().point.GetMapId() != endPos.GetMapId() || + endPos.isUnderWater()) + return; + for (auto& p : fullPath) + { + if (p.point.isUnderWater()) + p.point.setAtWaterSurface(); + } +} + 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 132cf3828..ab9ac8555 100644 --- a/src/Mgr/Travel/TravelNode.h +++ b/src/Mgr/Travel/TravelNode.h @@ -486,6 +486,14 @@ public: bool makeShortCut(WorldPosition startPos, float maxDist, Unit* bot = nullptr); + // For each waypoint that's in/under water, snap its Z to the water + // surface. No-op when destination is itself underwater (caller wants + // the bot to dive) or path's front map differs from dest map. + // Mirrors the reference's underwater→surface snap so bots swim + // along the top of shallow water on land-bound paths instead of + // diving and air-walking the seafloor. + void surfaceSnapWaypoints(WorldPosition endPos); + // Trim the path up to (and optionally including) the given point. // Returns true if the point was found. Used by upcoming special- // movement detection to advance the path past a portal/transport/