v1.0.0.18: Minimap automations (Desynth, Extract, Repair, Equip, Coffers); player movement cancels automation; >> button with tooltip
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Automation actions ported from AutoDuty for the minimap pop-out menu:
|
||||
/// Desynth all, Extract Materia, Repair (self), Equip Gear, Open Coffers.
|
||||
/// </summary>
|
||||
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<IFramework> 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<AgentSalvage.SalvageItemCategory>(); _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<Item>()?.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<AgentSalvage.SalvageItemCategory>();
|
||||
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<uint, int> _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<Item>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user