qstbak/Questionable/Questionable.Controller/MovementController.cs
2025-10-09 07:47:19 +10:00

493 lines
14 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 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<Vector3> PartialRoute { get; } = new List<Vector3>();
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<MovementController> _logger;
private CancellationTokenSource? _cancellationTokenSource;
private Task<List<Vector3>>? _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<List<Vector3>> 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<MovementController> 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<Vector3> 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<Vector3>, 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<Vector3> 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<Vector3> list = new List<Vector3>(num);
CollectionsMarshal.SetCount(list, num);
Span<Vector3> 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<Vector3> 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<Vector3> 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<Vector3> 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();
}
}