using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.Interop; using LLib.GameData; using LLib.Gear; using LLib.Inventory; using Lumina.Excel.Sheets; using Questionable.Model; using Questionable.Model.Questing; namespace Questionable.Controller.Steps.Interactions; internal static class EquipRecommended { internal sealed class Factory : SimpleTaskFactory { public override ITask? CreateTask(Questionable.Model.Quest quest, QuestSequence sequence, QuestStep step) { if (step.InteractionType != EInteractionType.EquipRecommended) { return null; } return new EquipTask(); } } internal sealed class BeforeDutyOrInstance : SimpleTaskFactory { public override ITask? CreateTask(Questionable.Model.Quest quest, QuestSequence sequence, QuestStep step) { if (step.InteractionType != EInteractionType.Duty && step.InteractionType != EInteractionType.SinglePlayerDuty && step.InteractionType != EInteractionType.Combat) { return null; } return new EquipTask(); } } internal sealed class EquipTask : ITask { public override string ToString() { return "EquipRecommended"; } } internal sealed class DoEquipRecommended(IObjectTable objectTable, IChatGui chatGui, ICondition condition, IDataManager dataManager, Configuration configuration, BestItemFinder bestItemFinder) : TaskExecutor() { private readonly record struct PendingMove(InventoryType SourceContainer, int SourceSlot, uint SourceItemId, InventoryType DestinationContainer, int DestinationSlot); private static readonly (string Name, uint[] Categories)[] SlotMap = new(string, uint[])[13] { ("MainHand", new uint[2] { 1u, 13u }), ("OffHand", new uint[1] { 2u }), ("Head", new uint[1] { 3u }), ("Body", new uint[1] { 4u }), ("Hands", new uint[1] { 5u }), ("Belt", Array.Empty()), ("Legs", new uint[1] { 7u }), ("Feet", new uint[1] { 8u }), ("Ears", new uint[1] { 9u }), ("Neck", new uint[1] { 10u }), ("Wrists", new uint[1] { 11u }), ("Ring1", new uint[1] { 12u }), ("Ring2", new uint[1] { 12u }) }; private const long EquipApplyTimeoutMs = 5000L; private const long MoveApplyTimeoutMs = 2000L; private bool _useSmart; private bool _checkedOrTriggeredEquipmentUpdate; private long _timeoutAt; private List<(int Slot, BestItemRef Item)>? _smartUpgrades; private List? _smartMoves; private int _smartMoveCursor; private bool _currentMoveStarted; private bool _smartApplied; private bool _useDirectEquip; private uint _smartClassJobId; private int _directEquipCursor; private bool _directEquipStarted; protected unsafe override bool Start() { if (configuration.General.EquipMode == Configuration.EEquipMode.Disabled) { return false; } if (condition[ConditionFlag.InCombat]) { return false; } IPlayerCharacter localPlayer = objectTable.LocalPlayer; if (localPlayer == null) { return false; } _useSmart = configuration.General.EquipMode == Configuration.EEquipMode.Smart; if (_useSmart && TrySmartStart(localPlayer)) { return true; } _useSmart = false; RecommendEquipModule.Instance()->SetupForClassJob((byte)localPlayer.ClassJob.RowId); return true; } public override ETaskResult Update() { if (_useSmart) { return SmartUpdate(); } return GameUpdate(); } private unsafe bool TrySmartStart(ICharacter localPlayer) { uint rowId = localPlayer.ClassJob.RowId; if (!Enum.IsDefined(typeof(EClassJob), rowId)) { return false; } RaptureGearsetModule* ptr = RaptureGearsetModule.Instance(); if (ptr == null) { return false; } int currentGearsetIndex = ptr->CurrentGearsetIndex; if (currentGearsetIndex < 0 || !ptr->IsValidGearset(currentGearsetIndex)) { return false; } EClassJob job = (EClassJob)rowId; StatPriority priority = StatPriority.ForJob(job); byte playerLevel = GetPlayerLevel(job); _smartClassJobId = rowId; _useDirectEquip = configuration.General.PreserveGearset || !CurrentGearsetMatchesEquippedItems(); if (!TryComputeUpgradesAndMoves(job, priority, playerLevel, out _smartUpgrades, out _smartMoves)) { _smartUpgrades = null; _smartMoves = null; return false; } _smartMoveCursor = 0; _currentMoveStarted = false; return true; } private ETaskResult SmartUpdate() { List smartMoves = _smartMoves; if (smartMoves != null && smartMoves.Count > 0 && _smartMoveCursor < smartMoves.Count) { PendingMove pendingMove = smartMoves[_smartMoveCursor]; if (!_currentMoveStarted) { if (!TryStartMove(pendingMove)) { chatGui.Print($"Equip move {_smartMoveCursor + 1}/{smartMoves.Count} rejected; skipping equip step.", "Questionable", 576); return ETaskResult.TaskComplete; } _currentMoveStarted = true; _timeoutAt = Environment.TickCount64 + 2000; return ETaskResult.StillRunning; } if (IsMoveApplied(pendingMove)) { if (_smartUpgrades != null) { NotifyStagingMoveApplied(_smartUpgrades, pendingMove); } _smartMoveCursor++; _currentMoveStarted = false; return ETaskResult.StillRunning; } if (Environment.TickCount64 >= _timeoutAt) { chatGui.Print($"Equip move {_smartMoveCursor + 1}/{smartMoves.Count} timed out; skipping equip step.", "Questionable", 576); return ETaskResult.TaskComplete; } return ETaskResult.StillRunning; } if (!_useDirectEquip && !CurrentGearsetMatchesEquippedItems()) { _useDirectEquip = true; } if (_useDirectEquip) { return DirectEquipPhase(); } if (!_smartApplied) { List<(int, BestItemRef)> smartUpgrades = _smartUpgrades; if (smartUpgrades != null && smartUpgrades.Count > 0) { chatGui.Print($"Equipping {smartUpgrades.Count} gear upgrade(s) (smart).", "Questionable", 576); ApplyUpgrades(smartUpgrades); _timeoutAt = Environment.TickCount64 + 5000; _smartApplied = true; return ETaskResult.StillRunning; } return ETaskResult.TaskComplete; } List<(int, BestItemRef)> smartUpgrades2 = _smartUpgrades; if (smartUpgrades2 != null && AreUpgradesApplied(smartUpgrades2)) { return ETaskResult.TaskComplete; } if (Environment.TickCount64 < _timeoutAt) { return ETaskResult.StillRunning; } return ETaskResult.TaskComplete; } private ETaskResult DirectEquipPhase() { List<(int, BestItemRef)> smartUpgrades = _smartUpgrades; if (smartUpgrades == null) { return ETaskResult.TaskComplete; } if (_directEquipCursor >= smartUpgrades.Count) { if (AreUpgradesApplied(smartUpgrades)) { return ETaskResult.TaskComplete; } if (Environment.TickCount64 < _timeoutAt) { return ETaskResult.StillRunning; } return ETaskResult.TaskComplete; } var (num, item) = smartUpgrades[_directEquipCursor]; if (!_directEquipStarted) { if (!TryStartDirectEquip(item, num)) { chatGui.Print($"Direct equip for slot {num} rejected, skipping.", "Questionable", 576); _directEquipCursor++; return ETaskResult.StillRunning; } _directEquipStarted = true; _timeoutAt = Environment.TickCount64 + 2000; return ETaskResult.StillRunning; } if (IsDirectEquipApplied(item, num)) { _directEquipCursor++; _directEquipStarted = false; return ETaskResult.StillRunning; } if (Environment.TickCount64 >= _timeoutAt) { _directEquipCursor++; _directEquipStarted = false; } return ETaskResult.StillRunning; } private unsafe bool TryStartDirectEquip(BestItemRef item, int gearsetSlot) { InventoryManager* ptr = InventoryManager.Instance(); if (ptr == null) { return false; } return ptr->MoveItemSlot(item.Container, (ushort)item.Slot, InventoryType.EquippedItems, (ushort)gearsetSlot) == 0; } private unsafe bool IsDirectEquipApplied(BestItemRef item, int gearsetSlot) { InventoryManager* ptr = InventoryManager.Instance(); if (ptr == null) { return false; } InventoryContainer* inventoryContainer = ptr->GetInventoryContainer(InventoryType.EquippedItems); if (inventoryContainer == null) { return false; } InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(gearsetSlot); if (inventorySlot == null) { return false; } return ItemHandle.FromInventorySlot(inventorySlot).Id == ((ItemHandle)item.ItemId).Id; } private static void NotifyStagingMoveApplied(List<(int Slot, BestItemRef Item)> upgrades, PendingMove appliedMove) { for (int i = 0; i < upgrades.Count; i++) { var (item, bestItemRef) = upgrades[i]; if (bestItemRef.Container == appliedMove.SourceContainer && bestItemRef.Slot == appliedMove.SourceSlot) { upgrades[i] = (item, bestItemRef with { Container = appliedMove.DestinationContainer, Slot = appliedMove.DestinationSlot }); break; } } } private unsafe bool TryStartMove(PendingMove move) { InventoryManager* ptr = InventoryManager.Instance(); if (ptr == null) { return false; } return ptr->MoveItemSlot(move.SourceContainer, (ushort)move.SourceSlot, move.DestinationContainer, (ushort)move.DestinationSlot) == 0; } private unsafe bool IsMoveApplied(PendingMove move) { InventoryManager* ptr = InventoryManager.Instance(); if (ptr == null) { return false; } InventoryContainer* inventoryContainer = ptr->GetInventoryContainer(move.DestinationContainer); if (inventoryContainer == null) { return false; } InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(move.DestinationSlot); if (inventorySlot == null) { return false; } return ItemHandle.FromInventorySlot(inventorySlot).Id == ((ItemHandle)move.SourceItemId).Id; } private unsafe ETaskResult GameUpdate() { RecommendEquipModule* ptr = RecommendEquipModule.Instance(); if (ptr->IsUpdating) { return ETaskResult.StillRunning; } if (!_checkedOrTriggeredEquipmentUpdate) { if (IsAllRecommendedGearEquipped()) { return ETaskResult.TaskComplete; } chatGui.Print("Equipping recommended gear.", "Questionable", 576); ptr->EquipRecommendedGear(); _timeoutAt = Environment.TickCount64 + 5000; _checkedOrTriggeredEquipmentUpdate = true; return ETaskResult.StillRunning; } if (IsAllRecommendedGearEquipped()) { return ETaskResult.TaskComplete; } if (Environment.TickCount64 < _timeoutAt) { return ETaskResult.StillRunning; } return ETaskResult.TaskComplete; } private unsafe bool AreUpgradesApplied(IReadOnlyList<(int Slot, BestItemRef Item)> upgrades) { InventoryManager* ptr = InventoryManager.Instance(); if (ptr == null) { return false; } InventoryContainer* inventoryContainer = ptr->GetInventoryContainer(InventoryType.EquippedItems); if (inventoryContainer == null) { return false; } foreach (var upgrade in upgrades) { int item = upgrade.Slot; BestItemRef item2 = upgrade.Item; InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(item); if (inventorySlot == null || ItemHandle.FromInventorySlot(inventorySlot).Id != ((ItemHandle)item2.ItemId).Id) { return false; } } return true; } private unsafe bool TryComputeUpgradesAndMoves(EClassJob job, StatPriority priority, byte playerLevel, out List<(int Slot, BestItemRef Item)> upgrades, out List moves) { upgrades = new List<(int, BestItemRef)>(); moves = new List(); InventoryManager* ptr = InventoryManager.Instance(); if (ptr == null) { return false; } InventoryContainer* inventoryContainer = ptr->GetInventoryContainer(InventoryType.EquippedItems); if (inventoryContainer == null) { return false; } BestItemRef? bestItemRef = null; bool flag = job.IsCrafter() || job.IsGatherer(); Dictionary cursors = new Dictionary(); for (int i = 0; i < SlotMap.Length; i++) { uint[] item = SlotMap[i].Categories; if (item.Length == 0) { continue; } BestItemRef? excludeContainerSlot = ((i == 12 && bestItemRef.HasValue) ? bestItemRef : ((BestItemRef?)null)); bool flag2 = !flag; if (flag2) { bool flag3 = (uint)i <= 1u; flag2 = flag3; } EItemRankingMode rankingMode = ((!flag2) ? EItemRankingMode.ScoreFirst : EItemRankingMode.DamageFirst); BestItemRef? bestItemRef2 = bestItemFinder.FindBest(job, item, priority, BestItemFinder.ArmoryEquippedAndBags, playerLevel, rankingMode, excludeContainerSlot); if (!bestItemRef2.HasValue) { continue; } if (i == 11) { bestItemRef = bestItemRef2; } InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(i); if (((inventorySlot != null) ? ItemHandle.FromInventorySlot(inventorySlot).Id : 0) == ((ItemHandle)bestItemRef2.Value.ItemId).Id) { continue; } if (IsBagContainer(bestItemRef2.Value.Container)) { InventoryType? inventoryType = ResolveArmoryContainer(item); if (!inventoryType.HasValue) { continue; } int nextFreeSlot = GetNextFreeSlot(inventoryType.Value, cursors); if (nextFreeSlot < 0) { return false; } moves.Add(new PendingMove(bestItemRef2.Value.Container, bestItemRef2.Value.Slot, bestItemRef2.Value.ItemId, inventoryType.Value, nextFreeSlot)); } upgrades.Add((i, bestItemRef2.Value)); } return true; } private unsafe bool CurrentGearsetMatchesEquippedItems() { RaptureGearsetModule* ptr = RaptureGearsetModule.Instance(); if (ptr == null) { return false; } int currentGearsetIndex = ptr->CurrentGearsetIndex; if (currentGearsetIndex < 0 || !ptr->IsValidGearset(currentGearsetIndex)) { return false; } RaptureGearsetModule.GearsetEntry* ptr2 = (RaptureGearsetModule.GearsetEntry*)Unsafe.AsPointer(in ptr->Entries[currentGearsetIndex]); if (ptr2->ClassJob != _smartClassJobId) { return false; } InventoryManager* ptr3 = InventoryManager.Instance(); if (ptr3 == null) { return false; } InventoryContainer* inventoryContainer = ptr3->GetInventoryContainer(InventoryType.EquippedItems); if (inventoryContainer == null) { return false; } for (int i = 0; i <= 13; i++) { if (i != 5) { InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(i); uint num = ((inventorySlot != null) ? ItemHandle.FromInventorySlot(inventorySlot).Id : 0u); if (((ItemHandle)ptr2->GetItem((RaptureGearsetModule.GearsetItemIndex)i).ItemId).Id != num) { return false; } } } return true; } private static bool IsBagContainer(InventoryType type) { if (type <= InventoryType.Inventory4) { return true; } return false; } private static InventoryType? ResolveArmoryContainer(uint[] acceptedCategories) { for (int i = 0; i < acceptedCategories.Length; i++) { InventoryType? armoryContainer = ((EEquipSlotCategory)acceptedCategories[i]).GetArmoryContainer(); if (armoryContainer.HasValue) { return armoryContainer; } } return null; } private unsafe static int GetNextFreeSlot(InventoryType container, Dictionary cursors) { InventoryManager* ptr = InventoryManager.Instance(); if (ptr == null) { return -1; } InventoryContainer* inventoryContainer = ptr->GetInventoryContainer(container); if (inventoryContainer == null) { return -1; } int value; for (int i = (cursors.TryGetValue(container, out value) ? value : 0); i < inventoryContainer->Size; i++) { InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(i); if (inventorySlot != null && inventorySlot->ItemId == 0) { cursors[container] = i + 1; return i; } } return -1; } private unsafe void ApplyUpgrades(IReadOnlyList<(int Slot, BestItemRef Item)> upgrades) { RaptureGearsetModule* ptr = RaptureGearsetModule.Instance(); if (ptr == null) { return; } int currentGearsetIndex = ptr->CurrentGearsetIndex; if (currentGearsetIndex < 0 || !ptr->IsValidGearset(currentGearsetIndex)) { return; } RaptureGearsetModule.GearsetEntry* ptr2 = (RaptureGearsetModule.GearsetEntry*)Unsafe.AsPointer(in ptr->Entries[currentGearsetIndex]); InventoryManager* ptr3 = InventoryManager.Instance(); if (ptr3 == null) { return; } foreach (var upgrade in upgrades) { int item = upgrade.Slot; BestItemRef item2 = upgrade.Item; InventoryContainer* inventoryContainer = ptr3->GetInventoryContainer(item2.Container); if (inventoryContainer == null) { continue; } InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(item2.Slot); if (inventorySlot != null && inventorySlot->ItemId != 0) { RaptureGearsetModule.GearsetItem* ptr4 = (RaptureGearsetModule.GearsetItem*)Unsafe.AsPointer(in ptr2->Items[item]); ptr4->ItemId = inventorySlot->ItemId; ptr4->GlamourId = inventorySlot->GlamourId; ptr4->Stain0Id = inventorySlot->Stains[0]; ptr4->Stain1Id = inventorySlot->Stains[1]; for (int i = 0; i < 5; i++) { ptr4->Materia[i] = inventorySlot->Materia[i]; ptr4->MateriaGrades[i] = inventorySlot->MateriaGrades[i]; } } } ptr->EquipGearset(currentGearsetIndex, 0); } private byte GetPlayerLevel(EClassJob job) { short classJobLevel = PlayerStateHelper.GetClassJobLevel(dataManager.GetExcelSheet().GetRow((uint)job).ExpArrayIndex); if (classJobLevel <= 0) { return 0; } return (byte)classJobLevel; } private unsafe bool IsAllRecommendedGearEquipped() { Span> recommendedItems = RecommendEquipModule.Instance()->RecommendedItems; for (int i = 0; i < recommendedItems.Length; i++) { Pointer pointer = recommendedItems[i]; InventoryItem* value = pointer.Value; if (value != null && value->ItemId != 0 && !InventoryHelper.IsItemEquipped(value->ItemId)) { return false; } } return true; } public override bool ShouldInterruptOnDamage() { return true; } } }