diff --git a/Questionable/Data/QuestData.cs b/Questionable/Data/QuestData.cs index 4c01199d4..f9164806f 100644 --- a/Questionable/Data/QuestData.cs +++ b/Questionable/Data/QuestData.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.UI; using LLib.GameData; using Lumina.Excel.GeneratedSheets; using Questionable.Model; @@ -21,6 +22,7 @@ internal sealed class QuestData public static readonly IReadOnlyList MeleeRoleQuests = [138, 156, 180]; public static readonly IReadOnlyList PhysicalRangedRoleQuests = [138, 157, 181]; public static readonly IReadOnlyList CasterRoleQuests = [139, 158, 182]; + public static readonly IReadOnlyList> AllRoleQuestChapters = [ TankRoleQuests, @@ -59,7 +61,7 @@ internal sealed class QuestData _quests = quests.ToDictionary(x => x.QuestId, x => x); // workaround because the game doesn't require completion of the CT questline through normal means - QuestInfo aTimeToEveryPurpose = (QuestInfo) _quests[new QuestId(425)]; + QuestInfo aTimeToEveryPurpose = (QuestInfo)_quests[new QuestId(425)]; aTimeToEveryPurpose.AddPreviousQuest(new QuestId(495)); } @@ -180,4 +182,32 @@ internal sealed class QuestData .Where(x => chapterIds.Contains(x.NewGamePlusChapter)) .ToList(); } + + public List GetLockedClassQuests() + { + EClassJob startingClass; + unsafe + { + var playerState = PlayerState.Instance(); + if (playerState != null) + startingClass = (EClassJob)playerState->FirstClass; + else + startingClass = EClassJob.Adventurer; + } + + if (startingClass == EClassJob.Adventurer) + return []; + + return + [ + startingClass == EClassJob.Gladiator ? new(177) : new(253), + startingClass == EClassJob.Pugilist ? new(178) : new(533), + startingClass == EClassJob.Marauder ? new(179) : new(311), + startingClass == EClassJob.Lancer ? new(180) : new(23), + startingClass == EClassJob.Archer ? new(181) : new(21), + startingClass == EClassJob.Conjurer ? new(182) : new(22), + startingClass == EClassJob.Thaumaturge ? new(183) : new(345), + startingClass == EClassJob.Arcanist ? new(451) : new(453), + ]; + } } diff --git a/Questionable/Functions/AetheryteFunctions.cs b/Questionable/Functions/AetheryteFunctions.cs index 21f5a13ed..a4e65661a 100644 --- a/Questionable/Functions/AetheryteFunctions.cs +++ b/Questionable/Functions/AetheryteFunctions.cs @@ -1,6 +1,9 @@ using System; +using System.Linq; +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.UI; +using Lumina.Excel.GeneratedSheets; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Questionable.Model.Common; @@ -10,13 +13,19 @@ namespace Questionable.Functions; internal sealed unsafe class AetheryteFunctions { + private const uint TeleportAction = 5; + private const uint ReturnAction = 8; + private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; + private readonly IDataManager _dataManager; - public AetheryteFunctions(IServiceProvider serviceProvider, ILogger logger) + public AetheryteFunctions(IServiceProvider serviceProvider, ILogger logger, + IDataManager dataManager) { _serviceProvider = serviceProvider; _logger = logger; + _dataManager = dataManager; } public DateTime ReturnRequestedAt { get; set; } = DateTime.MinValue; @@ -39,10 +48,18 @@ internal sealed unsafe class AetheryteFunctions public bool CanTeleport(EAetheryteLocation aetheryteLocation) { if ((ushort)aetheryteLocation == PlayerState.Instance()->HomeAetheryteId && - ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0) + ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, ReturnAction) == 0) return true; - return ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0; + return ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, TeleportAction) == 0; + } + + public bool IsTeleportUnlocked() + { + ushort unlockLink = _dataManager.GetExcelSheet()! + .Single(x => x.Action.Row == 5) + .UnlockLink; + return UIState.Instance()->IsUnlockLinkUnlocked(unlockLink); } public bool TeleportAetheryte(uint aetheryteId) @@ -51,17 +68,17 @@ internal sealed unsafe class AetheryteFunctions if (IsAetheryteUnlocked(aetheryteId, out var subIndex)) { if (aetheryteId == PlayerState.Instance()->HomeAetheryteId && - ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 8) == 0) + ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, ReturnAction) == 0) { ReturnRequestedAt = DateTime.Now; - if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 8)) + if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, ReturnAction)) { _logger.LogInformation("Using 'return' for home aetheryte"); return true; } } - if (ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0) + if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, TeleportAction) == 0) { // fallback if return isn't available or (more likely) on a different aetheryte _logger.LogInformation("Teleporting to aetheryte {AetheryteId}", aetheryteId); diff --git a/Questionable/Functions/QuestFunctions.cs b/Questionable/Functions/QuestFunctions.cs index d1988668a..0ac8157f6 100644 --- a/Questionable/Functions/QuestFunctions.cs +++ b/Questionable/Functions/QuestFunctions.cs @@ -241,8 +241,21 @@ internal sealed unsafe class QuestFunctions public ElementId? GetNextPriorityQuestThatCanBeAccepted() { + // all priority quests assume we're able to teleport to the beginning (and for e.g. class quests, the end) + // ideally without having to wait 15m for Return. + if (!_aetheryteFunctions.IsTeleportUnlocked()) + return null; + + // ideally, we'd also be able to afford *some* teleports + // this implicitly makes sure we're not starting one of the lv1 class quests if we can't afford to teleport back + // + // Of course, they can still be accepted manually. + InventoryManager* inventoryManager = InventoryManager.Instance(); + if (inventoryManager->GetItemCountInContainer(1, InventoryType.Currency) < 2000) + return null; + return GetPriorityQuestsThatCanBeAccepted() - .FirstOrDefault(x => + .Where(x => { if (!_questRegistry.TryGetQuest(x, out Quest? quest)) return false; @@ -251,8 +264,7 @@ internal sealed unsafe class QuestFunctions if (firstStep == null) return false; - if (firstStep.AetheryteShortcut is { } aetheryteShortcut && - _aetheryteFunctions.IsAetheryteUnlocked(aetheryteShortcut)) + if (firstStep.AetheryteShortcut != null) return true; if (firstStep is @@ -260,6 +272,25 @@ internal sealed unsafe class QuestFunctions return true; return false; + }) + .FirstOrDefault(x => + { + if (!_questRegistry.TryGetQuest(x, out Quest? quest)) + return false; + + return quest.AllSteps().All(x => + { + if (x.Step.AetheryteShortcut is { } aetheryteShortcut && + _aetheryteFunctions.IsAetheryteUnlocked(aetheryteShortcut)) + return false; + + if (x.Step.AethernetShortcut is { } aethernetShortcut && + (!_aetheryteFunctions.IsAetheryteUnlocked(aethernetShortcut.From) || + !_aetheryteFunctions.IsAetheryteUnlocked(aethernetShortcut.To))) + return false; + + return true; + }); }); } @@ -415,6 +446,9 @@ internal sealed unsafe class QuestFunctions if (questInfo.GrandCompany != GrandCompany.None && questInfo.GrandCompany != GetGrandCompany()) return true; + if (_questData.GetLockedClassQuests().Contains(questId)) + return true; + return !HasCompletedPreviousQuests(questInfo, extraCompletedQuest) || !HasCompletedPreviousInstances(questInfo); } diff --git a/Questionable/Windows/DebugOverlay.cs b/Questionable/Windows/DebugOverlay.cs index 4fbb9074f..ffefba17a 100644 --- a/Questionable/Windows/DebugOverlay.cs +++ b/Questionable/Windows/DebugOverlay.cs @@ -1,16 +1,12 @@ -using System; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; using System.Numerics; using Dalamud.Game.ClientState.Conditions; -using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using ImGuiNET; using Questionable.Controller; using Questionable.Data; -using Questionable.Model; using Questionable.Model.Questing; namespace Questionable.Windows;