6 Commits

15 changed files with 1140 additions and 21 deletions
@@ -36,6 +36,10 @@ public unsafe class MapContextMenu
} }
} }
if (ImGui.MenuItem("Clear Movement Trail")) {
System.MovementTrailConfig.Clear();
}
if (ImGui.MenuItem("Place Flag")) { if (ImGui.MenuItem("Place Flag")) {
AgentMap.Instance()->FlagMarkerCount = 0; AgentMap.Instance()->FlagMarkerCount = 0;
AgentMap.Instance()->SetFlagMapMarker(AgentMap.Instance()->SelectedTerritoryId, AgentMap.Instance()->SelectedMapId, scaledResult.X, scaledResult.Y); AgentMap.Instance()->SetFlagMapMarker(AgentMap.Instance()->SelectedTerritoryId, AgentMap.Instance()->SelectedMapId, scaledResult.X, scaledResult.Y);
+61 -5
View File
@@ -2,6 +2,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
@@ -26,8 +28,10 @@ public unsafe class IntegrationsController : IDisposable
private bool _wasBetweenAreas; private bool _wasBetweenAreas;
private int _lastQuestCount = -1; private int _lastQuestCount = -1;
private int _lastTempMarkerCount = -1; private int _lastTempMarkerCount = -1;
/// <summary>Snapshot of (QuestId, Sequence) for each active quest; when this changes we refresh so markers update (e.g. multi-step objective).</summary> /// <summary>Snapshot of (QuestId, Sequence, variables) for each active quest; when this changes we refresh so markers update (e.g. multi-step objective).</summary>
private string _lastQuestSequenceSnapshot = string.Empty; private string _lastQuestSequenceSnapshot = string.Empty;
/// <summary>Snapshot of temp marker positions; when this changes we refresh so quest area circles update (e.g. marker moved from 1/3 to 2/3 location).</summary>
private string _lastTempMarkerSnapshot = string.Empty;
private bool _refreshedDuringLoad; private bool _refreshedDuringLoad;
/// <summary>When true, request a silent refresh on the next framework update (e.g. after plugin load).</summary> /// <summary>When true, request a silent refresh on the next framework update (e.g. after plugin load).</summary>
private bool _requestRefreshOnLoad = true; private bool _requestRefreshOnLoad = true;
@@ -112,6 +116,8 @@ public unsafe class IntegrationsController : IDisposable
_wasBetweenAreas = Service.Condition.IsBetweenAreas(); _wasBetweenAreas = Service.Condition.IsBetweenAreas();
_lastQuestCount = GetActiveQuestCount(); _lastQuestCount = GetActiveQuestCount();
try { _lastTempMarkerCount = (int)AgentMap.Instance()->TempMapMarkerCount; } catch { } try { _lastTempMarkerCount = (int)AgentMap.Instance()->TempMapMarkerCount; } catch { }
_lastQuestSequenceSnapshot = GetQuestSequenceSnapshot();
_lastTempMarkerSnapshot = GetTempMarkerSnapshot();
return; return;
} }
@@ -153,6 +159,7 @@ public unsafe class IntegrationsController : IDisposable
var tempCount = -1; var tempCount = -1;
try { tempCount = (int)AgentMap.Instance()->TempMapMarkerCount; } catch { } try { tempCount = (int)AgentMap.Instance()->TempMapMarkerCount; } catch { }
var sequenceSnapshot = GetQuestSequenceSnapshot(); var sequenceSnapshot = GetQuestSequenceSnapshot();
var tempMarkerSnapshot = GetTempMarkerSnapshot();
if (!skipQuestTempRefresh) { if (!skipQuestTempRefresh) {
if (_lastQuestCount >= 0 && questCount < _lastQuestCount) if (_lastQuestCount >= 0 && questCount < _lastQuestCount)
RequestSilentRefresh(); // quest turned in RequestSilentRefresh(); // quest turned in
@@ -164,27 +171,48 @@ public unsafe class IntegrationsController : IDisposable
RequestSilentRefresh(); // objectives added (e.g. new quest) RequestSilentRefresh(); // objectives added (e.g. new quest)
if (_lastQuestSequenceSnapshot.Length > 0 && sequenceSnapshot != _lastQuestSequenceSnapshot) if (_lastQuestSequenceSnapshot.Length > 0 && sequenceSnapshot != _lastQuestSequenceSnapshot)
RequestSilentRefresh(); // quest step advanced (multi-step objective) RequestSilentRefresh(); // quest step advanced (multi-step objective)
if (_lastTempMarkerSnapshot.Length > 0 && tempMarkerSnapshot.Length > 0 && tempMarkerSnapshot != _lastTempMarkerSnapshot)
RequestSilentRefresh(); // marker positions changed (e.g. 1/3 -> 2/3, circle moved)
_lastQuestCount = questCount; _lastQuestCount = questCount;
_lastTempMarkerCount = tempCount; _lastTempMarkerCount = tempCount;
_lastQuestSequenceSnapshot = sequenceSnapshot; _lastQuestSequenceSnapshot = sequenceSnapshot;
_lastTempMarkerSnapshot = tempMarkerSnapshot;
} else { } else {
// During suppression: only update temp baseline so we don't false-trigger when suppression // During suppression: only update temp baseline so we don't false-trigger when suppression
// ends (e.g. Duty List click repopulates markers). Keep _lastQuestCount and _lastQuestSequenceSnapshot // ends (e.g. Duty List click repopulates markers). Keep _lastQuestCount and _lastQuestSequenceSnapshot
// so quest turn-in and objective progression during suppression still trigger refresh when suppression ends. // so quest turn-in and objective progression during suppression still trigger refresh when suppression ends.
_lastTempMarkerCount = tempCount; _lastTempMarkerCount = tempCount;
} }
// Movement trail: record player position when enabled (Carbonite-style)
if (System.SystemConfig.ShowMovementTrail && Service.ObjectTable.LocalPlayer is { } localPlayer) {
try {
var agent = AgentMap.Instance();
System.MovementTrailConfig.TryAddPoint(
agent->CurrentTerritoryId,
agent->CurrentMapId,
localPlayer.Position.X,
localPlayer.Position.Z);
} catch { /* ignore */ }
}
} }
/// <summary>Build a string of (QuestId, Sequence) for each active quest so we can detect step advances.</summary> /// <summary>Build a string of (QuestId, Sequence, variables) for each active quest so we can detect step advances and multi-step objective progress (1/3, 2/3, etc.).</summary>
private static unsafe string GetQuestSequenceSnapshot() private static unsafe string GetQuestSequenceSnapshot()
{ {
try try
{ {
var parts = new List<string>(); var parts = new List<string>();
foreach (var q in QuestManager.Instance()->NormalQuests) var span = QuestManager.Instance()->NormalQuests;
for (var i = 0; i < span.Length; i++)
{ {
ref var q = ref span[i];
if (q.QuestId is 0) continue; if (q.QuestId is 0) continue;
parts.Add($"{q.QuestId}:{q.Sequence}"); // Include variables (objective progress) - changes when 1/3 -> 2/3 even if Sequence does not
var ptr = (byte*)Unsafe.AsPointer(ref q);
var varStr = string.Empty;
for (var j = 0; j < 6; j++) varStr += $"{ptr[0x0C + j]:X2}";
parts.Add($"{q.QuestId}:{q.Sequence}:{varStr}");
} }
return string.Join("|", parts); return string.Join("|", parts);
} }
@@ -194,6 +222,31 @@ public unsafe class IntegrationsController : IDisposable
} }
} }
/// <summary>Build a string of temp marker positions; when marker moves (e.g. 1/3 to 2/3 location) we detect and refresh.</summary>
private static unsafe string GetTempMarkerSnapshot()
{
try
{
var agent = AgentMap.Instance();
var count = agent->TempMapMarkerCount;
if (count == 0) return string.Empty;
var parts = new List<string>();
var seen = new HashSet<(int, int)>();
for (var i = 0; i < count; i++)
{
ref var m = ref agent->TempMapMarkers[i];
var key = (m.MapMarker.X, m.MapMarker.Y);
if (seen.Add(key))
parts.Add($"{m.MapMarker.X},{m.MapMarker.Y}");
}
return string.Join("|", parts.OrderBy(x => x));
}
catch
{
return string.Empty;
}
}
/// <summary>Call when user opens map via Duty List (quest/gathering/flag/teleport). Cancels any in-progress silent refresh so we never Hide() the map. Suppresses new quest/temp-marker-triggered refresh for ~1s. Must be called BEFORE openMapHook.Original so OnFrameworkUpdate cannot call Hide() first.</summary> /// <summary>Call when user opens map via Duty List (quest/gathering/flag/teleport). Cancels any in-progress silent refresh so we never Hide() the map. Suppresses new quest/temp-marker-triggered refresh for ~1s. Must be called BEFORE openMapHook.Original so OnFrameworkUpdate cannot call Hide() first.</summary>
private void SuppressSilentRefreshForUserMapOpen() private void SuppressSilentRefreshForUserMapOpen()
{ {
@@ -526,7 +579,10 @@ public unsafe class IntegrationsController : IDisposable
if (System.SystemConfig.HideInCombat && Service.Condition.IsInCombat()) return false; if (System.SystemConfig.HideInCombat && Service.Condition.IsInCombat()) return false;
if (System.SystemConfig.HideBetweenAreas && Service.Condition.IsBetweenAreas()) return false; if (System.SystemConfig.HideBetweenAreas && Service.Condition.IsBetweenAreas()) return false;
if (!System.SystemConfig.MinimapHideWithGameGui) return true; if (!System.SystemConfig.MinimapHideWithGameGui) return true;
// Same as main map // Don't hide during dialogue (Occupied = NPC dialogue, OccupiedInQuestEvent = quest dialogue)
if (Service.Condition[ConditionFlag.Occupied] || Service.Condition[ConditionFlag.OccupiedInQuestEvent])
return true;
// Same as main map for non-dialogue cases
if (System.SystemConfig.HideWithGameGui && !IsNamePlateAddonVisible()) return false; if (System.SystemConfig.HideWithGameGui && !IsNamePlateAddonVisible()) return false;
if (System.SystemConfig.HideWithGameGui && Control.Instance()->TargetSystem.TargetModeIndex is 1) return false; if (System.SystemConfig.HideWithGameGui && Control.Instance()->TargetSystem.TargetModeIndex is 1) return false;
return true; return true;
+43
View File
@@ -0,0 +1,43 @@
using System;
using FFXIVClientStructs.FFXIV.Client.Game;
namespace Mappy.Data;
/// <summary>
/// Returns the condition percentage of the player's most damaged equipped item.
/// Uses the same logic as RepairMe (chalkos/RepairMe): raw condition / 300.
/// </summary>
public static class EquipmentConditionHelper
{
private const uint EquipmentContainerSize = 13;
/// <summary>
/// Gets the lowest condition percent among equipped items (0100+).
/// Returns 100 if no gear or unavailable (e.g. not logged in).
/// </summary>
public static unsafe float GetLowestConditionPercent()
{
var inventoryManager = InventoryManager.Instance();
if (inventoryManager == null) return 100f;
var equipmentContainer = inventoryManager->GetInventoryContainer(InventoryType.EquippedItems);
if (equipmentContainer == null) return 100f;
var inventoryItem = equipmentContainer->GetInventorySlot(0);
if (inventoryItem == null) return 100f;
ushort lowestCondition = 60000; // max raw condition is 30000
var foundAny = false;
for (var i = 0; i < EquipmentContainerSize; i++, inventoryItem++)
{
if (inventoryItem->ItemId == 0) continue;
foundAny = true;
if (lowestCondition > inventoryItem->Condition)
lowestCondition = inventoryItem->Condition;
}
return foundAny ? lowestCondition / 300f : 100f;
}
}
+82
View File
@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace Mappy.Data;
/// <summary>Single point on the movement trail (Carbonite-style).</summary>
public record MovementTrailPoint(uint Territory, uint Map, float X, float Y, double TimeStamp);
/// <summary>Config and storage for the movement trail (where you've been on the map).</summary>
public class MovementTrailConfig
{
private readonly List<MovementTrailPoint> _points = [];
private int _nextIndex;
private float _lastX = float.MinValue;
private float _lastY = float.MinValue;
private uint _lastTerritory;
public int MaxPoints { get; set; } = 100;
public float MinDistance { get; set; } = 2f;
public float FadeTimeSeconds { get; set; } = 60f;
public Vector4 TrailColor { get; set; } = new(1f, 0f, 0f, 0.9f); // Red like Carbonite
public IReadOnlyList<MovementTrailPoint> Points => _points;
private float EffectiveMinDistance => Mappy.System.SystemConfig?.MovementTrailMinDistance ?? MinDistance;
private float EffectiveFadeTime => Mappy.System.SystemConfig?.MovementTrailFadeTimeSeconds ?? FadeTimeSeconds;
private int EffectiveMaxPoints => Mappy.System.SystemConfig?.MovementTrailMaxPoints ?? MaxPoints;
public void Clear()
{
_points.Clear();
_nextIndex = 0;
_lastX = float.MinValue;
_lastY = float.MinValue;
}
/// <summary>Record a new trail point if the player has moved enough. Call from Framework.Update.</summary>
public void TryAddPoint(uint territory, uint map, float x, float y)
{
// Reset when changing territory
if (territory != _lastTerritory)
{
Clear();
_lastTerritory = territory;
}
var dx = x - _lastX;
var dy = y - _lastY;
var moveDist = MathF.Sqrt(dx * dx + dy * dy);
if (moveDist < EffectiveMinDistance && _lastX > float.MinValue)
return;
_lastX = x;
_lastY = y;
var now = DateTime.UtcNow.Subtract(DateTime.UnixEpoch).TotalSeconds;
var max = EffectiveMaxPoints;
if (_points.Count < max)
{
_points.Add(new MovementTrailPoint(territory, map, x, y, now));
}
else
{
_points[_nextIndex] = new MovementTrailPoint(territory, map, x, y, now);
_nextIndex = (_nextIndex + 1) % max;
}
}
/// <summary>Get points for the current map that haven't faded yet.</summary>
public IEnumerable<MovementTrailPoint> GetVisiblePoints(uint territoryId, uint mapId)
{
var now = DateTime.UtcNow.Subtract(DateTime.UnixEpoch).TotalSeconds;
var fade = EffectiveFadeTime;
return _points
.Where(p => p.Territory == territoryId && p.Map == mapId && (now - p.TimeStamp) < fade)
.ToList();
}
}
+44
View File
@@ -122,6 +122,50 @@ public class SystemConfig : CharacterConfiguration
public bool MinimapShowPlayersAndNpcs = true; public bool MinimapShowPlayersAndNpcs = true;
/// <summary>Icon ID for other players on the minimap (default 60403, distinct from party 60421). Override if you prefer a different look.</summary> /// <summary>Icon ID for other players on the minimap (default 60403, distinct from party 60421). Override if you prefer a different look.</summary>
public uint MinimapOtherPlayerIconId = 60403; public uint MinimapOtherPlayerIconId = 60403;
/// <summary>Draw minimap underneath other plugin UI (e.g. HSUI). Uses ImGui BringWindowToDisplayBack.</summary>
public bool MinimapDrawUnderOtherUI = true;
/// <summary>Show current map info (region, map, area, sub-area) at the top of the minimap.</summary>
public bool MinimapShowMapInfoBar = true;
/// <summary>Order of map info: 0=Region, 1=Map, 2=Area, 3=SubArea. e.g. {0,1,2,3} = Region, Map, Area, SubArea.</summary>
public int[] MinimapMapInfoOrder = [0, 1, 2, 3];
/// <summary>Font scale multiplier for map info bar (0.5-2.0).</summary>
public float MinimapMapInfoFontScale = 1.0f;
/// <summary>Text color for map info bar.</summary>
public Vector4 MinimapMapInfoColor = KnownColor.White.Vector();
/// <summary>Font type for map info: 0=Default, 1=Axis12, 2=Axis18.</summary>
public int MinimapMapInfoFontType = 0;
/// <summary>Background color (RGBA) for map info bar. Alpha = opacity.</summary>
public Vector4 MinimapMapInfoBarBackground = new(0f, 0f, 0f, 0.2f);
/// <summary>Show player coordinates at the bottom of the minimap.</summary>
public bool MinimapShowCoordinateBar = true;
/// <summary>Show coordinates in the bottom bar.</summary>
public bool MinimapCoordBarShowCoordinates = true;
/// <summary>Show local time in the bottom bar.</summary>
public bool MinimapCoordBarShowTime = true;
/// <summary>Show repair % (lowest equipped item condition) in the bottom bar.</summary>
public bool MinimapCoordBarShowRepairPercent = true;
/// <summary>Order of bottom bar elements: 0=Coordinates, 1=Repair %, 2=Local Time.</summary>
public int[] MinimapBottomBarOrder = [0, 1, 2];
/// <summary>Font scale multiplier for coordinate bar (0.5-2.0).</summary>
public float MinimapCoordBarFontScale = 1.0f;
/// <summary>Text color for coordinate bar.</summary>
public Vector4 MinimapCoordBarColor = KnownColor.White.Vector();
/// <summary>Font type for coord bar: 0=Default, 1=Axis12, 2=Axis18.</summary>
public int MinimapCoordBarFontType = 0;
/// <summary>Background color (RGBA) for coordinate bar. Alpha = opacity.</summary>
public Vector4 MinimapCoordBarBackground = new(0f, 0f, 0f, 0.2f);
/// <summary>Show the ">>" action menu button in the top-right corner of the minimap (Desynth, Extract, Repair, Equip, Open Coffers).</summary>
public bool MinimapShowActionMenuButton = true;
// Movement Trail (Carbonite-style: show where you've been)
/// <summary>Draw a red trail of dots on the map showing where you've been.</summary>
public bool ShowMovementTrail = false;
/// <summary>Minimum distance (world units) before adding a new trail point.</summary>
public float MovementTrailMinDistance = 2f;
/// <summary>How long (seconds) trail points stay visible before fading out.</summary>
public float MovementTrailFadeTimeSeconds = 60f;
/// <summary>Maximum number of trail points to keep.</summary>
public int MovementTrailMaxPoints = 100;
// Do not persist this setting // Do not persist this setting
[JsonIgnore] [JsonIgnore]
+384
View File
@@ -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;
}
}
+16
View File
@@ -145,6 +145,21 @@ public unsafe partial class MapRenderer : IDisposable
/// </summary> /// </summary>
public bool HasMinimapCacheFor(uint mapId) => _minimapCache.ContainsKey(mapId); public bool HasMinimapCacheFor(uint mapId) => _minimapCache.ContainsKey(mapId);
/// <summary>
/// Returns the map transform (offsetX, offsetY, sizeFactor) for the current minimap map, for coordinate display.
/// Returns null if no cache exists for CurrentMapId.
/// </summary>
public (int offsetX, int offsetY, uint sizeFactor)? GetCurrentMinimapTransform()
{
var agent = AgentMap.Instance();
var currentMapId = agent->CurrentMapId;
if (currentMapId == 0 || !_minimapCache.TryGetValue(currentMapId, out var entry))
return null;
var sizeFactor = (uint)Math.Round(entry.ScaleFactor * 100f);
if (sizeFactor == 0) sizeFactor = 100;
return ((int)entry.OffsetX, (int)entry.OffsetY, sizeFactor);
}
/// <summary> /// <summary>
/// Try to load map texture and transform from Lumina (Map sheet) so the minimap can draw without opening the area map. /// Try to load map texture and transform from Lumina (Map sheet) so the minimap can draw without opening the area map.
/// Uses game map path conventions (ui/map/...) and Map.SizeFactor, Map.OffsetX/Y. On success, fills the cache for this map. /// Uses game map path conventions (ui/map/...) and Map.SizeFactor, Map.OffsetX/Y. On success, fills the cache for this map.
@@ -377,6 +392,7 @@ public unsafe partial class MapRenderer : IDisposable
DrawPlayer(); DrawPlayer();
DrawStaticTextMarkers(); DrawStaticTextMarkers();
DrawMapNotes(); DrawMapNotes();
DrawMovementTrail();
DrawFlag(); DrawFlag();
} }
} }
@@ -62,6 +62,8 @@ public partial class MapRenderer
DrawMinimapFlag(contentTopLeft, TexToContent, scaleFactor, offsetX, offsetY); DrawMinimapFlag(contentTopLeft, TexToContent, scaleFactor, offsetX, offsetY);
// User map notes // User map notes
DrawMinimapMapNotes(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY); DrawMinimapMapNotes(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY);
// Movement trail
DrawMinimapMovementTrail(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY);
// Temporary (quest objectives, etc.) // Temporary (quest objectives, etc.)
DrawMinimapTempMarkers(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY, scale); DrawMinimapTempMarkers(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY, scale);
// Field markers (waymarks) // Field markers (waymarks)
@@ -729,6 +731,33 @@ public partial class MapRenderer
} }
} }
private unsafe void DrawMinimapMovementTrail(Vector2 contentTopLeft, Func<float, float, Vector2> texToContent, Vector2 size, float scaleFactor, float offsetX, float offsetY)
{
if (!System.SystemConfig.ShowMovementTrail) return;
var agent = AgentMap.Instance();
var territoryId = agent->CurrentTerritoryId;
var mapId = agent->CurrentMapId;
var now = DateTime.UtcNow.Subtract(DateTime.UnixEpoch).TotalSeconds;
var fadeTime = System.SystemConfig.MovementTrailFadeTimeSeconds;
foreach (var pt in System.MovementTrailConfig.GetVisiblePoints(territoryId, mapId).ToList()) {
var age = now - pt.TimeStamp;
var alpha = (float)((fadeTime - age) / fadeTime * 0.9);
if (alpha <= 0f) continue;
var tx = 1024.0f + (pt.X - offsetX) * scaleFactor;
var ty = 1024.0f + (pt.Y - offsetY) * scaleFactor;
var contentPos = texToContent(tx, ty);
if (!IsInMinimapBounds(contentPos, size, MinimapBoundsMargin)) continue;
var centerScreen = contentPos + contentTopLeft;
var trailSize = 4f * 0.75f;
var color = System.MovementTrailConfig.TrailColor with { W = alpha };
ImGui.GetWindowDrawList().AddCircleFilled(centerScreen, trailSize, ImGui.GetColorU32(color));
}
}
private unsafe void DrawMinimapMapNotes(Vector2 contentTopLeft, Func<float, float, Vector2> texToContent, Vector2 size, float scaleFactor, float offsetX, float offsetY) private unsafe void DrawMinimapMapNotes(Vector2 contentTopLeft, Func<float, float, Vector2> texToContent, Vector2 size, float scaleFactor, float offsetX, float offsetY)
{ {
var agent = AgentMap.Instance(); var agent = AgentMap.Instance();
@@ -0,0 +1,41 @@
using System;
using System.Linq;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Mappy.Classes;
using Mappy.Data;
namespace Mappy.MapRenderer;
public partial class MapRenderer
{
private unsafe void DrawMovementTrail()
{
if (!System.SystemConfig.ShowMovementTrail) return;
var agent = AgentMap.Instance();
var territoryId = agent->SelectedTerritoryId;
var mapId = agent->SelectedMapId;
var now = DateTime.UtcNow.Subtract(DateTime.UnixEpoch).TotalSeconds;
var fadeTime = System.SystemConfig.MovementTrailFadeTimeSeconds;
foreach (var pt in System.MovementTrailConfig.GetVisiblePoints(territoryId, mapId).ToList()) {
var age = now - pt.TimeStamp;
var alpha = (float)((fadeTime - age) / fadeTime * 0.9);
if (alpha <= 0f) continue;
// Same coordinate space as map notes: world X,Z
var pos = new Vector2(pt.X, pt.Y) * Scale * DrawHelpers.GetMapScaleFactor() + DrawHelpers.GetCombinedOffsetVector() * Scale;
var size = Math.Clamp(4 * Scale, 3f, 25f);
var screenPos = ImGui.GetWindowPos() + DrawPosition + pos;
var color = System.MovementTrailConfig.TrailColor with { W = alpha };
var drawList = ImGui.GetWindowDrawList();
drawList.AddCircleFilled(screenPos, size, ImGui.GetColorU32(color));
}
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
<Name>HSMappy</Name> <Name>HSMappy</Name>
<InternalName>HSMappy</InternalName> <InternalName>HSMappy</InternalName>
<Author>Knack117</Author> <Author>Knack117</Author>
<Version>1.0.0.13</Version> <Version>1.0.0.19</Version>
<Punchline>A more versatile in-game map.</Punchline> <Punchline>A more versatile in-game map.</Punchline>
<Description>Replaces the in-game map with an ImGui implementation with several additional features. Fork with minimap improvements, quest radius on minimap, and more.</Description> <Description>Replaces the in-game map with an ImGui implementation with several additional features. Fork with minimap improvements, quest radius on minimap, and more.</Description>
<RepoUrl>http://brassnet.ddns.net:33983/KnackAtNite/HSMappy</RepoUrl> <RepoUrl>http://brassnet.ddns.net:33983/KnackAtNite/HSMappy</RepoUrl>
+4
View File
@@ -28,10 +28,14 @@ public sealed class MappyPlugin : IDalamudPlugin
BaseSkewStrength = 16f, BaseSkewStrength = 16f,
}); });
System.MinimapAxis12FontHandle = Service.PluginInterface.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Axis12));
System.MinimapAxis18FontHandle = Service.PluginInterface.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Axis18));
System.SystemConfig = SystemConfig.Load(); System.SystemConfig = SystemConfig.Load();
System.IconConfig = IconConfig.Load(); System.IconConfig = IconConfig.Load();
System.FlagConfig = FlagConfig.Load(); System.FlagConfig = FlagConfig.Load();
System.MapNoteConfig = MapNoteConfig.Load(); System.MapNoteConfig = MapNoteConfig.Load();
System.MovementTrailConfig = new MovementTrailConfig();
System.Teleporter = new Teleporter(Service.PluginInterface); System.Teleporter = new Teleporter(Service.PluginInterface);
+3
View File
@@ -18,6 +18,7 @@ public static class System
public static IconConfig IconConfig { get; set; } public static IconConfig IconConfig { get; set; }
public static FlagConfig FlagConfig { get; set; } public static FlagConfig FlagConfig { get; set; }
public static MapNoteConfig MapNoteConfig { get; set; } public static MapNoteConfig MapNoteConfig { get; set; }
public static MovementTrailConfig MovementTrailConfig { get; set; }
public static WindowManager WindowManager { get; set; } public static WindowManager WindowManager { get; set; }
public static MapWindow MapWindow { get; set; } public static MapWindow MapWindow { get; set; }
public static MinimapWindow MinimapWindow { get; set; } public static MinimapWindow MinimapWindow { get; set; }
@@ -44,4 +45,6 @@ public static class System
public static AetheryteAethernetCache AetheryteAethernetCache { get; set; } = new(); public static AetheryteAethernetCache AetheryteAethernetCache { get; set; } = new();
public static IFontHandle LargeAxisFontHandle { get; set; } public static IFontHandle LargeAxisFontHandle { get; set; }
public static IFontHandle MinimapAxis12FontHandle { get; set; }
public static IFontHandle MinimapAxis18FontHandle { get; set; }
} }
+117 -1
View File
@@ -80,6 +80,18 @@ public class MapFunctionsTab : ITabItem
configChanged |= ImGui.Checkbox("Center on Quest", ref System.SystemConfig.CenterOnQuest); configChanged |= ImGui.Checkbox("Center on Quest", ref System.SystemConfig.CenterOnQuest);
} }
ImGuiTweaks.Header("Movement Trail (Carbonite-style)");
using (ImRaii.PushIndent()) {
configChanged |= ImGui.Checkbox("Show Movement Trail", ref System.SystemConfig.ShowMovementTrail);
ImGui.TextDisabled("Draw a trail of red dots showing where you've been on the map.");
if (System.SystemConfig.ShowMovementTrail) {
configChanged |= ImGui.SliderFloat("Min Distance (world units)", ref System.SystemConfig.MovementTrailMinDistance, 0.5f, 10f);
configChanged |= ImGui.SliderFloat("Fade Time (seconds)", ref System.SystemConfig.MovementTrailFadeTimeSeconds, 10f, 300f);
configChanged |= ImGui.SliderInt("Max Points", ref System.SystemConfig.MovementTrailMaxPoints, 20, 500);
}
ImGuiHelpers.ScaledDummy(5.0f);
}
ImGuiTweaks.Header("Misc Options"); ImGuiTweaks.Header("Misc Options");
using (ImRaii.PushIndent()) { using (ImRaii.PushIndent()) {
configChanged |= ImGui.Checkbox("Show Misc Tooltips", ref System.SystemConfig.ShowMiscTooltips); configChanged |= ImGui.Checkbox("Show Misc Tooltips", ref System.SystemConfig.ShowMiscTooltips);
@@ -266,7 +278,7 @@ public class MinimapOptionsTab : ITabItem
ImGuiHelpers.ScaledDummy(5.0f); ImGuiHelpers.ScaledDummy(5.0f);
configChanged |= ImGui.DragFloat("Size", ref System.SystemConfig.MinimapSize, 5.0f, 80.0f, 400.0f, "%.0f"); configChanged |= ImGui.DragFloat("Size", ref System.SystemConfig.MinimapSize, 5.0f, 200.0f, 400.0f, "%.0f");
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip("Minimap size. Resize only via this setting (no corner grip)."); ImGui.SetTooltip("Minimap size. Resize only via this setting (no corner grip).");
configChanged |= ImGui.DragFloat2("Position", ref System.SystemConfig.MinimapPosition); configChanged |= ImGui.DragFloat2("Position", ref System.SystemConfig.MinimapPosition);
@@ -300,12 +312,116 @@ public class MinimapOptionsTab : ITabItem
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) 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."); 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("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.");
configChanged |= ImGui.Checkbox("Show Top Info Bar", ref System.SystemConfig.MinimapShowMapInfoBar);
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip("Show current map info (region, map, area, sub-area) at the top of the minimap. Respects the main map label settings (Show Region/Map/Area/Sub-Area Text).");
if (System.SystemConfig.MinimapShowMapInfoBar) {
using (ImRaii.PushIndent()) {
configChanged |= DrawMapInfoOrderControls();
configChanged |= ImGuiTweaks.ColorEditWithDefault("Top Info Bar Text Color", ref System.SystemConfig.MinimapMapInfoColor, KnownColor.White.Vector());
configChanged |= ImGui.DragFloat("Top Info Bar Font Scale", ref System.SystemConfig.MinimapMapInfoFontScale, 0.05f, 0.5f, 2.0f, "%.2f");
configChanged |= ImGuiTweaks.ColorEditWithDefault("Top Info Bar Background", ref System.SystemConfig.MinimapMapInfoBarBackground, new Vector4(0f, 0f, 0f, 0.2f));
var mapInfoFont = System.SystemConfig.MinimapMapInfoFontType;
if (ImGui.Combo("Top Info Bar Font", ref mapInfoFont, "Default\0Game Font (Axis 12pt)\0Game Font (Axis 18pt)\0")) {
System.SystemConfig.MinimapMapInfoFontType = mapInfoFont;
configChanged = true;
}
}
}
configChanged |= ImGui.Checkbox("Show Bottom Info Bar", ref System.SystemConfig.MinimapShowCoordinateBar);
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip("Show player coordinates, repair % (most damaged item condition), and local time at the bottom of the minimap.");
if (System.SystemConfig.MinimapShowCoordinateBar) {
using (ImRaii.PushIndent()) {
configChanged |= DrawBottomBarOrderControls();
configChanged |= ImGui.Checkbox("Show Coordinates", ref System.SystemConfig.MinimapCoordBarShowCoordinates);
configChanged |= ImGui.Checkbox("Show Local Time", ref System.SystemConfig.MinimapCoordBarShowTime);
configChanged |= ImGui.Checkbox("Show Repair %", ref System.SystemConfig.MinimapCoordBarShowRepairPercent);
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip("Show condition % of your most damaged equipped item.");
configChanged |= ImGuiTweaks.ColorEditWithDefault("Bottom Info Bar Text Color", ref System.SystemConfig.MinimapCoordBarColor, KnownColor.White.Vector());
configChanged |= ImGuiTweaks.ColorEditWithDefault("Bottom Info Bar Background", ref System.SystemConfig.MinimapCoordBarBackground, new Vector4(0f, 0f, 0f, 0.2f));
configChanged |= ImGui.DragFloat("Bottom Info Bar Font Scale", ref System.SystemConfig.MinimapCoordBarFontScale, 0.05f, 0.5f, 2.0f, "%.2f");
var coordFont = System.SystemConfig.MinimapCoordBarFontType;
if (ImGui.Combo("Bottom Info Bar Font", ref coordFont, "Default\0Game Font (Axis 12pt)\0Game Font (Axis 18pt)\0")) {
System.SystemConfig.MinimapCoordBarFontType = coordFont;
configChanged = true;
}
}
}
} }
if (configChanged) { if (configChanged) {
SystemConfig.Save(); SystemConfig.Save();
} }
} }
private static bool DrawMapInfoOrderControls()
{
var configChanged = false;
var order = System.SystemConfig.MinimapMapInfoOrder;
if (order.Length != 4) {
System.SystemConfig.MinimapMapInfoOrder = [0, 1, 2, 3];
order = System.SystemConfig.MinimapMapInfoOrder;
}
var labels = new[] { "Region", "Map", "Area", "SubArea" };
if (ImGui.TreeNode("Top Info Bar Order (line 1: first two, line 2: next two)")) {
for (var i = 0; i < 4; i++) {
var idx = Math.Clamp(order[i], 0, 3);
ImGui.Text($"{(i + 1)}. {labels[idx]}");
ImGui.SameLine(120f * ImGuiHelpers.GlobalScale);
if (ImGui.Button($"↑##up{i}") && i > 0) {
(order[i], order[i - 1]) = (order[i - 1], order[i]);
configChanged = true;
}
ImGui.SameLine();
if (ImGui.Button($"↓##down{i}") && i < 3) {
(order[i], order[i + 1]) = (order[i + 1], order[i]);
configChanged = true;
}
}
ImGui.TreePop();
}
return configChanged;
}
private static bool DrawBottomBarOrderControls()
{
var configChanged = false;
var order = System.SystemConfig.MinimapBottomBarOrder;
if (order.Length != 3) {
System.SystemConfig.MinimapBottomBarOrder = [0, 1, 2];
order = System.SystemConfig.MinimapBottomBarOrder;
}
var labels = new[] { "Coordinates", "Repair %", "Local Time" };
if (ImGui.TreeNode("Bottom Info Bar Order")) {
for (var i = 0; i < 3; i++) {
var idx = Math.Clamp(order[i], 0, 2);
ImGui.Text($"{(i + 1)}. {labels[idx]}");
ImGui.SameLine(140f * ImGuiHelpers.GlobalScale);
if (ImGui.Button($"↑##bottomup{i}") && i > 0) {
(order[i], order[i - 1]) = (order[i - 1], order[i]);
configChanged = true;
}
ImGui.SameLine();
if (ImGui.Button($"↓##bottomdown{i}") && i < 2) {
(order[i], order[i + 1]) = (order[i + 1], order[i]);
configChanged = true;
}
}
ImGui.TreePop();
}
return configChanged;
}
} }
public class PlayerOptionsTab : ITabItem public class PlayerOptionsTab : ITabItem
+310 -13
View File
@@ -1,11 +1,18 @@
using System; using System;
using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Utility;
using KamiLib.Window; using KamiLib.Window;
using Lumina.Excel.Sheets;
using Mappy.Controllers; using Mappy.Controllers;
using Mappy.Data; using Mappy.Data;
using Mappy.Helpers;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Map = Lumina.Excel.Sheets.Map;
namespace Mappy.Windows; namespace Mappy.Windows;
@@ -40,6 +47,19 @@ public class MinimapWindow : Window
protected override unsafe void DrawContents() protected override unsafe void DrawContents()
{ {
if (System.SystemConfig.MinimapDrawUnderOtherUI)
{
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(); var agent = AgentMap.Instance();
// Try loading from Lumina first so minimap can show without ever opening the area map // Try loading from Lumina first so minimap can show without ever opening the area map
if (!System.MapRenderer.HasMinimapCacheFor(agent->CurrentMapId) && agent->SelectedMapId != agent->CurrentMapId) if (!System.MapRenderer.HasMinimapCacheFor(agent->CurrentMapId) && agent->SelectedMapId != agent->CurrentMapId)
@@ -63,46 +83,312 @@ public class MinimapWindow : Window
UpdateStyle(); UpdateStyle();
UpdateSizePosition(); UpdateSizePosition();
// Compensate for window padding: draw the minimap child so it fills the full window (no black bands). // Compensate for window padding (minimap gets zero padding from plugin)
var padding = ImGui.GetStyle().WindowPadding; var padding = ImGui.GetStyle().WindowPadding;
var winSize = ImGui.GetWindowSize();
ImGui.SetCursorPos(new Vector2(-padding.X, -padding.Y)); ImGui.SetCursorPos(new Vector2(-padding.X, -padding.Y));
var contentSize = winSize;
// Use actual window size so content scales when resized
var contentSize = ImGui.GetContentRegionAvail();
if (contentSize.X <= 0 || contentSize.Y <= 0) return; if (contentSize.X <= 0 || contentSize.Y <= 0) return;
var totalWidth = contentSize.X;
var totalHeight = contentSize.Y;
// Compute bar heights: top bar fits server info on one line; bottom bar = coord text + 2px
float topBarHeight = 0f;
float bottomBarHeight = 0f;
if (System.SystemConfig.MinimapShowMapInfoBar)
topBarHeight = ComputeMapInfoBarHeight(totalWidth);
if (System.SystemConfig.MinimapShowCoordinateBar)
bottomBarHeight = ComputeCoordinateBarHeight(totalWidth, totalHeight, topBarHeight);
var minimapSide = Math.Min(totalWidth, totalHeight - topBarHeight - bottomBarHeight);
minimapSide = Math.Max(1f, minimapSide);
var scale = minimapSide / 200f;
// Top bar (outside edge, above minimap)
if (System.SystemConfig.MinimapShowMapInfoBar && topBarHeight > 0) {
DrawMapInfoBar(totalWidth, topBarHeight, scale);
}
// Minimap (square, sized to fit available space)
var minimapSize = new Vector2(minimapSide, minimapSide);
using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, System.SystemConfig.MinimapOpacity)) using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, System.SystemConfig.MinimapOpacity))
using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, 0f)) using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, 0f))
using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, Vector2.Zero)) using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, Vector2.Zero))
using (var child = ImRaii.Child("minimap_render", contentSize, false, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)) using (var child = ImRaii.Child("minimap_render", minimapSize, false, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse))
{ {
if (child) { if (child) {
// Use window size so map fills the full window; renderer clamps draw position so map always covers the view. System.MapRenderer.DrawMinimapContents(minimapSize);
System.MapRenderer.DrawMinimapContents(contentSize);
// Mouse wheel over minimap: zoom in/out, and consume wheel so the window doesn't scroll
if (ImGui.IsItemHovered()) { if (ImGui.IsItemHovered()) {
var io = ImGui.GetIO(); var io = ImGui.GetIO();
var wheel = io.MouseWheel; var wheel = io.MouseWheel;
if (wheel != 0) { if (wheel != 0) {
var zoom = System.SystemConfig.MinimapZoom; var zoom = System.SystemConfig.MinimapZoom;
zoom -= wheel * 0.012f; // Small step so zoom is incremental between max out (0.1) and max in (0.03) zoom -= wheel * 0.012f;
System.SystemConfig.MinimapZoom = Math.Clamp(zoom, 0.03f, 0.112f); System.SystemConfig.MinimapZoom = Math.Clamp(zoom, 0.03f, 0.112f);
SystemConfig.Save(); SystemConfig.Save();
} }
// Consume wheel so the window doesn't scroll when at min/max zoom or when we handled it
io.MouseWheel = 0f; io.MouseWheel = 0f;
io.MouseWheelH = 0f; io.MouseWheelH = 0f;
} }
} }
} }
// Bottom bar (outside edge, below minimap)
if (System.SystemConfig.MinimapShowCoordinateBar && bottomBarHeight > 0) {
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). // Restore default padding for the next window is done in plugin Draw callback (PopStyleVar after all windows).
} }
public override void OnOpen() public override void OnOpen()
{ {
ImGui.SetWindowPos(System.SystemConfig.MinimapPosition); ImGui.SetWindowPos(System.SystemConfig.MinimapPosition);
ImGui.SetWindowSize(new Vector2(System.SystemConfig.MinimapSize, System.SystemConfig.MinimapSize)); var size = GetMinimapWindowSize();
ImGui.SetWindowSize(size);
}
private unsafe float ComputeMapInfoBarHeight(float width)
{
var (line1, line2) = GetMapInfoLines();
if (string.IsNullOrEmpty(line1) && string.IsNullOrEmpty(line2)) return 0f;
var fontScale = ComputeMapInfoFontScale(width, line1, line2);
ImGui.SetWindowFontScale(fontScale);
var h1 = string.IsNullOrEmpty(line1) ? 0f : ImGui.CalcTextSize(line1).Y;
var h2 = string.IsNullOrEmpty(line2) ? 0f : ImGui.CalcTextSize(line2).Y;
var lineSpacing = line1.Length > 0 && line2.Length > 0 ? 2f : 0f;
ImGui.SetWindowFontScale(1f);
return h1 + h2 + lineSpacing + 6f * ImGuiHelpers.GlobalScale;
}
private static float ComputeMapInfoFontScale(float width, string line1, string line2)
{
var horzPad = 8f * ImGuiHelpers.GlobalScale;
var maxWidth = Math.Max(1f, width - horzPad);
ImGui.SetWindowFontScale(1f);
var w1 = string.IsNullOrEmpty(line1) ? 0f : ImGui.CalcTextSize(line1).X;
var w2 = string.IsNullOrEmpty(line2) ? 0f : ImGui.CalcTextSize(line2).X;
var maxLineWidth = Math.Max(w1, w2);
if (maxLineWidth <= 0) return 1f;
var autoScale = Math.Clamp(maxWidth / maxLineWidth, 0.5f, 1f);
var mult = Math.Clamp(System.SystemConfig.MinimapMapInfoFontScale, 0.5f, 2f);
return Math.Clamp(autoScale * mult, 0.3f, 1.5f);
}
private unsafe (string line1, string line2) GetMapInfoLines()
{
var agent = AgentMap.Instance();
var currentMapId = agent->CurrentMapId;
if (currentMapId == 0) return (string.Empty, string.Empty);
var mapData = Service.DataManager.GetExcelSheet<Map>().GetRow(currentMapId);
if (mapData.RowId == 0) return (string.Empty, string.Empty);
var rawParts = new string?[4];
if (System.SystemConfig.ShowRegionLabel)
rawParts[0] = mapData.PlaceNameRegion.Value.Name.ExtractText();
if (System.SystemConfig.ShowMapLabel)
rawParts[1] = mapData.PlaceName.Value.Name.ExtractText();
if (agent->CurrentMapId == currentMapId) {
if (TerritoryInfo.Instance()->AreaPlaceNameId is not 0 && System.SystemConfig.ShowAreaLabel) {
var areaLabel = Service.DataManager.GetExcelSheet<PlaceName>().GetRow(TerritoryInfo.Instance()->AreaPlaceNameId);
rawParts[2] = areaLabel.Name.ExtractText();
}
if (TerritoryInfo.Instance()->SubAreaPlaceNameId is not 0 && System.SystemConfig.ShowSubAreaLabel) {
var subAreaLabel = Service.DataManager.GetExcelSheet<PlaceName>().GetRow(TerritoryInfo.Instance()->SubAreaPlaceNameId);
rawParts[3] = subAreaLabel.Name.ExtractText();
}
}
var order = System.SystemConfig.MinimapMapInfoOrder;
if (order.Length != 4) order = [0, 1, 2, 3];
var ordered = new List<string>();
foreach (var idx in order) {
var i = Math.Clamp(idx, 0, 3);
if (rawParts[i] is { } s)
ordered.Add(s);
}
var line1 = ordered.Count >= 2 ? string.Join(" - ", ordered[0], ordered[1]) : (ordered.Count == 1 ? ordered[0] : string.Empty);
var line2 = ordered.Count >= 4 ? string.Join(" - ", ordered[2], ordered[3]) : (ordered.Count == 3 ? ordered[2] : string.Empty);
return (line1, line2);
}
private static IDisposable? PushMinimapFont(int fontType)
{
return fontType switch {
1 when System.MinimapAxis12FontHandle != null => System.MinimapAxis12FontHandle.Push(),
2 when System.MinimapAxis18FontHandle != null => System.MinimapAxis18FontHandle.Push(),
_ => null
};
}
private unsafe void DrawMapInfoBar(float width, float height, float _)
{
using var childBg = ImRaii.PushColor(ImGuiCol.ChildBg, System.SystemConfig.MinimapMapInfoBarBackground);
using var child = ImRaii.Child("minimap_mapinfo_bar", new Vector2(width, height), false, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse);
if (!child) return;
var (line1, line2) = GetMapInfoLines();
if (string.IsNullOrEmpty(line1) && string.IsNullOrEmpty(line2)) return;
using var _font = PushMinimapFont(System.SystemConfig.MinimapMapInfoFontType);
var fontScale = ComputeMapInfoFontScale(width, line1, line2);
ImGui.SetWindowFontScale(fontScale);
try {
var color = System.SystemConfig.MinimapMapInfoColor;
var totalHeight = 0f;
if (line1.Length > 0) totalHeight += ImGui.CalcTextSize(line1).Y;
if (line2.Length > 0) totalHeight += (line1.Length > 0 ? 2f : 0f) + ImGui.CalcTextSize(line2).Y;
var y = (height - totalHeight) * 0.5f;
if (line1.Length > 0) {
var sz = ImGui.CalcTextSize(line1);
ImGui.SetCursorPos(new Vector2((width - sz.X) * 0.5f, y));
ImGui.TextColored(color, line1);
y += sz.Y + 2f;
}
if (line2.Length > 0) {
var sz = ImGui.CalcTextSize(line2);
ImGui.SetCursorPos(new Vector2((width - sz.X) * 0.5f, y));
ImGui.TextColored(color, line2);
}
} finally {
ImGui.SetWindowFontScale(1f);
}
}
private float ComputeCoordinateBarHeight(float totalWidth, float totalHeight, float topBarHeight)
{
var showCoords = System.SystemConfig.MinimapCoordBarShowCoordinates;
var showTime = System.SystemConfig.MinimapCoordBarShowTime;
var showRepair = System.SystemConfig.MinimapCoordBarShowRepairPercent;
if (!showCoords && !showTime && !showRepair) return 0f;
var minimapSide = Math.Min(totalWidth, totalHeight - topBarHeight - 20f);
minimapSide = Math.Max(1f, minimapSide);
var baseScale = Math.Clamp(minimapSide / 200f, 0.15f, 1f);
var mult = Math.Clamp(System.SystemConfig.MinimapCoordBarFontScale, 0.5f, 2f);
var fontScale = Math.Clamp(baseScale * mult, 0.15f, 1.5f);
ImGui.SetWindowFontScale(fontScale);
var h = 0f;
if (showCoords) h = Math.Max(h, ImGui.CalcTextSize(" 99.9 99.9 ").Y);
if (showTime) h = Math.Max(h, ImGui.CalcTextSize(DateTime.Now.ToString("h:mm tt")).Y);
if (showRepair) h = Math.Max(h, ImGui.CalcTextSize("100%").Y);
ImGui.SetWindowFontScale(1f);
return h + 8f * ImGuiHelpers.GlobalScale;
}
private void DrawCoordinateBar(float width, float height, float scale)
{
var showCoords = System.SystemConfig.MinimapCoordBarShowCoordinates;
var showTime = System.SystemConfig.MinimapCoordBarShowTime;
var showRepair = System.SystemConfig.MinimapCoordBarShowRepairPercent;
if (!showCoords && !showTime && !showRepair) return;
using var childBg = ImRaii.PushColor(ImGuiCol.ChildBg, System.SystemConfig.MinimapCoordBarBackground);
using var child = ImRaii.Child("minimap_coord_bar", new Vector2(width, height), false, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse);
if (!child) return;
var mult = Math.Clamp(System.SystemConfig.MinimapCoordBarFontScale, 0.5f, 2f);
var fontScale = Math.Clamp(scale * mult, 0.15f, 1.5f);
ImGui.SetWindowFontScale(fontScale);
using var _font = PushMinimapFont(System.SystemConfig.MinimapCoordBarFontType);
try {
var color = System.SystemConfig.MinimapCoordBarColor;
var horzPad = 6f * ImGuiHelpers.GlobalScale;
var lineHeight = ImGui.GetTextLineHeight();
var yPos = Math.Max(0f, (height - lineHeight) * 0.5f - 2f);
var gap = 8f * ImGuiHelpers.GlobalScale;
var order = System.SystemConfig.MinimapBottomBarOrder;
if (order.Length != 3) order = [0, 1, 2];
var texts = new Dictionary<int, string>();
if (showCoords && System.MapRenderer.GetCurrentMinimapTransform() is { } transform) {
var pos = Service.ObjectTable.LocalPlayer?.Position ?? Vector3.Zero;
var mapCoord = MapUtil.WorldToMap(new Vector2(pos.X, pos.Z), transform.Item1, transform.Item2, transform.Item3);
texts[0] = $"{mapCoord.X:F1} {mapCoord.Y:F1}";
}
if (showRepair) texts[1] = $"{EquipmentConditionHelper.GetLowestConditionPercent():F0}%";
if (showTime) texts[2] = DateTime.Now.ToString("h:mm tt");
var visibleOrder = new List<string>();
foreach (var idx in order) {
var i = Math.Clamp(idx, 0, 2);
if (texts.TryGetValue(i, out var t)) visibleOrder.Add(t);
}
var cursorX = horzPad;
for (var j = 0; j < visibleOrder.Count; j++) {
var text = visibleOrder[j];
var sz = ImGui.CalcTextSize(text);
var isLast = j == visibleOrder.Count - 1;
var x = isLast ? width - horzPad - sz.X : cursorX;
ImGui.SetCursorPos(new Vector2(x, yPos));
ImGui.TextColored(color, text);
if (!isLast) cursorX += sz.X + gap;
}
} finally {
ImGui.SetWindowFontScale(1f);
}
}
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() private void UpdateStyle()
@@ -113,16 +399,27 @@ public class MinimapWindow : Window
Flags &= ~ImGuiWindowFlags.NoMove; Flags &= ~ImGuiWindowFlags.NoMove;
} }
private unsafe Vector2 GetMinimapWindowSize()
{
var minimapSizePx = System.SystemConfig.MinimapSize;
var topBarHeight = System.SystemConfig.MinimapShowMapInfoBar ? ComputeMapInfoBarHeight(minimapSizePx) : 0f;
var bottomBarHeight = System.SystemConfig.MinimapShowCoordinateBar
? ComputeCoordinateBarHeight(minimapSizePx, minimapSizePx + topBarHeight + 24f, topBarHeight)
: 0f;
var totalHeight = minimapSizePx + topBarHeight + bottomBarHeight;
return new Vector2(minimapSizePx, totalHeight);
}
private void UpdateSizePosition() private void UpdateSizePosition()
{ {
var config = System.SystemConfig; var config = System.SystemConfig;
var windowPosition = ImGui.GetWindowPos(); var windowPosition = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize(); var windowSize = ImGui.GetWindowSize();
var configSize = config.MinimapSize; var expectedSize = GetMinimapWindowSize();
// Size is config-only (set in Mappy settings); always apply config size to window. // Size is config-only (set in Mappy settings); always apply config size to window.
if (Math.Abs(windowSize.X - configSize) > 0.1f || Math.Abs(windowSize.Y - configSize) > 0.1f) if (Math.Abs(windowSize.X - expectedSize.X) > 0.1f || Math.Abs(windowSize.Y - expectedSize.Y) > 0.1f)
ImGui.SetWindowSize(new Vector2(configSize, configSize)); ImGui.SetWindowSize(expectedSize);
if (!ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows)) { if (!ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows)) {
// Not focused: apply config position to window // Not focused: apply config position to window
+1 -1
View File
@@ -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.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.13","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.13/latest.zip","IsHide":false,"IsTestingExclusive":false,"DownloadLinkTesting":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.13/latest.zip","DownloadLinkUpdate":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.13/latest.zip","LastUpdate":"1760400000"}] [{"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.19: Minimap quest area circles refresh when multi-step objective progresses (1/3 -> 2/3, etc.). 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.19","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.19/latest.zip","IsHide":false,"IsTestingExclusive":false,"DownloadLinkTesting":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.19/latest.zip","DownloadLinkUpdate":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.19/latest.zip","LastUpdate":"1772407702"}]