Fix aethernet names, add new emotes, add EquipItem
authorLiza Carvelli <liza@carvel.li>
Thu, 13 Jun 2024 15:35:33 +0000 (17:35 +0200)
committerLiza Carvelli <liza@carvel.li>
Thu, 13 Jun 2024 15:35:51 +0000 (17:35 +0200)
QuestPaths/quest-v1.json
Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs
Questionable/Controller/Steps/InteractionFactory/EquipItem.cs [new file with mode: 0644]
Questionable/Model/V1/Converter/AethernetShortcutConverter.cs
Questionable/Model/V1/Converter/EmoteConverter.cs
Questionable/Model/V1/Converter/InteractionTypeConverter.cs
Questionable/Model/V1/EEmote.cs
Questionable/Model/V1/EInteractionType.cs
Questionable/QuestionablePlugin.cs
Questionable/Windows/DebugWindow.cs

index cc2d097ba0da04512de9ae2a8cc0403cbc32e0f7..6968fa2108e7b74346df363123b8b04cb886428a 100644 (file)
@@ -93,6 +93,7 @@
                     "AttuneAetherCurrent",
                     "Combat",
                     "UseItem",
+                    "EquipItem",
                     "Say",
                     "Emote",
                     "WaitForNpcAtPosition",
                 "AethernetShortcut": {
                   "type": "array",
                   "description": "A pair of aethernet locations (from + to) to use as a shortcut",
-                  "minItems": 1,
+                  "minItems": 2,
                   "maxItems": 2,
                   "items": {
                     "type": "string",
                     "enum": [
                       "[Gridania] Aetheryte Plaza",
-                      "[Gridania] Archer's Guild",
-                      "[Gridania] Leatherworker's Guild & Shaded Bower",
-                      "[Gridania] Lancer's Guild",
-                      "[Gridania] Conjurer's Guild",
-                      "[Gridania] Botanist's Guild",
+                      "[Gridania] Archers' Guild",
+                      "[Gridania] Leatherworkers' Guild & Shaded Bower",
+                      "[Gridania] Lancers' Guild",
+                      "[Gridania] Conjurers' Guild",
+                      "[Gridania] Botanists' Guild",
                       "[Gridania] Mih Khetto's Amphitheatre",
                       "[Gridania] Blue Badger Gate (Central Shroud)",
                       "[Gridania] Yellow Serpent Gate (North Shroud)",
                       "[Gridania] White Wolf Gate (Central Shroud)",
                       "[Gridania] Airship Landing",
                       "[Ul'dah] Aetheryte Plaza",
-                      "[Ul'dah] Adventurer's Guild",
-                      "[Ul'dah] Thaumaturge's Guild",
-                      "[Ul'dah] Gladiator's Guild",
-                      "[Ul'dah] Miner's Guild",
+                      "[Ul'dah] Adventurers' Guild",
+                      "[Ul'dah] Thaumaturges' Guild",
+                      "[Ul'dah] Gladiators' Guild",
+                      "[Ul'dah] Miners' Guild",
                       "[Ul'dah] Weavers' Guild",
                       "[Ul'dah] Goldsmiths' Guild",
                       "[Ul'dah] Sapphire Avenue Exchange",
                       "[Ul'dah] The Chamber of Rule",
                       "[Ul'dah] Airship Landing",
                       "[Limsa Lominsa] Aetheryte Plaza",
-                      "[Limsa Lominsa] Arcanist's Guild",
-                      "[Limsa Lominsa] Fishermen's Guild",
-                      "[Limsa Lominsa] Hawker's Alley",
+                      "[Limsa Lominsa] Arcanists' Guild",
+                      "[Limsa Lominsa] Fishermens' Guild",
+                      "[Limsa Lominsa] Hawkers' Alley",
                       "[Limsa Lominsa] The Aftcastle",
-                      "[Limsa Lominsa] Culinarian's Guild",
-                      "[Limsa Lominsa] Marauder's Guild",
+                      "[Limsa Lominsa] Culinarians' Guild",
+                      "[Limsa Lominsa] Marauders' Guild",
                       "[Limsa Lominsa] Zephyr Gate (Middle La Noscea)",
                       "[Limsa Lominsa] Tempest Gate (Lower La Noscea)",
                       "[Limsa Lominsa] Airship Landing",
                     ]
                   }
                 },
