Start available priority quests if no msq quest is available

This commit is contained in:
Liza 2024-08-11 02:29:34 +02:00
parent eb97c60065
commit ada2cf2833
Signed by: liza
GPG Key ID: 7199F8D727D55F67
15 changed files with 243 additions and 180 deletions

View File

@ -8,7 +8,6 @@ using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Event;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
@ -31,8 +30,8 @@ internal sealed class GameUiController : IDisposable
{
private readonly IAddonLifecycle _addonLifecycle;
private readonly IDataManager _dataManager;
private readonly GameFunctions _gameFunctions;
private readonly QuestFunctions _questFunctions;
private readonly AetheryteFunctions _aetheryteFunctions;
private readonly ExcelFunctions _excelFunctions;
private readonly QuestController _questController;
private readonly QuestRegistry _questRegistry;
@ -46,8 +45,8 @@ internal sealed class GameUiController : IDisposable
public GameUiController(
IAddonLifecycle addonLifecycle,
IDataManager dataManager,
GameFunctions gameFunctions,
QuestFunctions questFunctions,
AetheryteFunctions aetheryteFunctions,
ExcelFunctions excelFunctions,
QuestController questController,
QuestRegistry questRegistry,
@ -60,8 +59,8 @@ internal sealed class GameUiController : IDisposable
{
_addonLifecycle = addonLifecycle;
_dataManager = dataManager;
_gameFunctions = gameFunctions;
_questFunctions = questFunctions;
_aetheryteFunctions = aetheryteFunctions;
_excelFunctions = excelFunctions;
_questController = questController;
_questRegistry = questRegistry;
@ -570,7 +569,7 @@ internal sealed class GameUiController : IDisposable
private unsafe bool HandleTravelYesNo(AddonSelectYesno* addonSelectYesno,
QuestController.QuestProgress currentQuest, string actualPrompt)
{
if (_gameFunctions.ReturnRequestedAt >= DateTime.Now.AddSeconds(-2) && _returnRegex.IsMatch(actualPrompt))
if (_aetheryteFunctions.ReturnRequestedAt >= DateTime.Now.AddSeconds(-2) && _returnRegex.IsMatch(actualPrompt))
{
_logger.LogInformation("Automatically confirming return...");
addonSelectYesno->AtkUnitBase.FireCallbackInt(0);

View File

@ -752,95 +752,24 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
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;
ushort[] shadowbringersRoleQuestChapters = QuestData.AllRoleQuestChapters.Select(x => x[0]).ToArray();
if (classJob != EClassJob.Adventurer)
{
priorityQuests.AddRange(_questRegistry.GetKnownClassJobQuests(classJob)
.Where(x =>
{
if (!_questRegistry.TryGetQuest(x.QuestId, out Quest? quest) ||
quest.Info is not QuestInfo questInfo)
return false;
// if no shadowbringers role quest is complete, (at least one) is required
if (shadowbringersRoleQuestChapters.Contains(questInfo.NewGamePlusChapter))
return !QuestData.FinalShadowbringersRoleQuests.Any(_questFunctions.IsQuestComplete);
// ignore all other role quests
if (QuestData.AllRoleQuestChapters.Any(y => y.Contains(questInfo.NewGamePlusChapter)))
return false;
// even job quests for the later expacs (after role quests were introduced) might have skills locked
// behind them, e.g. reaper and sage
return true;
})
.Select(x => x.QuestId));
}
return priorityQuests;
}
public bool TryPickPriorityQuest()
{
if (!IsInterruptible() || _nextQuest != null || _gatheringQuest != null || _simulatedQuest != null)
return false;
// 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))
ElementId? priorityQuestId = _questFunctions.GetNextPriorityQuestThatCanBeAccepted();
if (priorityQuestId == null)
return false;
foreach (ElementId questId in priorityQuests)
{
if (!_questFunctions.IsReadyToAcceptQuest(questId) || !_questRegistry.TryGetQuest(questId, out var quest))
continue;
// don't start a second priority quest until the first one is resolved
if (_startedQuest != null && priorityQuestId == _startedQuest.Quest.Id)
return false;
var firstStep = quest.FindSequence(0)?.FindStep(0);
if (firstStep == null)
continue;
if (firstStep.AetheryteShortcut is { } aetheryteShortcut)
if (_questRegistry.TryGetQuest(priorityQuestId, out var quest))
{
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;
}

View File

@ -25,7 +25,10 @@ internal static class AethernetShard
}
}
internal sealed class DoAttune(GameFunctions gameFunctions, ILogger<DoAttune> logger) : ITask
internal sealed class DoAttune(
AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions,
ILogger<DoAttune> logger) : ITask
{
public EAetheryteLocation AetheryteLocation { get; set; }
@ -37,7 +40,7 @@ internal static class AethernetShard
public bool Start()
{
if (!gameFunctions.IsAetheryteUnlocked(AetheryteLocation))
if (!aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation))
{
logger.LogInformation("Attuning to aethernet shard {AethernetShard}", AetheryteLocation);
gameFunctions.InteractWith((uint)AetheryteLocation, ObjectKind.Aetheryte);
@ -49,7 +52,7 @@ internal static class AethernetShard
}
public ETaskResult Update() =>
gameFunctions.IsAetheryteUnlocked(AetheryteLocation)
aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation)
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;

View File

@ -24,7 +24,10 @@ internal static class Aetheryte
}
}
internal sealed class DoAttune(GameFunctions gameFunctions, ILogger<DoAttune> logger) : ITask
internal sealed class DoAttune(
AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions,
ILogger<DoAttune> logger) : ITask
{
public EAetheryteLocation AetheryteLocation { get; set; }
@ -36,7 +39,7 @@ internal static class Aetheryte
public bool Start()
{
if (!gameFunctions.IsAetheryteUnlocked(AetheryteLocation))
if (!aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation))
{
logger.LogInformation("Attuning to aetheryte {Aetheryte}", AetheryteLocation);
gameFunctions.InteractWith((uint)AetheryteLocation);
@ -48,7 +51,7 @@ internal static class Aetheryte
}
public ETaskResult Update() =>
gameFunctions.IsAetheryteUnlocked(AetheryteLocation)
aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation)
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;

View File

@ -32,7 +32,7 @@ internal static class AethernetShortcut
internal sealed class UseAethernetShortcut(
ILogger<UseAethernetShortcut> logger,
GameFunctions gameFunctions,
AetheryteFunctions aetheryteFunctions,
IClientState clientState,
AetheryteData aetheryteData,
LifestreamIpc lifestreamIpc,
@ -72,22 +72,22 @@ internal static class AethernetShortcut
}
if (SkipConditions.AetheryteLocked != null &&
!gameFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteLocked.Value))
!aetheryteFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteLocked.Value))
{
logger.LogInformation("Skipping aethernet shortcut because the target aetheryte is locked");
return false;
}
if (SkipConditions.AetheryteUnlocked != null &&
gameFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteUnlocked.Value))
aetheryteFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteUnlocked.Value))
{
logger.LogInformation("Skipping aethernet shortcut because the target aetheryte is unlocked");
return false;
}
}
if (gameFunctions.IsAetheryteUnlocked(From) &&
gameFunctions.IsAetheryteUnlocked(To))
if (aetheryteFunctions.IsAetheryteUnlocked(From) &&
aetheryteFunctions.IsAetheryteUnlocked(To))
{
ushort territoryType = clientState.TerritoryType;
Vector3 playerPosition = clientState.LocalPlayer!.Position;

View File

@ -17,7 +17,7 @@ internal static class AetheryteShortcut
{
internal sealed class Factory(
IServiceProvider serviceProvider,
GameFunctions gameFunctions,
AetheryteFunctions aetheryteFunctions,
AetheryteData aetheryteData) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
@ -29,7 +29,7 @@ internal static class AetheryteShortcut
.With(step, step.AetheryteShortcut.Value, aetheryteData.TerritoryIds[step.AetheryteShortcut.Value]);
return
[
new WaitConditionTask(() => gameFunctions.CanTeleport(step.AetheryteShortcut.Value), "CanTeleport"),
new WaitConditionTask(() => aetheryteFunctions.CanTeleport(step.AetheryteShortcut.Value), "CanTeleport"),
task
];
}
@ -40,7 +40,7 @@ internal static class AetheryteShortcut
internal sealed class UseAetheryteShortcut(
ILogger<UseAetheryteShortcut> logger,
GameFunctions gameFunctions,
AetheryteFunctions aetheryteFunctions,
IClientState clientState,
IChatGui chatGui,
AetheryteData aetheryteData) : ISkippableTask
@ -80,14 +80,14 @@ internal static class AetheryteShortcut
}
if (skipConditions.AetheryteLocked != null &&
!gameFunctions.IsAetheryteUnlocked(skipConditions.AetheryteLocked.Value))
!aetheryteFunctions.IsAetheryteUnlocked(skipConditions.AetheryteLocked.Value))
{
logger.LogInformation("Skipping aetheryte teleport due to SkipCondition (AetheryteLocked)");
return false;
}
if (skipConditions.AetheryteUnlocked != null &&
gameFunctions.IsAetheryteUnlocked(skipConditions.AetheryteUnlocked.Value))
aetheryteFunctions.IsAetheryteUnlocked(skipConditions.AetheryteUnlocked.Value))
{
logger.LogInformation("Skipping aetheryte teleport due to SkipCondition (AetheryteUnlocked)");
return false;
@ -124,12 +124,12 @@ internal static class AetheryteShortcut
}
}
if (!gameFunctions.IsAetheryteUnlocked(TargetAetheryte))
if (!aetheryteFunctions.IsAetheryteUnlocked(TargetAetheryte))
{
chatGui.PrintError($"[Questionable] Aetheryte {TargetAetheryte} is not unlocked.");
throw new TaskException("Aetheryte is not unlocked");
}
else if (gameFunctions.TeleportAetheryte(TargetAetheryte))
else if (aetheryteFunctions.TeleportAetheryte(TargetAetheryte))
{
logger.LogInformation("Travelling via aetheryte...");
return true;

View File

@ -41,6 +41,7 @@ internal static class SkipCondition
internal sealed class CheckSkip(
ILogger<CheckSkip> logger,
AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions,
QuestFunctions questFunctions,
IClientState clientState) : ITask
@ -145,21 +146,21 @@ internal static class SkipCondition
DataId: not null,
InteractionType: EInteractionType.AttuneAetheryte or EInteractionType.AttuneAethernetShard
} &&
gameFunctions.IsAetheryteUnlocked((EAetheryteLocation)Step.DataId.Value))
aetheryteFunctions.IsAetheryteUnlocked((EAetheryteLocation)Step.DataId.Value))
{
logger.LogInformation("Skipping step, as aetheryte/aethernet shard is unlocked");
return true;
}
if (SkipConditions.AetheryteLocked != null &&
!gameFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteLocked.Value))
!aetheryteFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteLocked.Value))
{
logger.LogInformation("Skipping step, as aetheryte is locked");
return true;
}
if (SkipConditions.AetheryteUnlocked != null &&
gameFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteUnlocked.Value))
aetheryteFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteUnlocked.Value))
{
logger.LogInformation("Skipping step, as aetheryte is unlocked");
return true;

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Plugin.Services;
using LLib.GameData;
@ -63,6 +64,11 @@ internal sealed class QuestData
return _quests[elementId] ?? throw new ArgumentOutOfRangeException(nameof(elementId));
}
public bool TryGetQuestInfo(ElementId elementId, [NotNullWhen(true)] out IQuestInfo? questInfo)
{
return _quests.TryGetValue(elementId, out questInfo);
}
public List<IQuestInfo> GetAllByIssuerDataId(uint targetId)
{
return _quests.Values

View File

@ -0,0 +1,77 @@
using System;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Questionable.Model.Common;
using Questionable.Model.Questing;
namespace Questionable.Functions;
internal sealed unsafe class AetheryteFunctions
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<AetheryteFunctions> _logger;
public AetheryteFunctions(IServiceProvider serviceProvider, ILogger<AetheryteFunctions> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
public DateTime ReturnRequestedAt { get; set; } = DateTime.MinValue;
public bool IsAetheryteUnlocked(uint aetheryteId, out byte subIndex)
{
subIndex = 0;
var uiState = UIState.Instance();
return uiState != null && uiState->IsAetheryteUnlocked(aetheryteId);
}
public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation)
{
if (aetheryteLocation == EAetheryteLocation.IshgardFirmament)
return _serviceProvider.GetRequiredService<QuestFunctions>().IsQuestComplete(new QuestId(3672));
return IsAetheryteUnlocked((uint)aetheryteLocation, out _);
}
public bool CanTeleport(EAetheryteLocation aetheryteLocation)
{
if ((ushort)aetheryteLocation == PlayerState.Instance()->HomeAetheryteId &&
ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0)
return true;
return ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0;
}
public bool TeleportAetheryte(uint aetheryteId)
{
_logger.LogDebug("Attempting to teleport to aetheryte {AetheryteId}", aetheryteId);
if (IsAetheryteUnlocked(aetheryteId, out var subIndex))
{
if (aetheryteId == PlayerState.Instance()->HomeAetheryteId &&
ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0)
{
ReturnRequestedAt = DateTime.Now;
if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 8))
{
_logger.LogInformation("Using 'return' for home aetheryte");
return true;
}
}
if (ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0)
{
// fallback if return isn't available or (more likely) on a different aetheryte
_logger.LogInformation("Teleporting to aetheryte {AetheryteId}", aetheryteId);
return Telepo.Instance()->Teleport(aetheryteId, subIndex);
}
}
return false;
}
public bool TeleportAetheryte(EAetheryteLocation aetheryteLocation)
=> TeleportAetheryte((uint)aetheryteLocation);
}

