Add Vanu Vanu quest pooling for talking to npcs on the island
authorLiza Carvelli <liza@carvel.li>
Sun, 1 Dec 2024 20:54:23 +0000 (21:54 +0100)
committerLiza Carvelli <liza@carvel.li>
Sun, 1 Dec 2024 20:54:23 +0000 (21:54 +0100)
Questionable/Controller/Steps/QuestCleanUp.cs
Questionable/Data/AlliedSocietyData.cs [new file with mode: 0644]
Questionable/Functions/QuestFunctions.cs
Questionable/QuestionablePlugin.cs

index 76f168f50dbd3785ee0b4f22f48cab5d9bb72e63..d5564180b7acc7892d51bc95cbc4010361f10463 100644 (file)
@@ -12,14 +12,7 @@ namespace Questionable.Controller.Steps;
 
 internal static class QuestCleanUp
 {
-    private static readonly Dictionary<ushort, MountConfiguration> AlliedSocietyMountConfiguration = new()
-    {
-        { 66, new(1016093, EAetheryteLocation.SeaOfCloudsOkZundu) },
-        { 79, new(1017031, EAetheryteLocation.DravanianForelandsAnyxTrine) },
-        { 369, new(1051798, EAetheryteLocation.KozamaukaDockPoga) },
-    };
-
-    internal sealed class CheckAlliedSocietyMount(GameFunctions gameFunctions, AetheryteData aetheryteData, ILogger<CheckAlliedSocietyMount> logger) : SimpleTaskFactory
+    internal sealed class CheckAlliedSocietyMount(GameFunctions gameFunctions, AetheryteData aetheryteData, AlliedSocietyData alliedSocietyData, ILogger<CheckAlliedSocietyMount> logger) : SimpleTaskFactory
     {
         public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
         {
@@ -28,7 +21,7 @@ internal static class QuestCleanUp
 
             // if you are on a allied society mount
             if (gameFunctions.GetMountId() is { } mountId &&
-                AlliedSocietyMountConfiguration.TryGetValue(mountId, out var mountConfiguration))
+                alliedSocietyData.Mounts.TryGetValue(mountId, out var mountConfiguration))
             {
                 logger.LogInformation("We are on a known allied society mount with id = {MountId}", mountId);
 
@@ -68,6 +61,4 @@ internal static class QuestCleanUp
             return null;
         }
     }
-
-    private sealed record MountConfiguration(uint IssuerDataId, EAetheryteLocation ClosestAetheryte);
 }
diff --git a/Questionable/Data/AlliedSocietyData.cs b/Questionable/Data/AlliedSocietyData.cs
new file mode 100644 (file)
index 0000000..3c39dfd
--- /dev/null
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+using Questionable.Model;
+using Questionable.Model.Common;
+using Questionable.Model.Questing;
+
+namespace Questionable.Data;
+
+[SuppressMessage("Performance", "CA1822")]
+internal sealed class AlliedSocietyData
+{
+    public ReadOnlyDictionary<ushort, AlliedSocietyMountConfiguration> Mounts { get; } =
+        new Dictionary<ushort, AlliedSocietyMountConfiguration>
+        {
+            { 66, new(1016093, EAetheryteLocation.SeaOfCloudsOkZundu) },
+            { 79, new(1017031, EAetheryteLocation.DravanianForelandsAnyxTrine) },
+            { 369, new(1051798, EAetheryteLocation.KozamaukaDockPoga) },
+        }.AsReadOnly();
+
+    public EAlliedSociety GetCommonAlliedSocietyTurnIn(ElementId elementId)
+    {
+        if (elementId is QuestId questId)
+        {
+            return questId.Value switch
+            {
+                >= 2171 and <= 2200 => EAlliedSociety.VanuVanu,
+                >= 2261 and <= 2280 => EAlliedSociety.Vath,
+                >= 5199 and <= 5226 => EAlliedSociety.Pelupelu,
+                _ => EAlliedSociety.None,
+            };
+        }
+
+        return EAlliedSociety.None;
+    }
+
+    public void GetCommonAlliedSocietyNpcs(EAlliedSociety alliedSociety, out uint[] normalNpcs, out uint[] mountNpcs)
+    {
+        if (alliedSociety == EAlliedSociety.VanuVanu)
+        {
+            normalNpcs = [1016088, 1016091, 1016092];
+            mountNpcs = [1016093];
+        }
+        else if (alliedSociety == EAlliedSociety.Vath)
+        {
+            normalNpcs = [];
+            mountNpcs = [1017031];
+        }
+        else
+        {
+            normalNpcs = [];
+            mountNpcs = [];
+        }
+    }
+}
+
+public sealed record AlliedSocietyMountConfiguration(uint IssuerDataId, EAetheryteLocation ClosestAetheryte);
index 067e0e7794c8ae886c4881d7583edfa02baadeca..e61997d95c781b14f7712efc4a025b0b452565cb 100644 (file)
@@ -6,6 +6,7 @@ using Dalamud.Memory;
 using Dalamud.Plugin.Services;
 using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
 using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.Game.Character;
 using FFXIVClientStructs.FFXIV.Client.Game.UI;
 using FFXIVClientStructs.FFXIV.Client.UI.Agent;
 using FFXIVClientStructs.FFXIV.Component.GUI;
