Add class quests as priority quests

This commit is contained in:
Liza 2024-08-10 19:50:15 +02:00
parent 348460a7c6
commit 7e9eb212e3
Signed by: liza
GPG Key ID: 7199F8D727D55F67
5 changed files with 113 additions and 21 deletions

View File

@ -4,11 +4,15 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using LLib.GameData;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps; using Questionable.Controller.Steps;
using Questionable.Controller.Steps.Interactions;
using Questionable.Controller.Steps.Shared; using Questionable.Controller.Steps.Shared;
using Questionable.Data;
using Questionable.External; using Questionable.External;
using Questionable.Functions; using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
@ -16,7 +20,7 @@ using Questionable.Model.Questing;
namespace Questionable.Controller; namespace Questionable.Controller;
internal sealed class QuestController : MiniTaskController<QuestController> internal sealed class QuestController : MiniTaskController<QuestController>, IDisposable
{ {
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly GameFunctions _gameFunctions; private readonly GameFunctions _gameFunctions;
@ -27,6 +31,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
private readonly QuestRegistry _questRegistry; private readonly QuestRegistry _questRegistry;
private readonly IKeyState _keyState; private readonly IKeyState _keyState;
private readonly ICondition _condition; private readonly ICondition _condition;
private readonly IToastGui _toastGui;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly YesAlreadyIpc _yesAlreadyIpc; private readonly YesAlreadyIpc _yesAlreadyIpc;
private readonly IReadOnlyList<ITaskFactory> _taskFactories; private readonly IReadOnlyList<ITaskFactory> _taskFactories;
@ -64,6 +69,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
IKeyState keyState, IKeyState keyState,
IChatGui chatGui, IChatGui chatGui,
ICondition condition, ICondition condition,
IToastGui toastGui,
Configuration configuration, Configuration configuration,
YesAlreadyIpc yesAlreadyIpc, YesAlreadyIpc yesAlreadyIpc,
IEnumerable<ITaskFactory> taskFactories) IEnumerable<ITaskFactory> taskFactories)
@ -78,11 +84,13 @@ internal sealed class QuestController : MiniTaskController<QuestController>
_questRegistry = questRegistry; _questRegistry = questRegistry;
_keyState = keyState; _keyState = keyState;
_condition = condition; _condition = condition;
_toastGui = toastGui;
_configuration = configuration; _configuration = configuration;
_yesAlreadyIpc = yesAlreadyIpc; _yesAlreadyIpc = yesAlreadyIpc;
_taskFactories = taskFactories.ToList().AsReadOnly(); _taskFactories = taskFactories.ToList().AsReadOnly();
_condition.ConditionChange += OnConditionChange; _condition.ConditionChange += OnConditionChange;
_toastGui.ErrorToast += OnErrorToast;
} }
public (QuestProgress Progress, ECurrentQuestType Type)? CurrentQuestDetails public (QuestProgress Progress, ECurrentQuestType Type)? CurrentQuestDetails
@ -216,6 +224,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
Stop("Pending quest accepted", continueIfAutomatic: true); Stop("Pending quest accepted", continueIfAutomatic: true);
} }
} }
if (_simulatedQuest == null && _nextQuest != null) if (_simulatedQuest == null && _nextQuest != null)
{ {
// if the quest is accepted, we no longer track it // if the quest is accepted, we no longer track it
@ -231,7 +240,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
_nextQuest.Quest.Id); _nextQuest.Quest.Id);
// if (_nextQuest.Quest.Id is LeveId) // if (_nextQuest.Quest.Id is LeveId)
// _startedQuest = _nextQuest; // _startedQuest = _nextQuest;
_nextQuest = null; _nextQuest = null;
} }
@ -693,34 +702,86 @@ internal sealed class QuestController : MiniTaskController<QuestController>
return false; return false;
QuestSequence? currentSequence = currentQuest.Quest.FindSequence(currentQuest.Sequence); 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 QuestStep? currentStep = currentSequence?.FindStep(currentQuest.Step);
// no longer relevant for the non-priority quest (after we're done with the priority quest)?
return currentStep?.AetheryteShortcut != null; 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() public bool TryPickPriorityQuest()
{ {
if (!IsInterruptible()) if (!IsInterruptible() || _nextQuest != null || _gatheringQuest != null || _simulatedQuest != null)
return false; return false;
ushort[] priorityQuests = // don't start a second priority quest until the first one is resolved
[ List<ElementId> priorityQuests = GetPriorityQuests();
1157, // Garuda (Hard) if (_startedQuest != null && priorityQuests.Contains(_startedQuest.Quest.Id))
1158, // Titan (Hard) 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); SetNextQuest(quest);
_chatGui.Print( _chatGui.Print(
$"[Questionable] Picking up quest '{quest.Info.Name}' as a priority over current main story/side quests."); $"[Questionable] Picking up quest '{quest.Info.Name}' as a priority over current main story/side quests.");
return true; 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; return false;
@ -738,6 +799,18 @@ internal sealed class QuestController : MiniTaskController<QuestController>
conditionChangeAware.OnConditionChange(flag, value); 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( public sealed record StepProgress(
DateTime StartedAt, DateTime StartedAt,
int PointMenuCounter = 0); int PointMenuCounter = 0);

View File

@ -0,0 +1,8 @@
using Dalamud.Game.Text.SeStringHandling;
namespace Questionable.Controller.Steps;
public interface IToastAware
{
void OnErrorToast(SeString message);
}

View File

@ -1,11 +1,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using LLib;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Functions;
using Questionable.Model.Questing; using Questionable.Model.Questing;
using Quest = Questionable.Model.Quest; using Quest = Questionable.Model.Quest;
@ -26,8 +29,9 @@ internal static class EquipItem
} }
} }
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 = private static readonly IReadOnlyList<InventoryType> SourceInventoryTypes =
[ [
InventoryType.ArmoryMainHand, InventoryType.ArmoryMainHand,
@ -98,7 +102,7 @@ internal static class EquipItem
private unsafe void Equip() private unsafe void Equip()
{ {
++_attempts; ++_attempts;
if (_attempts > 3) if (_attempts > MaxAttempts)
throw new TaskException("Unable to equip gear."); throw new TaskException("Unable to equip gear.");
var inventoryManager = InventoryManager.Instance(); var inventoryManager = InventoryManager.Instance();
@ -169,5 +173,12 @@ internal static class EquipItem
} }
public override string ToString() => $"Equip({_item.Name})"; 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;
}
} }
} }

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using LLib.GameData;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
@ -11,6 +12,8 @@ namespace Questionable.Data;
internal sealed class QuestData internal sealed class QuestData
{ {
public static readonly IReadOnlyList<QuestId> CrystalTowerQuests =
[new(1709), new(1200), new(1201), new(1202), new(1203), new(1474), new(494), new(495)];
private readonly Dictionary<ElementId, IQuestInfo> _quests; private readonly Dictionary<ElementId, IQuestInfo> _quests;
public QuestData(IDataManager dataManager) public QuestData(IDataManager dataManager)

View File

@ -16,9 +16,6 @@ internal sealed class ARealmRebornComponent
private static readonly QuestId GoodIntentions = new(363); private static readonly QuestId GoodIntentions = new(363);
private static readonly ushort[] RequiredPrimalInstances = [20004, 20006, 20005]; 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 QuestFunctions _questFunctions;
private readonly QuestData _questData; private readonly QuestData _questData;
private readonly TerritoryData _territoryData; private readonly TerritoryData _territoryData;
@ -64,7 +61,7 @@ internal sealed class ARealmRebornComponent
private void DrawAllianceRaids() private void DrawAllianceRaids()
{ {
bool complete = _questFunctions.IsQuestComplete(RequiredAllianceRaidQuests.Last()); bool complete = _questFunctions.IsQuestComplete(QuestData.CrystalTowerQuests[^1]);
bool hover = _uiUtils.ChecklistItem("Crystal Tower Raids", complete); bool hover = _uiUtils.ChecklistItem("Crystal Tower Raids", complete);
if (complete || !hover) if (complete || !hover)
return; return;
@ -73,7 +70,7 @@ internal sealed class ARealmRebornComponent
if (!tooltip) if (!tooltip)
return; return;
foreach (var questId in RequiredAllianceRaidQuests) foreach (var questId in QuestData.CrystalTowerQuests)
{ {
(Vector4 color, FontAwesomeIcon icon, _) = _uiUtils.GetQuestStyle(questId); (Vector4 color, FontAwesomeIcon icon, _) = _uiUtils.GetQuestStyle(questId);
_uiUtils.ChecklistItem(_questData.GetQuestInfo(questId).Name, color, icon); _uiUtils.ChecklistItem(_questData.GetQuestInfo(questId).Name, color, icon);