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;
namespace Questionable.Controller;
-internal sealed class QuestController : MiniTaskController<QuestController>
+internal sealed class QuestController : MiniTaskController<QuestController>, IDisposable
{
private readonly IClientState _clientState;
private readonly GameFunctions _gameFunctions;
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<ITaskFactory> _taskFactories;
IKeyState keyState,
IChatGui chatGui,
ICondition condition,
+ IToastGui toastGui,
Configuration configuration,
YesAlreadyIpc yesAlreadyIpc,
IEnumerable<ITaskFactory> taskFactories)
_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
Stop("Pending quest accepted", continueIfAutomatic: true);
}
}
+
if (_simulatedQuest == null && _nextQuest != null)
{
// if the quest is accepted, we no longer track it
_nextQuest.Quest.Id);
// if (_nextQuest.Quest.Id is LeveId)
- // _startedQuest = _nextQuest;
+ // _startedQuest = _nextQuest;
_nextQuest = null;
}
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<ElementId> GetPriorityQuests()
+ {
+ List<ElementId> 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<ElementId> 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;
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);
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;
}
}
- internal sealed class DoEquip(IDataManager dataManager, ILogger<DoEquip> logger) : ITask
+ internal sealed class DoEquip(IDataManager dataManager, ILogger<DoEquip> logger) : ITask, IToastAware
{
+ private const int MaxAttempts = 3;
private static readonly IReadOnlyList<InventoryType> SourceInventoryTypes =
[
InventoryType.ArmoryMainHand,
private unsafe void Equip()
{
++_attempts;
- if (_attempts > 3)
+ if (_attempts > MaxAttempts)
throw new TaskException("Unable to equip gear.");
var inventoryManager = InventoryManager.Instance();
}
public override string ToString() => $"Equip({_item.Name})";
+
+ public void OnErrorToast(SeString message)
+ {
+ string? insufficientArmoryChestSpace = dataManager.GetString<LogMessage>(709, x => x.Text);
+ if (GameFunctions.GameStringEquals(message.TextValue, insufficientArmoryChestSpace))
+ _attempts = MaxAttempts;
+ }
}
}
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;
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;
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);