@@ -27,6 +28,7 @@ internal sealed unsafe class QuestFunctions
     private readonly QuestRegistry _questRegistry;
     private readonly QuestData _questData;
     private readonly AetheryteFunctions _aetheryteFunctions;
+    private readonly AlliedSocietyData _alliedSocietyData;
     private readonly Configuration _configuration;
     private readonly IDataManager _dataManager;
     private readonly IClientState _clientState;
@@ -36,6 +38,7 @@ internal sealed unsafe class QuestFunctions
         QuestRegistry questRegistry,
         QuestData questData,
         AetheryteFunctions aetheryteFunctions,
+        AlliedSocietyData alliedSocietyData,
         Configuration configuration,
         IDataManager dataManager,
         IClientState clientState,
@@ -44,6 +47,7 @@ internal sealed unsafe class QuestFunctions
         _questRegistry = questRegistry;
         _questData = questData;
         _aetheryteFunctions = aetheryteFunctions;
+        _alliedSocietyData = alliedSocietyData;
         _configuration = configuration;
         _dataManager = dataManager;
         _clientState = clientState;
@@ -138,7 +142,8 @@ internal sealed unsafe class QuestFunctions
                     case 2: // leve
                         currentQuest = new LeveId(questManager->LeveQuests[trackedQuest.Index].LeveId);
                         if (_questRegistry.IsKnownQuest(currentQuest))
-                            trackedQuests.Add((currentQuest, questManager->GetLeveQuestById(currentQuest.Value)->Sequence));
+                            trackedQuests.Add((currentQuest,
+                                questManager->GetLeveQuestById(currentQuest.Value)->Sequence));
                         break;
                 }
             }
