forked from liza/Questionable
394 lines
14 KiB
C#
394 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.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.External;
|
|
using Questionable.Functions;
|
|
using Questionable.Model;
|
|
using Questionable.Model.Common;
|
|
using Questionable.Model.Common.Converter;
|
|
using Questionable.Model.Questing;
|
|
using Questionable.Model.Questing.Converter;
|
|
|
|
namespace Questionable.Controller;
|
|
|
|
internal sealed class MovementController : IDisposable
|
|
{
|
|
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 ILogger<MovementController> _logger;
|
|
private CancellationTokenSource? _cancellationTokenSource;
|
|
private Task<List<Vector3>>? _pathfindTask;
|
|
|
|
public MovementController(NavmeshIpc navmeshIpc, IClientState clientState, GameFunctions gameFunctions,
|
|
ChatFunctions chatFunctions, ICondition condition, MovementOverrideController movementOverrideController,
|
|
ILogger<MovementController> logger)
|
|
{
|
|
_navmeshIpc = navmeshIpc;
|
|
_clientState = clientState;
|
|
_gameFunctions = gameFunctions;
|
|
_chatFunctions = chatFunctions;
|
|
_condition = condition;
|
|
_movementOverrideController = movementOverrideController;
|
|
_logger = logger;
|
|
}
|
|
|
|
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 => _pathfindTask is { IsCompleted: false };
|
|
public DestinationData? Destination { get; set; }
|
|
public DateTime MovementStartedAt { get; private set; } = DateTime.Now;
|
|
|
|
public 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();
|
|
}
|
|
|
|
var navPoints = _pathfindTask.Result.Skip(1).ToList();
|
|
Vector3 start = _clientState.LocalPlayer?.Position ?? navPoints[0];
|
|
if (Destination.IsFlying && !_condition[ConditionFlag.InFlight] && _condition[ConditionFlag.Mounted])
|
|
{
|
|
if (IsOnFlightPath(start) || navPoints.Any(IsOnFlightPath))
|
|
{
|
|
unsafe
|
|
{
|
|
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!Destination.IsFlying)
|
|
{
|
|
(navPoints, bool recalculateNavmesh) = _movementOverrideController.AdjustPath(navPoints);
|
|
if (recalculateNavmesh && Destination.NavmeshCalculations < 10)
|
|
{
|
|
Destination.NavmeshCalculations++;
|
|
Destination.PartialRoute.AddRange(navPoints);
|
|
_logger.LogInformation("Running navmesh recalculation with fudged point ({From} to {To})",
|
|
navPoints.Last(), Destination.Position);
|
|
|
|
_cancellationTokenSource = new();
|
|
_cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(30));
|
|
_pathfindTask =
|
|
_navmeshIpc.Pathfind(navPoints.Last(), Destination.Position, Destination.IsFlying,
|
|
_cancellationTokenSource.Token);
|
|
return;
|
|
}
|
|
}
|
|
|
|
navPoints = Destination.PartialRoute.Concat(navPoints).ToList();
|
|
_logger.LogInformation("Navigating via route: [{Route}]",
|
|
string.Join(" → ",
|
|
_pathfindTask.Result.Select(x => x.ToString("G", CultureInfo.InvariantCulture))));
|
|
|
|
_navmeshIpc.MoveTo(navPoints, 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)
|
|
{
|
|
if (_gameFunctions.IsLoadingScreenVisible())
|
|
{
|
|
_logger.LogInformation("Stopping movement, loading screen visible");
|
|
Stop();
|
|
return;
|
|
}
|
|
|
|
if (Destination is { IsFlying: true } && _condition[ConditionFlag.Swimming])
|
|
{
|
|
_logger.LogInformation("Flying but swimming, restarting as non-flying path...");
|
|
Restart(Destination);
|
|
}
|
|
else if (Destination is { IsFlying: true } && !_condition[ConditionFlag.Mounted])
|
|
{
|
|
_logger.LogInformation("Flying but not mounted, restarting as non-flying path...");
|
|
Restart(Destination);
|
|
return;
|
|
}
|
|
|
|
Vector3 localPlayerPosition = _clientState.LocalPlayer?.Position ?? Vector3.Zero;
|
|
if (Destination.MovementType == EMovementType.Landing)
|
|
{
|
|
if (!_condition[ConditionFlag.InFlight])
|
|
Stop();
|
|
}
|
|
else if ((localPlayerPosition - Destination.Position).Length() < Destination.StopDistance)
|
|
{
|
|
if (Destination.IgnoreDistanceToObject)
|
|
{
|
|
Stop();
|
|
}
|
|
else if (Destination.DataId != null)
|
|
{
|
|
IGameObject? gameObject = _gameFunctions.FindObjectByDataId(Destination.DataId.Value);
|
|
if (gameObject is ICharacter or IEventObj)
|
|
{
|
|
if (Math.Abs(localPlayerPosition.Y - gameObject.Position.Y) <
|
|
DefaultVerticalInteractionDistance)
|
|
Stop();
|
|
}
|
|
else if (gameObject != null && gameObject.ObjectKind == ObjectKind.Aetheryte)
|
|
{
|
|
if (AetheryteConverter.IsLargeAetheryte((EAetheryteLocation)Destination.DataId))
|
|
{
|
|
/*
|
|
if ((EAetheryteLocation) Destination.DataId is EAetheryteLocation.OldSharlayan
|
|
or EAetheryteLocation.UltimaThuleAbodeOfTheEa)
|
|
Stop();
|
|
|
|
// TODO verify the first part of this, is there any aetheryte like that?
|
|
// TODO Unsure if this is per-aetheryte or what; because e.g. old sharlayan is at -1.53;
|
|
// but Elpis aetherytes fail at around -0.95
|
|
if (localPlayerPosition.Y - gameObject.Position.Y < 2.95f &&
|
|
localPlayerPosition.Y - gameObject.Position.Y > -0.9f)
|
|
Stop();
|
|
*/
|
|
Stop();
|
|
}
|
|
else
|
|
{
|
|
// aethernet shard
|
|
if (Math.Abs(localPlayerPosition.Y - gameObject.Position.Y) <
|
|
DefaultVerticalInteractionDistance)
|
|
Stop();
|
|
}
|
|
}
|
|
else
|
|
Stop();
|
|
}
|
|
else
|
|
Stop();
|
|
}
|
|
else if (!Destination.IsFlying && !_condition[ConditionFlag.Mounted] &&
|
|
!_gameFunctions.HasStatusPreventingSprint() && Destination.CanSprint)
|
|
{
|
|
List<Vector3> navPoints = _navmeshIpc.GetWaypoints();
|
|
Vector3? start = _clientState.LocalPlayer?.Position;
|
|
if (start != null)
|
|
{
|
|
float actualDistance = 0;
|
|
foreach (Vector3 end in navPoints)
|
|
{
|
|
actualDistance += (start.Value - end).Length();
|
|
start = end;
|
|
}
|
|
|
|
unsafe
|
|
{
|
|
// 70 is ~10 seconds of sprint
|
|
if (actualDistance > 100f &&
|
|
ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 4) == 0)
|
|
{
|
|
_logger.LogInformation("Triggering Sprint");
|
|
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 4);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Restart(DestinationData destination)
|
|
{
|
|
Stop();
|
|
|
|
if (destination.UseNavmesh)
|
|
{
|
|
NavigateTo(EMovementType.None, destination.DataId, destination.Position, false, false,
|
|
destination.StopDistance, destination.IgnoreDistanceToObject);
|
|
}
|
|
else
|
|
{
|
|
NavigateTo(EMovementType.None, destination.DataId, [destination.Position], false, false,
|
|
destination.StopDistance, destination.IgnoreDistanceToObject);
|
|
}
|
|
}
|
|
|
|
private bool IsOnFlightPath(Vector3 p)
|
|
{
|
|
Vector3? pointOnFloor = _navmeshIpc.GetPointOnFloor(p, true);
|
|
return pointOnFloor != null && Math.Abs(pointOnFloor.Value.Y - p.Y) > 0.5f;
|
|
}
|
|
|
|
[MemberNotNull(nameof(Destination))]
|
|
private void PrepareNavigation(EMovementType type, uint? dataId, Vector3 to, bool fly, bool sprint,
|
|
float? stopDistance, bool ignoreDistanceToObject, 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 ?? (QuestStep.DefaultStopDistance - 0.2f), fly,
|
|
sprint, ignoreDistanceToObject, land, useNavmesh);
|
|
MovementStartedAt = DateTime.MaxValue;
|
|
}
|
|
|
|
public void NavigateTo(EMovementType type, uint? dataId, Vector3 to, bool fly, bool sprint,
|
|
float? stopDistance = null, bool ignoreDistanceToObject = false, bool land = false)
|
|
{
|
|
fly |= _condition[ConditionFlag.Diving];
|
|
if (fly && land)
|
|
to = to with { Y = to.Y + 2.6f };
|
|
|
|
PrepareNavigation(type, dataId, to, fly, sprint, stopDistance, ignoreDistanceToObject, land, true);
|
|
_logger.LogInformation("Pathfinding to {Destination}", Destination);
|
|
|
|
Destination.NavmeshCalculations++;
|
|
_cancellationTokenSource = new();
|
|
_cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(30));
|
|
_pathfindTask =
|
|
_navmeshIpc.Pathfind(_clientState.LocalPlayer!.Position, to, fly, _cancellationTokenSource.Token);
|
|
}
|
|
|
|
public void NavigateTo(EMovementType type, uint? dataId, List<Vector3> to, bool fly, bool sprint,
|
|
float? stopDistance, bool ignoreDistanceToObject = false, bool land = false)
|
|
{
|
|
fly |= _condition[ConditionFlag.Diving];
|
|
if (fly && land && to.Count > 0)
|
|
to[^1] = to[^1] with { Y = to[^1].Y + 2.6f };
|
|
|
|
PrepareNavigation(type, dataId, to.Last(), fly, sprint, stopDistance, ignoreDistanceToObject, land, 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;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
public sealed record DestinationData(
|
|
EMovementType MovementType,
|
|
uint? DataId,
|
|
Vector3 Position,
|
|
float StopDistance,
|
|
bool IsFlying,
|
|
bool CanSprint,
|
|
bool IgnoreDistanceToObject,
|
|
bool Land,
|
|
bool UseNavmesh)
|
|
{
|
|
public int NavmeshCalculations { get; set; }
|
|
public List<Vector3> PartialRoute { get; } = [];
|
|
}
|
|
|
|
public sealed class PathfindingFailedException : Exception
|
|
{
|
|
public PathfindingFailedException()
|
|
{
|
|
}
|
|
|
|
public PathfindingFailedException(string message)
|
|
: base(message)
|
|
{
|
|
}
|
|
|
|
public PathfindingFailedException(string message, Exception innerException)
|
|
: base(message, innerException)
|
|
{
|
|
}
|
|
}
|
|
}
|