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 Microsoft.Extensions.Logging; using Questionable.Controller.NavigationOverrides; using Questionable.Data; using Questionable.External; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Common; using Questionable.Model.Common.Converter; using Questionable.Model.Questing; 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) { public int NavmeshCalculations { get; set; } public List PartialRoute { get; } = new List(); public LastWaypointData? LastWaypoint { get; set; } public bool ShouldRecalculateNavmesh() { return NavmeshCalculations < 10; } } 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 readonly NavmeshIpc _navmeshIpc; private readonly IClientState _clientState; private readonly GameFunctions _gameFunctions; private readonly ChatFunctions _chatFunctions; private readonly ICondition _condition; private readonly MovementOverrideController _movementOverrideController; private readonly AetheryteData _aetheryteData; private readonly ILogger _logger; private CancellationTokenSource? _cancellationTokenSource; private Task>? _pathfindTask; 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 DateTime MovementStartedAt { get; private set; } = DateTime.Now; public int BuiltNavmeshPercent => _navmeshIpc.GetBuildProgress(); public MovementController(NavmeshIpc navmeshIpc, IClientState clientState, GameFunctions gameFunctions, ChatFunctions chatFunctions, ICondition condition, MovementOverrideController movementOverrideController, AetheryteData aetheryteData, ILogger logger) { _navmeshIpc = navmeshIpc; _clientState = clientState; _gameFunctions = gameFunctions; _chatFunctions = chatFunctions; _condition = condition; _movementOverrideController = movementOverrideController; _aetheryteData = aetheryteData; _logger = logger; } public unsafe void Update() { if (_pathfindTask != null && Destination != null) { if (_pathfindTask.IsCompletedSuccessfully) { _logger.LogInformation("Pathfinding complete, got {Count} points", _pathfindTask.Result.Count); if (_pathfindTask.Result.Count == 0) { ResetPathfinding(); throw new PathfindingFailedException(); } List list = _pathfindTask.Result.Skip(1).ToList(); Vector3 p = _clientState.LocalPlayer?.Position ?? list[0]; if (Destination.IsFlying && !_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.ShouldRecalculateNavmesh()) { Destination.NavmeshCalculations++; Destination.PartialRoute.AddRange(list); _logger.LogInformation("Running navmesh recalculation with fudged point ({From} to {To})", list.Last(), Destination.Position); _cancellationTokenSource = new CancellationTokenSource(); _cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(30L)); _pathfindTask = _navmeshIpc.Pathfind(list.Last(), Destination.Position, Destination.IsFlying, _cancellationTokenSource.Token); return; } } list = Destination.PartialRoute.Concat(list).ToList(); _logger.LogInformation("Navigating via route: [{Route}]", string.Join(" → ", _pathfindTask.Result.Select((Vector3 x) => x.ToString("G", CultureInfo.InvariantCulture)))); _navmeshIpc.MoveTo(list, Destination.IsFlying); MovementStartedAt = DateTime.Now; ResetPathfinding(); } else if (_pathfindTask.IsCompleted) { _logger.LogWarning("Unable to complete pathfinding task"); ResetPathfinding(); throw new PathfindingFailedException(); } } if (!IsPathRunning || !(Destination != null)) { return; } if (_gameFunctions.IsLoadingScreenVisible()) { _logger.LogInformation("Stopping movement, loading screen visible"); Stop(); return; } DestinationData destination = Destination; if ((object)destination != null && destination.IsFlying && _condition[ConditionFlag.Swimming]) { _logger.LogInformation("Flying but swimming, restarting as non-flying path..."); Restart(Destination); return; } destination = Destination; if ((object)destination != null && destination.IsFlying && !_condition[ConditionFlag.Mounted]) { _logger.LogInformation("Flying but not mounted, restarting as non-flying path..."); Restart(Destination); return; } Vector3 vector = _clientState.LocalPlayer?.Position ?? Vector3.Zero; if (Destination.MovementType == EMovementType.Landing) { if (!_condition[ConditionFlag.InFlight]) { Stop(); } } else if ((vector - Destination.Position).Length() < Destination.StopDistance) { if (vector.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(vector.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(vector.Y - gameObject.Position.Y) < 1.95f) { Stop(); } } else { Stop(); } } else { Stop(); } } else { List waypoints = _navmeshIpc.GetWaypoints(); Vector3? vector2 = _clientState.LocalPlayer?.Position; if (vector2.HasValue && (!Destination.ShouldRecalculateNavmesh() || !RecalculateNavmesh(waypoints, vector2.Value)) && !Destination.IsFlying && !_condition[ConditionFlag.Mounted] && !_gameFunctions.HasStatusPreventingSprint() && Destination.CanSprint) { TriggerSprintIfNeeded(waypoints, vector2.Value); } } } private void Restart(DestinationData destination) { Stop(); if (destination.UseNavmesh) { NavigateTo(EMovementType.None, destination.DataId, destination.Position, fly: false, sprint: false, destination.StopDistance, destination.VerticalStopDistance); return; } uint? dataId = destination.DataId; int num = 1; List list = new List(num); CollectionsMarshal.SetCount(list, num); Span span = CollectionsMarshal.AsSpan(list); int index = 0; span[index] = destination.Position; NavigateTo(EMovementType.None, dataId, list, fly: false, sprint: false, destination.StopDistance, destination.VerticalStopDistance); } 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) { ResetPathfinding(); if (InputManager.IsAutoRunning()) { _logger.LogInformation("Turning off auto-move"); _chatFunctions.ExecuteCommand("/automove off"); } Destination = new DestinationData(type, dataId, to, stopDistance ?? 2.8f, fly, sprint, verticalStopDistance, land, useNavmesh); MovementStartedAt = DateTime.MaxValue; } public void NavigateTo(EMovementType type, uint? dataId, Vector3 to, bool fly, bool sprint, float? stopDistance = null, float? verticalStopDistance = null, bool land = false) { 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); _logger.LogInformation("Pathfinding to {Destination}", Destination); Destination.NavmeshCalculations++; _cancellationTokenSource = new CancellationTokenSource(); _cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(30L)); Vector3 vector2 = _clientState.LocalPlayer.Position; if (fly && _aetheryteData.CalculateDistance(vector2, _clientState.TerritoryType, EAetheryteLocation.CoerthasCentralHighlandsCampDragonhead) < 11f) { Vector3 vector = vector2; vector.Y = vector2.Y + 1f; vector2 = vector; _logger.LogInformation("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; } _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) { 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); _logger.LogInformation("Moving to {Destination}", Destination); _navmeshIpc.MoveTo(to, fly); MovementStartedAt = DateTime.Now; } public void ResetPathfinding() { if (_cancellationTokenSource != null) { try { _cancellationTokenSource.Cancel(); } catch (ObjectDisposedException) { } _cancellationTokenSource.Dispose(); } _pathfindTask = null; } private unsafe bool RecalculateNavmesh(List navPoints, Vector3 start) { if (Destination == null) { throw new InvalidOperationException("Destination is null"); } if (DateTime.Now - MovementStartedAt <= TimeSpan.FromSeconds(5L)) { 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; if (navmeshCalculations % 6 == 1) { _logger.LogWarning("Jumping to try and resolve navmesh problem (n = {Calculations})", navmeshCalculations); ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null); Destination.NavmeshCalculations++; Destination.LastWaypoint.UpdatedAt = Environment.TickCount64; } else { _logger.LogWarning("Recalculating navmesh (n = {Calculations})", navmeshCalculations); Restart(Destination); } Destination.NavmeshCalculations = navmeshCalculations + 1; return true; } Destination.LastWaypoint.Distance2DAtLastUpdate = num; Destination.LastWaypoint.UpdatedAt = Environment.TickCount64; return false; } return false; } private unsafe void TriggerSprintIfNeeded(IEnumerable navPoints, Vector3 start) { float num = 0f; foreach (Vector3 navPoint in navPoints) { num += (start - navPoint).Length(); start = navPoint; } float num2 = 100f; bool flag = !_gameFunctions.HasStatus(EStatus.Jog); if (flag) { bool flag2; switch (GameMain.Instance()->CurrentTerritoryIntendedUseId) { case 0: case 7: case 13: case 14: case 15: case 19: case 23: case 29: flag2 = true; break; default: flag2 = false; break; } flag = flag2; } if (flag) { num2 = 30f; } if (num > num2 && ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 4u, 3758096384uL, checkRecastActive: true, checkCastingActive: true, null) == 0) { _logger.LogInformation("Triggering Sprint"); ActionManager.Instance()->UseAction(ActionType.GeneralAction, 4u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null); } } public void Stop() { _navmeshIpc.Stop(); ResetPathfinding(); Destination = null; if (InputManager.IsAutoRunning()) { _logger.LogInformation("Turning off auto-move [stop]"); _chatFunctions.ExecuteCommand("/automove off"); } } public void Dispose() { Stop(); } }