qstbak/Questionable/Questionable.Controller.Steps.Interactions/EquipRecommended.cs
2026-08-19 13:19:57 +10:00

644 lines
18 KiB
C#

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<EquipTask>()
{
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<uint>()),
("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<PendingMove>? _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<PendingMove> 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<PendingMove> moves)
{
upgrades = new List<(int, BestItemRef)>();
moves = new List<PendingMove>();
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<InventoryType, int> cursors = new Dictionary<InventoryType, int>();
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<InventoryType, int> 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<ClassJob>().GetRow((uint)job).ExpArrayIndex);
if (classJobLevel <= 0)
{
return 0;
}
return (byte)classJobLevel;
}
private unsafe bool IsAllRecommendedGearEquipped()
{
Span<Pointer<InventoryItem>> recommendedItems = RecommendEquipModule.Instance()->RecommendedItems;
for (int i = 0; i < recommendedItems.Length; i++)
{
Pointer<InventoryItem> 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;
}
}
}