View File

@ -74,62 +74,6 @@ internal sealed unsafe class GameFunctions
.AsReadOnly();
}
public DateTime ReturnRequestedAt { get; set; } = DateTime.MinValue;
public bool IsAetheryteUnlocked(uint aetheryteId, out byte subIndex)
{
subIndex = 0;
var uiState = UIState.Instance();
return uiState != null && uiState->IsAetheryteUnlocked(aetheryteId);
}
public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation)
{
if (aetheryteLocation == EAetheryteLocation.IshgardFirmament)
return _questFunctions.IsQuestComplete(new QuestId(3672));
return IsAetheryteUnlocked((uint)aetheryteLocation, out _);
}
public bool CanTeleport(EAetheryteLocation aetheryteLocation)
{
if ((ushort)aetheryteLocation == PlayerState.Instance()->HomeAetheryteId &&
ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0)
return true;
return ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0;
}
public bool TeleportAetheryte(uint aetheryteId)
{
_logger.LogDebug("Attempting to teleport to aetheryte {AetheryteId}", aetheryteId);
if (IsAetheryteUnlocked(aetheryteId, out var subIndex))
{
if (aetheryteId == PlayerState.Instance()->HomeAetheryteId &&
ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0)
{
ReturnRequestedAt = DateTime.Now;
if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 8))
{
_logger.LogInformation("Using 'return' for home aetheryte");
return true;
}
}
if (ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0)
{
// fallback if return isn't available or (more likely) on a different aetheryte
_logger.LogInformation("Teleporting to aetheryte {AetheryteId}", aetheryteId);
return Telepo.Instance()->Teleport(aetheryteId, subIndex);
}
}
return false;
}
public bool TeleportAetheryte(EAetheryteLocation aetheryteLocation)
=> TeleportAetheryte((uint)aetheryteLocation);
public bool IsFlyingUnlocked(ushort territoryId)
{
if (_configuration.Advanced.NeverFly)

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Memory;
@ -12,8 +13,10 @@ using LLib.GameData;
using LLib.GameUI;
using Lumina.Excel.GeneratedSheets;
using Questionable.Controller;
using Questionable.Controller.Steps.Interactions;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing;
using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
using Quest = Questionable.Model.Quest;
@ -24,16 +27,24 @@ internal sealed unsafe class QuestFunctions
{
private readonly QuestRegistry _questRegistry;
private readonly QuestData _questData;
private readonly AetheryteFunctions _aetheryteFunctions;
private readonly Configuration _configuration;
private readonly IDataManager _dataManager;
private readonly IClientState _clientState;
private readonly IGameGui _gameGui;
public QuestFunctions(QuestRegistry questRegistry, QuestData questData, Configuration configuration,
IDataManager dataManager, IClientState clientState, IGameGui gameGui)
public QuestFunctions(
QuestRegistry questRegistry,
QuestData questData,
AetheryteFunctions aetheryteFunctions,
Configuration configuration,
IDataManager dataManager,
IClientState clientState,
IGameGui gameGui)
{
_questRegistry = questRegistry;
_questData = questData;
_aetheryteFunctions = aetheryteFunctions;
_configuration = configuration;
_dataManager = dataManager;
_clientState = clientState;
@ -99,8 +110,11 @@ internal sealed unsafe class QuestFunctions
{
// always prioritize accepting MSQ quests, to make sure we don't turn in one MSQ quest and then go off to do
// side quests until the end of time.
var msqQuest = GetMainScenarioQuest(questManager);
if (msqQuest.CurrentQuest is { Value: not 0 } && _questRegistry.IsKnownQuest(msqQuest.CurrentQuest))
var msqQuest = GetMainScenarioQuest();
if (msqQuest.CurrentQuest != null && !_questRegistry.IsKnownQuest(msqQuest.CurrentQuest))
msqQuest = default;
if (msqQuest.CurrentQuest != null && !IsQuestAccepted(msqQuest.CurrentQuest))
return msqQuest;
// Use the quests in the same order as they're shown in the to-do list, e.g. if the MSQ is the first item,
@ -133,14 +147,24 @@ internal sealed unsafe class QuestFunctions
return (currentQuest, QuestManager.GetQuestSequence(currentQuest.Value));
}
// if we know no quest of those currently in the to-do list, just do MSQ
ElementId? priorityQuest = GetNextPriorityQuestThatCanBeAccepted();
if (priorityQuest != null)
{
// if we have an accepted msq quest, and know of no quest of those currently in the to-do list...
// (1) try and find a priority quest to do
return (priorityQuest, QuestManager.GetQuestSequence(priorityQuest.Value));
}
else if (msqQuest.CurrentQuest != null)
{
// (2) just do a normal msq quest
return msqQuest;
}
}
return default;
}
private (QuestId? CurrentQuest, byte Sequence) GetMainScenarioQuest(QuestManager* questManager)
private (QuestId? CurrentQuest, byte Sequence) GetMainScenarioQuest()
{
if (QuestManager.IsQuestComplete(3759)) // Memories Rekindled
{
@ -177,6 +201,7 @@ internal sealed unsafe class QuestFunctions
return default;
// if the MSQ is hidden, we generally ignore it
QuestManager* questManager = QuestManager.Instance();
if (IsQuestAccepted(currentQuest) && questManager->GetQuestById(currentQuest.Value)->IsHidden)
return default;
@ -214,6 +239,72 @@ internal sealed unsafe class QuestFunctions
return null;
}
public ElementId? GetNextPriorityQuestThatCanBeAccepted()
{
return GetPriorityQuestsThatCanBeAccepted()
.FirstOrDefault(x =>
{
if (!_questRegistry.TryGetQuest(x, out Quest? quest))
return false;
var firstStep = quest.FindSequence(0)?.FindStep(0);
if (firstStep == null)
return false;
if (firstStep.AetheryteShortcut is { } aetheryteShortcut &&
_aetheryteFunctions.IsAetheryteUnlocked(aetheryteShortcut))
return true;
if (firstStep is
{ InteractionType: EInteractionType.UseItem, ItemId: UseItem.VesperBayAetheryteTicket })
return true;
return false;
});
}
private List<ElementId> GetPriorityQuestsThatCanBeAccepted()
{
List<ElementId> priorityQuests =
[
new QuestId(1157), // Garuda (Hard)
new QuestId(1158), // Titan (Hard)
..QuestData.CrystalTowerQuests
];
EClassJob classJob = (EClassJob?)_clientState.LocalPlayer?.ClassJob.Id ?? EClassJob.Adventurer;
ushort[] shadowbringersRoleQuestChapters = QuestData.AllRoleQuestChapters.Select(x => x[0]).ToArray();
if (classJob != EClassJob.Adventurer)
{
priorityQuests.AddRange(_questRegistry.GetKnownClassJobQuests(classJob)
.Where(x =>
{
if (!_questRegistry.TryGetQuest(x.QuestId, out Quest? quest) ||
quest.Info is not QuestInfo questInfo)
return false;
// if no shadowbringers role quest is complete, (at least one) is required
if (shadowbringersRoleQuestChapters.Contains(questInfo.NewGamePlusChapter))
return !QuestData.FinalShadowbringersRoleQuests.Any(IsQuestComplete);
// ignore all other role quests
if (QuestData.AllRoleQuestChapters.Any(y => y.Contains(questInfo.NewGamePlusChapter)))
return false;
// even job quests for the later expacs (after role quests were introduced) might have skills locked
// behind them, e.g. reaper and sage
return true;
})
.Select(x => x.QuestId));
}
return priorityQuests
.Where(_questRegistry.IsKnownQuest)
.Where(IsReadyToAcceptQuest)
.ToList();
}
public bool IsReadyToAcceptQuest(ElementId questId)
{
_questRegistry.TryGetQuest(questId, out var quest);

View File

@ -64,7 +64,7 @@ internal sealed class QuestInfo : IQuestInfo
public ushort Level { get; }
public uint IssuerDataId { get; }
public bool IsRepeatable { get; }
public ImmutableList<QuestId> PreviousQuests { get; }
public ImmutableList<QuestId> PreviousQuests { get; set; }
public QuestJoin PreviousQuestJoin { get; }
public ImmutableList<QuestId> QuestLocks { get; }
public QuestJoin QuestLockJoin { get; }
@ -88,4 +88,9 @@ internal sealed class QuestInfo : IQuestInfo
All = 1,
AtLeastOne = 2,
}
public void AddPreviousQuest(QuestId questId)
{
PreviousQuests = [..PreviousQuests, questId];
}
}

View File

@ -98,6 +98,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
private static void AddBasicFunctionsAndData(ServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<AetheryteFunctions>();
serviceCollection.AddSingleton<ExcelFunctions>();
serviceCollection.AddSingleton<GameFunctions>();
serviceCollection.AddSingleton<ChatFunctions>();

View File

@ -25,7 +25,6 @@ internal sealed class ActiveQuestComponent
private readonly GatheringController _gatheringController;
private readonly QuestFunctions _questFunctions;
private readonly ICommandManager _commandManager;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly Configuration _configuration;
private readonly QuestRegistry _questRegistry;
private readonly IChatGui _chatGui;
@ -37,7 +36,6 @@ internal sealed class ActiveQuestComponent
GatheringController gatheringController,
QuestFunctions questFunctions,
ICommandManager commandManager,
IDalamudPluginInterface pluginInterface,
Configuration configuration,
QuestRegistry questRegistry,
IChatGui chatGui)
@ -48,7 +46,6 @@ internal sealed class ActiveQuestComponent
_gatheringController = gatheringController;
_questFunctions = questFunctions;
_commandManager = commandManager;
_pluginInterface = pluginInterface;
_configuration = configuration;
_questRegistry = questRegistry;
_chatGui = chatGui;

View File

@ -94,7 +94,8 @@ internal sealed class QuestTooltipComponent
foreach (var q in quest.PreviousQuests)
{
var qInfo = _questData.GetQuestInfo(q);
if (_questData.TryGetQuestInfo(q, out var qInfo))
{
var (iconColor, icon, _) = _uiUtils.GetQuestStyle(q);
if (!_questRegistry.IsKnownQuest(qInfo.QuestId))
iconColor = ImGuiColors.DalamudGrey;
@ -104,6 +105,12 @@ internal sealed class QuestTooltipComponent
if (qInfo is QuestInfo qstInfo && (counter <= 2 || icon != FontAwesomeIcon.Check))
DrawQuestUnlocks(qstInfo, counter + 1);
}
else
{
using var _ = ImRaii.Disabled();
_uiUtils.ChecklistItem($"Unknown Quest ({q})", ImGuiColors.DalamudGrey, FontAwesomeIcon.Question);
}
}
}
if (counter == 0 && quest.QuestLocks.Count > 0)