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

View File

@ -752,94 +752,23 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
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;
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() public bool TryPickPriorityQuest()
{ {
if (!IsInterruptible() || _nextQuest != null || _gatheringQuest != null || _simulatedQuest != null) if (!IsInterruptible() || _nextQuest != null || _gatheringQuest != null || _simulatedQuest != null)
return false; return false;
// don't start a second priority quest until the first one is resolved ElementId? priorityQuestId = _questFunctions.GetNextPriorityQuestThatCanBeAccepted();
List<ElementId> priorityQuests = GetPriorityQuests(); if (priorityQuestId == null)
if (_startedQuest != null && priorityQuests.Contains(_startedQuest.Quest.Id))
return false; return false;
foreach (ElementId questId in priorityQuests) // don't start a second priority quest until the first one is resolved
if (_startedQuest != null && priorityQuestId == _startedQuest.Quest.Id)
return false;
if (_questRegistry.TryGetQuest(priorityQuestId, out var quest))
{ {
if (!_questFunctions.IsReadyToAcceptQuest(questId) || !_questRegistry.TryGetQuest(questId, out var quest)) SetNextQuest(quest);
continue; return true;
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; 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; } public EAetheryteLocation AetheryteLocation { get; set; }
@ -37,7 +40,7 @@ internal static class AethernetShard
public bool Start() public bool Start()
{ {
if (!gameFunctions.IsAetheryteUnlocked(AetheryteLocation)) if (!aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation))
{ {
logger.LogInformation("Attuning to aethernet shard {AethernetShard}", AetheryteLocation); logger.LogInformation("Attuning to aethernet shard {AethernetShard}", AetheryteLocation);
gameFunctions.InteractWith((uint)AetheryteLocation, ObjectKind.Aetheryte); gameFunctions.InteractWith((uint)AetheryteLocation, ObjectKind.Aetheryte);
@ -49,7 +52,7 @@ internal static class AethernetShard
} }
public ETaskResult Update() => public ETaskResult Update() =>
gameFunctions.IsAetheryteUnlocked(AetheryteLocation) aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : 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; } public EAetheryteLocation AetheryteLocation { get; set; }
@ -36,7 +39,7 @@ internal static class Aetheryte
public bool Start() public bool Start()
{ {
if (!gameFunctions.IsAetheryteUnlocked(AetheryteLocation)) if (!aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation))
{ {
logger.LogInformation("Attuning to aetheryte {Aetheryte}", AetheryteLocation); logger.LogInformation("Attuning to aetheryte {Aetheryte}", AetheryteLocation);
gameFunctions.InteractWith((uint)AetheryteLocation); gameFunctions.InteractWith((uint)AetheryteLocation);
@ -48,7 +51,7 @@ internal static class Aetheryte
} }
public ETaskResult Update() => public ETaskResult Update() =>
gameFunctions.IsAetheryteUnlocked(AetheryteLocation) aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : ETaskResult.StillRunning;

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using LLib.GameData; using LLib.GameData;
@ -63,6 +64,11 @@ internal sealed class QuestData
return _quests[elementId] ?? throw new ArgumentOutOfRangeException(nameof(elementId)); 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) public List<IQuestInfo> GetAllByIssuerDataId(uint targetId)
{ {
return _quests.Values 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(); .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) public bool IsFlyingUnlocked(ushort territoryId)
{ {
if (_configuration.Advanced.NeverFly) if (_configuration.Advanced.NeverFly)

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Dalamud.Memory; using Dalamud.Memory;
@ -12,8 +13,10 @@ using LLib.GameData;
using LLib.GameUI; using LLib.GameUI;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Questionable.Controller; using Questionable.Controller;
using Questionable.Controller.Steps.Interactions;
using Questionable.Data; using Questionable.Data;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing; using Questionable.Model.Questing;
using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany; using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
using Quest = Questionable.Model.Quest; using Quest = Questionable.Model.Quest;
@ -24,16 +27,24 @@ internal sealed unsafe class QuestFunctions
{ {
private readonly QuestRegistry _questRegistry; private readonly QuestRegistry _questRegistry;
private readonly QuestData _questData; private readonly QuestData _questData;
private readonly AetheryteFunctions _aetheryteFunctions;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly IDataManager _dataManager; private readonly IDataManager _dataManager;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly IGameGui _gameGui; private readonly IGameGui _gameGui;
public QuestFunctions(QuestRegistry questRegistry, QuestData questData, Configuration configuration, public QuestFunctions(
IDataManager dataManager, IClientState clientState, IGameGui gameGui) QuestRegistry questRegistry,
QuestData questData,
AetheryteFunctions aetheryteFunctions,
Configuration configuration,
IDataManager dataManager,
IClientState clientState,
IGameGui gameGui)
{ {
_questRegistry = questRegistry; _questRegistry = questRegistry;
_questData = questData; _questData = questData;
_aetheryteFunctions = aetheryteFunctions;
_configuration = configuration; _configuration = configuration;
_dataManager = dataManager; _dataManager = dataManager;
_clientState = clientState; _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 // 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. // side quests until the end of time.
var msqQuest = GetMainScenarioQuest(questManager); var msqQuest = GetMainScenarioQuest();
if (msqQuest.CurrentQuest is { Value: not 0 } && _questRegistry.IsKnownQuest(msqQuest.CurrentQuest)) if (msqQuest.CurrentQuest != null && !_questRegistry.IsKnownQuest(msqQuest.CurrentQuest))
msqQuest = default;
if (msqQuest.CurrentQuest != null && !IsQuestAccepted(msqQuest.CurrentQuest))
return msqQuest; 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, // 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)); 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();
return msqQuest; 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; return default;
} }
private (QuestId? CurrentQuest, byte Sequence) GetMainScenarioQuest(QuestManager* questManager) private (QuestId? CurrentQuest, byte Sequence) GetMainScenarioQuest()
{ {
if (QuestManager.IsQuestComplete(3759)) // Memories Rekindled if (QuestManager.IsQuestComplete(3759)) // Memories Rekindled
{ {
@ -177,6 +201,7 @@ internal sealed unsafe class QuestFunctions
return default; return default;
// if the MSQ is hidden, we generally ignore it // if the MSQ is hidden, we generally ignore it
QuestManager* questManager = QuestManager.Instance();
if (IsQuestAccepted(currentQuest) && questManager->GetQuestById(currentQuest.Value)->IsHidden) if (IsQuestAccepted(currentQuest) && questManager->GetQuestById(currentQuest.Value)->IsHidden)
return default; return default;
@ -214,6 +239,72 @@ internal sealed unsafe class QuestFunctions
return null; 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) public bool IsReadyToAcceptQuest(ElementId questId)
{ {
_questRegistry.TryGetQuest(questId, out var quest); _questRegistry.TryGetQuest(questId, out var quest);

View File

@ -64,7 +64,7 @@ internal sealed class QuestInfo : IQuestInfo
public ushort Level { get; } public ushort Level { get; }
public uint IssuerDataId { get; } public uint IssuerDataId { get; }
public bool IsRepeatable { get; } public bool IsRepeatable { get; }
public ImmutableList<QuestId> PreviousQuests { get; } public ImmutableList<QuestId> PreviousQuests { get; set; }
public QuestJoin PreviousQuestJoin { get; } public QuestJoin PreviousQuestJoin { get; }
public ImmutableList<QuestId> QuestLocks { get; } public ImmutableList<QuestId> QuestLocks { get; }
public QuestJoin QuestLockJoin { get; } public QuestJoin QuestLockJoin { get; }
@ -88,4 +88,9 @@ internal sealed class QuestInfo : IQuestInfo
All = 1, All = 1,
AtLeastOne = 2, 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) private static void AddBasicFunctionsAndData(ServiceCollection serviceCollection)
{ {
serviceCollection.AddSingleton<AetheryteFunctions>();
serviceCollection.AddSingleton<ExcelFunctions>(); serviceCollection.AddSingleton<ExcelFunctions>();
serviceCollection.AddSingleton<GameFunctions>(); serviceCollection.AddSingleton<GameFunctions>();
serviceCollection.AddSingleton<ChatFunctions>(); serviceCollection.AddSingleton<ChatFunctions>();

View File

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

View File

@ -94,15 +94,22 @@ internal sealed class QuestTooltipComponent
foreach (var q in quest.PreviousQuests) 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)) var (iconColor, icon, _) = _uiUtils.GetQuestStyle(q);
iconColor = ImGuiColors.DalamudGrey; if (!_questRegistry.IsKnownQuest(qInfo.QuestId))
iconColor = ImGuiColors.DalamudGrey;
_uiUtils.ChecklistItem(FormatQuestUnlockName(qInfo), iconColor, icon); _uiUtils.ChecklistItem(FormatQuestUnlockName(qInfo), iconColor, icon);
if (qInfo is QuestInfo qstInfo && (counter <= 2 || icon != FontAwesomeIcon.Check)) if (qInfo is QuestInfo qstInfo && (counter <= 2 || icon != FontAwesomeIcon.Check))
DrawQuestUnlocks(qstInfo, counter + 1); DrawQuestUnlocks(qstInfo, counter + 1);
}
else
{
using var _ = ImRaii.Disabled();
_uiUtils.ChecklistItem($"Unknown Quest ({q})", ImGuiColors.DalamudGrey, FontAwesomeIcon.Question);
}
} }
} }