Split 'MoveTo' into separate classes
authorLiza Carvelli <liza@carvel.li>
Mon, 31 Mar 2025 00:13:08 +0000 (02:13 +0200)
committerLiza Carvelli <liza@carvel.li>
Mon, 31 Mar 2025 00:13:08 +0000 (02:13 +0200)
Questionable/Controller/GatheringController.cs
Questionable/Controller/Steps/Gathering/MoveToLandingLocation.cs
Questionable/Controller/Steps/Interactions/UseItem.cs
Questionable/Controller/Steps/Movement/MoveExecutor.cs [new file with mode: 0644]
Questionable/Controller/Steps/Movement/MoveTask.cs [new file with mode: 0644]
Questionable/Controller/Steps/Movement/MoveTo.cs [new file with mode: 0644]
Questionable/Controller/Steps/Movement/NoOpTask.cs [new file with mode: 0644]
Questionable/Controller/Steps/Movement/NoOpTaskExecutor.cs [new file with mode: 0644]
Questionable/Controller/Steps/Shared/AetheryteShortcut.cs
Questionable/Controller/Steps/Shared/MoveTo.cs [deleted file]
Questionable/QuestionablePlugin.cs

index 8968ee0..b9248b0 100644 (file)
@@ -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<GatheringC
             if (pointOnFloor != null)
                 pointOnFloor = pointOnFloor.Value with { Y = pointOnFloor.Value.Y + (fly ? 3f : 0f) };
 
-            _taskQueue.Enqueue(new MoveTo.MoveTask(territoryId, pointOnFloor ?? averagePosition,
+            _taskQueue.Enqueue(new MoveTask(territoryId, pointOnFloor ?? averagePosition,
                 null, 50f, Fly: fly, IgnoreDistanceToObject: true, InteractionType: EInteractionType.WalkTo));
         }
 
index bc18301..5d82cb3 100644 (file)
@@ -5,7 +5,7 @@ using Dalamud.Game.ClientState.Objects.Enums;
 using Dalamud.Game.Text.SeStringHandling;
 using Dalamud.Plugin.Services;
 using Microsoft.Extensions.Logging;
-using Questionable.Controller.Steps.Shared;
+using Questionable.Controller.Steps.Movement;
 using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Gathering;
@@ -24,7 +24,7 @@ internal static class MoveToLandingLocation
     }
 
     internal sealed class MoveToLandingLocationExecutor(
-        MoveTo.MoveExecutor moveExecutor,
+        MoveExecutor moveExecutor,
         GameFunctions gameFunctions,
         IObjectTable objectTable,
         ILogger<MoveToLandingLocationExecutor> logger) : TaskExecutor<Task>, 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);
index abc427a..25799a7 100644 (file)
@@ -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 (file)
index 0000000..99e26d3
--- /dev/null
@@ -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<MoveTask>, IToastAware
+{
+    private readonly string _cannotExecuteAtThisTime;
+    private readonly MovementController _movementController;
+    private readonly GameFunctions _gameFunctions;
+    private readonly ILogger<MoveExecutor> _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<MoveExecutor> 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<LogMessage>(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 (file)
index 0000000..a6936bd
--- /dev/null
@@ -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 (file)
index 0000000..05d8684
--- /dev/null
@@ -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<Factory> logger) : ITaskFactory
+    {
+        public IEnumerable<ITask> 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<ITask> 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<WaitForNearDataId>
+    {
+        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<LandExecutor> logger)
+        : TaskExecutor<LandTask>
+    {
+        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 (file)
index 0000000..eebdc00
--- /dev/null
@@ -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 (file)
index 0000000..3cccf51
--- /dev/null
@@ -0,0 +1,10 @@
+namespace Questionable.Controller.Steps.Movement;
+
+internal sealed class NoOpTaskExecutor : TaskExecutor<NoOpTask>
+{
+    protected override bool Start() => true;
+
+    public override ETaskResult Update() => ETaskResult.TaskComplete;
+
+    public override bool ShouldInterruptOnDamage() => false;
+}
index 943265e..37561b3 100644 (file)
@@ -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<MoveAwayFromAetheryte>
     {
@@ -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 (file)
index c10cb29..0000000
+++ /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<Factory> logger) : ITaskFactory
-    {
-        public IEnumerable<ITask> 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<ITask> 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<MoveTask>, IToastAware
-    {
-        private readonly string _cannotExecuteAtThisTime;
-        private readonly MovementController _movementController;
-        private readonly GameFunctions _gameFunctions;
-        private readonly ILogger<MoveExecutor> _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<MoveExecutor> 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<LogMessage>(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<NoOpTask>
-    {
-        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<WaitForNearDataId>
-    {
-        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<LandExecutor> logger)
-        : TaskExecutor<LandTask>
-    {
-        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;
-    }
-}
index 8110293..ef15310 100644 (file)
@@ -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<WaitAtStart.WaitDelay, WaitAtStart.Factory, WaitAtStart.WaitDelayExecutor>();
-        serviceCollection.AddTaskFactoryAndExecutor<MoveTo.MoveTask, MoveTo.Factory, MoveTo.MoveExecutor>();
+        serviceCollection.AddTaskFactoryAndExecutor<MoveTask, MoveTo.Factory, MoveExecutor>();
         serviceCollection.AddTaskExecutor<MoveTo.WaitForNearDataId, MoveTo.WaitForNearDataIdExecutor>();
         serviceCollection.AddTaskExecutor<MoveTo.LandTask, MoveTo.LandExecutor>();
         serviceCollection