using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.Gui.Toast; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps; using Questionable.Controller.Steps.Interactions; using Questionable.Controller.Steps.Shared; using Questionable.Data; using Questionable.External; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; using Questionable.Windows.ConfigComponents; namespace Questionable.Controller; internal sealed class QuestController : MiniTaskController { public delegate void AutomationTypeChangedEventHandler(object sender, EAutomationType e); public sealed class QuestProgress { public Quest Quest { get; } public byte Sequence { get; private set; } public int Step { get; private set; } public StepProgress StepProgress { get; private set; } = new StepProgress(DateTime.Now); public QuestProgress(Quest quest, byte sequence = 0, int step = 0) { Quest = quest; SetSequence(sequence, step); } public void SetSequence(byte sequence, int step = 0) { Sequence = sequence; SetStep(step); } public void SetStep(int step) { Step = step; StepProgress = new StepProgress(DateTime.Now); } public void IncreasePointMenuCounter() { StepProgress = StepProgress with { PointMenuCounter = StepProgress.PointMenuCounter + 1 }; } } public sealed record StepProgress(DateTime StartedAt, int PointMenuCounter = 0); public enum ECurrentQuestType { Normal, Next, Gathering, Simulated } public enum EAutomationType { Manual, Automatic, GatheringOnly, SingleQuestA, SingleQuestB } private readonly IClientState _clientState; private readonly IObjectTable _objectTable; private readonly GameFunctions _gameFunctions; private readonly QuestFunctions _questFunctions; private readonly MovementController _movementController; private readonly CombatController _combatController; private readonly GatheringController _gatheringController; private readonly QuestRegistry _questRegistry; private readonly JournalData _journalData; private readonly IKeyState _keyState; private readonly IChatGui _chatGui; private readonly ICondition _condition; private readonly IToastGui _toastGui; private readonly Configuration _configuration; private readonly TaskCreator _taskCreator; private readonly SinglePlayerDutyConfigComponent _singlePlayerDutyConfigComponent; private readonly AutoDutyIpc _autoDutyIpc; private readonly ILogger _logger; private readonly object _progressLock = new object(); private QuestProgress? _startedQuest; private QuestProgress? _nextQuest; private QuestProgress? _simulatedQuest; private QuestProgress? _gatheringQuest; private QuestProgress? _pendingQuest; private EAutomationType _automationType; private DateTime _safeAnimationEnd = DateTime.MinValue; private DateTime _lastTaskUpdate = DateTime.Now; private Vector3 _lastPlayerPosition = Vector3.Zero; private int _lastQuestStep = -1; private byte _lastQuestSequence = byte.MaxValue; private ElementId? _lastQuestId; private DateTime _lastProgressUpdate = DateTime.Now; private DateTime _lastAutoRefresh = DateTime.MinValue; private bool _lastEscDown; private int _escPressCount; private DateTime _lastEscPressTime = DateTime.MinValue; private static readonly TimeSpan EscDoublePressWindow = TimeSpan.FromSeconds(1L); private HashSet _stopConditionsMetAtStart = new HashSet(); private const char ClipboardSeparator = ';'; public EAutomationType AutomationType { get { return _automationType; } set { if (value != _automationType) { _logger.LogInformation("Setting automation type to {NewAutomationType} (previous: {OldAutomationType})", value, _automationType); _automationType = value; this.AutomationTypeChanged?.Invoke(this, value); } } } public (QuestProgress Progress, ECurrentQuestType Type)? CurrentQuestDetails { get { if (_simulatedQuest != null) { return (_simulatedQuest, ECurrentQuestType.Simulated); } if (_nextQuest != null && _questFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id)) { return (_nextQuest, ECurrentQuestType.Next); } if (_gatheringQuest != null) { return (_gatheringQuest, ECurrentQuestType.Gathering); } if (_startedQuest != null) { return (_startedQuest, ECurrentQuestType.Normal); } return null; } } public QuestProgress? CurrentQuest => CurrentQuestDetails?.Progress; public QuestProgress? StartedQuest => _startedQuest; public QuestProgress? SimulatedQuest => _simulatedQuest; public QuestProgress? NextQuest => _nextQuest; public QuestProgress? GatheringQuest => _gatheringQuest; public QuestProgress? PendingQuest => _pendingQuest; public List ManualPriorityQuests { get; } = new List(); public string? DebugState { get; private set; } public bool IsQuestWindowOpen => IsQuestWindowOpenFunction?.Invoke() ?? true; public Func? IsQuestWindowOpenFunction { private get; set; } = () => true; public bool IsRunning => !_taskQueue.AllTasksComplete; public TaskQueue TaskQueue => _taskQueue; public string? CurrentTaskState { get { if (_taskQueue.CurrentTaskExecutor is IDebugStateProvider debugStateProvider) { return debugStateProvider.GetDebugState(); } return null; } } public event AutomationTypeChangedEventHandler? AutomationTypeChanged; public QuestController(IClientState clientState, IObjectTable objectTable, GameFunctions gameFunctions, QuestFunctions questFunctions, MovementController movementController, CombatController combatController, GatheringController gatheringController, ILogger logger, QuestRegistry questRegistry, JournalData journalData, IKeyState keyState, IChatGui chatGui, ICondition condition, IToastGui toastGui, Configuration configuration, TaskCreator taskCreator, IServiceProvider serviceProvider, InterruptHandler interruptHandler, IDataManager dataManager, SinglePlayerDutyConfigComponent singlePlayerDutyConfigComponent, AutoDutyIpc autoDutyIpc) : base(chatGui, condition, serviceProvider, interruptHandler, dataManager, logger) { _clientState = clientState; _objectTable = objectTable; _gameFunctions = gameFunctions; _questFunctions = questFunctions; _movementController = movementController; _combatController = combatController; _gatheringController = gatheringController; _questRegistry = questRegistry; _journalData = journalData; _keyState = keyState; _chatGui = chatGui; _condition = condition; _toastGui = toastGui; _configuration = configuration; _taskCreator = taskCreator; _singlePlayerDutyConfigComponent = singlePlayerDutyConfigComponent; _autoDutyIpc = autoDutyIpc; _logger = logger; _toastGui.ErrorToast += base.OnErrorToast; _toastGui.Toast += OnNormalToast; _condition.ConditionChange += OnConditionChange; _clientState.Logout += OnLogout; _movementController.PlayerInputDetected += OnPlayerInputDetected; } public void Reload() { lock (_progressLock) { _logger.LogInformation("Reload, resetting curent quest progress"); ResetInternalState(); ResetAutoRefreshState(); _questRegistry.Reload(); _singlePlayerDutyConfigComponent.Reload(); } } private void ResetInternalState() { _startedQuest = null; _nextQuest = null; _gatheringQuest = null; _pendingQuest = null; _simulatedQuest = null; _safeAnimationEnd = DateTime.MinValue; DebugState = null; } private void ResetAutoRefreshState() { _lastPlayerPosition = Vector3.Zero; _lastQuestStep = -1; _lastQuestSequence = byte.MaxValue; _lastQuestId = null; _lastProgressUpdate = DateTime.Now; _lastAutoRefresh = DateTime.Now; } public unsafe void Update() { ActionManager* ptr = ActionManager.Instance(); if (ptr != null) { float num = Math.Max(ptr->AnimationLock, (ptr->CastTimeElapsed > 0f) ? (ptr->CastTimeTotal - ptr->CastTimeElapsed) : 0f); if (num > 0f) { _safeAnimationEnd = DateTime.Now.AddSeconds(1f + num); } } if (AutomationType == EAutomationType.Manual && !IsRunning && !IsQuestWindowOpen) { return; } UpdateCurrentQuest(); if (!_clientState.IsLoggedIn) { StopAllDueToConditionFailed("Logged out"); } bool flag; if (_condition[ConditionFlag.Unconscious]) { if (!_condition[ConditionFlag.Unconscious] || !_condition[ConditionFlag.SufferingStatusAffliction63] || _clientState.TerritoryType != 1052) { ITaskExecutor currentTaskExecutor = _taskQueue.CurrentTaskExecutor; flag = ((currentTaskExecutor is Duty.WaitAutoDutyExecutor || currentTaskExecutor is Duty.WaitLevelingModeExecutor) ? true : false); if (!flag && !_taskQueue.AllTasksComplete) { StopAllDueToConditionFailed("HP = 0"); } } } else if (_configuration.General.UseEscToCancelQuesting) { if (_keyState[VirtualKey.ESCAPE] && !_lastEscDown) { DateTime now = DateTime.Now; if (now - _lastEscPressTime <= EscDoublePressWindow) { _escPressCount++; } else { _escPressCount = 1; } _lastEscPressTime = now; if (_escPressCount >= 2) { if (!_taskQueue.AllTasksComplete) { StopAllDueToConditionFailed("ESC pressed twice"); } _escPressCount = 0; } } if (!_keyState[VirtualKey.ESCAPE] && DateTime.Now - _lastEscPressTime > EscDoublePressWindow) { _escPressCount = 0; } _lastEscDown = _keyState[VirtualKey.ESCAPE]; } else { _lastEscDown = _keyState[VirtualKey.ESCAPE]; _escPressCount = 0; _lastEscPressTime = DateTime.MinValue; } if (_configuration.Stop.Enabled && _startedQuest != null) { string text = _startedQuest.Quest.Id.ToString(); if (_configuration.Stop.LevelStopMode != Configuration.EStopConditionMode.Off && IsRunning && _objectTable[0] is IPlayerCharacter playerCharacter && playerCharacter.Level >= _configuration.Stop.TargetLevel) { string item = $"level:{_configuration.Stop.TargetLevel}"; if (_configuration.Stop.LevelStopMode != Configuration.EStopConditionMode.Pause || !_stopConditionsMetAtStart.Contains(item)) { _logger.LogInformation("Reached level stop condition (current: {CurrentLevel}, target: {TargetLevel}, mode: {Mode})", playerCharacter.Level, _configuration.Stop.TargetLevel, _configuration.Stop.LevelStopMode); _chatGui.Print($"Character level {playerCharacter.Level} reached target level {_configuration.Stop.TargetLevel}.", "Questionable", 576); Stop($"Level stop condition reached [{playerCharacter.Level}]"); return; } } if (_configuration.Stop.QuestSequences.TryGetValue(text, out var value) && value.HasValue) { int sequence = _startedQuest.Sequence; if (sequence >= value.Value && IsRunning) { Configuration.EStopConditionMode valueOrDefault = _configuration.Stop.QuestStopModes.GetValueOrDefault(text, Configuration.EStopConditionMode.Pause); string item2 = $"questseq:{text}:{sequence}"; if (valueOrDefault != Configuration.EStopConditionMode.Pause || !_stopConditionsMetAtStart.Contains(item2)) { _logger.LogInformation("Reached quest-specific sequence stop condition (quest: {QuestId}, sequence: {CurrentSequence}, target: {TargetSequence})", _startedQuest.Quest.Id, sequence, value.Value); _chatGui.Print($"Quest '{_startedQuest.Quest.Info.Name}' reached sequence {sequence}, configured stop sequence is {value.Value}.", "Questionable", 576); Stop($"Quest-specific sequence stop condition reached [{text}@{sequence}]"); return; } } } else if (_configuration.Stop.SequenceStopMode != Configuration.EStopConditionMode.Off && CurrentQuest != null) { int sequence2 = CurrentQuest.Sequence; if (sequence2 >= _configuration.Stop.TargetSequence && IsRunning) { string item3 = $"sequence:{text}:{sequence2}"; if (_configuration.Stop.SequenceStopMode != Configuration.EStopConditionMode.Pause || !_stopConditionsMetAtStart.Contains(item3)) { _logger.LogInformation("Reached global quest sequence stop condition (sequence: {CurrentSequence}, target: {TargetSequence}, mode: {Mode})", sequence2, _configuration.Stop.TargetSequence, _configuration.Stop.SequenceStopMode); _chatGui.Print($"Quest sequence {sequence2} reached target sequence {_configuration.Stop.TargetSequence}.", "Questionable", 576); Stop($"Sequence stop condition reached [{sequence2}]"); return; } } } } flag = AutomationType == EAutomationType.Automatic && (_taskQueue.AllTasksComplete || _taskQueue.CurrentTaskExecutor?.CurrentTask is WaitAtEnd.WaitQuestAccepted); bool flag2; if (flag) { QuestProgress currentQuest = CurrentQuest; if (currentQuest != null && currentQuest.Sequence == 0) { int step = currentQuest.Step; if (step == 0 || step == 255) { flag2 = true; goto IL_0800; } } flag2 = false; goto IL_0800; } goto IL_0804; IL_0804: if (flag && DateTime.Now >= CurrentQuest.StepProgress.StartedAt.AddSeconds(15.0)) { lock (_progressLock) { _logger.LogWarning("Quest accept apparently didn't work out, resetting progress"); CurrentQuest.SetStep(0); } ExecuteNextStep(); } else { CheckAutoRefreshCondition(); UpdateCurrentTask(); } return; IL_0800: flag = flag2; goto IL_0804; } private void CheckAutoRefreshCondition() { if (!ShouldCheckAutoRefresh() || DateTime.Now < _lastAutoRefresh.AddSeconds(5.0)) { return; } if (ShouldPreventAutoRefresh()) { _lastProgressUpdate = DateTime.Now; return; } IGameObject gameObject = _objectTable[0]; if (gameObject == null) { return; } Vector3 position = gameObject.Position; if (CurrentQuest == null) { return; } ElementId id = CurrentQuest.Quest.Id; byte sequence = CurrentQuest.Sequence; int step = CurrentQuest.Step; if (Vector3.Distance(position, _lastPlayerPosition) > 0.5f || !id.Equals(_lastQuestId) || sequence != _lastQuestSequence || step != _lastQuestStep) { _lastPlayerPosition = position; _lastQuestId = id; _lastQuestSequence = sequence; _lastQuestStep = step; _lastProgressUpdate = DateTime.Now; return; } TimeSpan timeSpan = DateTime.Now - _lastProgressUpdate; TimeSpan timeSpan2 = TimeSpan.FromSeconds(_configuration.General.AutoStepRefreshDelaySeconds); if (timeSpan >= timeSpan2) { _logger.LogInformation("Automatically refreshing quest step as no progress detected for {TimeSinceProgress:F1} seconds (quest: {QuestId}, sequence: {Sequence}, step: {Step})", timeSpan.TotalSeconds, id, sequence, step); _chatGui.Print($"Automatically refreshing quest step as no progress detected for {timeSpan.TotalSeconds:F0} seconds.", "Questionable", 576); ClearTasksInternal(); Reload(); _lastAutoRefresh = DateTime.Now; } } private bool ShouldCheckAutoRefresh() { if (_configuration.General.AutoStepRefreshEnabled && AutomationType == EAutomationType.Automatic && IsRunning && CurrentQuest != null && _clientState.IsLoggedIn) { return _objectTable[0] != null; } return false; } private bool ShouldPreventAutoRefresh() { if (HasWaitingTasks()) { return true; } if (HasManualInterventionStep()) { return true; } if (HasSystemConditionsPreventingRefresh()) { return true; } if (HasConfigurationConditionsPreventingRefresh()) { return true; } return false; } private bool HasWaitingTasks() { ITask task = _taskQueue.CurrentTaskExecutor?.CurrentTask; if (task is WaitAtEnd.WaitObjectAtPosition || task is WaitAtEnd.WaitForCompletionFlags || task is Duty.StartLevelingModeTask || task is Duty.WaitLevelingModeTask) { return true; } return false; } private bool HasManualInterventionStep() { switch (GetNextStep().Step?.InteractionType) { case EInteractionType.WaitForManualProgress: case EInteractionType.Duty: case EInteractionType.SinglePlayerDuty: case EInteractionType.Snipe: case EInteractionType.Instruction: return true; default: return false; } } private bool HasSystemConditionsPreventingRefresh() { if (_movementController.IsNavmeshReady && !_condition[ConditionFlag.InCombat] && !_condition[ConditionFlag.Unconscious] && !_condition[ConditionFlag.BoundByDuty] && !_condition[ConditionFlag.InDutyQueue] && !_condition[ConditionFlag.InDeepDungeon] && !_condition[ConditionFlag.WatchingCutscene] && !_condition[ConditionFlag.WatchingCutscene78] && !_condition[ConditionFlag.BetweenAreas] && !_condition[ConditionFlag.BetweenAreas51] && !_gameFunctions.IsOccupied() && !_movementController.IsPathfinding && !_movementController.IsPathRunning) { return DateTime.Now < _safeAnimationEnd; } return true; } private bool HasConfigurationConditionsPreventingRefresh() { if (_configuration.Advanced.PreventQuestCompletion) { return CurrentQuest?.Sequence == byte.MaxValue; } return false; } private void UpdateCurrentQuest() { lock (_progressLock) { DebugState = null; if (!_clientState.IsLoggedIn) { ResetInternalState(); DebugState = "Not logged in"; return; } if (_configuration.General.ClearPriorityQuestsOnCompletion && ManualPriorityQuests.Count > 0) { int num = ManualPriorityQuests.RemoveAll((Quest q) => _questFunctions.IsQuestComplete(q.Id)); if (num > 0) { _logger.LogInformation("Removed {Count} completed priority {QuestWord}", num, (num == 1) ? "quest" : "quests"); } } if (_pendingQuest != null) { if (!_questFunctions.IsQuestAccepted(_pendingQuest.Quest.Id)) { DebugState = $"Waiting for Leve {_pendingQuest.Quest.Id}"; return; } _startedQuest = _pendingQuest; _pendingQuest = null; CheckNextTasks("Pending quest accepted"); } if (_simulatedQuest == null && _nextQuest != null && !((!_nextQuest.Quest.Info.IsRepeatable) ? (!_questFunctions.IsQuestAcceptedOrComplete(_nextQuest.Quest.Id)) : (!_questFunctions.IsQuestAccepted(_nextQuest.Quest.Id)))) { _logger.LogInformation("Next quest {QuestId} accepted or completed", _nextQuest.Quest.Id); if (AutomationType == EAutomationType.SingleQuestA) { _startedQuest = _nextQuest; AutomationType = EAutomationType.SingleQuestB; } _logger.LogDebug("Started: {StartedQuest}", _startedQuest?.Quest.Id); _nextQuest = null; } byte b; QuestProgress questProgress; if (_simulatedQuest != null) { b = _simulatedQuest.Sequence; questProgress = _simulatedQuest; } else if (_nextQuest != null) { questProgress = _nextQuest; b = _nextQuest.Sequence; if (_questFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id) && _nextQuest.Step == 0 && _taskQueue.AllTasksComplete && AutomationType == EAutomationType.Automatic) { ExecuteNextStep(); } } else if (_gatheringQuest != null) { questProgress = _gatheringQuest; b = _gatheringQuest.Sequence; if (_gatheringQuest.Step == 0 && _taskQueue.AllTasksComplete && AutomationType == EAutomationType.Automatic) { ExecuteNextStep(); } } else { _questFunctions.GetCurrentQuest(AutomationType != EAutomationType.SingleQuestB).Deconstruct(out ElementId CurrentQuest, out byte Sequence, out MainScenarioQuestState State); ElementId elementId = CurrentQuest; b = Sequence; MainScenarioQuestState mainScenarioQuestState = State; (ElementId, byte)? tuple = (from x in ManualPriorityQuests where _questFunctions.IsReadyToAcceptQuest(x.Id) || _questFunctions.IsQuestAccepted(x.Id) select (Id: x.Id, _questFunctions.GetQuestProgressInfo(x.Id)?.Sequence ?? 0)).FirstOrDefault(); if (tuple.HasValue) { (ElementId, byte) valueOrDefault = tuple.GetValueOrDefault(); if ((object)valueOrDefault.Item1 != null) { (elementId, b) = valueOrDefault; } } if (elementId == null || elementId.Value == 0) { ITask task = _taskQueue.CurrentTaskExecutor?.CurrentTask; bool flag = ((task is Duty.StartLevelingModeTask || task is Duty.WaitLevelingModeTask) ? true : false); if (flag || _taskQueue.RemainingTasks.Any((ITask t) => (t is Duty.StartLevelingModeTask || t is Duty.WaitLevelingModeTask) ? true : false)) { DebugState = "Leveling mode active"; return; } (bool isLevelLocked, int levelsNeeded, int requiredLevel, string? questName) msqLevelLockInfo = _questFunctions.GetMsqLevelLockInfo(); bool item = msqLevelLockInfo.isLevelLocked; int item2 = msqLevelLockInfo.levelsNeeded; int item3 = msqLevelLockInfo.requiredLevel; string item4 = msqLevelLockInfo.questName; int currentPlayerLevel = (_objectTable[0] as IPlayerCharacter)?.Level ?? 0; if (item && _autoDutyIpc.IsConfiguredToRunLevelingMode(currentPlayerLevel) && AutomationType == EAutomationType.Automatic && !_condition[ConditionFlag.BoundByDuty] && !_condition[ConditionFlag.InDutyQueue] && _taskQueue.AllTasksComplete) { _logger.LogInformation("MSQ '{QuestName}' requires level {RequiredLevel}, current level is {CurrentLevel} ({LevelsNeeded} levels needed). Starting AutoDuty Leveling mode.", item4, item3, item3 - item2, item2); _taskQueue.Enqueue(new Duty.StartLevelingModeTask(item3, item4)); _taskQueue.Enqueue(new Duty.WaitLevelingModeTask(item3)); return; } if (_startedQuest != null) { switch (mainScenarioQuestState) { case MainScenarioQuestState.Unavailable: _logger.LogWarning("MSQ information not available, doing nothing"); return; case MainScenarioQuestState.LoadingScreen: _logger.LogWarning("On loading screen, no MSQ - doing nothing"); return; } _logger.LogInformation("No current quest, resetting data [CQI: {CurrrentQuestData}], [CQ: {QuestData}], [MSQ: {MsqData}]", _questFunctions.GetCurrentQuestInternal(allowNewMsq: true), _questFunctions.GetCurrentQuest(), _questFunctions.GetMainScenarioQuest()); _startedQuest = null; Stop("Resetting current quest"); } questProgress = null; } else if (_startedQuest == null || _startedQuest.Quest.Id != elementId) { if (_startedQuest != null && !_taskQueue.AllTasksComplete) { _logger.LogTrace("Not switching from quest {CurrentQuestId} to {NewQuestId} because tasks are still running", _startedQuest.Quest.Id, elementId); questProgress = _startedQuest; b = _startedQuest.Sequence; } else { if (_configuration.Stop.Enabled && _startedQuest != null && _configuration.Stop.QuestsToStopAfter.Contains(_startedQuest.Quest.Id) && _questFunctions.IsQuestComplete(_startedQuest.Quest.Id)) { ElementId id = _startedQuest.Quest.Id; _logger.LogInformation("Reached stopping point (quest: {QuestId})", id); _chatGui.Print("Completed quest '" + _startedQuest.Quest.Info.Name + "', which is configured as a stopping point.", "Questionable", 576); _startedQuest = null; Stop($"Stopping point [{id}] reached"); return; } if (_questRegistry.TryGetQuest(elementId, out Quest quest)) { _logger.LogInformation("New quest: {QuestName}", quest.Info.Name); _startedQuest = new QuestProgress(quest, b); if (_objectTable[0] is IPlayerCharacter playerCharacter && playerCharacter.Level < quest.Info.Level) { if (_autoDutyIpc.IsConfiguredToRunLevelingMode(playerCharacter.Level) && AutomationType == EAutomationType.Automatic && !_condition[ConditionFlag.BoundByDuty]) { _logger.LogInformation("Player level ({PlayerLevel}) < quest level ({QuestLevel}), starting AutoDuty Leveling mode", playerCharacter.Level, quest.Info.Level); ClearTasksInternal(); _taskQueue.Enqueue(new Duty.StartLevelingModeTask(quest.Info.Level, quest.Info.Name)); _taskQueue.Enqueue(new Duty.WaitLevelingModeTask(quest.Info.Level)); } else { _logger.LogInformation("Stopping automation, player level ({PlayerLevel}) < quest level ({QuestLevel})", playerCharacter.Level, quest.Info.Level); Stop("Quest level too high"); } } else { if (AutomationType == EAutomationType.SingleQuestB) { _logger.LogInformation("Single quest is finished"); AutomationType = EAutomationType.Manual; } CheckNextTasks("Different Quest"); } return; } if (_startedQuest != null) { _logger.LogInformation("No active quest anymore? Not sure what happened..."); _startedQuest = null; Stop("No active Quest"); return; } questProgress = null; } } else { questProgress = _startedQuest; } } if (questProgress == null) { ITask task = _taskQueue.CurrentTaskExecutor?.CurrentTask; bool flag = ((task is Duty.StartLevelingModeTask || task is Duty.WaitLevelingModeTask) ? true : false); if (flag || _taskQueue.RemainingTasks.Any((ITask t) => (t is Duty.StartLevelingModeTask || t is Duty.WaitLevelingModeTask) ? true : false)) { DebugState = "Leveling mode active"; return; } DebugState = "No quest active"; Stop("No quest active"); } else if (questProgress.Step == 255) { DebugState = $"Waiting for sequence update (current: {questProgress.Sequence})"; if (!_taskQueue.AllTasksComplete) { DebugState = "Step 255 - processing interrupted tasks"; } else { if (this.CurrentQuest == null) { return; } TimeSpan timeSpan = DateTime.Now - this.CurrentQuest.StepProgress.StartedAt; if (timeSpan > TimeSpan.FromSeconds(3L)) { _logger.LogWarning("Step 255 with no tasks for {WaitTime:F1}s, retrying step to ensure completion (quest: {QuestId}, sequence: {Sequence})", timeSpan.TotalSeconds, questProgress.Quest.Id, questProgress.Sequence); QuestSequence questSequence = questProgress.Quest.FindSequence(questProgress.Sequence); if (questSequence != null && questSequence.Steps.Count > 0) { this.CurrentQuest.SetStep(questSequence.Steps.Count - 1); CheckNextTasks("Retry last step at 255"); } } } } else if (_gameFunctions.IsOccupied() && !_gameFunctions.IsOccupiedWithCustomDeliveryNpc(questProgress.Quest)) { DebugState = "Occupied"; } else if (_movementController.IsPathfinding) { DebugState = "Pathfinding is running"; } else if (_movementController.IsPathRunning) { DebugState = "Path is running"; } else if (DateTime.Now < _safeAnimationEnd) { DebugState = "Waiting for Animation"; } else if (questProgress.Sequence != b) { questProgress.SetSequence(b); CheckNextTasks($"New sequence {questProgress == _startedQuest}/{_questFunctions.GetCurrentQuestInternal(allowNewMsq: true)}"); } else { QuestSequence questSequence2 = questProgress.Quest.FindSequence(questProgress.Sequence); if (questSequence2 == null) { DebugState = $"Sequence {questProgress.Sequence} not found"; Stop("Unknown sequence"); } else if (questSequence2.Steps.Count > 0 && questProgress.Step >= questSequence2.Steps.Count) { DebugState = "Step not found"; Stop("Unknown step"); } else { DebugState = null; } } } } public (QuestSequence? Sequence, QuestStep? Step, bool createTasks) GetNextStep() { if (CurrentQuest == null) { return (Sequence: null, Step: null, createTasks: false); } QuestSequence questSequence = CurrentQuest.Quest.FindSequence(CurrentQuest.Sequence); if (questSequence == null) { return (Sequence: null, Step: null, createTasks: true); } if (questSequence.Steps.Count == 0) { return (Sequence: questSequence, Step: null, createTasks: true); } if (CurrentQuest.Step >= questSequence.Steps.Count) { return (Sequence: null, Step: null, createTasks: false); } return (Sequence: questSequence, Step: questSequence.Steps[CurrentQuest.Step], createTasks: true); } public void IncreaseStepCount(ElementId? questId, int? sequence, bool shouldContinue = false) { lock (_progressLock) { var (questSequence, questStep, _) = GetNextStep(); if (CurrentQuest == null || questSequence == null || questStep == null) { _logger.LogWarning("Unable to retrieve next quest step, not increasing step count"); return; } if (questId != null && CurrentQuest.Quest.Id != questId) { _logger.LogWarning("Ignoring 'increase step count' for different quest (expected {ExpectedQuestId}, but we are at {CurrentQuestId}", questId, CurrentQuest.Quest.Id); return; } if (sequence.HasValue && questSequence.Sequence != sequence.Value) { _logger.LogWarning("Ignoring 'increase step count' for different sequence (expected {ExpectedSequence}, but we are at {CurrentSequence}", sequence, questSequence.Sequence); } _logger.LogInformation("Increasing step count from {CurrentValue}", CurrentQuest.Step); bool num = CurrentQuest.Step + 1 >= questSequence.Steps.Count; if (num) { CurrentQuest.SetStep(255); } else { CurrentQuest.SetStep(CurrentQuest.Step + 1); } ResetAutoRefreshState(); if (num) { _logger.LogInformation("Completed last step in sequence, waiting for game to update sequence"); return; } } using (_logger.BeginScope("IncStepCt")) { if (shouldContinue && AutomationType != EAutomationType.Manual) { ExecuteNextStep(); } } } private void ClearTasksInternal() { if (_taskQueue.CurrentTaskExecutor is IStoppableTaskExecutor stoppableTaskExecutor) { stoppableTaskExecutor.StopNow(); } _taskQueue.Reset(); _combatController.Stop("ClearTasksInternal"); _gatheringController.Stop("ClearTasksInternal"); } public override void Stop(string label) { using (_logger.BeginScope("Stop/" + label)) { if (IsRunning || AutomationType != EAutomationType.Manual) { ClearTasksInternal(); _logger.LogInformation("Stopping automatic questing"); AutomationType = EAutomationType.Manual; _nextQuest = null; _gatheringQuest = null; _lastTaskUpdate = DateTime.Now; _stopConditionsMetAtStart.Clear(); ResetAutoRefreshState(); } } } private void StopAllDueToConditionFailed(string label) { Stop(label); _movementController.Stop(); _combatController.Stop(label); _gatheringController.Stop(label); } private void CheckNextTasks(string label) { EAutomationType automationType = AutomationType; if ((automationType == EAutomationType.Automatic || (uint)(automationType - 3) <= 1u) ? true : false) { using (_logger.BeginScope(label)) { ClearTasksInternal(); int? num = CurrentQuest?.Step; if (num.HasValue) { int valueOrDefault = num.GetValueOrDefault(); if (valueOrDefault >= 0 && valueOrDefault < 255) { ExecuteNextStep(); goto IL_008e; } } _logger.LogInformation("Couldn't execute next step during Stop() call"); goto IL_008e; IL_008e: _lastTaskUpdate = DateTime.Now; ResetAutoRefreshState(); return; } } Stop(label); } public void SimulateQuest(Quest? quest, byte sequence, int step) { _logger.LogInformation("SimulateQuest: {QuestId}", quest?.Id); if (quest != null) { _simulatedQuest = new QuestProgress(quest, sequence, step); } else { _simulatedQuest = null; } } public void SetNextQuest(Quest? quest) { _logger.LogInformation("NextQuest: {QuestId}", quest?.Id); if (quest != null) { _nextQuest = new QuestProgress(quest, 0); } else { _nextQuest = null; } } public void SetGatheringQuest(Quest? quest) { _logger.LogInformation("GatheringQuest: {QuestId}", quest?.Id); if (quest != null) { _gatheringQuest = new QuestProgress(quest, 0); } else { _gatheringQuest = null; } } public void SetPendingQuest(QuestProgress? quest) { _logger.LogInformation("PendingQuest: {QuestId}", quest?.Quest.Id); _pendingQuest = quest; } protected override void UpdateCurrentTask() { if (!_gameFunctions.IsOccupied() || _gameFunctions.IsOccupiedWithCustomDeliveryNpc(CurrentQuest?.Quest)) { base.UpdateCurrentTask(); } } protected override void OnTaskComplete(ITask task) { if (task is WaitAtEnd.WaitQuestCompleted) { _simulatedQuest = null; } } protected override void OnNextStep(ILastTask task) { IncreaseStepCount(task.ElementId, task.Sequence, shouldContinue: true); } public void Start(string label) { using (_logger.BeginScope("Q/" + label)) { if (!CheckAndBlockForStopConditions()) { RecordStopConditionsMetAtStart(); AutomationType = EAutomationType.Automatic; ExecuteNextStep(); } } } public void StartGatheringQuest(string label) { using (_logger.BeginScope("GQ/" + label)) { if (!CheckAndBlockForStopConditions()) { RecordStopConditionsMetAtStart(); AutomationType = EAutomationType.GatheringOnly; ExecuteNextStep(); } } } public void StartSingleQuest(string label) { using (_logger.BeginScope("SQ/" + label)) { if (!CheckAndBlockForStopConditions()) { RecordStopConditionsMetAtStart(); AutomationType = EAutomationType.SingleQuestA; ExecuteNextStep(); } } } public void StartSingleStep(string label) { using (_logger.BeginScope("SS/" + label)) { AutomationType = EAutomationType.Manual; ExecuteNextStep(); } } private void RecordStopConditionsMetAtStart() { _stopConditionsMetAtStart.Clear(); if (!_configuration.Stop.Enabled) { return; } if (_configuration.Stop.LevelStopMode == Configuration.EStopConditionMode.Pause && _objectTable[0] is IPlayerCharacter playerCharacter && playerCharacter.Level >= _configuration.Stop.TargetLevel) { _stopConditionsMetAtStart.Add($"level:{_configuration.Stop.TargetLevel}"); _logger.LogDebug("Recording level stop condition as already met at start: {Level}", _configuration.Stop.TargetLevel); } if (_configuration.Stop.SequenceStopMode == Configuration.EStopConditionMode.Pause && _startedQuest != null) { string text = _startedQuest.Quest.Id.ToString(); if (!_configuration.Stop.QuestSequences.ContainsKey(text)) { int sequence = _startedQuest.Sequence; if (sequence >= _configuration.Stop.TargetSequence) { _stopConditionsMetAtStart.Add($"sequence:{text}:{sequence}"); _logger.LogDebug("Recording global sequence stop condition as already met at start: quest {QuestId}, sequence {Sequence}", text, sequence); } } } foreach (ElementId item in _configuration.Stop.QuestsToStopAfter) { string text2 = item.ToString(); if (_configuration.Stop.QuestStopModes.GetValueOrDefault(text2, Configuration.EStopConditionMode.Pause) == Configuration.EStopConditionMode.Pause && _configuration.Stop.QuestSequences.TryGetValue(text2, out var value) && value.HasValue && _questFunctions.IsQuestAccepted(item)) { QuestProgressInfo questProgressInfo = _questFunctions.GetQuestProgressInfo(item); if (questProgressInfo != null && questProgressInfo.Sequence >= value.Value) { _stopConditionsMetAtStart.Add($"questseq:{text2}:{questProgressInfo.Sequence}"); _logger.LogDebug("Recording quest sequence stop condition as already met at start: quest {QuestId}, sequence {Sequence}", text2, questProgressInfo.Sequence); } } } } private bool CheckAndBlockForStopConditions() { if (!_configuration.Stop.Enabled) { return false; } if (_configuration.Stop.LevelStopMode == Configuration.EStopConditionMode.Stop && _objectTable[0] is IPlayerCharacter playerCharacter && playerCharacter.Level >= _configuration.Stop.TargetLevel) { _logger.LogInformation("Blocking start: Level stop condition already met (current: {CurrentLevel}, target: {TargetLevel})", playerCharacter.Level, _configuration.Stop.TargetLevel); _chatGui.Print($"Cannot start: Character level {playerCharacter.Level} has reached target level {_configuration.Stop.TargetLevel}.", "Questionable", 576); return true; } foreach (ElementId item in _configuration.Stop.QuestsToStopAfter) { string key = item.ToString(); if (_configuration.Stop.QuestStopModes.GetValueOrDefault(key, Configuration.EStopConditionMode.Pause) != Configuration.EStopConditionMode.Stop) { continue; } if (_configuration.Stop.QuestSequences.TryGetValue(key, out var value) && value.HasValue) { if (!_questFunctions.IsQuestAccepted(item)) { continue; } QuestProgressInfo questProgressInfo = _questFunctions.GetQuestProgressInfo(item); if (questProgressInfo != null && questProgressInfo.Sequence >= value.Value) { if (_questRegistry.TryGetQuest(item, out Quest quest)) { _logger.LogInformation("Blocking start: Quest '{QuestName}' is at sequence {CurrentSequence}, stop sequence is {StopSequence}", quest.Info.Name, questProgressInfo.Sequence, value.Value); _chatGui.Print($"Cannot start: Quest '{quest.Info.Name}' is at sequence {questProgressInfo.Sequence}, configured stop sequence is {value.Value}.", "Questionable", 576); } return true; } } else if (_questFunctions.IsQuestComplete(item)) { if (_questRegistry.TryGetQuest(item, out Quest quest2)) { _logger.LogInformation("Blocking start: Quest '{QuestName}' is already complete and configured as a stop condition", quest2.Info.Name); _chatGui.Print("Cannot start: Quest '" + quest2.Info.Name + "' is complete and configured as a stopping point.", "Questionable", 576); } return true; } } if (_configuration.Stop.SequenceStopMode == Configuration.EStopConditionMode.Stop && _startedQuest != null) { string key2 = _startedQuest.Quest.Id.ToString(); if (!_configuration.Stop.QuestSequences.ContainsKey(key2)) { int sequence = _startedQuest.Sequence; if (sequence >= _configuration.Stop.TargetSequence) { _logger.LogInformation("Blocking start: Global sequence stop condition already met (current: {CurrentSequence}, target: {TargetSequence})", sequence, _configuration.Stop.TargetSequence); _chatGui.Print($"Cannot start: Quest sequence {sequence} has reached target sequence {_configuration.Stop.TargetSequence}.", "Questionable", 576); return true; } } } return false; } private void ExecuteNextStep() { ClearTasksInternal(); if (TryPickPriorityQuest()) { _logger.LogInformation("Using priority quest over current quest"); } var (questSequence, step, flag) = GetNextStep(); if (CurrentQuest == null || questSequence == null) { _logger.LogDebug("ExecuteNextStep: No current quest or sequence. Checking leveling mode conditions."); if (AutomationType == EAutomationType.Automatic && !_condition[ConditionFlag.BoundByDuty]) { (bool isLevelLocked, int levelsNeeded, int requiredLevel, string? questName) msqLevelLockInfo = _questFunctions.GetMsqLevelLockInfo(); bool item = msqLevelLockInfo.isLevelLocked; int item2 = msqLevelLockInfo.levelsNeeded; int item3 = msqLevelLockInfo.requiredLevel; string item4 = msqLevelLockInfo.questName; int currentPlayerLevel = (_objectTable[0] as IPlayerCharacter)?.Level ?? 0; if (item && _autoDutyIpc.IsConfiguredToRunLevelingMode(currentPlayerLevel)) { _logger.LogInformation("MSQ '{QuestName}' requires level {RequiredLevel}, current level is {CurrentLevel} ({LevelsNeeded} levels needed). Starting AutoDuty Leveling mode.", item4, item3, item3 - item2, item2); _taskQueue.Enqueue(new Duty.StartLevelingModeTask(item3, item4)); _taskQueue.Enqueue(new Duty.WaitLevelingModeTask(item3)); return; } } if (CurrentQuestDetails?.Progress.Quest.Id is SatisfactionSupplyNpcId && CurrentQuestDetails?.Progress.Sequence == 1) { (QuestProgress, ECurrentQuestType)? currentQuestDetails = CurrentQuestDetails; if (currentQuestDetails.HasValue && currentQuestDetails.GetValueOrDefault().Item1.Step == 255) { currentQuestDetails = CurrentQuestDetails; if (currentQuestDetails.HasValue && currentQuestDetails.GetValueOrDefault().Item2 == ECurrentQuestType.Gathering) { _logger.LogInformation("Completed delivery quest"); SetGatheringQuest(null); Stop("Gathering quest complete"); goto IL_02d0; } } } _logger.LogWarning("Could not retrieve next quest step, not doing anything [{QuestId}, {Sequence}, {Step}]", CurrentQuest?.Quest.Id, CurrentQuest?.Sequence, CurrentQuest?.Step); goto IL_02d0; } goto IL_02dc; IL_02dc: _movementController.Stop(); _combatController.Stop("Execute next step"); _gatheringController.Stop("Execute next step"); try { foreach (ITask item5 in _taskCreator.CreateTasks(CurrentQuest.Quest, CurrentQuest.Sequence, questSequence, step)) { _taskQueue.Enqueue(item5); } ResetAutoRefreshState(); return; } catch (Exception exception) { _logger.LogError(exception, "Failed to create tasks"); _chatGui.PrintError("Failed to start next task sequence, please check /xllog for details.", "Questionable", 576); Stop("Tasks failed to create"); return; } IL_02d0: if (CurrentQuest == null || !flag) { return; } goto IL_02dc; } public string ToStatString() { ITask task = _taskQueue.CurrentTaskExecutor?.CurrentTask; if (task != null) { return $"{task} (+{_taskQueue.RemainingTasks.Count()})"; } return $"- (+{_taskQueue.RemainingTasks.Count()})"; } public bool HasCurrentTaskExecutorMatching([NotNullWhen(true)] out T? task) where T : class, ITaskExecutor { if (_taskQueue.CurrentTaskExecutor is T val) { task = val; return true; } task = null; return false; } public bool HasCurrentTaskMatching([NotNullWhen(true)] out T? task) where T : class, ITask { if (_taskQueue.CurrentTaskExecutor?.CurrentTask is T val) { task = val; return true; } task = null; return false; } public void Skip(ElementId elementId, byte currentQuestSequence) { lock (_progressLock) { if (_taskQueue.CurrentTaskExecutor?.CurrentTask is ISkippableTask) { _taskQueue.CurrentTaskExecutor = null; } else if (_taskQueue.CurrentTaskExecutor != null) { _taskQueue.CurrentTaskExecutor = null; ITask task; while (_taskQueue.TryPeek(out task)) { _taskQueue.TryDequeue(out ITask _); if (task is ISkippableTask) { return; } } if (_taskQueue.AllTasksComplete) { Stop("Skip"); IncreaseStepCount(elementId, currentQuestSequence); } } else { Stop("SkipNx"); IncreaseStepCount(elementId, currentQuestSequence); } } } public void SkipSimulatedTask() { _taskQueue.CurrentTaskExecutor = null; } public bool IsInterruptible() { EAutomationType automationType = AutomationType; if ((uint)(automationType - 3) <= 1u) { return false; } (QuestProgress, ECurrentQuestType)? currentQuestDetails = CurrentQuestDetails; if (!currentQuestDetails.HasValue) { return false; } (QuestProgress, ECurrentQuestType) value = currentQuestDetails.Value; var (questProgress, _) = value; if (value.Item2 != ECurrentQuestType.Normal || !questProgress.Quest.Root.Interruptible || questProgress.Sequence == 0) { return false; } if (ManualPriorityQuests.Contains(questProgress.Quest)) { return false; } if (QuestData.HardModePrimals.Contains(questProgress.Quest.Id)) { return false; } if (questProgress.Quest.Info.AlliedSociety != EAlliedSociety.None) { return false; } QuestSequence questSequence = questProgress.Quest.FindSequence(questProgress.Sequence); if (questProgress.Step > 0) { return false; } QuestStep questStep = questSequence?.FindStep(questProgress.Step); if (questStep != null && questStep.AetheryteShortcut.HasValue && (questStep.SkipConditions?.AetheryteShortcutIf?.QuestsCompleted.Count).GetValueOrDefault() == 0) { return (questStep.SkipConditions?.AetheryteShortcutIf?.QuestsAccepted.Count).GetValueOrDefault() == 0; } return false; } public bool TryPickPriorityQuest() { if (!IsInterruptible() || _nextQuest != null || _gatheringQuest != null || _simulatedQuest != null) { return false; } ElementId elementId = (from x in _questFunctions.GetNextPriorityQuestsThatCanBeAccepted() where x.IsAvailable select x.QuestId).FirstOrDefault(); if (elementId == null) { return false; } if (_startedQuest != null && elementId == _startedQuest.Quest.Id) { return false; } if (_questRegistry.TryGetQuest(elementId, out Quest quest)) { SetNextQuest(quest); return true; } return false; } public void ImportQuestPriority(List questElements) { ManualPriorityQuests.Clear(); foreach (ElementId questElement in questElements) { if (_questRegistry.TryGetQuest(questElement, out Quest quest)) { ManualPriorityQuests.Add(quest); continue; } _logger.LogWarning("Could not find quest {QuestId} during import", questElement); } _logger.LogInformation("Imported {Count} priority quests", ManualPriorityQuests.Count); } public string ExportQuestPriority() { return string.Join(';', ManualPriorityQuests.Select((Quest x) => x.Id.ToString())); } public void ClearQuestPriority() { ManualPriorityQuests.Clear(); } public bool AddQuestPriority(ElementId elementId) { if (_questRegistry.TryGetQuest(elementId, out Quest quest) && !ManualPriorityQuests.Contains(quest)) { ManualPriorityQuests.Add(quest); return true; } return false; } public int AddAllAvailableQuests() { List list = (from q in _questRegistry.AllQuests.Where(delegate(Quest quest) { if (quest.Root.Disabled || _questFunctions.IsQuestRemoved(quest.Id) || _questFunctions.IsQuestComplete(quest.Id) || _questFunctions.IsQuestAccepted(quest.Id) || quest.Info.IsRepeatable) { return false; } if (quest.Info.AlliedSociety != EAlliedSociety.None) { _logger.LogDebug("Excluding allied society quest {QuestId} from bulk add", quest.Id); return false; } if (quest.Info is QuestInfo questInfo && _journalData.MoogleDeliveryGenreId.HasValue && questInfo.JournalGenre == _journalData.MoogleDeliveryGenreId.Value) { _logger.LogDebug("Excluding moogle delivery quest {QuestId} from bulk add", quest.Id); return false; } if (!_questFunctions.IsReadyToAcceptQuest(quest.Id)) { _logger.LogTrace("Quest {QuestId} not ready to accept", quest.Id); return false; } return true; }) select q.Id).ToList(); _logger.LogInformation("Adding {Count} available quests to priority queue", list.Count); int num = 0; foreach (ElementId item in list) { if (AddQuestPriority(item)) { num++; } } return num; } public bool InsertQuestPriority(int index, ElementId elementId) { try { if (_questRegistry.TryGetQuest(elementId, out Quest quest) && !ManualPriorityQuests.Contains(quest)) { ManualPriorityQuests.Insert(index, quest); } return true; } catch (Exception exception) { _logger.LogError(exception, "Failed to insert quest in priority list"); _chatGui.PrintError("Failed to insert quest in priority list, please check /xllog for details.", "Questionable", 576); return false; } } public bool WasLastTaskUpdateWithin(TimeSpan timeSpan) { _logger.LogInformation("Last update: {Update}", _lastTaskUpdate); if (!IsRunning) { return DateTime.Now <= _lastTaskUpdate.Add(timeSpan); } return true; } private void OnConditionChange(ConditionFlag flag, bool value) { if (_taskQueue.CurrentTaskExecutor is IConditionChangeAware conditionChangeAware) { conditionChangeAware.OnConditionChange(flag, value); } } private void OnNormalToast(ref SeString message, ref ToastOptions options, ref bool isHandled) { _gatheringController.OnNormalToast(message); } protected override void HandleInterruption(object? sender, EventArgs e) { if (IsRunning && AutomationType != EAutomationType.Manual) { base.HandleInterruption(sender, e); } } private void OnLogout(int type, int code) { if (_configuration.General.ClearPriorityQuestsOnLogout) { _logger.LogInformation("Clearing priority quests on logout"); ManualPriorityQuests.Clear(); } } private void OnPlayerInputDetected(object? sender, EventArgs e) { if (AutomationType != EAutomationType.Manual && IsRunning) { _logger.LogInformation("Player input detected during movement, stopping quest automation"); _chatGui.Print("Player input detected - stopping quest automation.", "Questionable", 576); Stop("Player input detected"); } } public override void Dispose() { _toastGui.ErrorToast -= base.OnErrorToast; _toastGui.Toast -= OnNormalToast; _condition.ConditionChange -= OnConditionChange; _clientState.Logout -= OnLogout; _movementController.PlayerInputDetected -= OnPlayerInputDetected; base.Dispose(); } }