@@ -147,14 +152,63 @@ internal sealed unsafe class QuestFunctions
             {
                 // if we have multiple quests to turn in for an allied society, try and complete all of them
                 var (firstTrackedQuest, firstTrackedSequence) = trackedQuests.First();
-                EAlliedSociety firstTrackedAlliedSociety = GetCommonAlliedSocietyTurnIn(firstTrackedQuest);
-                if (firstTrackedAlliedSociety != EAlliedSociety.None && firstTrackedSequence == 255)
+                EAlliedSociety firstTrackedAlliedSociety = _alliedSocietyData.GetCommonAlliedSocietyTurnIn(firstTrackedQuest);
+                if (firstTrackedAlliedSociety != EAlliedSociety.None)
                 {
-                    foreach (var (quest, sequence) in trackedQuests.Skip(1))
+                    var alliedQuestsForSameSociety = trackedQuests.Skip(1)
+                        .Where(quest => _alliedSocietyData.GetCommonAlliedSocietyTurnIn(quest.Quest) == firstTrackedAlliedSociety)
+                        .ToList();
+                    if (alliedQuestsForSameSociety.Count > 0)
                     {
-                        // only if the other quest isn't ready to be turned in
-                        if (GetCommonAlliedSocietyTurnIn(quest) == firstTrackedAlliedSociety && sequence != 255)
-                            return (quest, sequence);
+                        if (firstTrackedSequence == 255)
+                        {
+                            foreach (var (quest, sequence) in alliedQuestsForSameSociety)
+                            {
+                                // only if the other quest isn't ready to be turned in
+                                if (sequence != 255)
+                                    return (quest, sequence);
+                            }
+                        }
+                        else if (!IsOnAlliedSocietyMount())
+                        {
+                            // a few of the vanu quests require you to talk to one of the npcs near the issuer, so we
+                            // give priority to those
+
+                            // also include the first quest in the list for those
+                            alliedQuestsForSameSociety.Insert(0, (firstTrackedQuest, firstTrackedSequence));
+
+                            _alliedSocietyData.GetCommonAlliedSocietyNpcs(firstTrackedAlliedSociety, out uint[]? normalNpcs,
+                                out uint[]? mountNpcs);
+
+                            if (normalNpcs.Length > 0)
+                            {
+                                var talkToNormalNpcs = alliedQuestsForSameSociety
+                                    .Where(x => x.Sequence < 255)
+                                    .Where(x => IsInteractStep(x.Quest, x.Sequence, normalNpcs))
+                                    .Cast<(ElementId, byte)?>()
+                                    .FirstOrDefault();
+                                if (talkToNormalNpcs != null)
+                                    return talkToNormalNpcs.Value;
+                            }
+
+                            /*
+                             * TODO: If you have e.g. a mount quest in the middle of 3, it should temporarily make you
+                             *       do that quest first, even if it isn't the first in the list. Otherwise, the logic
+                             *       here won't make much sense.
+                             *
+                             * TODO: This also won't work if two or three daily quests use a mount.
+                            if (mountNpcs.Length > 0)
+                            {
+                                var talkToMountNpc = alliedQuestsForSameSociety
+                                    .Where(x => x.Sequence < 255)
+                                    .Where(x => IsInteractStep(x.Quest, x.Sequence, mountNpcs))
+                                    .Cast<(ElementId, byte)?>()
+                                    .FirstOrDefault();
+                                if (talkToMountNpc != null)
+                                    return talkToMountNpc.Value;
+                            }
+                            */
+                        }
                     }
                 }
 
@@ -237,20 +291,24 @@ internal sealed unsafe class QuestFunctions
         return (currentQuest, QuestManager.GetQuestSequence(currentQuest.Value));
     }
 
-    private static EAlliedSociety GetCommonAlliedSocietyTurnIn(ElementId elementId)
+    private bool IsOnAlliedSocietyMount()
     {
-        if (elementId is QuestId questId)
+        BattleChara* battleChara = (BattleChara*)(_clientState.LocalPlayer?.Address ?? 0);
+        return battleChara != null &&
+               battleChara->Mount.MountId != 0 &&
+               _alliedSocietyData.Mounts.ContainsKey(battleChara->Mount.MountId);
+    }
+
+    private bool IsInteractStep(ElementId questId, byte sequence, uint[] dataIds)
+    {
+        if (_questRegistry.TryGetQuest(questId, out var quest))
         {
-            return questId.Value switch
-            {
-                >= 2171 and <= 2200 => EAlliedSociety.VanuVanu,
-                >= 2261 and <= 2280 => EAlliedSociety.Vath,
-                >= 5199 and <= 5226 => EAlliedSociety.Pelupelu,
-                _ => EAlliedSociety.None,
-            };
+            QuestStep? firstStepOfSequence = quest.FindSequence(sequence)?.FindStep(0);
+            return firstStepOfSequence is { InteractionType: EInteractionType.Interact, DataId: { } dataId } &&
+                   dataIds.Contains(dataId);
         }
 
-        return EAlliedSociety.None;
+        return false;
     }
 
     public QuestProgressInfo? GetQuestProgressInfo(ElementId elementId)
index 7b17db4c0138a1303be8e19787951844c18958fe..a4eabbaa2924e9b0b2f192662836332b02408a1f 100644 (file)
@@ -115,6 +115,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
 
         serviceCollection.AddSingleton<AetherCurrentData>();
         serviceCollection.AddSingleton<AetheryteData>();
+        serviceCollection.AddSingleton<AlliedSocietyData>();
         serviceCollection.AddSingleton<GatheringData>();
         serviceCollection.AddSingleton<LeveData>();
         serviceCollection.AddSingleton<JournalData>();