using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Services; using LLib; using Lumina.Excel.Sheets; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps.Common; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; using SmartNav.Data; using SmartNav.Model; using SmartNav.Model.Converter; namespace Questionable.Controller.Steps.Movement; internal sealed class MoveExecutor : TaskExecutor, IToastAware, ITaskExecutor { private const float DescendDrop = 100f; private readonly string _cannotExecuteAtThisTime; private readonly MovementController _movementController; private readonly GameFunctions _gameFunctions; private readonly ILogger _logger; private readonly IClientState _clientState; private readonly IObjectTable _objectTable; private readonly ICondition _condition; private readonly Questionable.Controller.Steps.Common.Mount.MountEvaluator _mountEvaluator; private readonly IServiceProvider _serviceProvider; private readonly AetheryteData _aetheryteData; private readonly Configuration _configuration; private System.Action? _startAction; private Vector3 _destination; private bool _canRestart; private bool _descendAttempted; private bool _adjustedToObject; private (Questionable.Controller.Steps.Common.Mount.MountExecutor Executor, Questionable.Controller.Steps.Common.Mount.MountTask Task)? _mountBeforeMovement; private (Questionable.Controller.Steps.Common.Mount.UnmountExecutor Executor, Questionable.Controller.Steps.Common.Mount.UnmountTask Task)? _unmountBeforeMovement; private (Questionable.Controller.Steps.Common.Mount.MountExecutor Executor, Questionable.Controller.Steps.Common.Mount.MountTask Task)? _mountDuringMovement; public MoveExecutor(MovementController movementController, GameFunctions gameFunctions, AetheryteData aetheryteData, Configuration configuration, ILogger logger, IClientState clientState, IObjectTable objectTable, ICondition condition, IDataManager dataManager, Questionable.Controller.Steps.Common.Mount.MountEvaluator mountEvaluator, IServiceProvider serviceProvider) { _movementController = movementController; _gameFunctions = gameFunctions; _aetheryteData = aetheryteData; _configuration = configuration; _logger = logger; _clientState = clientState; _objectTable = objectTable; _condition = condition; _serviceProvider = serviceProvider; _mountEvaluator = mountEvaluator; _cannotExecuteAtThisTime = dataManager.GetString(579u, (LogMessage x) => x.Text); } private void PrepareMovementIfNeeded() { if (!_gameFunctions.IsFlyingUnlocked(base.Task.TerritoryId)) { base.Task = base.Task with { Fly = false, Land = false }; } if (!base.Task.DisableNavmesh) { _startAction = delegate { _movementController.NavigateTo(EMovementType.Quest, base.Task.DataId, _destination, base.Task.Fly, base.Task.Sprint ?? (!_mountDuringMovement.HasValue), base.Task.StopDistance, base.Task.IgnoreDistanceToObject ? new float?(float.MaxValue) : ((float?)null), base.Task.Land, base.Task.SmartNavRouted); }; return; } _startAction = delegate { MovementController movementController = _movementController; uint? dataId = base.Task.DataId; int num = 1; List list = new List(num); CollectionsMarshal.SetCount(list, num); CollectionsMarshal.AsSpan(list)[0] = _destination; movementController.NavigateTo(EMovementType.Quest, dataId, list, base.Task.Fly, base.Task.Sprint ?? (!_mountDuringMovement.HasValue), base.Task.StopDistance, base.Task.IgnoreDistanceToObject ? new float?(float.MaxValue) : ((float?)null), base.Task.Land); }; } protected override bool Start() { if (_clientState.TerritoryType != base.Task.TerritoryId) { _logger.LogDebug("Skipping movement - already in territory {Current} (task expects {Expected})", _clientState.TerritoryType, base.Task.TerritoryId); return false; } _canRestart = base.Task.RestartNavigation; _destination = base.Task.Destination; if (_aetheryteData.Locations.Any((KeyValuePair kvp) => AetheryteConverter.IsLargeAetheryte(kvp.Key) && Vector3.Distance(kvp.Value, _destination) < 1f)) { float num = 7f; if ((base.Task.StopDistance ?? 3f) < num) { base.Task = base.Task with { StopDistance = num }; } } float num2 = base.Task.StopDistance ?? 3f; Vector3? vector = _objectTable.LocalPlayer?.Position; float num3 = ((!vector.HasValue) ? float.MaxValue : Vector3.Distance(vector.Value, _destination)); if (num3 > num2) { PrepareMovementIfNeeded(); } if (base.Task.MountRequired) { Questionable.Controller.Steps.Common.Mount.MountTask mountTask = new Questionable.Controller.Steps.Common.Mount.MountTask(base.Task.TerritoryId, Questionable.Controller.Steps.Common.Mount.EMountIf.Always); _mountBeforeMovement = (_serviceProvider.GetRequiredService(), mountTask); if (!_mountBeforeMovement.Value.Executor.Start(mountTask)) { _mountBeforeMovement = null; } } else if (base.Task.DismountRequired) { Questionable.Controller.Steps.Common.Mount.UnmountTask unmountTask = new Questionable.Controller.Steps.Common.Mount.UnmountTask(); _unmountBeforeMovement = (_serviceProvider.GetRequiredService(), unmountTask); if (!_unmountBeforeMovement.Value.Executor.Start(unmountTask)) { _unmountBeforeMovement = null; } } else if (base.Task.Mount == true) { Questionable.Controller.Steps.Common.Mount.MountTask mountTask2 = new Questionable.Controller.Steps.Common.Mount.MountTask(base.Task.TerritoryId, Questionable.Controller.Steps.Common.Mount.EMountIf.Always); _mountBeforeMovement = (_serviceProvider.GetRequiredService(), mountTask2); if (!_mountBeforeMovement.Value.Executor.Start(mountTask2)) { _mountBeforeMovement = null; } } else if (base.Task.Mount == false) { Questionable.Controller.Steps.Common.Mount.UnmountTask unmountTask2 = new Questionable.Controller.Steps.Common.Mount.UnmountTask(); _unmountBeforeMovement = (_serviceProvider.GetRequiredService(), unmountTask2); if (!_unmountBeforeMovement.Value.Executor.Start(unmountTask2)) { _unmountBeforeMovement = null; } } else if (!base.Task.DisableNavmesh) { bool flag = _gameFunctions.IsFlyingUnlocked(base.Task.TerritoryId); if (!base.Task.SmartNavRouted && !base.Task.Fly && flag && num3 > _configuration.Navigation.MountFlyDistance) { base.Task = base.Task with { Fly = true, Land = true }; } float num4 = (base.Task.Fly ? _configuration.Navigation.MountFlyDistance : _configuration.Navigation.MountGroundDistance); Questionable.Controller.Steps.Common.Mount.EMountIf mountIf = ((!(num3 > num4)) ? Questionable.Controller.Steps.Common.Mount.EMountIf.AwayFromPosition : Questionable.Controller.Steps.Common.Mount.EMountIf.Always); Questionable.Controller.Steps.Common.Mount.MountTask mountTask3 = new Questionable.Controller.Steps.Common.Mount.MountTask(base.Task.TerritoryId, mountIf, _destination); long retryAtMs = 0L; (Questionable.Controller.Steps.Common.Mount.MountExecutor, Questionable.Controller.Steps.Common.Mount.MountTask)? tuple; if (!_condition[ConditionFlag.Mounted] && _mountEvaluator.EvaluateMountState(mountTask3, dryRun: true, ref retryAtMs) != Questionable.Controller.Steps.Common.Mount.MountResult.DontMount) { tuple = (_serviceProvider.GetRequiredService(), mountTask3); tuple.Value.Item1.Start(mountTask3); } else { tuple = null; } if (base.Task.Fly) { _mountBeforeMovement = tuple; if (!tuple.HasValue && !_condition[ConditionFlag.Mounted]) { _logger.LogDebug("Not mounting for short flight movement; navigating on foot instead"); base.Task = base.Task with { Fly = false, Land = false }; } } else { _mountDuringMovement = tuple; } } if (!_mountBeforeMovement.HasValue && !_unmountBeforeMovement.HasValue && _startAction != null) { _startAction(); } return true; } public override ETaskResult Update() { ETaskResult? eTaskResult = UpdateMountState(); if (eTaskResult.HasValue) { return eTaskResult.GetValueOrDefault(); } if (_startAction == null) { return ETaskResult.TaskComplete; } if (_movementController.IsPathfinding || _movementController.IsPathRunning) { return ETaskResult.StillRunning; } if (!_movementController.HasBeenMovingForAtLeast(2000)) { return ETaskResult.StillRunning; } IPlayerCharacter localPlayer = _objectTable.LocalPlayer; float num = base.Task.StopDistance ?? 3f; if (base.Task.Fly && base.Task.Land && _canRestart && localPlayer != null && _clientState.TerritoryType == base.Task.TerritoryId && Vector3.Distance(localPlayer.Position, _destination) > num) { if (_condition[ConditionFlag.InFlight]) { if (_descendAttempted) { _logger.LogWarning("Still airborne after descending, treating as unreachable"); throw new MovementController.PathfindingFailedException("Still airborne after descending, cannot close gap on foot"); } if (!_movementController.IsNavmeshReady) { _logger.LogDebug("Airborne outside stop distance, waiting for navmesh to descend"); return ETaskResult.StillRunning; } Vector3 position = localPlayer.Position; position.Y = localPlayer.Position.Y - 100f; Vector3 vector = position; _descendAttempted = true; _logger.LogDebug("Outside stop distance but still airborne, descending to {Target}", vector.ToString("G", CultureInfo.InvariantCulture)); MovementController movementController = _movementController; uint? dataId = base.Task.DataId; int num2 = 1; List list = new List(num2); CollectionsMarshal.SetCount(list, num2); CollectionsMarshal.AsSpan(list)[0] = vector; movementController.NavigateTo(EMovementType.Landing, dataId, list, fly: true, sprint: false, null); return ETaskResult.StillRunning; } _canRestart = false; _logger.LogDebug("Landed outside stop distance, closing gap on foot"); MovementController movementController2 = _movementController; uint? dataId2 = base.Task.DataId; Vector3 destination = _destination; float? stopDistance = base.Task.StopDistance; bool smartNavRouted = base.Task.SmartNavRouted; movementController2.NavigateTo(EMovementType.Quest, dataId2, destination, fly: false, sprint: false, stopDistance, null, land: false, smartNavRouted); return ETaskResult.StillRunning; } if (base.Task.DataId.HasValue && !_adjustedToObject && localPlayer != null && !_condition[ConditionFlag.InFlight] && _clientState.TerritoryType == base.Task.TerritoryId) { IGameObject gameObject = _gameFunctions.FindObjectByDataId(base.Task.DataId.Value, null, warnIfMissing: false); if (gameObject != null) { float num3 = Vector3.Distance(localPlayer.Position, gameObject.Position); if (num3 > num) { _adjustedToObject = true; _logger.LogDebug("Adjusting to actual object position ({Distance:F1}y away)", num3); _movementController.NavigateTo(EMovementType.Quest, base.Task.DataId, gameObject.Position, fly: false, sprint: false, base.Task.StopDistance); return ETaskResult.StillRunning; } } } if (_canRestart && localPlayer != null && Vector3.Distance(localPlayer.Position, _destination) > num + 5f) { _canRestart = false; if (_clientState.TerritoryType == base.Task.TerritoryId) { _logger.LogInformation("Looks like movement was interrupted, re-attempting to move"); _startAction(); return ETaskResult.StillRunning; } if (base.Task.AllowZoneTransition) { _logger.LogDebug("Zone transition during boundary crossing (now in {Current}, expected {Expected}), completing task", _clientState.TerritoryType, base.Task.TerritoryId); return ETaskResult.TaskComplete; } _logger.LogWarning("Movement interrupted by zone transition (now in {Current}, task expected {Expected})", _clientState.TerritoryType, base.Task.TerritoryId); throw new MovementController.PathfindingFailedException($"Zone transition during movement: now in territory {_clientState.TerritoryType}, expected {base.Task.TerritoryId}"); } if (base.Task.SmartNavRouted && !_adjustedToObject && localPlayer != null && _clientState.TerritoryType == base.Task.TerritoryId && Vector3.Distance(localPlayer.Position, _destination) > num + 5f) { float num4 = Vector3.Distance(localPlayer.Position, _destination); _logger.LogWarning("SmartNav movement ended {Distance:F1}y from target (stop distance {StopDistance:F1}y), treating as unreachable", num4, num); throw new MovementController.PathfindingFailedException($"SmartNav movement ended {num4:F1}y from target (stop distance {num:F1}y)"); } return ETaskResult.TaskComplete; } private ETaskResult? UpdateMountState() { (Questionable.Controller.Steps.Common.Mount.MountExecutor, Questionable.Controller.Steps.Common.Mount.MountTask)? mountBeforeMovement = _mountBeforeMovement; if (mountBeforeMovement.HasValue) { Questionable.Controller.Steps.Common.Mount.MountExecutor item = mountBeforeMovement.GetValueOrDefault().Item1; if (item != null) { if (item.Update() == ETaskResult.TaskComplete) { _logger.LogDebug("MountBeforeMovement complete"); _mountBeforeMovement = null; _startAction?.Invoke(); return null; } return ETaskResult.StillRunning; } } (Questionable.Controller.Steps.Common.Mount.UnmountExecutor, Questionable.Controller.Steps.Common.Mount.UnmountTask)? unmountBeforeMovement = _unmountBeforeMovement; if (unmountBeforeMovement.HasValue) { Questionable.Controller.Steps.Common.Mount.UnmountExecutor item2 = unmountBeforeMovement.GetValueOrDefault().Item1; if (item2 != null) { if (item2.Update() == ETaskResult.TaskComplete) { _logger.LogDebug("UnmountBeforeMovement complete"); _unmountBeforeMovement = null; _startAction?.Invoke(); return null; } return ETaskResult.StillRunning; } } mountBeforeMovement = _mountDuringMovement; if (mountBeforeMovement.HasValue) { (Questionable.Controller.Steps.Common.Mount.MountExecutor, Questionable.Controller.Steps.Common.Mount.MountTask) valueOrDefault = mountBeforeMovement.GetValueOrDefault(); var (mountExecutor, _) = valueOrDefault; if (mountExecutor != null) { Questionable.Controller.Steps.Common.Mount.MountTask item3 = valueOrDefault.Item2; if ((object)item3 != null) { if (mountExecutor.Update() == ETaskResult.TaskComplete) { _logger.LogDebug("MountDuringMovement complete (mounted)"); _mountDuringMovement = null; return null; } long retryAtMs = 0L; if (_mountEvaluator.EvaluateMountState(item3, dryRun: true, ref retryAtMs) == Questionable.Controller.Steps.Common.Mount.MountResult.DontMount) { _logger.LogDebug("MountDuringMovement implicitly complete (shouldn't mount anymore)"); _mountDuringMovement = null; return null; } return null; } } } return null; } public override bool WasInterrupted() { long retryAtMs = 0L; if (base.Task.Fly && _condition[ConditionFlag.InCombat] && !_condition[ConditionFlag.Mounted]) { (Questionable.Controller.Steps.Common.Mount.MountExecutor, Questionable.Controller.Steps.Common.Mount.MountTask)? mountBeforeMovement = _mountBeforeMovement; if (mountBeforeMovement.HasValue) { Questionable.Controller.Steps.Common.Mount.MountTask item = mountBeforeMovement.GetValueOrDefault().Item2; if ((object)item != null && _mountEvaluator.EvaluateMountState(item, dryRun: true, ref retryAtMs) == Questionable.Controller.Steps.Common.Mount.MountResult.WhenOutOfCombat) { return true; } } } return base.WasInterrupted(); } public override bool ShouldInterruptOnDamage() { if (!_mountBeforeMovement.HasValue) { return ShouldResolveCombatBeforeNextInteraction(); } return true; } private bool ShouldResolveCombatBeforeNextInteraction() { return base.Task.InteractionType == EInteractionType.Jump; } public bool OnErrorToast(SeString message) { return GameFunctions.GameStringEquals(_cannotExecuteAtThisTime, message.TextValue); } }