using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Control; using LLib; using Microsoft.Extensions.Logging; using Questionable.Controller.NavigationOverrides; using Questionable.Data; using Questionable.External; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; using SmartNav.Data; using SmartNav.Model; using SmartNav.Model.Converter; namespace Questionable.Controller; internal sealed class MovementController : IDisposable { public sealed record DestinationData(EMovementType MovementType, uint? DataId, Vector3 Position, float StopDistance, bool IsFlying, bool CanSprint, float VerticalStopDistance, bool Land, bool UseNavmesh, bool IsSmartNavRouted) { public int NavmeshCalculations { get; set; } public List PartialRoute { get; } = new List(); public LastWaypointData? LastWaypoint { get; set; } } public sealed record LastWaypointData(Vector3 Position) { public long UpdatedAt { get; set; } public double Distance2DAtLastUpdate { get; set; } } public sealed class PathfindingFailedException : Exception { public PathfindingFailedException() { } public PathfindingFailedException(string message) : base(message) { } public PathfindingFailedException(string message, Exception innerException) : base(message, innerException) { } } public const float DefaultVerticalInteractionDistance = 1.95f; private const float FlyLandHeightOffset = 2.6f; private const float FlyLandPathfindTolerance = 5f; private const float FlyLandLastResortTolerance = 10f; private const float RouteEndTolerance = 5f; private const float DefaultArrivalTolerance = 0.25f; private const int LandingSettleTime = 500; private const int LandingTimeout = 10000; private const float FlyingStartYNudge = 0.2f; private const float DragonheadStartYNudge = 1f; private readonly NavmeshIpc _navmeshIpc; private readonly WigglyNavAvailability _wigglyNavAvailability; private readonly IFramework _framework; private readonly IClientState _clientState; private readonly IObjectTable _objectTable; private readonly GameFunctions _gameFunctions; private readonly ICondition _condition; private readonly MovementOverrideController _movementOverrideController; private readonly AetheryteData _aetheryteData; private readonly TerritoryData _territoryData; private readonly Configuration _configuration; private readonly ILogger _logger; private CancellationTokenSource? _cancellationTokenSource; private Task? _pathfindTask; private float _pathfindTolerance = 0.25f; private long _pathfindStartTime; private bool _pathfindTimeoutLogged; private Vector3? _lastKnownPosition; private long _lastPositionUpdateTime; private Vector3? _expectedPosition; private bool _isTrackingPlayerInput; private volatile bool _disposed; public bool IsNavmeshReady { get { try { return _navmeshIpc.IsReady; } catch (IpcNotReadyError) { return false; } } } public bool IsPathRunning { get { try { return _navmeshIpc.IsPathRunning; } catch (IpcNotReadyError) { return false; } } } public bool IsPathfinding { get { Task pathfindTask = _pathfindTask; if (pathfindTask != null) { return !pathfindTask.IsCompleted; } return false; } } public DestinationData? Destination { get; set; } public long? MovementStartedAt { get; private set; } = Environment.TickCount64; public int BuiltNavmeshPercent => _navmeshIpc.GetBuildProgress(); public bool IsNavmeshPathfindInProgress => _navmeshIpc.IsPathfindInProgress; public int NumQueuedPathfindRequests => _navmeshIpc.NumQueuedPathfindRequests; public event EventHandler? PlayerInputDetected; public MovementController(NavmeshIpc navmeshIpc, WigglyNavAvailability wigglyNavAvailability, IFramework framework, IClientState clientState, IObjectTable objectTable, GameFunctions gameFunctions, ICondition condition, MovementOverrideController movementOverrideController, AetheryteData aetheryteData, TerritoryData territoryData, Configuration configuration, ILogger logger) { _navmeshIpc = navmeshIpc; _wigglyNavAvailability = wigglyNavAvailability; _framework = framework; _clientState = clientState; _objectTable = objectTable; _gameFunctions = gameFunctions; _condition = condition; _movementOverrideController = movementOverrideController; _aetheryteData = aetheryteData; _territoryData = territoryData; _configuration = configuration; _logger = logger; _wigglyNavAvailability.CompatibilityChanged += OnWigglyNavCompatibilityChanged; } private void OnWigglyNavCompatibilityChanged() { _framework.RunOnFrameworkThread(delegate { if (!_disposed && _configuration.General.NavigationProvider == Configuration.ENavigationProvider.WigglyNav && (!(Destination == null) || IsPathfinding)) { DestinationData destination = Destination; _navmeshIpc.StopAll(); if (destination != null && IsNavmeshReady) { _logger.LogWarning("WigglyNav compatibility changed with a path running or a pathfind in flight, restarting movement on the new provider"); Restart(destination); } else { _logger.LogWarning("WigglyNav compatibility changed with a path running or a pathfind in flight, stopping movement (no destination to resume, or the new provider's navmesh is not ready)"); Stop(); } } }); } public bool HasBeenMovingForAtLeast(int ms) { long? movementStartedAt = MovementStartedAt; if (movementStartedAt.HasValue) { long valueOrDefault = movementStartedAt.GetValueOrDefault(); return Environment.TickCount64 - valueOrDefault >= ms; } return false; } public unsafe void Update() { DestinationData destination; if (IsPathRunning && _isTrackingPlayerInput) { destination = Destination; if (((object)destination == null || destination.MovementType != EMovementType.Landing) && DetectPlayerInputInterference()) { _logger.LogInformation("Player input detected during automatic movement, raising event to stop automation"); this.PlayerInputDetected?.Invoke(this, EventArgs.Empty); Stop(); return; } } if (_pathfindTask != null && Destination != null) { if (!_pathfindTask.IsCompleted && Environment.TickCount64 - _pathfindStartTime > 30000 && _navmeshIpc.NumQueuedPathfindRequests > 5) { if (!_pathfindTimeoutLogged) { _logger.LogWarning("Pathfinding appears stuck: {QueuedRequests} queued requests, task running for {Duration}ms", _navmeshIpc.NumQueuedPathfindRequests, Environment.TickCount64 - _pathfindStartTime); _pathfindTimeoutLogged = true; } ResetPathfinding(); throw new PathfindingFailedException("Pathfinding computation timed out after 30s"); } if (_pathfindTask.IsCompletedSuccessfully) { _logger.LogDebug("Pathfinding complete, got {Count} points", _pathfindTask.Result.Waypoints.Count); if (_pathfindTask.Result.Waypoints.Count == 0) { if (Destination.NavmeshCalculations == 1) { _logger.LogWarning("Initial pathfinding returned 0 points, attempting to find accessible destination"); Vector3? vector = TryFindAccessibleDestination(Destination.Position, Destination.IsFlying, Destination.Land); if (vector.HasValue && Vector3.Distance(Destination.Position, vector.Value) < 30f) { _logger.LogDebug("Retrying pathfinding with adjusted destination: {AdjustedDestination}", vector.Value.ToString("G", CultureInfo.InvariantCulture)); int navmeshCalculations = Destination.NavmeshCalculations; Restart(Destination with { Position = vector.Value }); if (Destination != null) { Destination.NavmeshCalculations = navmeshCalculations + 1; } return; } if (Destination.IsFlying && Destination.Land) { _logger.LogWarning("Adjusted destination failed, trying tolerance-based pathfinding"); if (!IsNavmeshReady) { _logger.LogWarning("Navmesh not ready for tolerance-based pathfinding"); return; } Vector3 vector2 = _objectTable.LocalPlayer?.Position ?? Vector3.Zero; if (Destination.IsFlying) { Vector3 vector3 = vector2; vector3.Y = vector2.Y + 0.2f; vector2 = vector3; } _pathfindStartTime = Environment.TickCount64; _pathfindTolerance = 10f; _pathfindTask = _navmeshIpc.PathfindWithTolerance(vector2, Destination.Position, Destination.IsFlying, 10f); Destination.NavmeshCalculations++; return; } } ResetPathfinding(); throw new PathfindingFailedException(); } List waypoints = _pathfindTask.Result.Waypoints; List list = waypoints.Slice(1, waypoints.Count - 1); Vector3 p = _objectTable.LocalPlayer?.Position ?? list[0]; if (Destination.IsFlying && Destination.MovementType != EMovementType.Landing && !_condition[ConditionFlag.InFlight] && _condition[ConditionFlag.Mounted] && (IsOnFlightPath(p) || list.Any(IsOnFlightPath))) { ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null); } if (!Destination.IsFlying) { (List, bool) tuple = _movementOverrideController.AdjustPath(list); (list, _) = tuple; if (tuple.Item2 && Destination.NavmeshCalculations < 10) { if (!IsNavmeshReady) { _logger.LogWarning("Navmesh not ready for recalculation"); return; } Destination.NavmeshCalculations++; Destination.PartialRoute.AddRange(list); _logger.LogDebug("Running navmesh recalculation with fudged point ({From} to {To})", list.Last(), Destination.Position); _cancellationTokenSource = new CancellationTokenSource(); _cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(30L)); _pathfindStartTime = Environment.TickCount64; _pathfindTolerance = 0.25f; _pathfindTask = _navmeshIpc.Pathfind(list.Last(), Destination.Position, Destination.IsFlying, _cancellationTokenSource.Token); return; } } list = Destination.PartialRoute.Concat(list).ToList(); if (Destination.IsSmartNavRouted && !Destination.IsFlying) { NavPathfindResult result = _pathfindTask.Result; if ((object)result != null && result.HasPartialInfo && result.IsPartial && result.EndDistance > Destination.StopDistance) { _logger.LogWarning("Provider reports a partial ground route to {Target} (end distance {WireEndDistance:F1}y, {Count} points), treating as unreachable", Destination.Position.ToString("G", CultureInfo.InvariantCulture), _pathfindTask.Result.EndDistance, list.Count); float endDistance = _pathfindTask.Result.EndDistance; ResetPathfinding(); throw new PathfindingFailedException($"Provider reports a partial ground route to target (end distance {endDistance:F1}y)"); } } if (Destination.IsSmartNavRouted && list.Count > 0) { NavPathfindResult result2 = _pathfindTask.Result; List list2 = list; float num = Vector3.Distance(list2[list2.Count - 1], Destination.Position); if (!IsRouteEndAcceptable(num, Destination.StopDistance, Destination.IsFlying, Destination.Land, result2.HasPartialInfo, result2.IsPartial)) { if (result2.HasPartialInfo) { _logger.LogWarning("Computed route ends {Distance:F1}y from target {Target} (stop distance {StopDistance:F1}y), treating as unreachable (provider reports partial: {IsPartial}, end distance {WireEndDistance:F1}y)", num, Destination.Position.ToString("G", CultureInfo.InvariantCulture), Destination.StopDistance, result2.IsPartial, result2.EndDistance); } else { _logger.LogWarning("Computed route ends {Distance:F1}y from target {Target} (stop distance {StopDistance:F1}y), treating as unreachable", num, Destination.Position.ToString("G", CultureInfo.InvariantCulture), Destination.StopDistance); } ResetPathfinding(); throw new PathfindingFailedException($"Computed route ends {num:F1}y from target (stop distance {Destination.StopDistance:F1}y)"); } if (Destination.IsFlying && (object)result2 != null && result2.HasPartialInfo && result2.IsPartial) { if (Destination.Land) { _logger.LogInformation("Fly+land route is partial (EndDistance {WireEndDistance:F1}y): accepting - descend/land/gap-close adjudicates reachability", result2.EndDistance); } else { _logger.LogInformation("Flying route is partial (EndDistance {WireEndDistance:F1}y): accepting - route-end distance heuristic adjudicates reachability", result2.EndDistance); } } } _logger.LogDebug("Navigating via route: [{Route}]", string.Join(" → ", _pathfindTask.Result.Waypoints.Select((Vector3 x) => x.ToString("G", CultureInfo.InvariantCulture)))); _navmeshIpc.MoveTo(list, Destination.IsFlying, _pathfindTolerance); MovementStartedAt = Environment.TickCount64; StartPlayerInputTracking(); ResetPathfinding(); } else if (_pathfindTask.IsCompleted) { _logger.LogWarning("Unable to complete pathfinding task"); ResetPathfinding(); throw new PathfindingFailedException(); } } if (IsPathRunning && Destination != null) { if (_gameFunctions.IsLoadingScreenVisible()) { _logger.LogDebug("Stopping movement, loading screen visible"); Stop(); return; } destination = Destination; if ((object)destination != null && destination.IsFlying && (_condition[ConditionFlag.Swimming] || !_condition[ConditionFlag.Mounted])) { _logger.LogDebug("Flying but {Reason}, restarting as non-flying path...", _condition[ConditionFlag.Swimming] ? "swimming" : "not mounted"); Restart(Destination, false); return; } Vector3 vector4 = _objectTable.LocalPlayer?.Position ?? Vector3.Zero; if (Destination.MovementType == EMovementType.Landing) { if (!_condition[ConditionFlag.InFlight]) { Stop(); } } else if ((vector4 - Destination.Position).Length() < Destination.StopDistance) { if (vector4.Y - Destination.Position.Y <= Destination.VerticalStopDistance) { Stop(); } else if (Destination.DataId.HasValue) { IGameObject gameObject = _gameFunctions.FindObjectByDataId(Destination.DataId.Value); if ((gameObject is ICharacter || gameObject is IEventObj) ? true : false) { if (Math.Abs(vector4.Y - gameObject.Position.Y) < 1.95f) { Stop(); } } else if (gameObject != null && gameObject.ObjectKind == ObjectKind.Aetheryte) { if (AetheryteConverter.IsLargeAetheryte((EAetheryteLocation)Destination.DataId.Value)) { Stop(); } else if (Math.Abs(vector4.Y - gameObject.Position.Y) < 1.95f) { Stop(); } } else { Stop(); } } else { Stop(); } } else { List waypoints2 = _navmeshIpc.GetWaypoints(); Vector3? vector5 = _objectTable.LocalPlayer?.Position; if (vector5.HasValue) { if (RecalculateNavmesh(waypoints2, vector5.Value)) { return; } if (!Destination.IsFlying && !_condition[ConditionFlag.Mounted] && !_gameFunctions.HasStatusPreventingSprint() && Destination.CanSprint) { TriggerSprintIfNeeded(waypoints2, vector5.Value); } } } } destination = Destination; if ((object)destination != null && destination.MovementType == EMovementType.Landing && HasBeenMovingForAtLeast(500) && (!IsPathRunning || HasBeenMovingForAtLeast(10000))) { bool num2 = _condition[ConditionFlag.InFlight]; Stop(); if (num2) { _logger.LogWarning("Descend ended while still airborne, treating as unreachable"); throw new PathfindingFailedException("Descend ended while still airborne"); } } } internal static bool IsRouteEndAcceptable(float endDistance, float stopDistance, bool isFlying, bool land, bool hasPartialInfo, bool isPartial) { if (isFlying && land) { return true; } if (!isFlying && hasPartialInfo && isPartial) { return endDistance <= stopDistance; } return endDistance <= stopDistance + 5f; } private void StartPlayerInputTracking() { IPlayerCharacter localPlayer = _objectTable.LocalPlayer; if (localPlayer != null) { _lastKnownPosition = localPlayer.Position; _expectedPosition = localPlayer.Position; _lastPositionUpdateTime = Environment.TickCount64; _isTrackingPlayerInput = true; } } private bool DetectPlayerInputInterference() { if (!_configuration.General.StopOnPlayerInput) { return false; } if (!_isTrackingPlayerInput || !_lastKnownPosition.HasValue) { return false; } IPlayerCharacter localPlayer = _objectTable.LocalPlayer; if (localPlayer == null) { return false; } Vector3 position = localPlayer.Position; long tickCount = Environment.TickCount64; if (tickCount - _lastPositionUpdateTime < 100) { return false; } List waypoints = _navmeshIpc.GetWaypoints(); if (waypoints.Count > 0) { _expectedPosition = waypoints[0]; } if (_expectedPosition.HasValue) { Vector3 vector = Vector3.Normalize(_expectedPosition.Value - _lastKnownPosition.Value); Vector3 value = position - _lastKnownPosition.Value; if (value.Length() > 0.1f) { Vector3 vector2 = Vector3.Normalize(value); float num = Vector3.Dot(vector, vector2); if (num < 0.7f) { _logger.LogDebug("Player movement detected: alignment={Alignment:F2}, actual={Actual}, expected={Expected}", num, value.ToString("G", CultureInfo.InvariantCulture), vector.ToString("G", CultureInfo.InvariantCulture)); return true; } } } _lastKnownPosition = position; _lastPositionUpdateTime = tickCount; return false; } private void StopPlayerInputTracking() { _isTrackingPlayerInput = false; _lastKnownPosition = null; _expectedPosition = null; _lastPositionUpdateTime = 0L; } private void Restart(DestinationData destination, bool? fly = null) { Stop(); Vector3 vector = destination.Position; if ((object)destination != null && destination.IsFlying && destination.Land) { Vector3 vector2 = vector; vector2.Y = vector.Y - 2.6f; vector = vector2; } bool fly2 = fly ?? destination.IsFlying; if (destination.UseNavmesh) { NavigateTo(destination.MovementType, destination.DataId, vector, fly2, destination.CanSprint, destination.StopDistance, destination.VerticalStopDistance, destination.Land, destination.IsSmartNavRouted); return; } EMovementType movementType = destination.MovementType; uint? dataId = destination.DataId; int num = 1; List list = new List(num); CollectionsMarshal.SetCount(list, num); CollectionsMarshal.AsSpan(list)[0] = vector; NavigateTo(movementType, dataId, list, fly2, destination.CanSprint, destination.StopDistance, destination.VerticalStopDistance, destination.Land); } private bool IsOnFlightPath(Vector3 p) { Vector3? pointOnFloor = _navmeshIpc.GetPointOnFloor(p, unlandable: true); if (pointOnFloor.HasValue) { return Math.Abs(pointOnFloor.Value.Y - p.Y) > 0.5f; } return false; } [MemberNotNull("Destination")] private void PrepareNavigation(EMovementType type, uint? dataId, Vector3 to, bool fly, bool sprint, float? stopDistance, float verticalStopDistance, bool land, bool useNavmesh, bool isSmartNavRouted) { ResetPathfinding(); if (InputManager.IsAutoRunning()) { _logger.LogDebug("Turning off auto-move"); ChatHelper.SendCommand("/automove off"); } Destination = new DestinationData(type, dataId, to, stopDistance ?? 2.8f, fly, sprint, verticalStopDistance, land, useNavmesh, isSmartNavRouted); MovementStartedAt = null; } public void NavigateTo(EMovementType type, uint? dataId, Vector3 to, bool fly, bool sprint, float? stopDistance = null, float? verticalStopDistance = null, bool land = false, bool isSmartNavRouted = false) { if (!IsNavmeshReady) { _logger.LogWarning("Navmesh not ready, cannot start navigation to {Position}", to.ToString("G", CultureInfo.InvariantCulture)); return; } fly |= _condition[ConditionFlag.Diving]; if (fly && land) { Vector3 vector = to; vector.Y = to.Y + 2.6f; to = vector; } PrepareNavigation(type, dataId, to, fly, sprint, stopDistance, verticalStopDistance ?? 1.95f, land, useNavmesh: true, isSmartNavRouted); _logger.LogDebug("Pathfinding to {Destination}", Destination); Destination.NavmeshCalculations++; Vector3 vector2 = _objectTable.LocalPlayer?.Position ?? Vector3.Zero; if (fly && _aetheryteData.CalculateDistance(vector2, _clientState.TerritoryType, EAetheryteLocation.CoerthasCentralHighlandsCampDragonhead) < 11f) { Vector3 vector = vector2; vector.Y = vector2.Y + 1f; vector2 = vector; _logger.LogDebug("Using modified start position for flying pathfinding: {StartPosition}", vector2.ToString("G", CultureInfo.InvariantCulture)); } else if (fly) { Vector3 vector = vector2; vector.Y = vector2.Y + 0.2f; vector2 = vector; } _pathfindStartTime = Environment.TickCount64; if (fly && land) { _logger.LogDebug("Using tolerance-based pathfinding for landing (tolerance: {Tolerance})", 5f); _pathfindTolerance = 5f; _pathfindTask = _navmeshIpc.PathfindWithTolerance(vector2, to, fly, 5f); } else { _cancellationTokenSource = new CancellationTokenSource(); _cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(30L)); _pathfindTolerance = 0.25f; _pathfindTask = _navmeshIpc.Pathfind(vector2, to, fly, _cancellationTokenSource.Token); } } public void NavigateTo(EMovementType type, uint? dataId, List to, bool fly, bool sprint, float? stopDistance, float? verticalStopDistance = null, bool land = false) { if (!IsNavmeshReady) { _logger.LogWarning("Navmesh not ready, cannot start navigation to {Position}", to.Last().ToString("G", CultureInfo.InvariantCulture)); return; } fly |= _condition[ConditionFlag.Diving]; if (fly && land && to.Count > 0) { int index = to.Count - 1; Vector3 value = to[to.Count - 1]; value.Y = to[to.Count - 1].Y + 2.6f; to[index] = value; } PrepareNavigation(type, dataId, to.Last(), fly, sprint, stopDistance, verticalStopDistance ?? 1.95f, land, useNavmesh: false, isSmartNavRouted: false); _logger.LogDebug("Moving to {Destination}", Destination); _navmeshIpc.MoveTo(to, fly, 0.25f); MovementStartedAt = Environment.TickCount64; StartPlayerInputTracking(); } public void ResetPathfinding() { if (_cancellationTokenSource != null) { try { _cancellationTokenSource.Cancel(); } catch (ObjectDisposedException) { } _cancellationTokenSource.Dispose(); _cancellationTokenSource = null; } _pathfindTask = null; _pathfindTimeoutLogged = false; } private Vector3? TryFindAccessibleDestination(Vector3 target, bool flying, bool landing) { if (!IsNavmeshReady) { _logger.LogWarning("Navmesh not ready, cannot find accessible destination"); return null; } float[] array = ((!(flying && landing)) ? ((!flying) ? new float[3] { 1f, 3f, 5f } : new float[3] { 2f, 5f, 10f }) : new float[3] { 5f, 10f, 15f }); float[] array2 = ((!flying) ? new float[3] { 1f, 2f, 3f } : new float[3] { 3f, 5f, 10f }); for (int i = 0; i < array.Length; i++) { float num = array[i]; float num2 = array2[Math.Min(i, array2.Length - 1)]; Vector3? vector = _navmeshIpc.FindNearestReachableMeshPoint(target, num, num2); if (vector.HasValue) { float num3 = Vector3.Distance(target, vector.Value); if (num3 <= num * 1.5f) { if (i > 0) { _logger.LogDebug("Adjusted destination from {Original} to {Adjusted} (distance: {Distance:F2}, extent: {ExtentXZ}/{ExtentY})", target.ToString("G", CultureInfo.InvariantCulture), vector.Value.ToString("G", CultureInfo.InvariantCulture), num3, num, num2); } return vector.Value; } } Vector3? pointOnFloor = _navmeshIpc.GetPointOnFloor(target, flying, num); if (!pointOnFloor.HasValue) { continue; } float num4 = Vector3.Distance(target, pointOnFloor.Value); if (num4 <= num * 1.5f) { if (i > 0) { _logger.LogDebug("Adjusted destination via floor point from {Original} to {Adjusted} (distance: {Distance:F2}, extent: {ExtentXZ})", target.ToString("G", CultureInfo.InvariantCulture), pointOnFloor.Value.ToString("G", CultureInfo.InvariantCulture), num4, num); } return pointOnFloor.Value; } } _logger.LogWarning("Could not find accessible mesh point near {Target} within max extent {MaxExtent}", target.ToString("G", CultureInfo.InvariantCulture), array[^1]); return null; } private unsafe bool RecalculateNavmesh(List navPoints, Vector3 start) { if (Destination == null) { throw new InvalidOperationException("Destination is null"); } if (!HasBeenMovingForAtLeast(5000)) { return false; } Vector3 vector = navPoints.FirstOrDefault(); if (vector == default(Vector3)) { return false; } float num = Vector2.Distance(new Vector2(start.X, start.Z), new Vector2(vector.X, vector.Z)); if (Destination.LastWaypoint == null || (Destination.LastWaypoint.Position - vector).Length() > 0.1f) { Destination.LastWaypoint = new LastWaypointData(vector) { Distance2DAtLastUpdate = num, UpdatedAt = Environment.TickCount64 }; return false; } if (Environment.TickCount64 - Destination.LastWaypoint.UpdatedAt > 500) { if (Math.Abs((double)num - Destination.LastWaypoint.Distance2DAtLastUpdate) < 0.5) { int navmeshCalculations = Destination.NavmeshCalculations; EStuckRecoveryMode stuckRecoveryMode = _configuration.Navigation.StuckRecoveryMode; int recoveryCap = GetRecoveryCap(stuckRecoveryMode); if (navmeshCalculations >= recoveryCap) { throw new PathfindingFailedException($"Stuck recovery exhausted after {navmeshCalculations} attempts (mode: {stuckRecoveryMode})"); } if (stuckRecoveryMode == EStuckRecoveryMode.Passive) { _logger.LogDebug("Stuck detected but recovery mode is Passive, skipping (n = {Calculations})", navmeshCalculations); Destination.NavmeshCalculations = navmeshCalculations + 1; return true; } switch (navmeshCalculations) { case 1: case 7: _logger.LogWarning("Jumping to try and resolve navmesh problem (n = {Calculations}) at {Position}", navmeshCalculations, Destination.Position.ToString("G", CultureInfo.InvariantCulture)); ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null); Destination.LastWaypoint.UpdatedAt = Environment.TickCount64; break; case 5: _logger.LogWarning("Reloading navmesh (n = {Calculations}) at {Position}", navmeshCalculations, Destination.Position.ToString("G", CultureInfo.InvariantCulture)); _navmeshIpc.Reload(); Destination.LastWaypoint.UpdatedAt = Environment.TickCount64; break; case 6: if (!IsNavmeshReady) { _logger.LogWarning("Navmesh not ready after reload (n = {Calculations})", navmeshCalculations); return false; } _logger.LogDebug("Navmesh ready after reload, restarting navigation (n = {Calculations})", navmeshCalculations); Restart(Destination); break; case 8: _logger.LogWarning("Rebuilding navmesh (n = {Calculations}) at {Position}", navmeshCalculations, Destination.Position.ToString("G", CultureInfo.InvariantCulture)); _navmeshIpc.Rebuild(); Destination.LastWaypoint.UpdatedAt = Environment.TickCount64; break; case 9: if (!IsNavmeshReady) { _logger.LogWarning("Navmesh not ready after rebuild (n = {Calculations})", navmeshCalculations); return false; } _logger.LogDebug("Navmesh ready after rebuild, restarting navigation (n = {Calculations})", navmeshCalculations); Restart(Destination); break; default: if (!IsNavmeshReady) { _logger.LogWarning("Navmesh not ready for recalculation (n = {Calculations})", navmeshCalculations); return false; } _logger.LogWarning("Recalculating navmesh (n = {Calculations}) at {Position}", navmeshCalculations, Destination.Position.ToString("G", CultureInfo.InvariantCulture)); Restart(Destination); break; } Destination.NavmeshCalculations = navmeshCalculations + 1; return true; } Destination.LastWaypoint.Distance2DAtLastUpdate = num; Destination.LastWaypoint.UpdatedAt = Environment.TickCount64; return false; } return false; } private static int GetRecoveryCap(EStuckRecoveryMode mode) { return mode switch { EStuckRecoveryMode.Aggressive => 10, EStuckRecoveryMode.Conservative => 5, EStuckRecoveryMode.Passive => 2, _ => 10, }; } private unsafe void TriggerSprintIfNeeded(IEnumerable navPoints, Vector3 start) { float num = 0f; foreach (Vector3 navPoint in navPoints) { num += (start - navPoint).Length(); start = navPoint; } float num2 = _configuration.Navigation.SprintDistanceOpenWorld; if (!_gameFunctions.HasStatus(EStatus.Jog) && !_territoryData.CanUseMount(_clientState.TerritoryType)) { num2 = _configuration.Navigation.SprintDistanceTown; } if (num > num2 && ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 4u, 3758096384uL, checkRecastActive: true, checkCastingActive: true, null) == 0) { _logger.LogDebug("Triggering Sprint"); ActionManager.Instance()->UseAction(ActionType.GeneralAction, 4u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null); } } public void Stop() { StopPlayerInputTracking(); _navmeshIpc.Stop(); ResetPathfinding(); Destination = null; if (InputManager.IsAutoRunning()) { _logger.LogDebug("Turning off auto-move [stop]"); ChatHelper.SendCommand("/automove off"); } } public void Dispose() { _disposed = true; _wigglyNavAvailability.CompatibilityChanged -= OnWigglyNavCompatibilityChanged; Stop(); } }