+                {
+                  "if": {
+                    "properties": {
+                      "InteractionType": {
+                        "const": "EquipItem"
+                      }
+                    }
+                  },
+                  "then": {
+                    "required": [
+                      "ItemId"
+                    ]
+                  }
+                },
                 {
                   "if": {
                     "properties": {
                           "wave",
                           "rally",
                           "deny",
-                          "pray"
+                          "pray",
+                          "slap",
+                          "doubt",
+                          "psych"
                         ]
                       }
                     },
index c4c97b07ec99fae6e8abdf0749e78f60d79cfdd2..5c374fad42cb3b2dd929c463f424bb4ab6fc82b1 100644 (file)
@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Numerics;
+using Dalamud.Game.ClientState.Conditions;
 using Dalamud.Plugin.Services;
 using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
 using FFXIVClientStructs.FFXIV.Client.Game;
@@ -15,7 +16,7 @@ namespace Questionable.Controller.Steps.BaseFactory;
 
 internal static class WaitAtEnd
 {
-    internal sealed class Factory(IServiceProvider serviceProvider, IClientState clientState) : ITaskFactory
+    internal sealed class Factory(IServiceProvider serviceProvider, IClientState clientState, ICondition condition) : ITaskFactory
     {
         public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
         {
@@ -30,6 +31,13 @@ internal static class WaitAtEnd
             switch (step.InteractionType)
             {
                 case EInteractionType.Combat:
+                    var notInCombat = new WaitConditionTask(() => !condition[ConditionFlag.InCombat], "Wait(not in combat)");
+                    return [
+                        serviceProvider.GetRequiredService<WaitDelay>(),
+                        notInCombat,
+                        Next(quest, sequence, step)
+                    ];
+
                 case EInteractionType.WaitForManualProgress:
                 case EInteractionType.ShouldBeAJump:
                 case EInteractionType.Instruction:
diff --git a/Questionable/Controller/Steps/InteractionFactory/EquipItem.cs b/Questionable/Controller/Steps/InteractionFactory/EquipItem.cs
new file mode 100644 (file)
index 0000000..802bec6
--- /dev/null
@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using Lumina.Excel.GeneratedSheets;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Questionable.Controller.Steps.BaseTasks;
+using Questionable.Model.V1;
+using Quest = Questionable.Model.Quest;
+
+namespace Questionable.Controller.Steps.InteractionFactory;
+
+internal static class EquipItem
+{
+    internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
+    {
+        public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+        {
+            if (step.InteractionType != EInteractionType.EquipItem)
+                return null;
+
+            ArgumentNullException.ThrowIfNull(step.ItemId);
+            return serviceProvider.GetRequiredService<DoEquip>()
+                .With(step.ItemId.Value);
+        }
+    }
+
+    internal sealed class DoEquip(IDataManager dataManager, ILogger<DoEquip> logger)
+        : AbstractDelayedTask(TimeSpan.FromSeconds(1))
+    {
+        private static readonly IReadOnlyList<InventoryType> SourceInventoryTypes =
+        [
+            InventoryType.ArmoryMainHand,
+            InventoryType.ArmoryOffHand,
+            InventoryType.ArmoryHead,
+            InventoryType.ArmoryBody,
+            InventoryType.ArmoryHands,
+            InventoryType.ArmoryLegs,
+            InventoryType.ArmoryFeets,
+
+            InventoryType.ArmoryEar,
+            InventoryType.ArmoryNeck,
+            InventoryType.ArmoryWrist,
+            InventoryType.ArmoryRings,
+
+            InventoryType.Inventory1,
+            InventoryType.Inventory2,
+            InventoryType.Inventory3,
+            InventoryType.Inventory4,
+        ];
+
+        private uint _itemId;
+        private Item _item = null!;
+        private List<ushort> _targetSlots = [];
+
+        public ITask With(uint itemId)
+        {
+            _itemId = itemId;
+            _item = dataManager.GetExcelSheet<Item>()!.GetRow(itemId) ??
+                    throw new ArgumentOutOfRangeException(nameof(itemId));
+            _targetSlots = GetEquipSlot(_item) ?? throw new InvalidOperationException("Not a piece of equipment");
+            return this;
+        }
+
+        protected override unsafe bool StartInternal()
+        {
+            var inventoryManager = InventoryManager.Instance();
+            if (inventoryManager == null)
+                return false;
+
+            var equippedContainer = inventoryManager->GetInventoryContainer(InventoryType.EquippedItems);
+            if (equippedContainer == null)
+                return false;
+
+            if (_targetSlots.Any(slot => equippedContainer->GetInventorySlot(slot)->ItemID == _itemId))
+            {
+                logger.LogInformation("Already equipped {Item}, skipping step", _item.Name?.ToString());
+                return false;
+            }
+
+            foreach (InventoryType sourceInventoryType in SourceInventoryTypes)
+            {
+                var sourceContainer = inventoryManager->GetInventoryContainer(sourceInventoryType);
+                if (sourceContainer == null)
+                    continue;
+
+                if (inventoryManager->GetItemCountInContainer(_itemId, sourceInventoryType, true) == 0 &&
+                    inventoryManager->GetItemCountInContainer(_itemId, sourceInventoryType) == 0)
+                    continue;
+
+                for (ushort sourceSlot = 0; sourceSlot < sourceContainer->Size; sourceSlot++)
+                {
+                    var sourceItem = sourceContainer->GetInventorySlot(sourceSlot);
+                    if (sourceItem == null || sourceItem->ItemID != _itemId)
+                        continue;
+
+                    // Move the item to the first available slot
+                    ushort targetSlot = _targetSlots
+                        .Where(x => inventoryManager->GetInventorySlot(InventoryType.EquippedItems, x)->ItemID == 0)
+                        .Concat(_targetSlots).First();
+
+                    logger.LogInformation(
+                        "Equipping item from {SourceInventory}, {SourceSlot} to {TargetInventory}, {TargetSlot}",
+                        sourceInventoryType, sourceSlot, InventoryType.EquippedItems, targetSlot);
+
+                    int result = inventoryManager->MoveItemSlot(sourceInventoryType, sourceSlot,
+                        InventoryType.EquippedItems, targetSlot, 1);
+                    logger.LogInformation("MoveItemSlot result: {Result}", result);
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        protected override unsafe ETaskResult UpdateInternal()
+        {
+            InventoryManager* inventoryManager = InventoryManager.Instance();
+            if (inventoryManager == null)
+                return ETaskResult.StillRunning;
+
+            if (_targetSlots.Any(x =>
+                    inventoryManager->GetInventorySlot(InventoryType.EquippedItems, x)->ItemID == _itemId))
+                return ETaskResult.TaskComplete;
+
+            return ETaskResult.StillRunning;
+        }
+
+        private static List<ushort>? GetEquipSlot(Item item)
+        {
+            return item.EquipSlotCategory.Row switch
+            {
+                >= 1 and <= 11 => [(ushort)(item.EquipSlotCategory.Row - 1)],
+                12 => [11, 12], // rings
+                17 => [14], // soul crystal
+                _ => null
+            };
+        }
+
+        public override string ToString() => $"Equip({_item.Name})";
+    }
+}
index 879c3ef8adb4b01093277e3a26950c2032d10355..9393e9c3c7f0fd6e20fc5d34182e9e68a480a602 100644 (file)
@@ -11,11 +11,11 @@ internal sealed class AethernetShortcutConverter : JsonConverter<AethernetShortc
     private static readonly Dictionary<EAetheryteLocation, string> EnumToString = new()
     {
         { EAetheryteLocation.Gridania, "[Gridania] Aetheryte Plaza" },
-        { EAetheryteLocation.GridaniaArcher, "[Gridania] Archer's Guild" },
-        { EAetheryteLocation.GridaniaLeatherworker, "[Gridania] Leatherworker's Guild & Shaded Bower" },
-        { EAetheryteLocation.GridaniaLancer, "[Gridania] Lancer's Guild" },
-        { EAetheryteLocation.GridaniaConjurer, "[Gridania] Conjurer's Guild" },
-        { EAetheryteLocation.GridaniaBotanist, "[Gridania] Botanist's Guild" },
+        { EAetheryteLocation.GridaniaArcher, "[Gridania] Archers' Guild" },
+        { EAetheryteLocation.GridaniaLeatherworker, "[Gridania] Leatherworkers' Guild & Shaded Bower" },
+        { EAetheryteLocation.GridaniaLancer, "[Gridania] Lancers' Guild" },
+        { EAetheryteLocation.GridaniaConjurer, "[Gridania] Conjurers' Guild" },
+        { EAetheryteLocation.GridaniaBotanist, "[Gridania] Botanists' Guild" },
         { EAetheryteLocation.GridaniaAmphitheatre, "[Gridania] Mih Khetto's Amphitheatre" },
         { EAetheryteLocation.GridaniaBlueBadgerGate, "[Gridania] Blue Badger Gate (Central Shroud)" },
         { EAetheryteLocation.GridaniaYellowSerpentGate, "[Gridania] Yellow Serpent Gate (North Shroud)" },
@@ -23,10 +23,10 @@ internal sealed class AethernetShortcutConverter : JsonConverter<AethernetShortc
         { EAetheryteLocation.GridaniaAirship, "[Gridania] Airship Landing" },
 
         { EAetheryteLocation.Uldah, "[Ul'dah] Aetheryte Plaza" },
-        { EAetheryteLocation.UldahAdventurers, "[Ul'dah] Adventurer's Guild" },
-        { EAetheryteLocation.UldahThaumaturge, "[Ul'dah] Thaumaturge's Guild" },
-        { EAetheryteLocation.UldahGladiator, "[Ul'dah] Gladiator's Guild" },
-        { EAetheryteLocation.UldahMiner, "[Ul'dah] Miner's Guild" },
+        { EAetheryteLocation.UldahAdventurers, "[Ul'dah] Adventurers' Guild" },
+        { EAetheryteLocation.UldahThaumaturge, "[Ul'dah] Thaumaturges' Guild" },
+        { EAetheryteLocation.UldahGladiator, "[Ul'dah] Gladiators' Guild" },
+        { EAetheryteLocation.UldahMiner, "[Ul'dah] Miners' Guild" },
         { EAetheryteLocation.UldahWeaver, "[Ul'dah] Weavers' Guild" },
         { EAetheryteLocation.UldahGoldsmith, "[Ul'dah] Goldsmiths' Guild" },
         { EAetheryteLocation.UldahSapphireAvenue, "[Ul'dah] Sapphire Avenue Exchange" },
@@ -38,12 +38,12 @@ internal sealed class AethernetShortcutConverter : JsonConverter<AethernetShortc
         { EAetheryteLocation.UldahAirship, "[Ul'dah] Airship Landing" },
 
         { EAetheryteLocation.Limsa, "[Limsa Lominsa] Aetheryte Plaza" },
-        { EAetheryteLocation.LimsaArcanist, "[Limsa Lominsa] Arcanist's Guild" },
-        { EAetheryteLocation.LimsaFisher, "[Limsa Lominsa] Fishermen's Guild" },
-        { EAetheryteLocation.LimsaHawkersAlley, "[Limsa Lominsa] Hawker's Alley" },
+        { EAetheryteLocation.LimsaArcanist, "[Limsa Lominsa] Arcanists' Guild" },
+        { EAetheryteLocation.LimsaFisher, "[Limsa Lominsa] Fishermens' Guild" },
+        { EAetheryteLocation.LimsaHawkersAlley, "[Limsa Lominsa] Hawkers' Alley" },
         { EAetheryteLocation.LimsaAftcastle, "[Limsa Lominsa] The Aftcastle" },
-        { EAetheryteLocation.LimsaCulinarian, "[Limsa Lominsa] Culinarian's Guild" },
-        { EAetheryteLocation.LimsaMarauder, "[Limsa Lominsa] Marauder's Guild" },
+        { EAetheryteLocation.LimsaCulinarian, "[Limsa Lominsa] Culinarians' Guild" },
+        { EAetheryteLocation.LimsaMarauder, "[Limsa Lominsa] Marauders' Guild" },
         { EAetheryteLocation.LimsaZephyrGate, "[Limsa Lominsa] Zephyr Gate (Middle La Noscea)" },
         { EAetheryteLocation.LimsaTempestGate, "[Limsa Lominsa] Tempest Gate (Lower La Noscea)" },
         { EAetheryteLocation.LimsaAirship, "[Limsa Lominsa] Airship Landing" },
index 44be7e46638307c7baa1120eb8d0b470874666bc..bf8e62de276d8cd9dbfaad82f2e74b6e72205813 100644 (file)
@@ -11,5 +11,8 @@ internal sealed class EmoteConverter() : EnumConverter<EEmote>(Values)
         { EEmote.Rally, "rally" },
         { EEmote.Deny, "deny" },
         { EEmote.Pray, "pray" },
+        { EEmote.Slap, "slap" },
+        { EEmote.Doubt, "doubt" },
+        { EEmote.Psych, "psych" },
     };
 }
index aadd504aa233dbbbd7486c69886ac73f88c3d02c..d67435f5b2fcfb09cd06710e85679bf60e389cdc 100644 (file)
@@ -13,6 +13,7 @@ internal sealed class InteractionTypeConverter() : EnumConverter<EInteractionTyp
         { EInteractionType.AttuneAetherCurrent, "AttuneAetherCurrent" },
         { EInteractionType.Combat, "Combat" },
         { EInteractionType.UseItem, "UseItem" },
+        { EInteractionType.EquipItem, "EquipItem" },
         { EInteractionType.Say, "Say" },
         { EInteractionType.Emote, "Emote" },
         { EInteractionType.WaitForObjectAtPosition, "WaitForNpcAtPosition" },
index 238e3ca60503968e2260f878c7b8706fa142fe29..2e751d365c413d21c5ec01c88f30029c580efd0f 100644 (file)
@@ -13,4 +13,7 @@ internal enum EEmote
     Rally = 34,
     Deny = 25,
     Pray = 58,
+    Slap = 111,
+    Doubt = 12,
+    Psych = 30,
 }
index 20d0c53164ae9052be82014d92013b50dd38ff5b..edb27ceec89fa4af922e1ac39e42e9fc673d6467 100644 (file)
@@ -13,6 +13,7 @@ internal enum EInteractionType
     AttuneAetherCurrent,
     Combat,
     UseItem,
+    EquipItem,
     Say,
     Emote,
     WaitForObjectAtPosition,
index c61828790998f531ce175ead5305225f7d1ba5bc..ee58324d76fea1b2edea47e70eafdf5647e792a0 100644 (file)
@@ -55,6 +55,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddSingleton(dataManager);
         serviceCollection.AddSingleton(sigScanner);
         serviceCollection.AddSingleton(objectTable);
+        serviceCollection.AddSingleton(pluginLog);
         serviceCollection.AddSingleton(condition);
         serviceCollection.AddSingleton(chatGui);
         serviceCollection.AddSingleton(commandManager);
@@ -91,6 +92,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddTaskWithFactory<Jump.Factory, Jump.DoJump>();
         serviceCollection.AddTaskWithFactory<Say.Factory, Say.UseChat>();
         serviceCollection.AddTaskWithFactory<UseItem.Factory, UseItem.UseOnGround, UseItem.UseOnObject, UseItem.Use>();
+        serviceCollection.AddTaskWithFactory<EquipItem.Factory, EquipItem.DoEquip>();
 
         serviceCollection
             .AddTaskWithFactory<WaitAtEnd.Factory,
index 03fdf7907044e0020b2b01d16c2c7bc361e0da20..beaeca6f7c6a507ed066ac2b3f7acafaafc013b0 100644 (file)
@@ -106,7 +106,11 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig
                 var qw = questWork.Value;
                 string vars = "";
                 for (int i = 0; i < 6; ++i)
+                {
                     vars += qw.Variables[i] + " ";
+                    if (i % 2 == 1)
+                        vars += "   ";
+                }
 
                 // For combat quests, a sequence to kill 3 enemies works a bit like this:
                 // Trigger enemies → 0
@@ -115,7 +119,7 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig
                 // Last enemy → increase sequence, reset variable to 0
                 // The order in which enemies are killed doesn't seem to matter.
                 // If multiple waves spawn, this continues to count up (e.g. 1 enemy from wave 1, 2 enemies from wave 2, 1 from wave 3) would count to 3 then 0
-                ImGui.Text($"QW: {vars.Trim()} / {qw.Flags}");
+                ImGui.Text($"QW: {vars.Trim()}");
             }
             else
                 ImGui.TextUnformatted("(Not accepted)");