diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index e031d988..420c2360 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -4,11 +4,15 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Keys; +using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; +using LLib.GameData; 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; @@ -16,7 +20,7 @@ using Questionable.Model.Questing; namespace Questionable.Controller; -internal sealed class QuestController : MiniTaskController +internal sealed class QuestController : MiniTaskController, IDisposable { private readonly IClientState _clientState; private readonly GameFunctions _gameFunctions; @@ -27,6 +31,7 @@ internal sealed class QuestController : MiniTaskController private readonly QuestRegistry _questRegistry; private readonly IKeyState _keyState; private readonly ICondition _condition; + private readonly IToastGui _toastGui; private readonly Configuration _configuration; private readonly YesAlreadyIpc _yesAlreadyIpc; private readonly IReadOnlyList _taskFactories; @@ -64,6 +69,7 @@ internal sealed class QuestController : MiniTaskController IKeyState keyState, IChatGui chatGui, ICondition condition, + IToastGui toastGui, Configuration configuration, YesAlreadyIpc yesAlreadyIpc, IEnumerable taskFactories) @@ -78,11 +84,13 @@ internal sealed class QuestController : MiniTaskController _questRegistry = questRegistry; _keyState = keyState; _condition = condition; + _toastGui = toastGui; _configuration = configuration; _yesAlreadyIpc = yesAlreadyIpc; _taskFactories = taskFactories.ToList().AsReadOnly(); _condition.ConditionChange += OnConditionChange; + _toastGui.ErrorToast += OnErrorToast; } public (QuestProgress Progress, ECurrentQuestType Type)? CurrentQuestDetails @@ -216,6 +224,7 @@ internal sealed class QuestController : MiniTaskController Stop("Pending quest accepted", continueIfAutomatic: true); } } + if (_simulatedQuest == null && _nextQuest != null) { // if the quest is accepted, we no longer track it @@ -231,7 +240,7 @@ internal sealed class QuestController : MiniTaskController _nextQuest.Quest.Id); // if (_nextQuest.Quest.Id is LeveId) - // _startedQuest = _nextQuest; + // _startedQuest = _nextQuest; _nextQuest = null; } @@ -693,34 +702,86 @@ internal sealed class QuestController : MiniTaskController return false; QuestSequence? currentSequence = currentQuest.Quest.FindSequence(currentQuest.Sequence); - QuestStep? currentStep = currentSequence?.FindStep(currentQuest.Step); + if (currentQuest.Step > 0) + return false; - // TODO Should this check that all previous steps have CompletionFlags so that we avoid running to places - // no longer relevant for the non-priority quest (after we're done with the priority quest)? + QuestStep? currentStep = currentSequence?.FindStep(currentQuest.Step); return currentStep?.AetheryteShortcut != null; } + private List GetPriorityQuests() + { + List priorityQuests = + [ + new QuestId(1157), // Garuda (Hard) + new QuestId(1158), // Titan (Hard) + ..QuestData.CrystalTowerQuests + ]; + + EClassJob classJob = (EClassJob?)_clientState.LocalPlayer?.ClassJob.Id ?? EClassJob.Adventurer; + if (classJob != EClassJob.Adventurer) + { + priorityQuests.AddRange(_questRegistry.GetKnownClassJobQuests(classJob) + .Where(x => _questRegistry.TryGetQuest(x.QuestId, out Quest? quest) && quest.Info is QuestInfo + { + // ignore Endwalker/Dawntrail, as the class quests are optional + Expansion: EExpansionVersion.ARealmReborn or EExpansionVersion.Heavensward or EExpansionVersion.Stormblood or EExpansionVersion.Shadowbringers + }) + .Select(x => x.QuestId)); + } + + return priorityQuests; + } + public bool TryPickPriorityQuest() { - if (!IsInterruptible()) + if (!IsInterruptible() || _nextQuest != null || _gatheringQuest != null || _simulatedQuest != null) return false; - ushort[] priorityQuests = - [ - 1157, // Garuda (Hard) - 1158, // Titan (Hard) - ]; + // don't start a second priority quest until the first one is resolved + List priorityQuests = GetPriorityQuests(); + if (_startedQuest != null && priorityQuests.Contains(_startedQuest.Quest.Id)) + return false; - foreach (var id in priorityQuests) + foreach (ElementId questId in priorityQuests) { - var questId = new QuestId(id); - if (_questFunctions.IsReadyToAcceptQuest(questId) && _questRegistry.TryGetQuest(questId, out var quest)) + if (!_questFunctions.IsReadyToAcceptQuest(questId) || !_questRegistry.TryGetQuest(questId, out var quest)) + continue; + + var firstStep = quest.FindSequence(0)?.FindStep(0); + if (firstStep == null) + continue; + + if (firstStep.AetheryteShortcut is { } aetheryteShortcut) { + if (_gameFunctions.IsAetheryteUnlocked(aetheryteShortcut)) + { + _logger.LogInformation("Priority quest is accessible via aetheryte {Aetheryte}", aetheryteShortcut); + SetNextQuest(quest); + + _chatGui.Print( + $"[Questionable] Picking up quest '{quest.Info.Name}' as a priority over current main story/side quests."); + return true; + } + else + { + _logger.LogWarning("Ignoring priority quest {QuestId} / {QuestName}, aetheryte locked", quest.Id, + quest.Info.Name); + } + } + + if (firstStep is { InteractionType: EInteractionType.UseItem, ItemId: UseItem.VesperBayAetheryteTicket }) + { + _logger.LogInformation("Priority quest is accessible via vesper bay"); SetNextQuest(quest); + _chatGui.Print( $"[Questionable] Picking up quest '{quest.Info.Name}' as a priority over current main story/side quests."); return true; } + else + _logger.LogTrace("Ignoring priority quest {QuestId} / {QuestName}, as we don't know how to get there", + questId, quest.Info.Name); } return false; @@ -738,6 +799,18 @@ internal sealed class QuestController : MiniTaskController conditionChangeAware.OnConditionChange(flag, value); } + private void OnErrorToast(ref SeString message, ref bool ishandled) + { + if (_currentTask is IToastAware toastAware) + toastAware.OnErrorToast(message); + } + + public void Dispose() + { + _toastGui.ErrorToast -= OnErrorToast; + _condition.ConditionChange -= OnConditionChange; + } + public sealed record StepProgress( DateTime StartedAt, int PointMenuCounter = 0); diff --git a/Questionable/Controller/Steps/IToastAware.cs b/Questionable/Controller/Steps/IToastAware.cs new file mode 100644 index 00000000..aea7eaff --- /dev/null +++ b/Questionable/Controller/Steps/IToastAware.cs @@ -0,0 +1,8 @@ +using Dalamud.Game.Text.SeStringHandling; + +namespace Questionable.Controller.Steps; + +public interface IToastAware +{ + void OnErrorToast(SeString message); +} diff --git a/Questionable/Controller/Steps/Interactions/EquipItem.cs b/Questionable/Controller/Steps/Interactions/EquipItem.cs index 95aad266..3f3b48f4 100644 --- a/Questionable/Controller/Steps/Interactions/EquipItem.cs +++ b/Questionable/Controller/Steps/Interactions/EquipItem.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; +using LLib; using Lumina.Excel.GeneratedSheets; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Questionable.Functions; using Questionable.Model.Questing; using Quest = Questionable.Model.Quest; @@ -26,8 +29,9 @@ internal static class EquipItem } } - internal sealed class DoEquip(IDataManager dataManager, ILogger logger) : ITask + internal sealed class DoEquip(IDataManager dataManager, ILogger logger) : ITask, IToastAware { + private const int MaxAttempts = 3; private static readonly IReadOnlyList SourceInventoryTypes = [ InventoryType.ArmoryMainHand, @@ -98,7 +102,7 @@ internal static class EquipItem private unsafe void Equip() { ++_attempts; - if (_attempts > 3) + if (_attempts > MaxAttempts) throw new TaskException("Unable to equip gear."); var inventoryManager = InventoryManager.Instance(); @@ -169,5 +173,12 @@ internal static class EquipItem } public override string ToString() => $"Equip({_item.Name})"; + + public void OnErrorToast(SeString message) + { + string? insufficientArmoryChestSpace = dataManager.GetString(709, x => x.Text); + if (GameFunctions.GameStringEquals(message.TextValue, insufficientArmoryChestSpace)) + _attempts = MaxAttempts; + } } } diff --git a/Questionable/Data/QuestData.cs b/Questionable/Data/QuestData.cs index 10224d1a..8b961c81 100644 --- a/Questionable/Data/QuestData.cs +++ b/Questionable/Data/QuestData.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Plugin.Services; +using LLib.GameData; using Lumina.Excel.GeneratedSheets; using Questionable.Model; using Questionable.Model.Questing; @@ -11,6 +12,8 @@ namespace Questionable.Data; internal sealed class QuestData { + public static readonly IReadOnlyList CrystalTowerQuests = + [new(1709), new(1200), new(1201), new(1202), new(1203), new(1474), new(494), new(495)]; private readonly Dictionary _quests; public QuestData(IDataManager dataManager) diff --git a/Questionable/Windows/QuestComponents/ARealmRebornComponent.cs b/Questionable/Windows/QuestComponents/ARealmRebornComponent.cs index a94f253b..6d139a16 100644 --- a/Questionable/Windows/QuestComponents/ARealmRebornComponent.cs +++ b/Questionable/Windows/QuestComponents/ARealmRebornComponent.cs @@ -16,9 +16,6 @@ internal sealed class ARealmRebornComponent private static readonly QuestId GoodIntentions = new(363); private static readonly ushort[] RequiredPrimalInstances = [20004, 20006, 20005]; - private static readonly QuestId[] RequiredAllianceRaidQuests = - [new(1709), new(1200), new(1201), new(1202), new(1203), new(1474), new(494), new(495)]; - private readonly QuestFunctions _questFunctions; private readonly QuestData _questData; private readonly TerritoryData _territoryData; @@ -64,7 +61,7 @@ internal sealed class ARealmRebornComponent private void DrawAllianceRaids() { - bool complete = _questFunctions.IsQuestComplete(RequiredAllianceRaidQuests.Last()); + bool complete = _questFunctions.IsQuestComplete(QuestData.CrystalTowerQuests[^1]); bool hover = _uiUtils.ChecklistItem("Crystal Tower Raids", complete); if (complete || !hover) return; @@ -73,7 +70,7 @@ internal sealed class ARealmRebornComponent if (!tooltip) return; - foreach (var questId in RequiredAllianceRaidQuests) + foreach (var questId in QuestData.CrystalTowerQuests) { (Vector4 color, FontAwesomeIcon icon, _) = _uiUtils.GetQuestStyle(questId); _uiUtils.ChecklistItem(_questData.GetQuestInfo(questId).Name, color, icon);