From 8763cf4c70880a97348ce3a1d8ad7d2669ef1f91 Mon Sep 17 00:00:00 2001 From: Knack117 Date: Sun, 1 Mar 2026 13:38:50 -0500 Subject: [PATCH] v1.0.0.18: Minimap automations (Desynth, Extract, Repair, Equip, Coffers); player movement cancels automation; >> button with tooltip Made-with: Cursor --- Mappy/Data/SystemConfig.cs | 2 + Mappy/Helpers/MinimapActions.cs | 384 +++++++++++++++++++++++++++ Mappy/Mappy.csproj | 2 +- Mappy/Windows/ConfigurationWindow.cs | 3 + Mappy/Windows/MinimapWindow.cs | 73 ++++- Mappy/pluginmaster.json | 2 +- 6 files changed, 463 insertions(+), 3 deletions(-) create mode 100644 Mappy/Helpers/MinimapActions.cs diff --git a/Mappy/Data/SystemConfig.cs b/Mappy/Data/SystemConfig.cs index 0dfbd6c..addb1dd 100644 --- a/Mappy/Data/SystemConfig.cs +++ b/Mappy/Data/SystemConfig.cs @@ -154,6 +154,8 @@ public class SystemConfig : CharacterConfiguration public int MinimapCoordBarFontType = 0; /// Background color (RGBA) for coordinate bar. Alpha = opacity. public Vector4 MinimapCoordBarBackground = new(0f, 0f, 0f, 0.2f); + /// Show the ">>" action menu button in the top-right corner of the minimap (Desynth, Extract, Repair, Equip, Open Coffers). + public bool MinimapShowActionMenuButton = true; // Movement Trail (Carbonite-style: show where you've been) /// Draw a red trail of dots on the map showing where you've been. diff --git a/Mappy/Helpers/MinimapActions.cs b/Mappy/Helpers/MinimapActions.cs new file mode 100644 index 0000000..7609db0 --- /dev/null +++ b/Mappy/Helpers/MinimapActions.cs @@ -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; + +/// +/// 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; + } +} diff --git a/Mappy/Mappy.csproj b/Mappy/Mappy.csproj index 1ee3cc2..fc78b03 100644 --- a/Mappy/Mappy.csproj +++ b/Mappy/Mappy.csproj @@ -4,7 +4,7 @@ HSMappy HSMappy Knack117 - 1.0.0.17 + 1.0.0.18 A more versatile in-game map. Replaces the in-game map with an ImGui implementation with several additional features. Fork with minimap improvements, quest radius on minimap, and more. http://brassnet.ddns.net:33983/KnackAtNite/HSMappy diff --git a/Mappy/Windows/ConfigurationWindow.cs b/Mappy/Windows/ConfigurationWindow.cs index 017ef78..4510a00 100644 --- a/Mappy/Windows/ConfigurationWindow.cs +++ b/Mappy/Windows/ConfigurationWindow.cs @@ -312,6 +312,9 @@ public class MinimapOptionsTab : ITabItem if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) ImGui.SetTooltip("When enabled, the minimap hides during NPC dialogue, object interaction, and when the game hides nameplates (same as the main map). When disabled, the minimap stays visible in those situations."); configChanged |= ImGui.Checkbox("Lock Position", ref System.SystemConfig.MinimapLockPosition); + configChanged |= ImGui.Checkbox("Show action menu button (>>)", ref System.SystemConfig.MinimapShowActionMenuButton); + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip("Show the >> button at top-right of minimap to quickly access: Desynth all items, Extract Materia, Repair, Equip Gear, Open Coffers."); configChanged |= ImGui.Checkbox("Draw Under Other UI", ref System.SystemConfig.MinimapDrawUnderOtherUI); if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) ImGui.SetTooltip("When enabled, the minimap draws underneath other plugin UI (e.g. HSUI). Disable to draw the minimap on top."); diff --git a/Mappy/Windows/MinimapWindow.cs b/Mappy/Windows/MinimapWindow.cs index 6fe2a0c..2443363 100644 --- a/Mappy/Windows/MinimapWindow.cs +++ b/Mappy/Windows/MinimapWindow.cs @@ -9,6 +9,7 @@ using KamiLib.Window; using Lumina.Excel.Sheets; using Mappy.Controllers; using Mappy.Data; +using Mappy.Helpers; using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Map = Lumina.Excel.Sheets.Map; @@ -47,7 +48,17 @@ public class MinimapWindow : Window protected override unsafe void DrawContents() { if (System.SystemConfig.MinimapDrawUnderOtherUI) - ImGuiP.BringWindowToDisplayBack(ImGuiP.GetCurrentWindow()); + { + var win = ImGuiP.GetCurrentWindow(); + var pos = ImGui.GetWindowPos(); + var size = ImGui.GetWindowSize(); + var mouse = ImGui.GetMousePos(); + var cursorOverMinimap = mouse.X >= pos.X && mouse.X <= pos.X + size.X && mouse.Y >= pos.Y && mouse.Y <= pos.Y + size.Y; + if (cursorOverMinimap) + ImGuiP.BringWindowToDisplayFront(win); + else + ImGuiP.BringWindowToDisplayBack(win); + } var agent = AgentMap.Instance(); // Try loading from Lumina first so minimap can show without ever opening the area map @@ -129,6 +140,11 @@ public class MinimapWindow : Window DrawCoordinateBar(totalWidth, bottomBarHeight, scale); } + // Action menu ">>" button at top-right of minimap + if (System.SystemConfig.MinimapShowActionMenuButton) { + DrawActionMenuButton(totalWidth, topBarHeight, minimapSide); + } + // Restore default padding for the next window is done in plugin Draw callback (PopStyleVar after all windows). } @@ -320,6 +336,61 @@ public class MinimapWindow : Window } } + private void DrawActionMenuButton(float totalWidth, float topBarHeight, float _) + { + const string buttonLabel = ">>"; + var pad = 6f * ImGuiHelpers.GlobalScale; + var textSize = ImGui.CalcTextSize(buttonLabel); + var buttonSize = textSize + new Vector2(pad * 2, pad); + var windowPos = ImGui.GetWindowPos(); + var screenPos = new Vector2( + windowPos.X + totalWidth - buttonSize.X - pad, + windowPos.Y + topBarHeight + pad); + var buttonMin = screenPos; + var buttonMax = screenPos + buttonSize; + + var drawList = ImGui.GetForegroundDrawList(); + var isHovered = ImGui.IsMouseHoveringRect(buttonMin, buttonMax); + var isClicked = isHovered && ImGui.IsMouseClicked(ImGuiMouseButton.Left); + + if (isClicked) + ImGui.OpenPopup("minimap_action_popup"); + + var bgColor = isHovered ? new Vector4(0.25f, 0.25f, 0.3f, 0.98f) : new Vector4(0.12f, 0.12f, 0.18f, 0.98f); + var borderColor = new Vector4(1f, 1f, 1f, 0.9f); + var textColor = new Vector4(1f, 1f, 1f, 1f); + + drawList.AddRectFilled(buttonMin, buttonMax, ImGui.GetColorU32(bgColor), 3f); + drawList.AddRect(buttonMin, buttonMax, ImGui.GetColorU32(borderColor), 3f, ImDrawFlags.None, 1.5f); + var textPos = screenPos + new Vector2((buttonSize.X - textSize.X) * 0.5f, (buttonSize.Y - textSize.Y) * 0.5f); + drawList.AddText(textPos, ImGui.GetColorU32(textColor), buttonLabel); + + if (isHovered) + { + ImGui.SetNextWindowBgAlpha(1f); + using var tooltip = ImRaii.Tooltip(); + ImGui.Text("Automations"); + } + + var popupX = windowPos.X + totalWidth + pad; + var popupY = windowPos.Y + topBarHeight; + ImGui.SetNextWindowPos(new Vector2(popupX, popupY), ImGuiCond.Appearing); + if (ImGui.BeginPopup("minimap_action_popup", ImGuiWindowFlags.NoMove)) + { + if (ImGui.MenuItem("Desynth all items in inventory")) + MinimapActions.InvokeDesynth(); + if (ImGui.MenuItem("Extract Materia")) + MinimapActions.InvokeExtract(); + if (ImGui.MenuItem("Repair")) + MinimapActions.InvokeRepair(); + if (ImGui.MenuItem("Equip Gear")) + MinimapActions.InvokeEquip(); + if (ImGui.MenuItem("Open Coffers")) + MinimapActions.InvokeCoffers(); + ImGui.EndPopup(); + } + } + private void UpdateStyle() { if (System.SystemConfig.MinimapLockPosition) diff --git a/Mappy/pluginmaster.json b/Mappy/pluginmaster.json index 5a51ae6..5c6dae0 100644 --- a/Mappy/pluginmaster.json +++ b/Mappy/pluginmaster.json @@ -1 +1 @@ -[{"Author":"Knack117","Name":"HSMappy","Punchline":"A more versatile in-game map.","Description":"Replaces the in-game map with an ImGui implementation with several additional features. Fork with minimap improvements, quest radius on minimap, white gradient player cone, and more.","Changelog":"1.0.0.17: Minimap stays visible during dialogue (NPC/quest); rest of hide behavior unchanged. 1.0.0.16: Draw minimap underneath other UI (HSUI etc); Draw Under Other UI config option. 1.0.0.15: Top/Bottom Info Bars: renamed from Map Info/Coordinate; configurable order for both; Repair % (most damaged item); font, size, color, background for both bars; right-align last bottom bar element. 1.0.0.14: Movement Trail (Carbonite-style) - red dots show where you've been on map and minimap; configurable distance, fade time, max points; Clear Trail in context menu. 1.0.0.13: User-placed map notes with Title/Description; custom white-page icon; notes on minimap; Remove Note via context menu; Note List layout fix. 1.0.0.12: Other players on minimap use distinct icon (60403) from party markers. 1.0.0.11: Player/NPC tracking on minimap with Show Players and NPCs toggle. 1.0.0.10: Release build. Suppress silent refresh at start of OnOpenMapHook; remove debug logging. 1.0.0.9: Duty List quest click: don't Hide() when viewing quest map (SelectedMapId != CurrentMapId). 1.0.0.8: Cancel silent refresh when opening map from Duty List so it doesn't immediately close. 1.0.0.7: Duty List quest click opens Area Map even when Hide With Game GUI would block it. 1.0.0.6: Minimap stays open after client restart (restore on login). 1.0.0.5: Fix crash when map texture path is invalid (ArgumentOutOfRangeException in Lumina GetFileHash). 1.0.0.4: Temp marker circle refreshes when quest objective is progressed. 1.0.0.3: Fix marker cache refresh after quest turn-in; invalidate temp cache so old markers don't persist. 1.0.0.2: Red direction arrow on minimap pointing to player flag. 1.0.0.1: Duty List quest click keeps Area Map open; player flags show on minimap. 1.0.0.0: Initial HSMappy release. Minimap: quest radius circle (orange, transparent), tooltip; cone drawn under markers; white gradient cone; /hsmappy commands.","InternalName":"HSMappy","AssemblyVersion":"1.0.0.17","RepoUrl":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy","ApplicableVersion":"any","Tags":["map","mapping","overlay","utility"],"CategoryTags":["jobs"],"DalamudApiLevel":14,"DownloadLinkInstall":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.17/latest.zip","IsHide":false,"IsTestingExclusive":false,"DownloadLinkTesting":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.17/latest.zip","DownloadLinkUpdate":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.17/latest.zip","LastUpdate":"1772366888"}] +[{"Author":"Knack117","Name":"HSMappy","Punchline":"A more versatile in-game map.","Description":"Replaces the in-game map with an ImGui implementation with several additional features. Fork with minimap improvements, quest radius on minimap, white gradient player cone, and more.","Changelog":"1.0.0.18: Minimap automations (Desynth, Extract, Repair, Equip, Coffers); player movement cancels automation; >> button with tooltip. 1.0.0.17: Minimap stays visible during dialogue (NPC/quest); rest of hide behavior unchanged. 1.0.0.16: Draw minimap underneath other UI (HSUI etc); Draw Under Other UI config option. 1.0.0.15: Top/Bottom Info Bars: renamed from Map Info/Coordinate; configurable order for both; Repair % (most damaged item); font, size, color, background for both bars; right-align last bottom bar element. 1.0.0.14: Movement Trail (Carbonite-style) - red dots show where you've been on map and minimap; configurable distance, fade time, max points; Clear Trail in context menu. 1.0.0.13: User-placed map notes with Title/Description; custom white-page icon; notes on minimap; Remove Note via context menu; Note List layout fix. 1.0.0.12: Other players on minimap use distinct icon (60403) from party markers. 1.0.0.11: Player/NPC tracking on minimap with Show Players and NPCs toggle. 1.0.0.10: Release build. Suppress silent refresh at start of OnOpenMapHook; remove debug logging. 1.0.0.9: Duty List quest click: don't Hide() when viewing quest map (SelectedMapId != CurrentMapId). 1.0.0.8: Cancel silent refresh when opening map from Duty List so it doesn't immediately close. 1.0.0.7: Duty List quest click opens Area Map even when Hide With Game GUI would block it. 1.0.0.6: Minimap stays open after client restart (restore on login). 1.0.0.5: Fix crash when map texture path is invalid (ArgumentOutOfRangeException in Lumina GetFileHash). 1.0.0.4: Temp marker circle refreshes when quest objective is progressed. 1.0.0.3: Fix marker cache refresh after quest turn-in; invalidate temp cache so old markers don't persist. 1.0.0.2: Red direction arrow on minimap pointing to player flag. 1.0.0.1: Duty List quest click keeps Area Map open; player flags show on minimap. 1.0.0.0: Initial HSMappy release. Minimap: quest radius circle (orange, transparent), tooltip; cone drawn under markers; white gradient cone; /hsmappy commands.","InternalName":"HSMappy","AssemblyVersion":"1.0.0.18","RepoUrl":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy","ApplicableVersion":"any","Tags":["map","mapping","overlay","utility"],"CategoryTags":["jobs"],"DalamudApiLevel":14,"DownloadLinkInstall":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.18/latest.zip","IsHide":false,"IsTestingExclusive":false,"DownloadLinkTesting":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.18/latest.zip","DownloadLinkUpdate":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.18/latest.zip","LastUpdate":"1772372275"}]