From b74f69c9818802d2ea1b716d8304c84bc6499ae1 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Thu, 13 Jun 2024 17:35:33 +0200 Subject: [PATCH] Fix aethernet names, add new emotes, add EquipItem --- QuestPaths/quest-v1.json | 50 ++++-- .../Controller/Steps/BaseFactory/WaitAtEnd.cs | 10 +- .../Steps/InteractionFactory/EquipItem.cs | 144 ++++++++++++++++++ .../Converter/AethernetShortcutConverter.cs | 28 ++-- .../Model/V1/Converter/EmoteConverter.cs | 3 + .../V1/Converter/InteractionTypeConverter.cs | 1 + Questionable/Model/V1/EEmote.cs | 3 + Questionable/Model/V1/EInteractionType.cs | 1 + Questionable/QuestionablePlugin.cs | 2 + Questionable/Windows/DebugWindow.cs | 6 +- 10 files changed, 216 insertions(+), 32 deletions(-) create mode 100644 Questionable/Controller/Steps/InteractionFactory/EquipItem.cs diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json index cc2d097ba..6968fa210 100644 --- a/QuestPaths/quest-v1.json +++ b/QuestPaths/quest-v1.json @@ -93,6 +93,7 @@ "AttuneAetherCurrent", "Combat", "UseItem", + "EquipItem", "Say", "Emote", "WaitForNpcAtPosition", @@ -226,27 +227,27 @@ "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", @@ -257,12 +258,12 @@ "[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", @@ -525,6 +526,20 @@ ] } }, + { + "if": { + "properties": { + "InteractionType": { + "const": "EquipItem" + } + } + }, + "then": { + "required": [ + "ItemId" + ] + } + }, { "if": { "properties": { @@ -543,7 +558,10 @@ "wave", "rally", "deny", - "pray" + "pray", + "slap", + "doubt", + "psych" ] } }, diff --git a/Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs b/Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs index c4c97b07e..5c374fad4 100644 --- a/Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs +++ b/Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs @@ -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 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(), + 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 index 000000000..802bec61c --- /dev/null +++ b/Questionable/Controller/Steps/InteractionFactory/EquipItem.cs @@ -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() + .With(step.ItemId.Value); + } + } + + internal sealed class DoEquip(IDataManager dataManager, ILogger logger) + : AbstractDelayedTask(TimeSpan.FromSeconds(1)) + { + private static readonly IReadOnlyList 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 _targetSlots = []; + + public ITask With(uint itemId) + { + _itemId = itemId; + _item = dataManager.GetExcelSheet()!.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? 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})"; + } +} diff --git a/Questionable/Model/V1/Converter/AethernetShortcutConverter.cs b/Questionable/Model/V1/Converter/AethernetShortcutConverter.cs index 879c3ef8a..9393e9c3c 100644 --- a/Questionable/Model/V1/Converter/AethernetShortcutConverter.cs +++ b/Questionable/Model/V1/Converter/AethernetShortcutConverter.cs @@ -11,11 +11,11 @@ internal sealed class AethernetShortcutConverter : JsonConverter 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(Values) { EEmote.Rally, "rally" }, { EEmote.Deny, "deny" }, { EEmote.Pray, "pray" }, + { EEmote.Slap, "slap" }, + { EEmote.Doubt, "doubt" }, + { EEmote.Psych, "psych" }, }; } diff --git a/Questionable/Model/V1/Converter/InteractionTypeConverter.cs b/Questionable/Model/V1/Converter/InteractionTypeConverter.cs index aadd504aa..d67435f5b 100644 --- a/Questionable/Model/V1/Converter/InteractionTypeConverter.cs +++ b/Questionable/Model/V1/Converter/InteractionTypeConverter.cs @@ -13,6 +13,7 @@ internal sealed class InteractionTypeConverter() : EnumConverter(); serviceCollection.AddTaskWithFactory(); serviceCollection.AddTaskWithFactory(); + serviceCollection.AddTaskWithFactory(); serviceCollection .AddTaskWithFactory