1
0
forked from liza/Questionable

Handle certain interaction interruptions

This commit is contained in:
Liza 2024-09-18 00:30:56 +02:00
parent 5288cc6e31
commit 721f9617a3
Signed by: liza
GPG Key ID: 7199F8D727D55F67
16 changed files with 244 additions and 104 deletions

View File

@ -114,8 +114,6 @@ internal sealed class CombatController : IDisposable
else
{
var nextTarget = FindNextTarget();
_logger.LogInformation("NT → {NT}", nextTarget);
if (nextTarget is { IsDead: false })
SetTarget(nextTarget);
}

View File

@ -43,6 +43,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
private readonly ILoggerFactory _loggerFactory;
private readonly IGameGui _gameGui;
private readonly IClientState _clientState;
private readonly ILogger<GatheringController> _logger;
private readonly Regex _revisitRegex;
private CurrentRequest? _currentRequest;
@ -51,6 +52,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
MovementController movementController,
MoveTo.Factory moveFactory,
Mount.Factory mountFactory,
Combat.Factory combatFactory,
Interact.Factory interactFactory,
GatheringPointRegistry gatheringPointRegistry,
GameFunctions gameFunctions,
@ -64,7 +66,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
IGameGui gameGui,
IClientState clientState,
IPluginLog pluginLog)
: base(chatGui, logger)
: base(chatGui, mountFactory, combatFactory, condition, logger)
{
_movementController = movementController;
_moveFactory = moveFactory;
@ -78,6 +80,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
_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");

View File

@ -1,23 +1,35 @@
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()
@ -56,6 +68,12 @@ internal abstract class MiniTaskController<T>
ETaskResult result;
try
{
if (_taskQueue.CurrentTask.WasInterrupted())
{
InterruptQueueWithCombat();
return;
}
result = _taskQueue.CurrentTask.Update();
}
catch (Exception e)
@ -122,11 +140,27 @@ internal abstract class MiniTaskController<T>
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()]);
}
}

View File

@ -35,13 +35,13 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
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;
@ -85,7 +85,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
Mount.Factory mountFactory,
Combat.Factory combatFactory,
IDataManager dataManager)
: base(chatGui, logger)
: base(chatGui, mountFactory, combatFactory, condition, logger)
{
_clientState = clientState;
_gameFunctions = gameFunctions;
@ -95,13 +95,13 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
_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;
@ -659,6 +659,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
}
public bool IsRunning => !_taskQueue.AllTasksComplete;
public TaskQueue TaskQueue => _taskQueue;
public sealed class QuestProgress
{
@ -813,18 +814,6 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
}
}
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;

View File

@ -18,6 +18,8 @@ internal abstract class AbstractDelayedTask : ITask
{
}
public virtual InteractionProgressContext? ProgressContext() => null;
public bool Start()
{
_continueAt = DateTime.Now.Add(Delay);

View File

@ -44,8 +44,11 @@ internal static class Mount
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()
@ -108,7 +111,8 @@ internal static class Mount
return ETaskResult.TaskComplete;
}
_mountTriggered = gameFunctions.Mount();
_progressContext = InteractionProgressContext.FromActionUse(() => _mountTriggered = gameFunctions.Mount());
_retryAt = DateTime.Now.AddSeconds(5);
return ETaskResult.StillRunning;
}

View File

@ -5,6 +5,20 @@ namespace Questionable.Controller.Steps;
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();

View File

@ -0,0 +1,97 @@
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()})";
}

View File

@ -32,19 +32,29 @@ internal static class AetherCurrent
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;
}

View File

@ -34,12 +34,17 @@ internal static class AethernetShard
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;
}

View File

@ -33,12 +33,18 @@ internal static class Aetheryte
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;
}

View File

@ -78,10 +78,10 @@ internal static class Interact
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;
@ -92,6 +92,8 @@ internal static class Interact
set => interactionType = value;
}
public InteractionProgressContext? ProgressContext() => _progressContext;
public bool Start()
{
IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId);
@ -121,9 +123,8 @@ internal static class Interact
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;
}
@ -159,7 +160,7 @@ internal static class Interact
}
else
{
if (_interactionState == EInteractionState.InteractionConfirmed)
if (_progressContext != null && _progressContext.WasSuccessful())
return ETaskResult.TaskComplete;
if (interactionType == EInteractionType.Gather && condition[ConditionFlag.Gathering])
@ -170,9 +171,8 @@ internal static class Interact
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;
}
@ -187,33 +187,5 @@ internal static class Interact
}
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,
}
}
}

View File

@ -160,6 +160,9 @@ internal static class UseItem
private bool _usedItem;
private DateTime _continueAt;
private int _itemCount;
private InteractionProgressContext? _progressContext;
public InteractionProgressContext? ProgressContext() => _progressContext;
public ElementId? QuestId => questId;
public uint ItemId => itemId;
@ -178,7 +181,7 @@ internal static class UseItem
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;
}
@ -221,7 +224,7 @@ internal static class UseItem
if (!_usedItem)
{
_usedItem = UseItem();
_progressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem());
_continueAt = DateTime.Now.Add(GetRetryDelay());
return ETaskResult.StillRunning;
}

View File

@ -60,6 +60,9 @@ internal static class AetheryteShortcut
{
private bool _teleported;
private DateTime _continueAt;
private InteractionProgressContext? _progressContext;
public InteractionProgressContext? ProgressContext() => _progressContext;
public bool Start() => !ShouldSkipTeleport();
@ -200,15 +203,21 @@ internal static class AetheryteShortcut
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");
}
}
}

View File

@ -173,6 +173,8 @@ internal static class MoveTo
_canRestart = moveParams.RestartNavigation;
}
public InteractionProgressContext? ProgressContext() => _mountTask?.ProgressContext();
public bool ShouldRedoOnInterrupt() => true;
public bool Start()

View File

@ -6,12 +6,12 @@ namespace Questionable.Controller.Steps;
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)
{
@ -20,48 +20,40 @@ internal sealed class TaskQueue
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>());
}
}