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 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 : 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 _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 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(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 (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(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 GetRemainingTaskNames() { ITask task = _taskQueue.CurrentTaskExecutor?.CurrentTask; if (task != null) { List list = new List(); 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 list = new List(); 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(), new List(), new List(), null)); list.Add(new WaitAtEnd.WaitDelay()); _taskQueue.InterruptWith(list); } else { TaskQueue taskQueue = _taskQueue; int num = 1; List list2 = new List(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 list = new List(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; } }