From 052c366ea0b5feb9c63e52655ee970d6e728d08f Mon Sep 17 00:00:00 2001
From: Liza Carvelli <liza@carvel.li>
Date: Tue, 13 Aug 2024 23:26:13 +0200
Subject: [PATCH] Consider alternative Lv1 class quests locked; don't do
 priority quests unless we can teleport + aren't broke

---
 Questionable/Data/QuestData.cs               | 32 +++++++++++++++-
 Questionable/Functions/AetheryteFunctions.cs | 29 +++++++++++---
 Questionable/Functions/QuestFunctions.cs     | 40 ++++++++++++++++++--
 Questionable/Windows/DebugOverlay.cs         |  6 +--
 4 files changed, 92 insertions(+), 15 deletions(-)

diff --git a/Questionable/Data/QuestData.cs b/Questionable/Data/QuestData.cs
index 4c01199d..f9164806 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<ushort> MeleeRoleQuests = [138, 156, 180];
     public static readonly IReadOnlyList<ushort> PhysicalRangedRoleQuests = [138, 157, 181];
     public static readonly IReadOnlyList<ushort> CasterRoleQuests = [139, 158, 182];
+
     public static readonly IReadOnlyList<IReadOnlyList<ushort>> 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<QuestId> 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 21f5a13e..a4e65661 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<AetheryteFunctions> _logger;
+    private readonly IDataManager _dataManager;
 
-    public AetheryteFunctions(IServiceProvider serviceProvider, ILogger<AetheryteFunctions> logger)
+    public AetheryteFunctions(IServiceProvider serviceProvider, ILogger<AetheryteFunctions> 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<GeneralAction>()!
+            .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 d1988668..0ac8157f 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 4fbb9074..ffefba17 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;