From bedd10b8bce5669a7965bc8bfeea8fa40a360395 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Mon, 31 Mar 2025 02:13:08 +0200 Subject: [PATCH] Split 'MoveTo' into separate classes --- .../Controller/GatheringController.cs | 3 +- .../Steps/Gathering/MoveToLandingLocation.cs | 6 +- .../Controller/Steps/Interactions/UseItem.cs | 5 +- .../Controller/Steps/Movement/MoveExecutor.cs | 264 ++++++++++ .../Controller/Steps/Movement/MoveTask.cs | 40 ++ .../Controller/Steps/Movement/MoveTo.cs | 158 ++++++ .../Controller/Steps/Movement/NoOpTask.cs | 3 + .../Steps/Movement/NoOpTaskExecutor.cs | 10 + .../Steps/Shared/AetheryteShortcut.cs | 5 +- .../Controller/Steps/Shared/MoveTo.cs | 459 ------------------ Questionable/QuestionablePlugin.cs | 3 +- 11 files changed, 488 insertions(+), 468 deletions(-) create mode 100644 Questionable/Controller/Steps/Movement/MoveExecutor.cs create mode 100644 Questionable/Controller/Steps/Movement/MoveTask.cs create mode 100644 Questionable/Controller/Steps/Movement/MoveTo.cs create mode 100644 Questionable/Controller/Steps/Movement/NoOpTask.cs create mode 100644 Questionable/Controller/Steps/Movement/NoOpTaskExecutor.cs delete mode 100644 Questionable/Controller/Steps/Shared/MoveTo.cs diff --git a/Questionable/Controller/GatheringController.cs b/Questionable/Controller/GatheringController.cs index 8968ee00..b9248b0c 100644 --- a/Questionable/Controller/GatheringController.cs +++ b/Questionable/Controller/GatheringController.cs @@ -17,6 +17,7 @@ using Microsoft.Extensions.Logging; using Questionable.Controller.Steps; using Questionable.Controller.Steps.Gathering; using Questionable.Controller.Steps.Interactions; +using Questionable.Controller.Steps.Movement; using Questionable.Controller.Steps.Shared; using Questionable.External; using Questionable.Functions; @@ -166,7 +167,7 @@ internal sealed unsafe class GatheringController : MiniTaskController logger) : TaskExecutor, IToastAware @@ -51,7 +51,7 @@ internal static class MoveToLandingLocation target.ToString("G", CultureInfo.InvariantCulture), degrees, range); bool fly = Task.FlyBetweenNodes && gameFunctions.IsFlyingUnlocked(Task.TerritoryId); - _moveTask = new MoveTo.MoveTask(Task.TerritoryId, target, null, 0.25f, + _moveTask = new MoveTask(Task.TerritoryId, target, null, 0.25f, DataId: Task.GatheringNode.DataId, Fly: fly, IgnoreDistanceToObject: true, InteractionType: EInteractionType.Gather); return moveExecutor.Start(_moveTask); diff --git a/Questionable/Controller/Steps/Interactions/UseItem.cs b/Questionable/Controller/Steps/Interactions/UseItem.cs index abc427ad..25799a76 100644 --- a/Questionable/Controller/Steps/Interactions/UseItem.cs +++ b/Questionable/Controller/Steps/Interactions/UseItem.cs @@ -8,6 +8,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps.Common; +using Questionable.Controller.Steps.Movement; using Questionable.Controller.Steps.Shared; using Questionable.Controller.Utils; using Questionable.Data; @@ -61,7 +62,7 @@ internal static class UseItem new Mount.MountTask(140, nextPosition != null ? Mount.EMountIf.AwayFromPosition : Mount.EMountIf.Always, nextPosition), - new MoveTo.MoveTask(140, new(-408.92343f, 23.167036f, -351.16223f), null, 0.25f, + new MoveTask(140, new(-408.92343f, 23.167036f, -351.16223f), null, 0.25f, DataId: null, DisableNavmesh: true, Sprint: false, Fly: false, InteractionType: EInteractionType.WalkTo) ]; @@ -106,7 +107,7 @@ internal static class UseItem yield return new AetheryteShortcut.Task(null, null, EAetheryteLocation.Limsa, territoryId); yield return new AethernetShortcut.Task(EAetheryteLocation.Limsa, EAetheryteLocation.LimsaArcanist); yield return new WaitAtEnd.WaitDelay(); - yield return new MoveTo.MoveTask(territoryId, destination, DataId: npcId, Sprint: false, + yield return new MoveTask(territoryId, destination, DataId: npcId, Sprint: false, InteractionType: EInteractionType.WalkTo); yield return new Interact.Task(npcId, null, EInteractionType.None, true); } diff --git a/Questionable/Controller/Steps/Movement/MoveExecutor.cs b/Questionable/Controller/Steps/Movement/MoveExecutor.cs new file mode 100644 index 00000000..99e26d38 --- /dev/null +++ b/Questionable/Controller/Steps/Movement/MoveExecutor.cs @@ -0,0 +1,264 @@ +using System; +using System.Globalization; +using System.Numerics; +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Plugin.Services; +using LLib; +using Lumina.Excel.Sheets; +using Microsoft.Extensions.Logging; +using Questionable.Functions; +using Questionable.Model; +using Questionable.Model.Questing; +using Action = System.Action; +using Mount = Questionable.Controller.Steps.Common.Mount; + +namespace Questionable.Controller.Steps.Movement; + +internal sealed class MoveExecutor : TaskExecutor, IToastAware +{ + private readonly string _cannotExecuteAtThisTime; + private readonly MovementController _movementController; + private readonly GameFunctions _gameFunctions; + private readonly ILogger _logger; + private readonly IClientState _clientState; + private readonly ICondition _condition; + private readonly Mount.MountExecutor _mountExecutor; + private readonly Mount.UnmountExecutor _unmountExecutor; + + private Action? _startAction; + private Vector3 _destination; + private bool _canRestart; + + private (ITaskExecutor Executor, ITask Task, bool Triggered)? _nestedExecutor = + (new NoOpTaskExecutor(), new NoOpTask(), true); + + public MoveExecutor( + MovementController movementController, + GameFunctions gameFunctions, + ILogger logger, + IClientState clientState, + ICondition condition, + IDataManager dataManager, + Mount.MountExecutor mountExecutor, + Mount.UnmountExecutor unmountExecutor) + { + _movementController = movementController; + _gameFunctions = gameFunctions; + _logger = logger; + _clientState = clientState; + _condition = condition; + _mountExecutor = mountExecutor; + _unmountExecutor = unmountExecutor; + _cannotExecuteAtThisTime = dataManager.GetString(579, x => x.Text)!; + } + + private void PrepareMovementIfNeeded() + { + if (!_gameFunctions.IsFlyingUnlocked(Task.TerritoryId)) + { + Task = Task with { Fly = false, Land = false }; + } + + if (!Task.DisableNavmesh) + { + _startAction = () => + _movementController.NavigateTo(EMovementType.Quest, Task.DataId, _destination, + fly: Task.Fly, + sprint: Task.Sprint, + stopDistance: Task.StopDistance, + ignoreDistanceToObject: Task.IgnoreDistanceToObject, + land: Task.Land); + } + else + { + _startAction = () => + _movementController.NavigateTo(EMovementType.Quest, Task.DataId, [_destination], + fly: Task.Fly, + sprint: Task.Sprint, + stopDistance: Task.StopDistance, + ignoreDistanceToObject: Task.IgnoreDistanceToObject, + land: Task.Land); + } + } + + protected override bool Start() + { + _canRestart = Task.RestartNavigation; + _destination = Task.Destination; + + + float stopDistance = Task.StopDistance ?? QuestStep.DefaultStopDistance; + Vector3? position = _clientState.LocalPlayer?.Position; + float actualDistance = position == null ? float.MaxValue : Vector3.Distance(position.Value, _destination); + bool requiresMovement = actualDistance > stopDistance; + if (requiresMovement) + PrepareMovementIfNeeded(); + + // might be able to make this optional + if (Task.Mount == true) + { + var mountTask = new Mount.MountTask(Task.TerritoryId, Mount.EMountIf.Always); + if (_mountExecutor.Start(mountTask)) + { + _nestedExecutor = (_mountExecutor, mountTask, true); + return true; + } + else if (_mountExecutor.EvaluateMountState() == Mount.MountResult.WhenOutOfCombat) + _nestedExecutor = (_mountExecutor, mountTask, false); + } + else if (Task.Mount == false) + { + var mountTask = new Mount.UnmountTask(); + if (_unmountExecutor.Start(mountTask)) + { + _nestedExecutor = (_unmountExecutor, mountTask, true); + return true; + } + } + + if (!Task.DisableNavmesh) + { + if (Task.Mount == null) + { + Mount.EMountIf mountIf = + actualDistance > stopDistance && Task.Fly && + _gameFunctions.IsFlyingUnlocked(Task.TerritoryId) + ? Mount.EMountIf.Always + : Mount.EMountIf.AwayFromPosition; + var mountTask = new Mount.MountTask(Task.TerritoryId, mountIf, _destination); + if (_mountExecutor.Start(mountTask)) + { + _nestedExecutor = (_mountExecutor, mountTask, true); + return true; + } + else if (_mountExecutor.EvaluateMountState() == Mount.MountResult.WhenOutOfCombat) + _nestedExecutor = (_mountExecutor, mountTask, false); + } + } + + if (_startAction != null && (_nestedExecutor == null || _nestedExecutor.Value.Triggered == false)) + _startAction(); + return true; + } + + public override ETaskResult Update() + { + if (_nestedExecutor is { } nestedExecutor) + { + if (nestedExecutor is { Triggered: false, Executor: Mount.MountExecutor mountExecutor }) + { + if (!_condition[ConditionFlag.InCombat]) + { + if (mountExecutor.EvaluateMountState() == Mount.MountResult.DontMount) + _nestedExecutor = (new NoOpTaskExecutor(), new NoOpTask(), true); + else + { + if (_movementController.IsPathfinding || _movementController.IsPathRunning) + _movementController.Stop(); + + if (nestedExecutor.Executor.Start(nestedExecutor.Task)) + { + _nestedExecutor = nestedExecutor with { Triggered = true }; + return ETaskResult.StillRunning; + } + } + } + else if (!ShouldResolveCombatBeforeNextInteraction() && + _movementController is { IsPathfinding: false, IsPathRunning: false } && + mountExecutor.EvaluateMountState() == Mount.MountResult.DontMount) + { + // except for e.g. jumping which would maybe break if combat navigates us away, if we don't + // need a mount anymore we can just skip combat and assume that the interruption is handled + // later. + // + // without this, the character would just stand around while getting hit + _nestedExecutor = (new NoOpTaskExecutor(), new NoOpTask(), true); + } + } + else if (nestedExecutor.Executor.Update() == ETaskResult.TaskComplete) + { + _nestedExecutor = null; + if (_startAction != null) + { + _logger.LogInformation("Moving to {Destination}", + _destination.ToString("G", CultureInfo.InvariantCulture)); + _startAction(); + } + else + return ETaskResult.TaskComplete; + } + else if (!_condition[ConditionFlag.Mounted] && _condition[ConditionFlag.InCombat] && + nestedExecutor is { Triggered: true, Executor: Mount.MountExecutor }) + { + // if the problem wasn't caused by combat, the normal mount retry should handle it + _logger.LogDebug("Resetting mount trigger state"); + _nestedExecutor = nestedExecutor with { Triggered = false }; + + // however, we're also explicitly ignoring combat here and walking away + _startAction?.Invoke(); + } + + return ETaskResult.StillRunning; + } + + if (_startAction == null) + return ETaskResult.TaskComplete; + + if (_movementController.IsPathfinding || _movementController.IsPathRunning) + return ETaskResult.StillRunning; + + DateTime movementStartedAt = _movementController.MovementStartedAt; + if (movementStartedAt == DateTime.MaxValue || movementStartedAt.AddSeconds(2) >= DateTime.Now) + return ETaskResult.StillRunning; + + if (_canRestart && + Vector3.Distance(_clientState.LocalPlayer!.Position, _destination) > + (Task.StopDistance ?? QuestStep.DefaultStopDistance) + 5f) + { + _canRestart = false; + if (_clientState.TerritoryType == Task.TerritoryId) + { + _logger.LogInformation("Looks like movement was interrupted, re-attempting to move"); + _startAction(); + return ETaskResult.StillRunning; + } + else + _logger.LogInformation( + "Looks like movement was interrupted, do nothing since we're in a different territory now"); + } + + return ETaskResult.TaskComplete; + } + + public override bool WasInterrupted() + { + if (Task.Fly && _condition[ConditionFlag.InCombat] && !_condition[ConditionFlag.Mounted] && + _nestedExecutor is { Triggered: false, Executor: Mount.MountExecutor mountExecutor } && + mountExecutor.EvaluateMountState() == Mount.MountResult.WhenOutOfCombat) + { + return true; + } + + return base.WasInterrupted(); + } + + public override bool ShouldInterruptOnDamage() + { + // have we stopped moving, and are we + // (a) waiting for a mount to complete, or + // (b) want combat to be done before any other interaction? + return _movementController is { IsPathfinding: false, IsPathRunning: false } && + (_nestedExecutor is { Triggered: false, Executor: Mount.MountExecutor } || ShouldResolveCombatBeforeNextInteraction()); + } + + private bool ShouldResolveCombatBeforeNextInteraction() => Task.InteractionType is EInteractionType.Jump; + + public bool OnErrorToast(SeString message) + { + if (GameFunctions.GameStringEquals(_cannotExecuteAtThisTime, message.TextValue)) + return true; + + return false; + } +} diff --git a/Questionable/Controller/Steps/Movement/MoveTask.cs b/Questionable/Controller/Steps/Movement/MoveTask.cs new file mode 100644 index 00000000..a6936bd3 --- /dev/null +++ b/Questionable/Controller/Steps/Movement/MoveTask.cs @@ -0,0 +1,40 @@ +using System.Globalization; +using System.Numerics; +using Questionable.Model.Questing; + +namespace Questionable.Controller.Steps.Movement; + +internal sealed record MoveTask( + ushort TerritoryId, + Vector3 Destination, + bool? Mount = null, + float? StopDistance = null, + uint? DataId = null, + bool DisableNavmesh = false, + bool Sprint = true, + bool Fly = false, + bool Land = false, + bool IgnoreDistanceToObject = false, + bool RestartNavigation = true, + EInteractionType InteractionType = EInteractionType.None) : ITask +{ + public MoveTask(QuestStep step, Vector3 destination) + : this(step.TerritoryId, + destination, + step.Mount, + step.CalculateActualStopDistance(), + step.DataId, + step.DisableNavmesh, + step.Sprint != false, + step.Fly == true, + step.Land == true, + step.IgnoreDistanceToObject == true, + step.RestartNavigationIfCancelled != false, + step.InteractionType) + { + } + + public bool ShouldRedoOnInterrupt() => true; + + public override string ToString() => $"MoveTo({Destination.ToString("G", CultureInfo.InvariantCulture)})"; +} diff --git a/Questionable/Controller/Steps/Movement/MoveTo.cs b/Questionable/Controller/Steps/Movement/MoveTo.cs new file mode 100644 index 00000000..05d8684e --- /dev/null +++ b/Questionable/Controller/Steps/Movement/MoveTo.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using Microsoft.Extensions.Logging; +using Questionable.Controller.Steps.Common; +using Questionable.Data; +using Questionable.Functions; +using Questionable.Model; +using Questionable.Model.Questing; + +namespace Questionable.Controller.Steps.Movement; + +internal static class MoveTo +{ + internal sealed class Factory( + IClientState clientState, + AetheryteData aetheryteData, + TerritoryData territoryData, + ILogger logger) : ITaskFactory + { + public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) + { + if (step.Position != null) + { + return CreateMoveTasks(step, step.Position.Value); + } + else if (step is { DataId: not null, StopDistance: not null }) + { + return [new WaitForNearDataId(step.DataId.Value, step.StopDistance.Value)]; + } + else if (step is { InteractionType: EInteractionType.AttuneAetheryte, Aetheryte: not null }) + { + return CreateMoveTasks(step, aetheryteData.Locations[step.Aetheryte.Value]); + } + else if (step is { InteractionType: EInteractionType.AttuneAethernetShard, AethernetShard: not null }) + { + return CreateMoveTasks(step, aetheryteData.Locations[step.AethernetShard.Value]); + } + + return []; + } + + private IEnumerable CreateMoveTasks(QuestStep step, Vector3 destination) + { + if (step.InteractionType == EInteractionType.Jump && step.JumpDestination != null && + (clientState.LocalPlayer!.Position - step.JumpDestination.Position).Length() <= + (step.JumpDestination.StopDistance ?? 1f)) + { + logger.LogInformation("We're at the jump destination, skipping movement"); + yield break; + } + + yield return new WaitCondition.Task(() => clientState.TerritoryType == step.TerritoryId, + $"Wait(territory: {territoryData.GetNameAndId(step.TerritoryId)})"); + + if (!step.DisableNavmesh) + yield return new WaitNavmesh.Task(); + + yield return new MoveTask(step, destination); + + if (step is { Fly: true, Land: true }) + yield return new LandTask(); + } + } + + internal sealed record WaitForNearDataId(uint DataId, float StopDistance) : ITask + { + public bool ShouldRedoOnInterrupt() => true; + } + + internal sealed class WaitForNearDataIdExecutor( + GameFunctions gameFunctions, + IClientState clientState) : TaskExecutor + { + protected override bool Start() => true; + + public override ETaskResult Update() + { + IGameObject? gameObject = gameFunctions.FindObjectByDataId(Task.DataId); + if (gameObject == null || + (gameObject.Position - clientState.LocalPlayer!.Position).Length() > Task.StopDistance) + { + throw new TaskException("Object not found or too far away, no position so we can't move"); + } + + return ETaskResult.TaskComplete; + } + + public override bool ShouldInterruptOnDamage() => false; + } + + internal sealed class LandTask : ITask + { + public bool ShouldRedoOnInterrupt() => true; + public override string ToString() => "Land"; + } + + internal sealed class LandExecutor(IClientState clientState, ICondition condition, ILogger logger) + : TaskExecutor + { + private bool _landing; + private DateTime _continueAt; + + protected override bool Start() + { + if (!condition[ConditionFlag.InFlight]) + { + logger.LogInformation("Not flying, not attempting to land"); + return false; + } + + _landing = AttemptLanding(); + _continueAt = DateTime.Now.AddSeconds(0.25); + return true; + } + + public override ETaskResult Update() + { + if (DateTime.Now < _continueAt) + return ETaskResult.StillRunning; + + if (condition[ConditionFlag.InFlight]) + { + if (!_landing) + { + _landing = AttemptLanding(); + _continueAt = DateTime.Now.AddSeconds(0.25); + } + + return ETaskResult.StillRunning; + } + + return ETaskResult.TaskComplete; + } + + private unsafe bool AttemptLanding() + { + var character = (Character*)(clientState.LocalPlayer?.Address ?? 0); + if (character != null) + { + if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23) == 0) + { + logger.LogInformation("Attempting to land"); + return ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23); + } + } + + return false; + } + + public override bool ShouldInterruptOnDamage() => false; + } +} diff --git a/Questionable/Controller/Steps/Movement/NoOpTask.cs b/Questionable/Controller/Steps/Movement/NoOpTask.cs new file mode 100644 index 00000000..eebdc005 --- /dev/null +++ b/Questionable/Controller/Steps/Movement/NoOpTask.cs @@ -0,0 +1,3 @@ +namespace Questionable.Controller.Steps.Movement; + +internal sealed record NoOpTask : ITask; diff --git a/Questionable/Controller/Steps/Movement/NoOpTaskExecutor.cs b/Questionable/Controller/Steps/Movement/NoOpTaskExecutor.cs new file mode 100644 index 00000000..3cccf51d --- /dev/null +++ b/Questionable/Controller/Steps/Movement/NoOpTaskExecutor.cs @@ -0,0 +1,10 @@ +namespace Questionable.Controller.Steps.Movement; + +internal sealed class NoOpTaskExecutor : TaskExecutor +{ + protected override bool Start() => true; + + public override ETaskResult Update() => ETaskResult.TaskComplete; + + public override bool ShouldInterruptOnDamage() => false; +} diff --git a/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs b/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs index 943265e4..37561b33 100644 --- a/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs +++ b/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs @@ -7,6 +7,7 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps.Common; +using Questionable.Controller.Steps.Movement; using Questionable.Controller.Utils; using Questionable.Data; using Questionable.Functions; @@ -250,7 +251,7 @@ internal static class AetheryteShortcut } internal sealed class MoveAwayFromAetheryteExecutor( - MoveTo.MoveExecutor moveExecutor, + MoveExecutor moveExecutor, AetheryteData aetheryteData, IClientState clientState) : TaskExecutor { @@ -278,7 +279,7 @@ internal static class AetheryteShortcut Vector3 closestPoint = AetherytesToMoveFrom[Task.TargetAetheryte] .MinBy(x => Vector3.Distance(x, playerPosition)); - MoveTo.MoveTask task = new MoveTo.MoveTask(aetheryteData.TerritoryIds[Task.TargetAetheryte], + MoveTask task = new MoveTask(aetheryteData.TerritoryIds[Task.TargetAetheryte], closestPoint, Mount: false, StopDistance: 0.25f, DisableNavmesh: true, InteractionType: EInteractionType.None, RestartNavigation: false); return moveExecutor.Start(task); diff --git a/Questionable/Controller/Steps/Shared/MoveTo.cs b/Questionable/Controller/Steps/Shared/MoveTo.cs deleted file mode 100644 index c10cb29e..00000000 --- a/Questionable/Controller/Steps/Shared/MoveTo.cs +++ /dev/null @@ -1,459 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Numerics; -using Dalamud.Game.ClientState.Conditions; -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Game; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using LLib; -using Lumina.Excel.Sheets; -using Microsoft.Extensions.Logging; -using Questionable.Controller.Steps.Common; -using Questionable.Data; -using Questionable.Functions; -using Questionable.Model; -using Questionable.Model.Questing; -using Action = System.Action; -using Mount = Questionable.Controller.Steps.Common.Mount; -using Quest = Questionable.Model.Quest; - -namespace Questionable.Controller.Steps.Shared; - -internal static class MoveTo -{ - internal sealed class Factory( - IClientState clientState, - AetheryteData aetheryteData, - TerritoryData territoryData, - ILogger logger) : ITaskFactory - { - public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) - { - if (step.Position != null) - { - return CreateMoveTasks(step, step.Position.Value); - } - else if (step is { DataId: not null, StopDistance: not null }) - { - return [new WaitForNearDataId(step.DataId.Value, step.StopDistance.Value)]; - } - else if (step is { InteractionType: EInteractionType.AttuneAetheryte, Aetheryte: not null }) - { - return CreateMoveTasks(step, aetheryteData.Locations[step.Aetheryte.Value]); - } - else if (step is { InteractionType: EInteractionType.AttuneAethernetShard, AethernetShard: not null }) - { - return CreateMoveTasks(step, aetheryteData.Locations[step.AethernetShard.Value]); - } - - return []; - } - - private IEnumerable CreateMoveTasks(QuestStep step, Vector3 destination) - { - if (step.InteractionType == EInteractionType.Jump && step.JumpDestination != null && - (clientState.LocalPlayer!.Position - step.JumpDestination.Position).Length() <= - (step.JumpDestination.StopDistance ?? 1f)) - { - logger.LogInformation("We're at the jump destination, skipping movement"); - yield break; - } - - yield return new WaitCondition.Task(() => clientState.TerritoryType == step.TerritoryId, - $"Wait(territory: {territoryData.GetNameAndId(step.TerritoryId)})"); - - if (!step.DisableNavmesh) - yield return new WaitNavmesh.Task(); - - yield return new MoveTask(step, destination); - - if (step is { Fly: true, Land: true }) - yield return new LandTask(); - } - } - - internal sealed class MoveExecutor : TaskExecutor, IToastAware - { - private readonly string _cannotExecuteAtThisTime; - private readonly MovementController _movementController; - private readonly GameFunctions _gameFunctions; - private readonly ILogger _logger; - private readonly IClientState _clientState; - private readonly ICondition _condition; - private readonly Mount.MountExecutor _mountExecutor; - private readonly Mount.UnmountExecutor _unmountExecutor; - - private Action? _startAction; - private Vector3 _destination; - private bool _canRestart; - - private (ITaskExecutor Executor, ITask Task, bool Triggered)? _nestedExecutor = - (new NoOpTaskExecutor(), new NoOpTask(), true); - - public MoveExecutor( - MovementController movementController, - GameFunctions gameFunctions, - ILogger logger, - IClientState clientState, - ICondition condition, - IDataManager dataManager, - Mount.MountExecutor mountExecutor, - Mount.UnmountExecutor unmountExecutor) - { - _movementController = movementController; - _gameFunctions = gameFunctions; - _logger = logger; - _clientState = clientState; - _condition = condition; - _mountExecutor = mountExecutor; - _unmountExecutor = unmountExecutor; - _cannotExecuteAtThisTime = dataManager.GetString(579, x => x.Text)!; - } - - private void PrepareMovementIfNeeded() - { - if (!_gameFunctions.IsFlyingUnlocked(Task.TerritoryId)) - { - Task = Task with { Fly = false, Land = false }; - } - - if (!Task.DisableNavmesh) - { - _startAction = () => - _movementController.NavigateTo(EMovementType.Quest, Task.DataId, _destination, - fly: Task.Fly, - sprint: Task.Sprint, - stopDistance: Task.StopDistance, - ignoreDistanceToObject: Task.IgnoreDistanceToObject, - land: Task.Land); - } - else - { - _startAction = () => - _movementController.NavigateTo(EMovementType.Quest, Task.DataId, [_destination], - fly: Task.Fly, - sprint: Task.Sprint, - stopDistance: Task.StopDistance, - ignoreDistanceToObject: Task.IgnoreDistanceToObject, - land: Task.Land); - } - } - - protected override bool Start() - { - _canRestart = Task.RestartNavigation; - _destination = Task.Destination; - - - float stopDistance = Task.StopDistance ?? QuestStep.DefaultStopDistance; - Vector3? position = _clientState.LocalPlayer?.Position; - float actualDistance = position == null ? float.MaxValue : Vector3.Distance(position.Value, _destination); - bool requiresMovement = actualDistance > stopDistance; - if (requiresMovement) - PrepareMovementIfNeeded(); - - // might be able to make this optional - if (Task.Mount == true) - { - var mountTask = new Mount.MountTask(Task.TerritoryId, Mount.EMountIf.Always); - if (_mountExecutor.Start(mountTask)) - { - _nestedExecutor = (_mountExecutor, mountTask, true); - return true; - } - else if (_mountExecutor.EvaluateMountState() == Mount.MountResult.WhenOutOfCombat) - _nestedExecutor = (_mountExecutor, mountTask, false); - } - else if (Task.Mount == false) - { - var mountTask = new Mount.UnmountTask(); - if (_unmountExecutor.Start(mountTask)) - { - _nestedExecutor = (_unmountExecutor, mountTask, true); - return true; - } - } - - if (!Task.DisableNavmesh) - { - if (Task.Mount == null) - { - Mount.EMountIf mountIf = - actualDistance > stopDistance && Task.Fly && - _gameFunctions.IsFlyingUnlocked(Task.TerritoryId) - ? Mount.EMountIf.Always - : Mount.EMountIf.AwayFromPosition; - var mountTask = new Mount.MountTask(Task.TerritoryId, mountIf, _destination); - if (_mountExecutor.Start(mountTask)) - { - _nestedExecutor = (_mountExecutor, mountTask, true); - return true; - } - else if (_mountExecutor.EvaluateMountState() == Mount.MountResult.WhenOutOfCombat) - _nestedExecutor = (_mountExecutor, mountTask, false); - } - } - - if (_startAction != null && (_nestedExecutor == null || _nestedExecutor.Value.Triggered == false)) - _startAction(); - return true; - } - - public override ETaskResult Update() - { - if (_nestedExecutor is { } nestedExecutor) - { - if (nestedExecutor is { Triggered: false, Executor: Mount.MountExecutor mountExecutor }) - { - if (!_condition[ConditionFlag.InCombat]) - { - if (mountExecutor.EvaluateMountState() == Mount.MountResult.DontMount) - _nestedExecutor = (new NoOpTaskExecutor(), new NoOpTask(), true); - else - { - if (_movementController.IsPathfinding || _movementController.IsPathRunning) - _movementController.Stop(); - - if (nestedExecutor.Executor.Start(nestedExecutor.Task)) - { - _nestedExecutor = nestedExecutor with { Triggered = true }; - return ETaskResult.StillRunning; - } - } - } - else if (!ShouldResolveCombatBeforeNextInteraction() && - _movementController is { IsPathfinding: false, IsPathRunning: false } && - mountExecutor.EvaluateMountState() == Mount.MountResult.DontMount) - { - // except for e.g. jumping which would maybe break if combat navigates us away, if we don't - // need a mount anymore we can just skip combat and assume that the interruption is handled - // later. - // - // without this, the character would just stand around while getting hit - _nestedExecutor = (new NoOpTaskExecutor(), new NoOpTask(), true); - } - } - else if (nestedExecutor.Executor.Update() == ETaskResult.TaskComplete) - { - _nestedExecutor = null; - if (_startAction != null) - { - _logger.LogInformation("Moving to {Destination}", - _destination.ToString("G", CultureInfo.InvariantCulture)); - _startAction(); - } - else - return ETaskResult.TaskComplete; - } - else if (!_condition[ConditionFlag.Mounted] && _condition[ConditionFlag.InCombat] && - nestedExecutor is { Triggered: true, Executor: Mount.MountExecutor }) - { - // if the problem wasn't caused by combat, the normal mount retry should handle it - _logger.LogDebug("Resetting mount trigger state"); - _nestedExecutor = nestedExecutor with { Triggered = false }; - - // however, we're also explicitly ignoring combat here and walking away - _startAction?.Invoke(); - } - - return ETaskResult.StillRunning; - } - - if (_startAction == null) - return ETaskResult.TaskComplete; - - if (_movementController.IsPathfinding || _movementController.IsPathRunning) - return ETaskResult.StillRunning; - - DateTime movementStartedAt = _movementController.MovementStartedAt; - if (movementStartedAt == DateTime.MaxValue || movementStartedAt.AddSeconds(2) >= DateTime.Now) - return ETaskResult.StillRunning; - - if (_canRestart && - Vector3.Distance(_clientState.LocalPlayer!.Position, _destination) > - (Task.StopDistance ?? QuestStep.DefaultStopDistance) + 5f) - { - _canRestart = false; - if (_clientState.TerritoryType == Task.TerritoryId) - { - _logger.LogInformation("Looks like movement was interrupted, re-attempting to move"); - _startAction(); - return ETaskResult.StillRunning; - } - else - _logger.LogInformation( - "Looks like movement was interrupted, do nothing since we're in a different territory now"); - } - - return ETaskResult.TaskComplete; - } - - public override bool WasInterrupted() - { - if (Task.Fly && _condition[ConditionFlag.InCombat] && !_condition[ConditionFlag.Mounted] && - _nestedExecutor is { Triggered: false, Executor: Mount.MountExecutor mountExecutor } && - mountExecutor.EvaluateMountState() == Mount.MountResult.WhenOutOfCombat) - { - return true; - } - - return base.WasInterrupted(); - } - - public override bool ShouldInterruptOnDamage() - { - // have we stopped moving, and are we - // (a) waiting for a mount to complete, or - // (b) want combat to be done before any other interaction? - return _movementController is { IsPathfinding: false, IsPathRunning: false } && - (_nestedExecutor is { Triggered: false, Executor: Mount.MountExecutor } || ShouldResolveCombatBeforeNextInteraction()); - } - - private bool ShouldResolveCombatBeforeNextInteraction() => Task.InteractionType is EInteractionType.Jump; - - public bool OnErrorToast(SeString message) - { - if (GameFunctions.GameStringEquals(_cannotExecuteAtThisTime, message.TextValue)) - return true; - - return false; - } - } - - private sealed record NoOpTask : ITask; - - private sealed class NoOpTaskExecutor : TaskExecutor - { - protected override bool Start() => true; - - public override ETaskResult Update() => ETaskResult.TaskComplete; - - public override bool ShouldInterruptOnDamage() => false; - } - - internal sealed record MoveTask( - ushort TerritoryId, - Vector3 Destination, - bool? Mount = null, - float? StopDistance = null, - uint? DataId = null, - bool DisableNavmesh = false, - bool Sprint = true, - bool Fly = false, - bool Land = false, - bool IgnoreDistanceToObject = false, - bool RestartNavigation = true, - EInteractionType InteractionType = EInteractionType.None) : ITask - { - public MoveTask(QuestStep step, Vector3 destination) - : this(step.TerritoryId, - destination, - step.Mount, - step.CalculateActualStopDistance(), - step.DataId, - step.DisableNavmesh, - step.Sprint != false, - step.Fly == true, - step.Land == true, - step.IgnoreDistanceToObject == true, - step.RestartNavigationIfCancelled != false, - step.InteractionType) - { - } - - public bool ShouldRedoOnInterrupt() => true; - - public override string ToString() => $"MoveTo({Destination.ToString("G", CultureInfo.InvariantCulture)})"; - } - - internal sealed record WaitForNearDataId(uint DataId, float StopDistance) : ITask - { - public bool ShouldRedoOnInterrupt() => true; - } - - internal sealed class WaitForNearDataIdExecutor( - GameFunctions gameFunctions, - IClientState clientState) : TaskExecutor - { - protected override bool Start() => true; - - public override ETaskResult Update() - { - IGameObject? gameObject = gameFunctions.FindObjectByDataId(Task.DataId); - if (gameObject == null || - (gameObject.Position - clientState.LocalPlayer!.Position).Length() > Task.StopDistance) - { - throw new TaskException("Object not found or too far away, no position so we can't move"); - } - - return ETaskResult.TaskComplete; - } - - public override bool ShouldInterruptOnDamage() => false; - } - - internal sealed class LandTask : ITask - { - public bool ShouldRedoOnInterrupt() => true; - public override string ToString() => "Land"; - } - - internal sealed class LandExecutor(IClientState clientState, ICondition condition, ILogger logger) - : TaskExecutor - { - private bool _landing; - private DateTime _continueAt; - - protected override bool Start() - { - if (!condition[ConditionFlag.InFlight]) - { - logger.LogInformation("Not flying, not attempting to land"); - return false; - } - - _landing = AttemptLanding(); - _continueAt = DateTime.Now.AddSeconds(0.25); - return true; - } - - public override ETaskResult Update() - { - if (DateTime.Now < _continueAt) - return ETaskResult.StillRunning; - - if (condition[ConditionFlag.InFlight]) - { - if (!_landing) - { - _landing = AttemptLanding(); - _continueAt = DateTime.Now.AddSeconds(0.25); - } - - return ETaskResult.StillRunning; - } - - return ETaskResult.TaskComplete; - } - - private unsafe bool AttemptLanding() - { - var character = (Character*)(clientState.LocalPlayer?.Address ?? 0); - if (character != null) - { - if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23) == 0) - { - logger.LogInformation("Attempting to land"); - return ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23); - } - } - - return false; - } - - public override bool ShouldInterruptOnDamage() => false; - } -} diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index 81102938..ef153105 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -20,6 +20,7 @@ using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Gathering; using Questionable.Controller.Steps.Interactions; using Questionable.Controller.Steps.Leves; +using Questionable.Controller.Steps.Movement; using Questionable.Controller.Utils; using Questionable.Data; using Questionable.External; @@ -174,7 +175,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin AethernetShortcut.UseAethernetShortcut>(); serviceCollection .AddTaskFactoryAndExecutor(); - serviceCollection.AddTaskFactoryAndExecutor(); + serviceCollection.AddTaskFactoryAndExecutor(); serviceCollection.AddTaskExecutor(); serviceCollection.AddTaskExecutor(); serviceCollection -- 2.30.2