Add fallback for when scenario tree agent data is unavailable
authorLiza Carvelli <liza@carvel.li>
Sat, 5 Apr 2025 02:27:00 +0000 (04:27 +0200)
committerLiza Carvelli <liza@carvel.li>
Sat, 5 Apr 2025 02:27:00 +0000 (04:27 +0200)
Questionable.sln.DotSettings
Questionable/Data/QuestData.cs
Questionable/Functions/QuestFunctions.cs

index 528fc57..e598240 100644 (file)
@@ -1,6 +1,8 @@
 <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
        <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=vendor/@EntryIndexedValue">ExplicitlyExcluded</s:String>
        <s:Boolean x:Key="/Default/Environment/AutoImport2/=CSHARP/BlackLists/=Newtonsoft_002E_002A/@EntryIndexedValue">True</s:Boolean>
+       <s:Boolean x:Key="/Default/UserDictionary/Words/=alisaie/@EntryIndexedValue">True</s:Boolean>
+       <s:Boolean x:Key="/Default/UserDictionary/Words/=alphinaud/@EntryIndexedValue">True</s:Boolean>
        <s:Boolean x:Key="/Default/UserDictionary/Words/=aporia/@EntryIndexedValue">True</s:Boolean>
        <s:Boolean x:Key="/Default/UserDictionary/Words/=arcadion/@EntryIndexedValue">True</s:Boolean>
        <s:Boolean x:Key="/Default/UserDictionary/Words/=archeion/@EntryIndexedValue">True</s:Boolean>
index d2ec2b1..c724642 100644 (file)
@@ -7,6 +7,7 @@ using Dalamud.Plugin.Services;
 using FFXIVClientStructs.FFXIV.Client.Game.UI;
 using LLib.GameData;
 using Lumina.Excel.Sheets;
+using Microsoft.Extensions.Logging;
 using Questionable.Model;
 using Questionable.Model.Questing;
 using Quest = Lumina.Excel.Sheets.Quest;
@@ -225,13 +226,23 @@ internal sealed class QuestData
         // follow-up quests to picking a GC
         AddGcFollowUpQuests();
 
+        MainScenarioQuests = _quests.Values.Where(x => x is QuestInfo { IsMainScenarioQuest: true })
+            .Cast<QuestInfo>()
+            .ToList();
+
+        LastMainScenarioQuestId = MainScenarioQuests
+            .Where(x => !MainScenarioQuests.Any(y => y.PreviousQuests.Any(z => z.QuestId == x.QuestId)))
+            .Select(x => (QuestId)x.QuestId)
+            .FirstOrDefault() ?? new QuestId(0);
         RedeemableItems = quests.Where(x => x is QuestInfo)
             .Cast<QuestInfo>()
             .SelectMany(x => x.ItemRewards)
             .ToImmutableHashSet();
     }
 
+    public IReadOnlyList<QuestInfo> MainScenarioQuests { get; }
     public ImmutableHashSet<ItemReward> RedeemableItems { get; }
+    public QuestId LastMainScenarioQuestId { get; }
 
     private void AddPreviousQuest(QuestId questToUpdate, QuestId requiredQuestId)
     {
index b7d8bd5..0fb53dc 100644 (file)
@@ -13,6 +13,7 @@ using FFXIVClientStructs.FFXIV.Component.GUI;
 using LLib.GameData;
 using LLib.GameUI;
 using Lumina.Excel.Sheets;
+using Microsoft.Extensions.Logging;
 using Questionable.Controller;
 using Questionable.Data;
 using Questionable.Model;
@@ -272,7 +273,42 @@ internal sealed unsafe class QuestFunctions
 
         QuestId currentQuest = new QuestId(scenarioTree->Data->CurrentScenarioQuest);
         if (currentQuest.Value == 0)
-            return default;
+        {
+            if (IsQuestComplete(_questData.LastMainScenarioQuestId))
+                return default;
+
+            // fallback lookup; find a quest which isn't completed but where all prequisites are met
+            // excluding branching quests
+
+            var playerState = PlayerState.Instance();
+            var potentialQuests = _questData.MainScenarioQuests
+                .Where(x => x.StartingCity == 0 || x.StartingCity == playerState->StartTown)
+                .Where(q => IsReadyToAcceptQuest(q.QuestId, true))
+                .ToList();
+            if (potentialQuests.Count == 0)
+                return default;
+            else if (potentialQuests.Count > 1)
+            {
+                // for all of these (except the GC quests), questionable normally auto-picks the next quest based on the
+                // agent data. This should (hopefully) pick the exact same quest when the agent does not know for a bit.
+                if (potentialQuests.All(x => x.QuestId.Value is 680 or 681 or 682))
+                    currentQuest = new QuestId(681); // The Company You Keep; actual quest will be resolved later in GetCurrentQuest
+                else if (potentialQuests.Any(x => x.QuestId.Value == 1583))
+                    currentQuest = new QuestId(1583); // HW: Over the Wall vs. Onwards and Upwards
+                else if (potentialQuests.Any(x => x.QuestId.Value == 2451))
+                    currentQuest = new QuestId(2451); // SB: A Friend of a Friend in Need vs. A Familiar Face Forgotten
+                else if (potentialQuests.Any(x => x.QuestId.Value == 3282))
+                    currentQuest = new QuestId(3282); // ShB: In Search of Alphinaud vs. In Search of Alisaie
+                else if (potentialQuests.Any(x => x.QuestId.Value == 4359))
+                    currentQuest = new QuestId(4359); // EW: Hitting the Books vs. For Thavnair Bound
+                else if (potentialQuests.Any(x => x.QuestId.Value == 4865))
+                    currentQuest = new QuestId(4865); // DT: To Kozama'uk vs. To Urqopacha
+                if (potentialQuests.Count != 1)
+                    return default;
+            }
+            else
+                currentQuest = (QuestId)potentialQuests.Single().QuestId;
+        }
 
         // if the MSQ is hidden, we generally ignore it
         QuestManager* questManager = QuestManager.Instance();
@@ -350,7 +386,7 @@ internal sealed unsafe class QuestFunctions
         int gil = inventoryManager->GetItemCountInContainer(1, InventoryType.Currency);
 
         return GetPriorityQuests()
-            .Where(IsReadyToAcceptQuest)
+            .Where(x => IsReadyToAcceptQuest(x))
             .Where(x =>
             {
                 if (!_questRegistry.TryGetQuest(x, out Quest? quest))
@@ -443,7 +479,7 @@ internal sealed unsafe class QuestFunctions
             .ToList();
     }
 
-    public bool IsReadyToAcceptQuest(ElementId questId)
+    public bool IsReadyToAcceptQuest(ElementId questId, bool ignoreLevel = false)
     {
         _questRegistry.TryGetQuest(questId, out var quest);
         if (quest is { Info.IsRepeatable: true })
@@ -474,10 +510,13 @@ internal sealed unsafe class QuestFunctions
         if (IsQuestLocked(questId))
             return false;
 
-        // if we're not at a high enough level to continue, we also ignore it
-        var currentLevel = _clientState.LocalPlayer?.Level ?? 0;
-        if (currentLevel != 0 && quest != null && quest.Info.Level > currentLevel)
-            return false;
+        if (!ignoreLevel)
+        {
+            // if we're not at a high enough level to continue, we also ignore it
+            var currentLevel = _clientState.LocalPlayer?.Level ?? 0;
+            if (currentLevel != 0 && quest != null && quest.Info.Level > currentLevel)
+                return false;
+        }
 
         return true;
     }