366 lines
11 KiB
C#
366 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text.RegularExpressions;
|
|
using Dalamud.Game.ClientState.Conditions;
|
|
using Dalamud.Game.Text.SeStringHandling;
|
|
using Dalamud.Plugin.Services;
|
|
using LLib;
|
|
using LLib.GameData;
|
|
using Lumina.Excel.Sheets;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
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.Functions;
|
|
using Questionable.Model.Questing;
|
|
|
|
namespace Questionable.Controller;
|
|
|
|
internal abstract class MiniTaskController<T> : IDisposable
|
|
{
|
|
protected readonly TaskQueue _taskQueue = new TaskQueue();
|
|
|
|
private readonly IChatGui _chatGui;
|
|
|
|
private readonly ICondition _condition;
|
|
|
|
private readonly IServiceProvider _serviceProvider;
|
|
|
|
private readonly InterruptHandler _interruptHandler;
|
|
|
|
private readonly Configuration _configuration;
|
|
|
|
protected readonly ILogger<T> _logger;
|
|
|
|
private readonly Regex _actionCanceledText;
|
|
|
|
private readonly string _eventCanceledText;
|
|
|
|
private readonly string _cantExecuteDueToStatusText;
|
|
|
|
private ITask? _lastInterruptedTask;
|
|
|
|
private int _interruptRetryCount;
|
|
|
|
private int _retryStepCount;
|
|
|
|
protected MiniTaskController(IChatGui chatGui, ICondition condition, IServiceProvider serviceProvider, InterruptHandler interruptHandler, IDataManager dataManager, Configuration configuration, ILogger<T> logger)
|
|
{
|
|
_chatGui = chatGui;
|
|
_logger = logger;
|
|
_serviceProvider = serviceProvider;
|
|
_interruptHandler = interruptHandler;
|
|
_condition = condition;
|
|
_configuration = configuration;
|
|
_eventCanceledText = dataManager.GetString(1318u, (LogMessage x) => x.Text);
|
|
_actionCanceledText = dataManager.GetRegex(1314u, (LogMessage x) => x.Text);
|
|
_cantExecuteDueToStatusText = dataManager.GetString(7728u, (LogMessage x) => x.Text);
|
|
_interruptHandler.Interrupted += HandleInterruption;
|
|
}
|
|
|
|
protected virtual void UpdateCurrentTask()
|
|
{
|
|
if (_taskQueue.CurrentTaskExecutor == null)
|
|
{
|
|
if (!_taskQueue.TryDequeue(out ITask task))
|
|
{
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
_logger.LogDebug("Starting task {TaskName}", task.ToString());
|
|
ITaskExecutor requiredKeyedService = _serviceProvider.GetRequiredKeyedService<ITaskExecutor>(task.GetType());
|
|
if (requiredKeyedService.Start(task))
|
|
{
|
|
_taskQueue.CurrentTaskExecutor = requiredKeyedService;
|
|
if (task != _lastInterruptedTask)
|
|
{
|
|
_lastInterruptedTask = null;
|
|
_interruptRetryCount = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_logger.LogTrace("Task {TaskName} was skipped", task.ToString());
|
|
}
|
|
return;
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
_logger.LogError(exception, "Failed to start task {TaskName}", task.ToString());
|
|
_chatGui.PrintError($"Failed to start task '{task}', please check /xllog for details.", "Questionable", 576);
|
|
Stop("Task failed to start");
|
|
return;
|
|
}
|
|
}
|
|
ITaskExecutor currentTaskExecutor = _taskQueue.CurrentTaskExecutor;
|
|
ITask currentTask = currentTaskExecutor.CurrentTask;
|
|
ETaskResult eTaskResult;
|
|
try
|
|
{
|
|
if (currentTaskExecutor.WasInterrupted())
|
|
{
|
|
InterruptQueueWithCombat();
|
|
return;
|
|
}
|
|
if (ConditionHelper.IsInCutscene(_condition))
|
|
{
|
|
currentTaskExecutor.ResetTimeout();
|
|
}
|
|
else if (currentTaskExecutor.HasTimedOut(_configuration.Advanced.InteractionTimeoutSeconds))
|
|
{
|
|
_logger.LogWarning("Task {TaskName} timed out after {Timeout}s", currentTask, _configuration.Advanced.InteractionTimeoutSeconds);
|
|
if (_condition[ConditionFlag.InCombat])
|
|
{
|
|
InterruptQueueWithCombat();
|
|
return;
|
|
}
|
|
_retryStepCount++;
|
|
if (_retryStepCount > _configuration.Advanced.InteractionRetryLimit)
|
|
{
|
|
_logger.LogError("Non-combat timeout retry limit exceeded after {Count} retries, stopping", _retryStepCount);
|
|
Stop("Non-combat timeout retry limit exceeded");
|
|
}
|
|
else
|
|
{
|
|
_logger.LogDebug("Not in combat, retrying step (attempt {Count}/{Limit})", _retryStepCount, _configuration.Advanced.InteractionRetryLimit);
|
|
_taskQueue.Reset();
|
|
OnRetryStep();
|
|
}
|
|
return;
|
|
}
|
|
eTaskResult = currentTaskExecutor.Update();
|
|
}
|
|
catch (MovementController.PathfindingFailedException)
|
|
{
|
|
throw;
|
|
}
|
|
catch (Exception exception2)
|
|
{
|
|
_logger.LogError(exception2, "Failed to update task {TaskName}", currentTask);
|
|
_chatGui.PrintError($"Failed to update task '{currentTask}', please check /xllog for details.", "Questionable", 576);
|
|
Stop("Task failed to update");
|
|
return;
|
|
}
|
|
if (_taskQueue.CurrentTaskExecutor != currentTaskExecutor)
|
|
{
|
|
return;
|
|
}
|
|
switch (eTaskResult)
|
|
{
|
|
case ETaskResult.StillRunning:
|
|
break;
|
|
case ETaskResult.SkipRemainingTasksForStep:
|
|
{
|
|
_logger.LogDebug("{Task} → {Result}, skipping remaining tasks for step", currentTask, eTaskResult);
|
|
_taskQueue.CurrentTaskExecutor = null;
|
|
ITask task3;
|
|
while (_taskQueue.TryDequeue(out task3))
|
|
{
|
|
if ((task3 is ILastTask || task3 is Gather.SkipMarker) ? true : false)
|
|
{
|
|
ITaskExecutor requiredKeyedService2 = _serviceProvider.GetRequiredKeyedService<ITaskExecutor>(task3.GetType());
|
|
requiredKeyedService2.Start(task3);
|
|
_taskQueue.CurrentTaskExecutor = requiredKeyedService2;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ETaskResult.TaskComplete:
|
|
case ETaskResult.CreateNewTasks:
|
|
_logger.LogDebug("{Task} → {Result}, remaining tasks: {RemainingTaskCount}", currentTask, eTaskResult, _taskQueue.RemainingTasks.Count());
|
|
OnTaskComplete(currentTask);
|
|
if (eTaskResult == ETaskResult.CreateNewTasks && currentTaskExecutor is IExtraTaskCreator extraTaskCreator)
|
|
{
|
|
_taskQueue.EnqueueAll(extraTaskCreator.CreateExtraTasks());
|
|
}
|
|
_taskQueue.CurrentTaskExecutor = null;
|
|
break;
|
|
case ETaskResult.RetryStep:
|
|
_retryStepCount++;
|
|
if (_retryStepCount > _configuration.Advanced.InteractionRetryLimit)
|
|
{
|
|
_logger.LogError("RetryStep limit exceeded after {Count} retries, stopping", _retryStepCount);
|
|
Stop("RetryStep limit exceeded");
|
|
break;
|
|
}
|
|
_logger.LogDebug("{Task} → {Result}, retrying current step (attempt {Count}/{Limit})", currentTask, eTaskResult, _retryStepCount, _configuration.Advanced.InteractionRetryLimit);
|
|
_taskQueue.Reset();
|
|
OnRetryStep();
|
|
break;
|
|
case ETaskResult.NextStep:
|
|
{
|
|
_logger.LogDebug("{Task} → {Result}", currentTask, eTaskResult);
|
|
_retryStepCount = 0;
|
|
ILastTask task2 = (ILastTask)currentTask;
|
|
_taskQueue.CurrentTaskExecutor = null;
|
|
OnNextStep(task2);
|
|
break;
|
|
}
|
|
case ETaskResult.End:
|
|
_logger.LogDebug("{Task} → {Result}", currentTask, eTaskResult);
|
|
_taskQueue.CurrentTaskExecutor = null;
|
|
Stop("Task end");
|
|
break;
|
|
}
|
|
}
|
|
|
|
protected virtual void OnTaskComplete(ITask task)
|
|
{
|
|
}
|
|
|
|
protected virtual void OnRetryStep()
|
|
{
|
|
_logger.LogWarning("RetryStep not handled by this controller, stopping");
|
|
Stop("RetryStep not supported");
|
|
}
|
|
|
|
protected void ResetStepRetryCounter()
|
|
{
|
|
_retryStepCount = 0;
|
|
}
|
|
|
|
protected virtual void OnNextStep(ILastTask task)
|
|
{
|
|
}
|
|
|
|
public virtual void Stop(string label)
|
|
{
|
|
if (_taskQueue.CurrentTaskExecutor is IStoppableTaskExecutor stoppableTaskExecutor)
|
|
{
|
|
stoppableTaskExecutor.StopNow();
|
|
}
|
|
_retryStepCount = 0;
|
|
}
|
|
|
|
public virtual IList<string> GetRemainingTaskNames()
|
|
{
|
|
ITask task = _taskQueue.CurrentTaskExecutor?.CurrentTask;
|
|
if (task != null)
|
|
{
|
|
List<string> list = new List<string>();
|
|
list.Add(task.ToString() ?? "?");
|
|
list.AddRange(_taskQueue.RemainingTasks.Select((ITask x) => x.ToString() ?? "?"));
|
|
return list;
|
|
}
|
|
return _taskQueue.RemainingTasks.Select((ITask x) => x.ToString() ?? "?").ToList();
|
|
}
|
|
|
|
public void InterruptQueueWithCombat()
|
|
{
|
|
ITask task = _taskQueue.CurrentTaskExecutor?.CurrentTask;
|
|
if (task == _lastInterruptedTask)
|
|
{
|
|
_interruptRetryCount++;
|
|
if (_interruptRetryCount > _configuration.Advanced.InteractionRetryLimit)
|
|
{
|
|
_logger.LogError("Task {TaskName} exceeded retry limit of {Limit}, stopping", task, _configuration.Advanced.InteractionRetryLimit);
|
|
_chatGui.PrintError($"Task '{task}' failed after {_configuration.Advanced.InteractionRetryLimit} retries.", "Questionable", 576);
|
|
Stop("Retry limit exceeded");
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_lastInterruptedTask = task;
|
|
_interruptRetryCount = 1;
|
|
}
|
|
_logger.LogWarning("Interrupted (attempt {Retry}/{Limit}), attempting to resolve (if in combat)", _interruptRetryCount, _configuration.Advanced.InteractionRetryLimit);
|
|
if (_condition[ConditionFlag.InCombat])
|
|
{
|
|
List<ITask> list = new List<ITask>();
|
|
if (_condition[ConditionFlag.Mounted])
|
|
{
|
|
list.Add(new Questionable.Controller.Steps.Common.Mount.UnmountTask());
|
|
}
|
|
list.Add(Combat.Factory.CreateTask(null, -1, isLastStep: false, EEnemySpawnType.QuestInterruption, new List<uint>(), new List<QuestWorkValue>(), new List<ComplexCombatData>(), null));
|
|
list.Add(new WaitAtEnd.WaitDelay());
|
|
_taskQueue.InterruptWith(list);
|
|
}
|
|
else
|
|
{
|
|
TaskQueue taskQueue = _taskQueue;
|
|
int num = 1;
|
|
List<ITask> list2 = new List<ITask>(num);
|
|
CollectionsMarshal.SetCount(list2, num);
|
|
CollectionsMarshal.AsSpan(list2)[0] = new WaitAtEnd.WaitDelay();
|
|
taskQueue.InterruptWith(list2);
|
|
}
|
|
LogTasksAfterInterruption();
|
|
}
|
|
|
|
private void InterruptWithoutCombat()
|
|
{
|
|
if (!(_taskQueue.CurrentTaskExecutor is SinglePlayerDuty.WaitSinglePlayerDutyExecutor))
|
|
{
|
|
_logger.LogWarning("Interrupted, attempting to redo previous tasks (not in combat)");
|
|
TaskQueue taskQueue = _taskQueue;
|
|
int num = 1;
|
|
List<ITask> list = new List<ITask>(num);
|
|
CollectionsMarshal.SetCount(list, num);
|
|
CollectionsMarshal.AsSpan(list)[0] = new WaitAtEnd.WaitDelay();
|
|
taskQueue.InterruptWith(list);
|
|
LogTasksAfterInterruption();
|
|
}
|
|
}
|
|
|
|
private void LogTasksAfterInterruption()
|
|
{
|
|
_logger.LogDebug("Remaining tasks after interruption:");
|
|
foreach (ITask remainingTask in _taskQueue.RemainingTasks)
|
|
{
|
|
_logger.LogDebug("- {TaskName}", remainingTask);
|
|
}
|
|
}
|
|
|
|
public void OnErrorToast(ref SeString message, ref bool isHandled)
|
|
{
|
|
if (_taskQueue.AllTasksComplete)
|
|
{
|
|
return;
|
|
}
|
|
if (_taskQueue.CurrentTaskExecutor is IToastAware toastAware && toastAware.OnErrorToast(message))
|
|
{
|
|
isHandled = true;
|
|
}
|
|
if (isHandled)
|
|
{
|
|
return;
|
|
}
|
|
if (_actionCanceledText.IsMatch(message.TextValue) && !_condition[ConditionFlag.InFlight])
|
|
{
|
|
ITaskExecutor? currentTaskExecutor = _taskQueue.CurrentTaskExecutor;
|
|
if (currentTaskExecutor != null && currentTaskExecutor.ShouldInterruptOnDamage())
|
|
{
|
|
InterruptQueueWithCombat();
|
|
return;
|
|
}
|
|
}
|
|
if (GameFunctions.GameStringEquals(_cantExecuteDueToStatusText, message.TextValue) || GameFunctions.GameStringEquals(_eventCanceledText, message.TextValue))
|
|
{
|
|
InterruptWithoutCombat();
|
|
}
|
|
}
|
|
|
|
protected virtual void HandleInterruption(object? sender, EventArgs e)
|
|
{
|
|
if (!_condition[ConditionFlag.InFlight])
|
|
{
|
|
ITaskExecutor? currentTaskExecutor = _taskQueue.CurrentTaskExecutor;
|
|
if (currentTaskExecutor != null && currentTaskExecutor.ShouldInterruptOnDamage())
|
|
{
|
|
InterruptQueueWithCombat();
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void Dispose()
|
|
{
|
|
_interruptHandler.Interrupted -= HandleInterruption;
|
|
}
|
|
}
|