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.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.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 GameFunctions _gameFunctions; private readonly QuestFunctions _questFunctions; private readonly MovementController _movementController; private readonly CombatController _combatController; 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 TaskCreator _taskCreator; private readonly SinglePlayerDutyConfigComponent _singlePlayerDutyConfigComponent; 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 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, GameFunctions gameFunctions, QuestFunctions questFunctions, MovementController movementController, CombatController combatController, GatheringController gatheringController, ILogger logger, QuestRegistry questRegistry, IKeyState keyState, IChatGui chatGui, ICondition condition, IToastGui toastGui, Configuration configuration, TaskCreator taskCreator, IServiceProvider serviceProvider, InterruptHandler interruptHandler, IDataManager dataManager, SinglePlayerDutyConfigComponent singlePlayerDutyConfigComponent) : base(chatGui, condition, serviceProvider, interruptHandler, dataManager, logger) { _clientState = clientState; _gameFunctions = gameFunctions; _questFunctions = questFunctions; _movementController = movementController; _combatController = combatController; _gatheringController = gatheringController; _questRegistry = questRegistry; _keyState = keyState; _chatGui = chatGui; _condition = condition; _toastGui = toastGui; _configuration = configuration; _taskCreator = taskCreator; _singlePlayerDutyConfigComponent = singlePlayerDutyConfigComponent; _logger = logger; _condition.ConditionChange += OnConditionChange; _toastGui.Toast += OnNormalToast; _toastGui.ErrorToast += base.OnErrorToast; } 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); } } UpdateCurrentQuest(); if (AutomationType == EAutomationType.Manual && !IsRunning && !IsQuestWindowOpen) { return; } if (!_clientState.IsLoggedIn) { StopAllDueToConditionFailed("Logged out"); } if (_condition[ConditionFlag.Unconscious]) { if ((!_condition[ConditionFlag.Unconscious] || !_condition[ConditionFlag.SufferingStatusAffliction63] || _clientState.TerritoryType != 1052) && !(_taskQueue.CurrentTaskExecutor is Duty.WaitAutoDutyExecutor) && !_taskQueue.AllTasksComplete) { StopAllDueToConditionFailed("HP = 0"); } } else if (_configuration.General.UseEscToCancelQuesting && _keyState[VirtualKey.ESCAPE] && !_taskQueue.AllTasksComplete) { StopAllDueToConditionFailed("ESC pressed"); } if (_configuration.Stop.Enabled && _configuration.Stop.LevelToStopAfter && _clientState.LocalPlayer != null) { int level = _clientState.LocalPlayer.Level; if (level >= _configuration.Stop.TargetLevel && IsRunning) { _logger.LogInformation("Reached level stop condition (level: {CurrentLevel}, target: {TargetLevel})", level, _configuration.Stop.TargetLevel); _chatGui.Print($"Reached or exceeded target level {_configuration.Stop.TargetLevel}.", "Questionable", 576); Stop($"Level stop condition reached [{level}]"); return; } } if (_configuration.Stop.Enabled && _configuration.Stop.SequenceToStopAfter && CurrentQuest != null) { int sequence = CurrentQuest.Sequence; if (sequence >= _configuration.Stop.TargetSequence && IsRunning) { _logger.LogInformation("Reached quest sequence stop condition (sequence: {CurrentSequence}, target: {TargetSequence})", sequence, _configuration.Stop.TargetSequence); _chatGui.Print($"Quest sequence {sequence} reached target sequence {_configuration.Stop.TargetSequence}.", "Questionable", 576); Stop($"Sequence stop condition reached [{sequence}]"); return; } } bool 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_0422; } } flag2 = false; goto IL_0422; } goto IL_0426; IL_0426: 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_0422: flag = flag2; goto IL_0426; } private void CheckAutoRefreshCondition() { if (!ShouldCheckAutoRefresh() || DateTime.Now < _lastAutoRefresh.AddSeconds(5.0)) { return; } if (ShouldPreventAutoRefresh()) { _lastProgressUpdate = DateTime.Now; return; } IPlayerCharacter localPlayer = _clientState.LocalPlayer; if (localPlayer == null) { return; } Vector3 position = localPlayer.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 _clientState.LocalPlayer != 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) { 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.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 (_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 (_startedQuest != null && !_questFunctions.IsQuestAccepted(_startedQuest.Quest.Id)) { if (_startedQuest.Quest.Info.IsRepeatable) { _logger.LogInformation("Repeatable quest {QuestId} is no longer accepted, clearing started quest", _startedQuest.Quest.Id); } else if (!_questFunctions.IsQuestComplete(_startedQuest.Quest.Id)) { _logger.LogInformation("Quest {QuestId} was abandoned, clearing started quest", _startedQuest.Quest.Id); (ElementId, byte)? tuple = (from x in ManualPriorityQuests where _questFunctions.IsReadyToAcceptQuest(x.Id) select ((ElementId Id, byte))(Id: x.Id, 0)).FirstOrDefault(); if (tuple.HasValue) { (ElementId, byte) valueOrDefault = tuple.GetValueOrDefault(); if ((object)valueOrDefault.Item1 != null && _questRegistry.TryGetQuest(valueOrDefault.Item1, out Quest quest)) { _logger.LogInformation("Setting priority quest {QuestId} as next quest", valueOrDefault.Item1); SetNextQuest(quest); } } _startedQuest = null; Stop("Quest abandoned"); return; } } 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; } else if (_questFunctions.IsQuestAccepted(_nextQuest.Quest.Id)) { QuestProgressInfo questProgressInfo = _questFunctions.GetQuestProgressInfo(_nextQuest.Quest.Id); if (questProgressInfo != null) { _startedQuest = new QuestProgress(_nextQuest.Quest, questProgressInfo.Sequence); _logger.LogInformation("Moving accepted next quest to started quest (sequence: {Sequence})", questProgressInfo.Sequence); _nextQuest = null; CheckNextTasks("Next quest already accepted"); return; } _logger.LogWarning("Could not get quest progress info for accepted quest {QuestId}", _nextQuest.Quest.Id); } _logger.LogDebug("Started: {StartedQuest}", _startedQuest?.Quest.Id); _nextQuest = null; } byte b; QuestProgress questProgress; ElementId elementId; MainScenarioQuestState mainScenarioQuestState; 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 { ElementId CurrentQuest; byte Sequence; MainScenarioQuestState State; if (_startedQuest == null) { _questFunctions.GetCurrentQuest(AutomationType != EAutomationType.SingleQuestB).Deconstruct(out CurrentQuest, out Sequence, out State); elementId = CurrentQuest; b = Sequence; mainScenarioQuestState = State; (ElementId, byte)? tuple2 = (from x in ManualPriorityQuests where _questFunctions.IsQuestAccepted(x.Id) select (Id: x.Id, _questFunctions.GetQuestProgressInfo(x.Id)?.Sequence ?? 0)).FirstOrDefault(); if (tuple2.HasValue) { (ElementId, byte) valueOrDefault2 = tuple2.GetValueOrDefault(); if ((object)valueOrDefault2.Item1 != null) { (elementId, b) = valueOrDefault2; goto IL_0813; } } Quest quest2 = ManualPriorityQuests.FirstOrDefault((Quest x) => _questFunctions.IsReadyToAcceptQuest(x.Id)); if (quest2 != null) { _logger.LogInformation("Setting ready priority quest {QuestId} as next quest", quest2.Id); SetNextQuest(quest2); return; } goto IL_0813; } questProgress = _startedQuest; b = _startedQuest.Sequence; QuestProgressInfo questProgressInfo2 = _questFunctions.GetQuestProgressInfo(_startedQuest.Quest.Id); if (questProgressInfo2 != null && questProgressInfo2.Sequence != b) { _logger.LogInformation("Updating started quest sequence from {OldSequence} to {NewSequence}", b, questProgressInfo2.Sequence); b = questProgressInfo2.Sequence; } if (AutomationType == EAutomationType.Manual || !IsRunning) { _questFunctions.GetCurrentQuest(AutomationType != EAutomationType.SingleQuestB).Deconstruct(out CurrentQuest, out Sequence, out State); ElementId elementId2 = CurrentQuest; byte sequence = Sequence; (ElementId, byte)? tuple4 = (from x in ManualPriorityQuests where _questFunctions.IsQuestAccepted(x.Id) select (Id: x.Id, _questFunctions.GetQuestProgressInfo(x.Id)?.Sequence ?? 0)).FirstOrDefault(); if (tuple4.HasValue) { (ElementId, byte) valueOrDefault3 = tuple4.GetValueOrDefault(); if ((object)valueOrDefault3.Item1 != null) { (elementId2, sequence) = valueOrDefault3; } } if (elementId2 != null && elementId2.Value != 0 && _startedQuest.Quest.Id != elementId2) { _logger.LogInformation("Game current quest changed from {OldQuest} to {NewQuest}, updating started quest", _startedQuest.Quest.Id, elementId2); if (_questRegistry.TryGetQuest(elementId2, out Quest quest3)) { _logger.LogInformation("Switching to new quest: {QuestName}", quest3.Info.Name); _startedQuest = new QuestProgress(quest3, sequence); if (_clientState.LocalPlayer != null && _clientState.LocalPlayer.Level < quest3.Info.Level) { _logger.LogInformation("Stopping automation, player level ({PlayerLevel}) < quest level ({QuestLevel}", _clientState.LocalPlayer.Level, quest3.Info.Level); Stop("Quest level too high"); } questProgress = _startedQuest; } else { _logger.LogInformation("New quest {QuestId} not found in registry", elementId2); } } } } goto IL_0b43; IL_0b43: if (questProgress == null) { DebugState = "No quest active"; if (!IsRunning) { Stop("No quest active"); } return; } if (_gameFunctions.IsOccupied() && !_gameFunctions.IsOccupiedWithCustomDeliveryNpc(questProgress.Quest)) { DebugState = "Occupied"; return; } if (_movementController.IsPathfinding) { DebugState = "Pathfinding is running"; return; } if (_movementController.IsPathRunning) { DebugState = "Path is running"; return; } if (DateTime.Now < _safeAnimationEnd) { DebugState = "Waiting for Animation"; return; } if (questProgress.Sequence != b) { questProgress.SetSequence(b); CheckNextTasks($"New sequence {questProgress == _startedQuest}/{_questFunctions.GetCurrentQuestInternal(allowNewMsq: true)}"); } QuestSequence questSequence = questProgress.Quest.FindSequence(questProgress.Sequence); if (questSequence == null) { DebugState = $"Sequence {questProgress.Sequence} not found"; Stop("Unknown sequence"); } else if (questProgress.Step == 255) { DebugState = "Step completed"; if (!_taskQueue.AllTasksComplete) { CheckNextTasks("Step complete"); } } else if (questSequence.Steps.Count > 0 && questProgress.Step >= questSequence.Steps.Count) { DebugState = "Step not found"; Stop("Unknown step"); } else { DebugState = null; } return; IL_0813: if (elementId == null || elementId.Value == 0) { 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) { Quest quest4; 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"); } else if (_questRegistry.TryGetQuest(elementId, out quest4)) { _logger.LogInformation("New quest: {QuestName}", quest4.Info.Name); _startedQuest = new QuestProgress(quest4, b); if (_clientState.LocalPlayer != null && _clientState.LocalPlayer.Level < quest4.Info.Level) { _logger.LogInformation("Stopping automation, player level ({PlayerLevel}) < quest level ({QuestLevel}", _clientState.LocalPlayer.Level, quest4.Info.Level); Stop("Quest level too high"); return; } if (AutomationType == EAutomationType.SingleQuestB) { _logger.LogInformation("Single quest is finished"); AutomationType = EAutomationType.Manual; } CheckNextTasks("Different Quest"); } else if (_startedQuest != null) { _logger.LogInformation("No active quest anymore? Not sure what happened..."); _startedQuest = null; Stop("No active Quest"); } return; } questProgress = _startedQuest; } goto IL_0b43; } } 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); if (CurrentQuest.Step + 1 < questSequence.Steps.Count) { CurrentQuest.SetStep(CurrentQuest.Step + 1); } else { CurrentQuest.SetStep(255); } ResetAutoRefreshState(); } 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; 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 SetStartedQuest(Quest quest, byte sequence = 0) { _logger.LogInformation("Setting started quest: {QuestId}", quest.Id); _startedQuest = new QuestProgress(quest, sequence); _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)) { AutomationType = EAutomationType.Automatic; ExecuteNextStep(); } } public void StartGatheringQuest(string label) { using (_logger.BeginScope("GQ/" + label)) { AutomationType = EAutomationType.GatheringOnly; ExecuteNextStep(); } } public void StartSingleQuest(string label) { using (_logger.BeginScope("SQ/" + label)) { AutomationType = EAutomationType.SingleQuestA; ExecuteNextStep(); } } public void StartSingleStep(string label) { using (_logger.BeginScope("SS/" + label)) { AutomationType = EAutomationType.Manual; ExecuteNextStep(); } } private void ExecuteNextStep() { ClearTasksInternal(); if (TryPickPriorityQuest()) { _logger.LogInformation("Using priority quest over current quest"); } var (questSequence, step, flag) = GetNextStep(); if (CurrentQuest == null || questSequence == null) { 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_01dc; } } } _logger.LogWarning("Could not retrieve next quest step, not doing anything [{QuestId}, {Sequence}, {Step}]", CurrentQuest?.Quest.Id, CurrentQuest?.Sequence, CurrentQuest?.Step); goto IL_01dc; } goto IL_01e8; IL_01e8: _movementController.Stop(); _combatController.Stop("Execute next step"); _gatheringController.Stop("Execute next step"); try { foreach (ITask item in _taskCreator.CreateTasks(CurrentQuest.Quest, CurrentQuest.Sequence, questSequence, step)) { _taskQueue.Enqueue(item); } 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_01dc: if (CurrentQuest == null || !flag) { return; } goto IL_01e8; } 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) { foreach (ElementId questElement in questElements) { if (_questRegistry.TryGetQuest(questElement, out Quest quest) && !ManualPriorityQuests.Contains(quest)) { ManualPriorityQuests.Add(quest); } } } 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; } 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); } } public override void Dispose() { _toastGui.ErrorToast -= base.OnErrorToast; _toastGui.Toast -= OnNormalToast; _condition.ConditionChange -= OnConditionChange; base.Dispose(); } }