1
0
Fork 0
forked from aly/qstbak
qstbak/Questionable/Questionable.Controller/MovementController.cs
2026-08-17 20:29:32 +10:00

891 lines
30 KiB
C#

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<Vector3> PartialRoute { get; } = new List<Vector3>();
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<MovementController> _logger;
private CancellationTokenSource? _cancellationTokenSource;
private Task<NavPathfindResult>? _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<NavPathfindResult> 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<MovementController> 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<Vector3> waypoints = _pathfindTask.Result.Waypoints;
List<Vector3> 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<Vector3>, 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<Vector3> 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<Vector3> 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<Vector3> 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<Vector3> list = new List<Vector3>(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<Vector3> 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<Vector3> 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<Vector3> 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();
}
}