else
{
var nextTarget = FindNextTarget();
- _logger.LogInformation("NT → {NT}", nextTarget);
-
if (nextTarget is { IsDead: false })
SetTarget(nextTarget);
}
private readonly ILoggerFactory _loggerFactory;
private readonly IGameGui _gameGui;
private readonly IClientState _clientState;
+ private readonly ILogger<GatheringController> _logger;
private readonly Regex _revisitRegex;
private CurrentRequest? _currentRequest;
MovementController movementController,
MoveTo.Factory moveFactory,
Mount.Factory mountFactory,
+ Combat.Factory combatFactory,
Interact.Factory interactFactory,
GatheringPointRegistry gatheringPointRegistry,
GameFunctions gameFunctions,
IGameGui gameGui,
IClientState clientState,
IPluginLog pluginLog)
- : base(chatGui, logger)
+ : base(chatGui, mountFactory, combatFactory, condition, logger)
{
_movementController = movementController;
_moveFactory = moveFactory;
_loggerFactory = loggerFactory;
_gameGui = gameGui;
_clientState = clientState;
+ _logger = logger;
_revisitRegex = dataManager.GetRegex<LogMessage>(5574, x => x.Text, pluginLog)
?? throw new InvalidDataException("No regex found for revisit message");
using System;
using System.Collections.Generic;
using System.Linq;
+using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps;
+using Questionable.Controller.Steps.Common;
+using Questionable.Controller.Steps.Interactions;
using Questionable.Controller.Steps.Shared;
+using Questionable.Model.Questing;
namespace Questionable.Controller;
internal abstract class MiniTaskController<T>
{
- protected readonly IChatGui _chatGui;
- protected readonly ILogger<T> _logger;
protected readonly TaskQueue _taskQueue = new();
- protected MiniTaskController(IChatGui chatGui, ILogger<T> logger)
+ private readonly IChatGui _chatGui;
+ private readonly Mount.Factory _mountFactory;
+ private readonly Combat.Factory _combatFactory;
+ private readonly ICondition _condition;
+ private readonly ILogger<T> _logger;
+
+ protected MiniTaskController(IChatGui chatGui, Mount.Factory mountFactory, Combat.Factory combatFactory,
+ ICondition condition, ILogger<T> logger)
{
_chatGui = chatGui;
_logger = logger;
+ _mountFactory = mountFactory;
+ _combatFactory = combatFactory;
+ _condition = condition;
}
protected virtual void UpdateCurrentTask()
ETaskResult result;
try
{
+ if (_taskQueue.CurrentTask.WasInterrupted())
+ {
+ InterruptQueueWithCombat();
+ return;
+ }
+
result = _taskQueue.CurrentTask.Update();
}
catch (Exception e)
protected virtual void OnNextStep(ILastTask task)
{
-
}
public abstract void Stop(string label);
public virtual IList<string> GetRemainingTaskNames() =>
_taskQueue.RemainingTasks.Select(x => x.ToString() ?? "?").ToList();
+
+ public void InterruptQueueWithCombat()
+ {
+ _logger.LogWarning("Interrupted, attempting to resolve (if in combat)");
+ if (_condition[ConditionFlag.InCombat])
+ {
+ List<ITask> tasks = [];
+ if (_condition[ConditionFlag.Mounted])
+ tasks.Add(_mountFactory.Unmount());
+
+ tasks.Add(_combatFactory.CreateTask(null, false, EEnemySpawnType.QuestInterruption, [], [], []));
+ tasks.Add(new WaitAtEnd.WaitDelay());
+ _taskQueue.InterruptWith(tasks);
+ }
+ else
+ _taskQueue.InterruptWith([new WaitAtEnd.WaitDelay()]);
+ }
}
private readonly GatheringController _gatheringController;
private readonly QuestRegistry _questRegistry;
private readonly IKeyState _keyState;
+ private readonly IChatGui _chatGui;
private readonly ICondition _condition;
private readonly IToastGui _toastGui;
private readonly Configuration _configuration;
private readonly YesAlreadyIpc _yesAlreadyIpc;
private readonly TaskCreator _taskCreator;
- private readonly Mount.Factory _mountFactory;
- private readonly Combat.Factory _combatFactory;
+ private readonly ILogger<QuestController> _logger;
private readonly string _actionCanceledText;
Mount.Factory mountFactory,
Combat.Factory combatFactory,
IDataManager dataManager)
- : base(chatGui, logger)
+ : base(chatGui, mountFactory, combatFactory, condition, logger)
{
_clientState = clientState;
_gameFunctions = gameFunctions;
_gatheringController = gatheringController;
_questRegistry = questRegistry;
_keyState = keyState;
+ _chatGui = chatGui;
_condition = condition;
_toastGui = toastGui;
_configuration = configuration;
_yesAlreadyIpc = yesAlreadyIpc;
_taskCreator = taskCreator;
- _mountFactory = mountFactory;
- _combatFactory = combatFactory;
+ _logger = logger;
_condition.ConditionChange += OnConditionChange;
_toastGui.Toast += OnNormalToast;
}
public bool IsRunning => !_taskQueue.AllTasksComplete;
+ public TaskQueue TaskQueue => _taskQueue;
public sealed class QuestProgress
{
}
}
- public void InterruptQueueWithCombat()
- {
- _logger.LogWarning("Interrupted with action canceled message, attempting to resolve");
- List<ITask> tasks = [];
- if (_condition[ConditionFlag.Mounted])
- tasks.Add(_mountFactory.Unmount());
-
- tasks.Add(_combatFactory.CreateTask(null, false, EEnemySpawnType.QuestInterruption, [], [], []));
- tasks.Add(new WaitAtEnd.WaitDelay());
- _taskQueue.InterruptWith(tasks);
- }
-
public void Dispose()
{
_toastGui.ErrorToast -= OnErrorToast;
{
}
+ public virtual InteractionProgressContext? ProgressContext() => null;
+
public bool Start()
{
_continueAt = DateTime.Now.Add(Delay);
ILogger<MountTask> logger) : ITask
{
private bool _mountTriggered;
+ private InteractionProgressContext? _progressContext;
private DateTime _retryAt = DateTime.MinValue;
+ public InteractionProgressContext? ProgressContext() => _progressContext;
+
public bool ShouldRedoOnInterrupt() => true;
public bool Start()
return ETaskResult.TaskComplete;
}
- _mountTriggered = gameFunctions.Mount();
+ _progressContext = InteractionProgressContext.FromActionUse(() => _mountTriggered = gameFunctions.Mount());
+
_retryAt = DateTime.Now.AddSeconds(5);
return ETaskResult.StillRunning;
}
internal interface ITask
{
+ InteractionProgressContext? ProgressContext() => null;
+
+ bool WasInterrupted()
+ {
+ var progressContext = ProgressContext();
+ if (progressContext != null)
+ {
+ progressContext.Update();
+ return progressContext.WasInterrupted();
+ }
+
+ return false;
+ }
+
bool ShouldRedoOnInterrupt() => false;
bool Start();
--- /dev/null
+using System;
+using FFXIVClientStructs.FFXIV.Client.Game;
+
+namespace Questionable.Controller.Steps;
+
+internal sealed class InteractionProgressContext
+{
+ private bool _firstUpdateDone;
+ public bool CheckSequence { get; private set; }
+ public int CurrentSequence { get; private set; }
+
+ private InteractionProgressContext(bool checkSequence, int currentSequence)
+ {
+ CheckSequence = checkSequence;
+ CurrentSequence = currentSequence;
+ }
+
+ public static unsafe InteractionProgressContext Create(bool checkSequence)
+ {
+ if (!checkSequence)
+ {
+ // this is a silly hack; we assume that the previous cast was successful
+ // if not for this, we'd instantly be seen as interrupted
+ ActionManager.Instance()->CastTimeElapsed = ActionManager.Instance()->CastTimeTotal;
+ }
+
+ return new InteractionProgressContext(checkSequence, ActionManager.Instance()->LastUsedActionSequence);
+ }
+
+ private static unsafe (bool, InteractionProgressContext?) FromActionUseInternal(Func<bool> func)
+ {
+ int oldSequence = ActionManager.Instance()->LastUsedActionSequence;
+ if (!func())
+ return (false, null);
+ int newSequence = ActionManager.Instance()->LastUsedActionSequence;
+ if (oldSequence == newSequence)
+ return (true, null);
+ return (true, Create(true));
+ }
+
+ public static InteractionProgressContext? FromActionUse(Func<bool> func)
+ {
+ return FromActionUseInternal(func).Item2;
+ }
+
+ public static InteractionProgressContext? FromActionUseOrDefault(Func<bool> func)
+ {
+ var result = FromActionUseInternal(func);
+ if (!result.Item1)
+ return null;
+ return result.Item2 ?? Create(false);
+ }
+
+ public unsafe void Update()
+ {
+ if (!_firstUpdateDone)
+ {
+ int lastSequence = ActionManager.Instance()->LastUsedActionSequence;
+ if (!CheckSequence && lastSequence > CurrentSequence)
+ {
+ CheckSequence = true;
+ CurrentSequence = lastSequence;
+ }
+
+ _firstUpdateDone = true;
+ }
+ }
+
+ public unsafe bool WasSuccessful()
+ {
+ if (CheckSequence)
+ {
+ if (CurrentSequence != ActionManager.Instance()->LastUsedActionSequence ||
+ CurrentSequence != ActionManager.Instance()->LastHandledActionSequence)
+ return false;
+ }
+
+ return ActionManager.Instance()->CastTimeElapsed > 0 &&
+ Math.Abs(ActionManager.Instance()->CastTimeElapsed - ActionManager.Instance()->CastTimeTotal) < 0.001f;
+ }
+
+ public unsafe bool WasInterrupted()
+ {
+ if (CheckSequence)
+ {
+ if (CurrentSequence == ActionManager.Instance()->LastHandledActionSequence &&
+ CurrentSequence == ActionManager.Instance()->LastUsedActionSequence)
+ return false;
+ }
+
+ return ActionManager.Instance()->CastTimeElapsed == 0 &&
+ ActionManager.Instance()->CastTimeTotal > 0;
+ }
+
+ public override string ToString() =>
+ $"IPCtx({(CheckSequence ? CurrentSequence : "-")} - {WasSuccessful()}, {WasInterrupted()})";
+}
return null;
}
- return new DoAttune(step.DataId.Value, step.AetherCurrentId.Value, gameFunctions, loggerFactory.CreateLogger<DoAttune>());
+ return new DoAttune(step.DataId.Value, step.AetherCurrentId.Value, gameFunctions,
+ loggerFactory.CreateLogger<DoAttune>());
}
}
- private sealed class DoAttune(uint dataId, uint aetherCurrentId, GameFunctions gameFunctions, ILogger<DoAttune> logger) : ITask
+ private sealed class DoAttune(
+ uint dataId,
+ uint aetherCurrentId,
+ GameFunctions gameFunctions,
+ ILogger<DoAttune> logger) : ITask
{
+ private InteractionProgressContext? _progressContext;
+
+ public InteractionProgressContext? ProgressContext() => _progressContext;
+
public bool Start()
{
if (!gameFunctions.IsAetherCurrentUnlocked(aetherCurrentId))
{
logger.LogInformation("Attuning to aether current {AetherCurrentId} / {DataId}", aetherCurrentId,
dataId);
- gameFunctions.InteractWith(dataId);
+ _progressContext =
+ InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(dataId));
return true;
}
GameFunctions gameFunctions,
ILogger<DoAttune> logger) : ITask
{
+ private InteractionProgressContext? _progressContext;
+
+ public InteractionProgressContext? ProgressContext() => _progressContext;
+
public bool Start()
{
if (!aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation))
{
logger.LogInformation("Attuning to aethernet shard {AethernetShard}", aetheryteLocation);
- gameFunctions.InteractWith((uint)aetheryteLocation, ObjectKind.Aetheryte);
+ _progressContext = InteractionProgressContext.FromActionUseOrDefault(() =>
+ gameFunctions.InteractWith((uint)aetheryteLocation, ObjectKind.Aetheryte));
return true;
}
GameFunctions gameFunctions,
ILogger<DoAttune> logger) : ITask
{
+ private InteractionProgressContext? _progressContext;
+
+ public InteractionProgressContext? ProgressContext() => _progressContext;
+
public bool Start()
{
if (!aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation))
{
logger.LogInformation("Attuning to aetheryte {Aetheryte}", aetheryteLocation);
- gameFunctions.InteractWith((uint)aetheryteLocation);
+ _progressContext =
+ InteractionProgressContext.FromActionUseOrDefault(() =>
+ gameFunctions.InteractWith((uint)aetheryteLocation));
return true;
}
GameFunctions gameFunctions,
ICondition condition,
ILogger<DoInteract> logger)
- : ITask, IConditionChangeAware
+ : ITask
{
private bool _needsUnmount;
- private EInteractionState _interactionState = EInteractionState.None;
+ private InteractionProgressContext? _progressContext;
private DateTime _continueAt = DateTime.MinValue;
public Quest? Quest => quest;
set => interactionType = value;
}
+ public InteractionProgressContext? ProgressContext() => _progressContext;
+
public bool Start()
{
IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId);
if (gameObject.IsTargetable && HasAnyMarker(gameObject))
{
- _interactionState = gameFunctions.InteractWith(gameObject)
- ? EInteractionState.InteractionTriggered
- : EInteractionState.None;
+ _progressContext =
+ InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(gameObject));
_continueAt = DateTime.Now.AddSeconds(0.5);
return true;
}
}
else
{
- if (_interactionState == EInteractionState.InteractionConfirmed)
+ if (_progressContext != null && _progressContext.WasSuccessful())
return ETaskResult.TaskComplete;
if (interactionType == EInteractionType.Gather && condition[ConditionFlag.Gathering])
if (gameObject == null || !gameObject.IsTargetable || !HasAnyMarker(gameObject))
return ETaskResult.StillRunning;
- _interactionState = gameFunctions.InteractWith(gameObject)
- ? EInteractionState.InteractionTriggered
- : EInteractionState.None;
+ _progressContext =
+ InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(gameObject));
_continueAt = DateTime.Now.AddSeconds(0.5);
return ETaskResult.StillRunning;
}
}
public override string ToString() => $"Interact({dataId})";
-
- public void OnConditionChange(ConditionFlag flag, bool value)
- {
- logger.LogDebug("Condition change: {Flag} = {Value}", flag, value);
- if (_interactionState == EInteractionState.InteractionTriggered &&
- flag is ConditionFlag.OccupiedInQuestEvent or ConditionFlag.OccupiedInEvent &&
- value)
- {
- logger.LogInformation("Interaction was most likely triggered");
- _interactionState = EInteractionState.InteractionConfirmed;
- }
- else if (dataId is >= 1047901 and <= 1047905 &&
- condition[ConditionFlag.Disguised] &&
- flag == ConditionFlag
- .Mounting71 && // why the fuck is this the flag that's used, instead of OccupiedIn[Quest]Event
- value)
- {
- logger.LogInformation("(A Knight of Alexandria) Interaction was most likely triggered");
- _interactionState = EInteractionState.InteractionConfirmed;
- }
- }
-
- private enum EInteractionState
- {
- None,
- InteractionTriggered,
- InteractionConfirmed,
- }
}
}
private bool _usedItem;
private DateTime _continueAt;
private int _itemCount;
+ private InteractionProgressContext? _progressContext;
+
+ public InteractionProgressContext? ProgressContext() => _progressContext;
public ElementId? QuestId => questId;
public uint ItemId => itemId;
if (_itemCount == 0)
throw new TaskException($"Don't have any {ItemId} in inventory (checks NQ only)");
- _usedItem = UseItem();
+ _progressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem());
_continueAt = DateTime.Now.Add(GetRetryDelay());
return true;
}
if (!_usedItem)
{
- _usedItem = UseItem();
+ _progressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem());
_continueAt = DateTime.Now.Add(GetRetryDelay());
return ETaskResult.StillRunning;
}
{
private bool _teleported;
private DateTime _continueAt;
+ private InteractionProgressContext? _progressContext;
+
+ public InteractionProgressContext? ProgressContext() => _progressContext;
public bool Start() => !ShouldSkipTeleport();
chatGui.PrintError($"[Questionable] Aetheryte {targetAetheryte} is not unlocked.");
throw new TaskException("Aetheryte is not unlocked");
}
- else if (aetheryteFunctions.TeleportAetheryte(targetAetheryte))
- {
- logger.LogInformation("Travelling via aetheryte...");
- return true;
- }
else
{
- chatGui.Print("[Questionable] Unable to teleport to aetheryte.");
- throw new TaskException("Unable to teleport to aetheryte");
+ _progressContext =
+ InteractionProgressContext.FromActionUseOrDefault(() => aetheryteFunctions.TeleportAetheryte(targetAetheryte));
+ logger.LogInformation("Ctx = {C}", _progressContext);
+ if (_progressContext != null)
+ {
+ logger.LogInformation("Travelling via aetheryte...");
+ return true;
+ }
+ else
+ {
+ chatGui.Print("[Questionable] Unable to teleport to aetheryte.");
+ throw new TaskException("Unable to teleport to aetheryte");
+ }
}
}
_canRestart = moveParams.RestartNavigation;
}
+ public InteractionProgressContext? ProgressContext() => _mountTask?.ProgressContext();
+
public bool ShouldRedoOnInterrupt() => true;
public bool Start()
internal sealed class TaskQueue
{
+ private readonly List<ITask> _completedTasks = [];
private readonly List<ITask> _tasks = [];
- private int _currentTaskIndex;
public ITask? CurrentTask { get; set; }
- public IEnumerable<ITask> RemainingTasks => _tasks.Skip(_currentTaskIndex);
- public bool AllTasksComplete => CurrentTask == null && _currentTaskIndex >= _tasks.Count;
+ public IEnumerable<ITask> RemainingTasks => _tasks;
+ public bool AllTasksComplete => CurrentTask == null && _tasks.Count == 0;
public void Enqueue(ITask task)
{
public bool TryDequeue([NotNullWhen(true)] out ITask? task)
{
- if (_currentTaskIndex >= _tasks.Count)
- {
- task = null;
+ task = _tasks.FirstOrDefault();
+ if (task == null)
return false;
- }
- task = _tasks[_currentTaskIndex];
if (task.ShouldRedoOnInterrupt())
- _currentTaskIndex++;
- else
- _tasks.RemoveAt(0);
+ _completedTasks.Add(task);
+
+ _tasks.RemoveAt(0);
return true;
}
public bool TryPeek([NotNullWhen(true)] out ITask? task)
{
- if (_currentTaskIndex >= _tasks.Count)
- {
- task = null;
- return false;
- }
-
- task = _tasks[_currentTaskIndex];
- return true;
+ task = _tasks.FirstOrDefault();
+ return task != null;
}
public void Reset()
{
_tasks.Clear();
- _currentTaskIndex = 0;
+ _completedTasks.Clear();
CurrentTask = null;
}
public void InterruptWith(List<ITask> interruptionTasks)
{
- if (CurrentTask != null)
- {
- _tasks.Insert(0, CurrentTask);
- CurrentTask = null;
- _currentTaskIndex = 0;
- }
-
- _tasks.InsertRange(0, interruptionTasks);
+ List<ITask?> newTasks =
+ [
+ ..interruptionTasks,
+ .._completedTasks.Where(x => !ReferenceEquals(x, CurrentTask)).ToList(),
+ CurrentTask,
+ .._tasks
+ ];
+ Reset();
+ _tasks.AddRange(newTasks.Where(x => x != null).Cast<ITask>());
}
}