"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"
]
}
},
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;
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)
{
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:
--- /dev/null
+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})";
+ }
+}
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)" },
{ 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" },
{ 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" },
{ EEmote.Rally, "rally" },
{ EEmote.Deny, "deny" },
{ EEmote.Pray, "pray" },
+ { EEmote.Slap, "slap" },
+ { EEmote.Doubt, "doubt" },
+ { EEmote.Psych, "psych" },
};
}
{ EInteractionType.AttuneAetherCurrent, "AttuneAetherCurrent" },
{ EInteractionType.Combat, "Combat" },
{ EInteractionType.UseItem, "UseItem" },
+ { EInteractionType.EquipItem, "EquipItem" },
{ EInteractionType.Say, "Say" },
{ EInteractionType.Emote, "Emote" },
{ EInteractionType.WaitForObjectAtPosition, "WaitForNpcAtPosition" },
Rally = 34,
Deny = 25,
Pray = 58,
+ Slap = 111,
+ Doubt = 12,
+ Psych = 30,
}
AttuneAetherCurrent,
Combat,
UseItem,
+ EquipItem,
Say,
Emote,
WaitForObjectAtPosition,
serviceCollection.AddSingleton(dataManager);
serviceCollection.AddSingleton(sigScanner);
serviceCollection.AddSingleton(objectTable);
+ serviceCollection.AddSingleton(pluginLog);
serviceCollection.AddSingleton(condition);
serviceCollection.AddSingleton(chatGui);
serviceCollection.AddSingleton(commandManager);
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,
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
// 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)");