using System; using System.Collections.Generic; using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Excel.Sheets; using AtkValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; namespace Mappy.Helpers; /// /// Automation actions ported from AutoDuty for the minimap pop-out menu: /// Desynth all, Extract Materia, Repair (self), Equip Gear, Open Coffers. /// internal static class MinimapActions { private static IFramework.OnUpdateDelegate? _updateHandler; private static long _lastThrottleMs; private static string _activeAction = string.Empty; private static Vector3? _anchorPosition; private const int ThrottleMs = 250; private const float MovementEpsilon = 0.5f; private static bool CheckPlayerMoved() { if (_anchorPosition is not { } anchor) return false; var current = Service.ObjectTable?.LocalPlayer?.Position; if (!current.HasValue) return false; return Vector3.Distance(anchor, current.Value) > MovementEpsilon; } private static bool Throttle(string key) { var now = Environment.TickCount64; if (now - _lastThrottleMs < ThrottleMs) return false; _lastThrottleMs = now; return true; } private static unsafe bool TryGetAddon(string name, out AtkUnitBase* addon) { var handle = Service.GameGui.GetAddonByName(name); addon = handle.Address != nint.Zero ? (AtkUnitBase*)handle.Address : null; return addon != null; } private static unsafe bool IsAddonReady(AtkUnitBase* addon) => addon != null && addon->IsVisible && addon->UldManager.LoadedState == AtkLoadState.Loaded; private static unsafe void FireCallback(AtkUnitBase* addon, params object[] args) { if (addon == null || args.Length == 0) return; var atkValues = CreateAtkValueArray(args); if (atkValues == null) return; try { addon->FireCallback((uint)args.Length, atkValues); } finally { FreeAtkValueArray(atkValues, args.Length); } } private static unsafe AtkValue* CreateAtkValueArray(object[] values) { var ptr = (AtkValue*)Marshal.AllocHGlobal(values.Length * sizeof(AtkValue)); for (var i = 0; i < values.Length; i++) { switch (values[i]) { case int n: ptr[i].Type = AtkValueType.Int; ptr[i].Int = n; break; case uint u: ptr[i].Type = AtkValueType.UInt; ptr[i].UInt = u; break; case bool b: ptr[i].Type = AtkValueType.Bool; ptr[i].Byte = (byte)(b ? 1 : 0); break; default: ptr[i].Type = AtkValueType.Int; ptr[i].Int = 0; break; } } return ptr; } private static unsafe void FreeAtkValueArray(AtkValue* ptr, int count) { Marshal.FreeHGlobal(new IntPtr(ptr)); } public static void InvokeDesynth() => StartRunner(RunDesynth); public static void InvokeExtract() => StartRunner(RunExtract); public static void InvokeRepair() => StartRunner(RunRepair); public static void InvokeEquip() => StartRunner(RunEquip); public static void InvokeCoffers() => StartRunner(RunCoffers); private static void StartRunner(Action runner) { if (_updateHandler != null) return; _anchorPosition = Service.ObjectTable?.LocalPlayer?.Position; _updateHandler = framework => { try { runner(framework); } catch (Exception ex) { Service.Log.Warning(ex, "MinimapActions error"); StopRunner(); } }; Service.Framework.Update += _updateHandler; } private static void StopRunner() { if (_updateHandler == null) return; Service.Framework.Update -= _updateHandler; _updateHandler = null; _activeAction = string.Empty; _anchorPosition = null; } // --- Desynth (ported from AutoDuty DesynthHelper) --- private static AgentSalvage.SalvageItemCategory _desynthCategory; private static bool _desynthInitialized; private static unsafe void RunDesynth(IFramework framework) { if (CheckPlayerMoved()) { StopRunner(); return; } if (!Throttle("Desynth")) return; if (Service.ClientState is not { IsLoggedIn: true } || Service.Condition[ConditionFlag.InCombat]) { StopRunner(); return; } _activeAction = "Desynth"; if (Conditions.Instance()->Mounted) { ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23); return; } if (InventoryManager.Instance()->GetEmptySlotsInBag() < 1) { StopRunner(); return; } if (GenericHelpersIsOccupied()) return; if (TryGetAddon("SalvageResult", out var salvageResult) && IsAddonReady(salvageResult)) { salvageResult->Close(true); return; } if (TryGetAddon("SalvageDialog", out var salvageDialog) && IsAddonReady(salvageDialog)) { FireCallback(salvageDialog, true, 15, false); // NQ only = false FireCallback(salvageDialog, true, 0, false); return; } if (!TryGetAddon("SalvageItemSelector", out var selectorBase)) { AgentSalvage.Instance()->AgentInterface.Show(); return; } var selector = (AddonSalvageItemSelector*)selectorBase; if (!IsAddonReady(selectorBase) || !selector->IsReady) return; AgentSalvage.Instance()->ItemListRefresh(true); if (!_desynthInitialized) { var cats = Enum.GetValues(); _desynthCategory = cats.Length > 0 ? cats[0] : 0; _desynthInitialized = true; } if (AgentSalvage.Instance()->SelectedCategory != _desynthCategory) { AgentSalvage.Instance()->SelectedCategory = _desynthCategory; return; } if (selector->ItemCount > 0) { var agent = AgentSalvage.Instance(); for (var i = 0; i < agent->ItemCount; i++) { var item = agent->ItemList[i]; var invItem = InventoryManager.Instance()->GetInventorySlot(item.InventoryType, (int)item.InventorySlot); if (invItem->ItemId == 10146) continue; var itemSheet = Service.DataManager.GetExcelSheet()?.GetRow(invItem->ItemId); if (itemSheet == null) continue; FireCallback((AtkUnitBase*)selector, true, 12, i); return; } } if (!NextDesynthCategory()) { selector->Close(true); StopRunner(); _desynthInitialized = false; } } private static bool NextDesynthCategory() { var cats = Enum.GetValues(); var idx = Array.IndexOf(cats, _desynthCategory) + 1; for (; idx < cats.Length; idx++) { _desynthCategory = cats[idx]; return true; } return false; } // --- Extract (ported from AutoDuty ExtractHelper) --- private static int _extractCategory; private static bool _extractSwitched; private static unsafe void RunExtract(IFramework framework) { if (CheckPlayerMoved()) { StopRunner(); return; } if (!Throttle("Extract")) return; if (Service.ClientState is not { IsLoggedIn: true }) { StopRunner(); return; } _activeAction = "Extract"; if (Conditions.Instance()->Mounted) { ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23); return; } if (InventoryManager.Instance()->GetEmptySlotsInBag() < 1) { StopRunner(); return; } if (GenericHelpersIsOccupied()) return; if (!QuestManager.IsQuestComplete(66174)) { Service.Log.Info("Materia Extraction requires quest: Forging the Spirit"); StopRunner(); return; } if (TryGetAddon("MaterializeDialog", out var matDialog) && IsAddonReady(matDialog)) { FireCallback(matDialog, true, 2, 0); return; } if (!TryGetAddon("Materialize", out var materialize)) { ActionManager.Instance()->UseAction(ActionType.GeneralAction, 14); return; } if (!IsAddonReady(materialize)) return; if (_extractCategory <= 6) { var listNode = materialize->GetNodeById(12); if (listNode == null) return; var list = listNode->GetAsAtkComponentList(); if (list == null || list->UldManager.NodeListCount < 3) return; var textNode = list->UldManager.NodeList[2]->GetComponent()->GetTextNodeById(5); if (textNode == null) return; var spiritbond = textNode->NodeText.ToString(); if (!_extractSwitched) { FireCallback(materialize, false, 1, _extractCategory); _extractSwitched = true; return; } if (spiritbond?.Replace(" ", "") == "100%") { FireCallback(materialize, true, 2, 0); return; } _extractCategory++; _extractSwitched = false; } else { materialize->Close(true); StopRunner(); } } // --- Repair (self-repair only, ported from AutoDuty RepairHelper) --- private static bool _repairSeenAddon; private static unsafe void RunRepair(IFramework framework) { if (CheckPlayerMoved()) { StopRunner(); return; } if (!Throttle("Repair")) return; if (Service.ClientState is not { IsLoggedIn: true }) { StopRunner(); return; } _activeAction = "Repair"; if (Conditions.Instance()->Mounted) { ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23); return; } if (Service.Condition[Dalamud.Game.ClientState.Conditions.ConditionFlag.Occupied39]) { StopRunner(); return; } if (!TryGetAddon("Repair", out var repair) && !TryGetAddon("SelectYesno", out var yesno)) { ActionManager.Instance()->UseAction(ActionType.GeneralAction, 6); return; } if (!_repairSeenAddon && (!TryGetAddon("SelectYesno", out yesno) || !IsAddonReady(yesno))) { if (TryGetAddon("Repair", out repair) && IsAddonReady(repair)) { // Repair All: fire callback (same pattern as AddonMaster.Repair.RepairAll) FireCallback(repair, true, 0); _repairSeenAddon = true; } return; } if (TryGetAddon("SelectYesno", out yesno) && IsAddonReady(yesno)) { yesno->FireCallbackInt(0); _repairSeenAddon = true; return; } if (_repairSeenAddon && (!TryGetAddon("SelectYesno", out _) || !IsAddonReady(yesno))) StopRunner(); } // --- Equip (vanilla RecommendEquipModule, ported from AutoDuty AutoEquipHelper) --- private static int _equipState; private static unsafe void RunEquip(IFramework framework) { if (CheckPlayerMoved()) { StopRunner(); return; } if (!Throttle("Equip")) return; if (Service.ClientState is not { IsLoggedIn: true } || Service.ObjectTable?.LocalPlayer == null) { StopRunner(); return; } _activeAction = "Equip"; if (RecommendEquipModule.Instance()->IsUpdating) return; if (_equipState == 0) { var job = Service.ObjectTable.LocalPlayer.ClassJob; var jobId = (byte)job.RowId; RecommendEquipModule.Instance()->SetupForClassJob(jobId); _equipState = 1; return; } RecommendEquipModule.Instance()->EquipRecommendedGear(); StopRunner(); _equipState = 0; } // --- Coffers (ported from AutoDuty CofferHelper) --- private static readonly Dictionary _cofferDone = new(); private static int _cofferInitialGearset = -1; private static unsafe void RunCoffers(IFramework framework) { if (CheckPlayerMoved()) { StopRunner(); return; } if (!Throttle("Coffer")) return; if (Service.ClientState is not { IsLoggedIn: true }) { StopRunner(); return; } _activeAction = "Coffer"; if (Conditions.Instance()->Mounted) { ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23); return; } if (InventoryManager.Instance()->GetEmptySlotsInBag() < 1) { StopRunner(); return; } if (GenericHelpersIsOccupied() || Service.ObjectTable?.LocalPlayer?.IsCasting == true) return; if (_cofferInitialGearset < 0) _cofferInitialGearset = RaptureGearsetModule.Instance()->CurrentGearsetIndex; var items = GetCofferItems(); var module = RaptureGearsetModule.Instance(); if (items.Count > 0) { var (itemId, invType, slot, qty) = items[0]; if (!_cofferDone.TryGetValue(itemId, out var prevQty) || prevQty != qty) { UseItem(invType, (ushort)slot); if (Service.ObjectTable?.LocalPlayer?.IsCasting == true) _cofferDone[itemId] = qty; } } else if (_cofferInitialGearset >= 0 && module->CurrentGearsetIndex != _cofferInitialGearset) { module->EquipGearset(_cofferInitialGearset); } else { StopRunner(); _cofferDone.Clear(); _cofferInitialGearset = -1; } } private static List<(uint ItemId, InventoryType InvType, int Slot, int Qty)> GetCofferItems() { var result = new List<(uint, InventoryType, int, int)>(); var sheet = Service.DataManager.GetExcelSheet(); if (sheet == null) return result; unsafe { var container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.Inventory1); if (container == null) return result; for (var i = 0; i < container->Size; i++) { var slot = container->Items[i]; if (slot.ItemId == 0) continue; var itemRow = sheet.GetRow(slot.ItemId); if (itemRow.RowId == 0 || !ValidCoffer(itemRow)) continue; if (_cofferDone.TryGetValue(slot.ItemId, out var prev) && prev == slot.Quantity) continue; result.Add((slot.ItemId, InventoryType.Inventory1, i, slot.Quantity)); } container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.Inventory2); if (container == null) return result; for (var i = 0; i < container->Size; i++) { var slot = container->Items[i]; if (slot.ItemId == 0) continue; var itemRow = sheet.GetRow(slot.ItemId); if (itemRow.RowId == 0 || !ValidCoffer(itemRow)) continue; if (_cofferDone.TryGetValue(slot.ItemId, out var prev) && prev == slot.Quantity) continue; result.Add((slot.ItemId, InventoryType.Inventory2, i, slot.Quantity)); } } return result; } private static bool ValidCoffer(Item item) => item.ItemAction.RowId is 1085 or 388 or 367 && item.ItemUICategory.RowId == 61; private static unsafe void UseItem(InventoryType invType, ushort slot) { var container = InventoryManager.Instance()->GetInventoryContainer(invType); if (container == null) return; var item = container->Items[slot]; if (item.ItemId == 0) return; ActionManager.Instance()->UseAction(ActionType.Item, item.ItemId, 65535); } private static bool GenericHelpersIsOccupied() { if (Service.ObjectTable?.LocalPlayer == null) return true; var player = Service.ObjectTable.LocalPlayer; return player.IsCasting; } }