Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 448d3ca933 | |||
| 611e61967b | |||
| 8763cf4c70 | |||
| 851b450a17 | |||
| 340030f826 | |||
| e160af2ab5 | |||
| b4638eb60b | |||
| 542da3a71b | |||
| b1c833ebcf | |||
| 3afa7af645 | |||
| 1cccf8967a | |||
| f1864f4cac | |||
| d8457e8d87 | |||
| daaac71c83 | |||
| d10a550136 | |||
| c9b50f8f72 | |||
| 015d7ee191 | |||
| 2c54907cd5 | |||
| 31ab36d645 |
@@ -38,6 +38,9 @@ public static class DrawHelpers
|
|||||||
|
|
||||||
public const uint QuestionMarkIcon = 60071;
|
public const uint QuestionMarkIcon = 60071;
|
||||||
|
|
||||||
|
/// <summary>Sentinel IconId for user-placed map notes (custom-drawn white page with writing).</summary>
|
||||||
|
public const uint MapNoteIconId = 65000;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Offset Vector of SelectedX, SelectedY, scaled with SelectedSizeFactor
|
/// Offset Vector of SelectedX, SelectedY, scaled with SelectedSizeFactor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -143,9 +146,7 @@ public static class DrawHelpers
|
|||||||
|
|
||||||
private static void DrawIcon(MarkerInfo markerInfo)
|
private static void DrawIcon(MarkerInfo markerInfo)
|
||||||
{
|
{
|
||||||
var texture = Service.TextureProvider.GetFromGameIcon(markerInfo.IconId).GetWrapOrEmpty();
|
|
||||||
var scale = System.SystemConfig.ScaleWithZoom ? markerInfo.Scale : 1.0f;
|
var scale = System.SystemConfig.ScaleWithZoom ? markerInfo.Scale : 1.0f;
|
||||||
|
|
||||||
var iconScale = System.SystemConfig.IconScale;
|
var iconScale = System.SystemConfig.IconScale;
|
||||||
|
|
||||||
if (markerInfo.IconId is 60401 or 60402) {
|
if (markerInfo.IconId is 60401 or 60402) {
|
||||||
@@ -158,11 +159,27 @@ public static class DrawHelpers
|
|||||||
iconScale = 0.42f;
|
iconScale = 0.42f;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SetCursorPos(markerInfo.Position + markerInfo.Offset - texture.Size * iconScale / 2.0f * scale * System.IconConfig.IconSettingMap[markerInfo.IconId].Scale);
|
var setting = System.IconConfig.IconSettingMap[markerInfo.IconId];
|
||||||
var cursorScreenPos = ImGui.GetCursorScreenPos();
|
var sizeMultiplier = scale * iconScale * setting.Scale;
|
||||||
var iconSize = texture.Size * scale * iconScale * System.IconConfig.IconSettingMap[markerInfo.IconId].Scale;
|
|
||||||
|
|
||||||
ImGui.Image(texture.Handle, iconSize, Vector2.Zero, Vector2.One, System.IconConfig.IconSettingMap[markerInfo.IconId].Color);
|
Vector2 iconSize;
|
||||||
|
Vector2 cursorScreenPos;
|
||||||
|
|
||||||
|
if (markerInfo.IconId == MapNoteIconId) {
|
||||||
|
// Custom-drawn white page with writing icon (roughly 24x30 aspect)
|
||||||
|
iconSize = new Vector2(20f, 26f) * sizeMultiplier;
|
||||||
|
ImGui.SetCursorPos(markerInfo.Position + markerInfo.Offset - iconSize / 2f);
|
||||||
|
cursorScreenPos = ImGui.GetCursorScreenPos();
|
||||||
|
ImGui.InvisibleButton($"mapnote_{markerInfo.Position.X}_{markerInfo.Position.Y}", iconSize);
|
||||||
|
DrawMapNoteIcon(cursorScreenPos, iconSize, setting.Color);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var texture = Service.TextureProvider.GetFromGameIcon(markerInfo.IconId).GetWrapOrEmpty();
|
||||||
|
iconSize = texture.Size * sizeMultiplier;
|
||||||
|
ImGui.SetCursorPos(markerInfo.Position + markerInfo.Offset - iconSize / 2f);
|
||||||
|
cursorScreenPos = ImGui.GetCursorScreenPos();
|
||||||
|
ImGui.Image(texture.Handle, iconSize, Vector2.Zero, Vector2.One, setting.Color);
|
||||||
|
}
|
||||||
|
|
||||||
if (DebugMode) {
|
if (DebugMode) {
|
||||||
foreach (var x in Enumerable.Range(-1, 3)) {
|
foreach (var x in Enumerable.Range(-1, 3)) {
|
||||||
@@ -241,7 +258,14 @@ public static class DrawHelpers
|
|||||||
if (markerInfo.PrimaryText?.Invoke() is { Length: > 0 } primaryText) {
|
if (markerInfo.PrimaryText?.Invoke() is { Length: > 0 } primaryText) {
|
||||||
using var tooltip = ImRaii.Tooltip();
|
using var tooltip = ImRaii.Tooltip();
|
||||||
|
|
||||||
|
if (markerInfo.IconId == MapNoteIconId) {
|
||||||
|
var iconSize = ImGuiHelpers.ScaledVector2(24f, 31f);
|
||||||
|
DrawMapNoteIcon(ImGui.GetCursorScreenPos(), iconSize, Vector4.One);
|
||||||
|
ImGui.Dummy(iconSize);
|
||||||
|
}
|
||||||
|
else {
|
||||||
ImGui.Image(Service.TextureProvider.GetFromGameIcon(markerInfo.IconId).GetWrapOrEmpty().Handle, ImGuiHelpers.ScaledVector2(32.0f, 32.0f));
|
ImGui.Image(Service.TextureProvider.GetFromGameIcon(markerInfo.IconId).GetWrapOrEmpty().Handle, ImGuiHelpers.ScaledVector2(32.0f, 32.0f));
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 7.5f * ImGuiHelpers.GlobalScale);
|
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 7.5f * ImGuiHelpers.GlobalScale);
|
||||||
@@ -257,6 +281,34 @@ public static class DrawHelpers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Draw a custom "white page with writing" icon for map notes, centered at the given position.</summary>
|
||||||
|
public static void DrawMapNoteIconCentered(Vector2 center, Vector2 size, Vector4 tint) =>
|
||||||
|
DrawMapNoteIcon(center - size * 0.5f, size, tint);
|
||||||
|
|
||||||
|
/// <summary>Draw a custom "white page with writing" icon for map notes.</summary>
|
||||||
|
private static void DrawMapNoteIcon(Vector2 topLeft, Vector2 size, Vector4 tint)
|
||||||
|
{
|
||||||
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
var p1 = topLeft;
|
||||||
|
var p2 = topLeft + size;
|
||||||
|
var rounding = MathF.Min(size.X * 0.15f, size.Y * 0.12f);
|
||||||
|
var white = ImGui.GetColorU32(new Vector4(0.98f, 0.98f, 0.96f, tint.W));
|
||||||
|
var border = ImGui.GetColorU32(new Vector4(0.7f, 0.7f, 0.65f, tint.W));
|
||||||
|
var writing = ImGui.GetColorU32(new Vector4(0.25f, 0.22f, 0.2f, tint.W));
|
||||||
|
var padding = size * 0.12f;
|
||||||
|
var lineHeight = (size.Y - padding.Y * 2f) / 4f;
|
||||||
|
var lineLeft = p1.X + padding.X;
|
||||||
|
var lineRight = p2.X - padding.X;
|
||||||
|
|
||||||
|
drawList.AddRectFilled(p1, p2, white, rounding);
|
||||||
|
drawList.AddRect(p1, p2, border, rounding, 0, 1.2f);
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
var y = p1.Y + padding.Y + lineHeight * (i + 0.5f);
|
||||||
|
var w = (i == 1 ? 0.9f : 0.7f) * (lineRight - lineLeft);
|
||||||
|
drawList.AddLine(new Vector2(lineLeft, y), new Vector2(lineLeft + w, y), writing, 1.5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static bool IsDisallowedIcon(uint iconId) =>
|
public static bool IsDisallowedIcon(uint iconId) =>
|
||||||
iconId switch
|
iconId switch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
@@ -17,14 +17,30 @@ public unsafe class MapContextMenu
|
|||||||
|
|
||||||
if (!contextMenu) return;
|
if (!contextMenu) return;
|
||||||
|
|
||||||
if (ImGui.MenuItem("Place Flag")) {
|
var cursorPosition = ImGui.GetMousePosOnOpeningCurrentPopup();
|
||||||
var cursorPosition = ImGui.GetMousePosOnOpeningCurrentPopup(); // Get initial cursor position (screen relative)
|
var textureClickLocation = (cursorPosition - mapDrawOffset - System.MapRenderer.DrawPosition) / MapRenderer.MapRenderer.Scale;
|
||||||
var mapChildOffset = mapDrawOffset; // Get the screen position we started drawing the map at
|
var result = textureClickLocation - new Vector2(1024.0f, 1024.0f);
|
||||||
var mapDrawPositionOffset = System.MapRenderer.DrawPosition; // Get the map texture top left offset vector
|
var scaledResult = result / DrawHelpers.GetMapScaleFactor() + DrawHelpers.GetRawMapOffsetVector();
|
||||||
var textureClickLocation = (cursorPosition - mapChildOffset - mapDrawPositionOffset) / MapRenderer.MapRenderer.Scale; // Math
|
var noteAtCursor = System.MapNoteConfig.FindNoteAt(
|
||||||
var result = textureClickLocation - new Vector2(1024.0f, 1024.0f); // One of our vectors made the map centered, undo it.
|
AgentMap.Instance()->SelectedTerritoryId, AgentMap.Instance()->SelectedMapId, scaledResult.X, scaledResult.Y);
|
||||||
var scaledResult = result / DrawHelpers.GetMapScaleFactor() + DrawHelpers.GetRawMapOffsetVector(); // Apply offset x/y and scalefactor
|
|
||||||
|
|
||||||
|
if (ImGui.MenuItem("Place Note")) {
|
||||||
|
MapWindow.PendingMapNotePosition = (AgentMap.Instance()->SelectedTerritoryId, AgentMap.Instance()->SelectedMapId, scaledResult.X, scaledResult.Y);
|
||||||
|
MapWindow.RequestOpenAddNotePopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.MenuItem("Remove Note", false, noteAtCursor is not null)) {
|
||||||
|
if (noteAtCursor is { } note) {
|
||||||
|
System.MapNoteConfig.Notes.Remove(note);
|
||||||
|
System.MapNoteConfig.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.MenuItem("Clear Movement Trail")) {
|
||||||
|
System.MovementTrailConfig.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
AgentChatLog.Instance()->InsertTextCommandParam(1048, false);
|
AgentChatLog.Instance()->InsertTextCommandParam(1048, false);
|
||||||
@@ -65,5 +81,9 @@ public unsafe class MapContextMenu
|
|||||||
if (ImGui.MenuItem("Open Flag List", false, System.WindowManager.GetWindow<FlagHistoryWindow>() is null)) {
|
if (ImGui.MenuItem("Open Flag List", false, System.WindowManager.GetWindow<FlagHistoryWindow>() is null)) {
|
||||||
System.WindowManager.AddWindow(new FlagHistoryWindow(), WindowFlags.OpenImmediately | WindowFlags.RequireLoggedIn);
|
System.WindowManager.AddWindow(new FlagHistoryWindow(), WindowFlags.OpenImmediately | WindowFlags.RequireLoggedIn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ImGui.MenuItem("Open Note List", false, System.WindowManager.GetWindow<MapNoteListWindow>() is null)) {
|
||||||
|
System.WindowManager.AddWindow(new MapNoteListWindow(), WindowFlags.OpenImmediately | WindowFlags.RequireLoggedIn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,6 @@ public unsafe class AddonAreaMapController : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the window actually considered closed by the agent.
|
|
||||||
if (AgentMap.Instance()->AddonId is 0)
|
if (AgentMap.Instance()->AddonId is 0)
|
||||||
{
|
{
|
||||||
System.WindowManager.GetWindow<MapWindow>()?.Close();
|
System.WindowManager.GetWindow<MapWindow>()?.Close();
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -35,6 +39,8 @@ public unsafe class IntegrationsController : IDisposable
|
|||||||
private int _silentRefreshHideFramesRemaining;
|
private int _silentRefreshHideFramesRemaining;
|
||||||
/// <summary>Frames to skip quest/temp-marker-triggered silent refresh after user opened map via Duty List (quest/gathering/flag/teleport).</summary>
|
/// <summary>Frames to skip quest/temp-marker-triggered silent refresh after user opened map via Duty List (quest/gathering/flag/teleport).</summary>
|
||||||
private int _suppressSilentRefreshFramesRemaining;
|
private int _suppressSilentRefreshFramesRemaining;
|
||||||
|
/// <summary>Frames after user opened map via Duty List; OnShowHook should not Hide() during this window (ProcessingCommand is cleared when MapWindow opens).</summary>
|
||||||
|
private int _userOpenedMapFramesRemaining;
|
||||||
|
|
||||||
/// <summary>True while we're doing a silent refresh; OnAreaMapPreShow should not open the MapWindow.</summary>
|
/// <summary>True while we're doing a silent refresh; OnAreaMapPreShow should not open the MapWindow.</summary>
|
||||||
public static bool SilentRefreshInProgress { get; private set; }
|
public static bool SilentRefreshInProgress { get; private set; }
|
||||||
@@ -107,9 +113,9 @@ public unsafe class IntegrationsController : IDisposable
|
|||||||
try { AgentMap.Instance()->Hide(); } catch { }
|
try { AgentMap.Instance()->Hide(); } catch { }
|
||||||
SilentRefreshInProgress = false;
|
SilentRefreshInProgress = false;
|
||||||
}
|
}
|
||||||
|
// Do NOT update quest/temp baselines here: if the user accepts a quest or advances a step during
|
||||||
|
// these frames, we would overwrite the baseline and never trigger a refresh for that change.
|
||||||
_wasBetweenAreas = Service.Condition.IsBetweenAreas();
|
_wasBetweenAreas = Service.Condition.IsBetweenAreas();
|
||||||
_lastQuestCount = GetActiveQuestCount();
|
|
||||||
try { _lastTempMarkerCount = (int)AgentMap.Instance()->TempMapMarkerCount; } catch { }
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,12 +148,16 @@ public unsafe class IntegrationsController : IDisposable
|
|||||||
if (_suppressSilentRefreshFramesRemaining > 0) {
|
if (_suppressSilentRefreshFramesRemaining > 0) {
|
||||||
_suppressSilentRefreshFramesRemaining--;
|
_suppressSilentRefreshFramesRemaining--;
|
||||||
}
|
}
|
||||||
|
if (_userOpenedMapFramesRemaining > 0) {
|
||||||
|
_userOpenedMapFramesRemaining--;
|
||||||
|
}
|
||||||
var skipQuestTempRefresh = _suppressSilentRefreshFramesRemaining > 0;
|
var skipQuestTempRefresh = _suppressSilentRefreshFramesRemaining > 0;
|
||||||
|
|
||||||
var questCount = GetActiveQuestCount();
|
var questCount = GetActiveQuestCount();
|
||||||
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
|
||||||
@@ -159,22 +169,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 {
|
||||||
|
// 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
|
||||||
|
// so quest turn-in and objective progression during suppression still trigger refresh when suppression ends.
|
||||||
|
_lastTempMarkerCount = tempCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Build a string of (QuestId, Sequence) for each active quest so we can detect step advances.</summary>
|
// 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, 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);
|
||||||
}
|
}
|
||||||
@@ -184,9 +220,36 @@ public unsafe class IntegrationsController : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Call when user opens map via Duty List (quest/gathering/flag/teleport). Suppresses quest/temp-marker-triggered silent refresh for ~1s so we don't close the map.</summary>
|
/// <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>
|
||||||
private void SuppressSilentRefreshForUserMapOpen()
|
private void SuppressSilentRefreshForUserMapOpen()
|
||||||
{
|
{
|
||||||
|
_silentRefreshHideFramesRemaining = 0; // Cancel in-progress silent refresh so we never Hide() the map the user just opened
|
||||||
|
SilentRefreshInProgress = false;
|
||||||
_suppressSilentRefreshFramesRemaining = 30; // ~1 second at typical framerate
|
_suppressSilentRefreshFramesRemaining = 30; // ~1 second at typical framerate
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +267,9 @@ public unsafe class IntegrationsController : IDisposable
|
|||||||
var currentMapId = agent->CurrentMapId;
|
var currentMapId = agent->CurrentMapId;
|
||||||
if (currentMapId == 0) return;
|
if (currentMapId == 0) return;
|
||||||
|
|
||||||
|
// Clear temp marker cache so old markers (e.g. from turned-in quest) don't persist
|
||||||
|
MapRenderer.MapRenderer.InvalidateTempMarkerCache(currentMapId);
|
||||||
|
|
||||||
SilentRefreshInProgress = true;
|
SilentRefreshInProgress = true;
|
||||||
agent->OpenMapByMapId(currentMapId, 0, true);
|
agent->OpenMapByMapId(currentMapId, 0, true);
|
||||||
agent->ResetMapMarkers();
|
agent->ResetMapMarkers();
|
||||||
@@ -251,8 +317,19 @@ public unsafe class IntegrationsController : IDisposable
|
|||||||
{
|
{
|
||||||
Service.Log.Verbose("[OnShow] Beginning Show");
|
Service.Log.Verbose("[OnShow] Beginning Show");
|
||||||
|
|
||||||
// If you managed to open the window while the agent says it should be closed
|
// When user just opened via Duty List / gathering / flag / teleport, pass through immediately.
|
||||||
if (System.MapWindow.IsOpen && AgentMap.Instance()->AddonId is 0)
|
var userRequestedMap = System.MapWindow.ProcessingCommand || _userOpenedMapFramesRemaining > 0;
|
||||||
|
if (userRequestedMap)
|
||||||
|
{
|
||||||
|
showMapHook!.Original(agent, a1, a2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var addonId = AgentMap.Instance()->AddonId;
|
||||||
|
var currentMapId = AgentMap.Instance()->CurrentMapId;
|
||||||
|
var selectedMapId = AgentMap.Instance()->SelectedMapId;
|
||||||
|
|
||||||
|
if (System.MapWindow.IsOpen && addonId is 0)
|
||||||
{
|
{
|
||||||
Service.Log.Debug("[OnShow] MapWindow can not be open now.");
|
Service.Log.Debug("[OnShow] MapWindow can not be open now.");
|
||||||
System.MapWindow.Close();
|
System.MapWindow.Close();
|
||||||
@@ -264,15 +341,10 @@ public unsafe class IntegrationsController : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (AgentMap.Instance()->AddonId is not 0 &&
|
// CurrentMapId != SelectedMapId = viewing quest map in different zone; pass through, don't Hide()
|
||||||
AgentMap.Instance()->CurrentMapId != AgentMap.Instance()->SelectedMapId)
|
if (addonId is not 0 && currentMapId != selectedMapId)
|
||||||
{
|
{
|
||||||
if (!System.SystemConfig.KeepOpen)
|
showMapHook!.Original(agent, a1, a2);
|
||||||
{
|
|
||||||
AgentMap.Instance()->Hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
Service.Log.Verbose("[OnShow] Vanilla tried to return to current map, aborted.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,9 +357,22 @@ public unsafe class IntegrationsController : IDisposable
|
|||||||
showMapHook!.Original(agent, a1, a2);
|
showMapHook!.Original(agent, a1, a2);
|
||||||
}, Service.Log, "Exception during OnShowHook");
|
}, Service.Log, "Exception during OnShowHook");
|
||||||
|
|
||||||
|
private static bool IsUserInitiatedMapOpen(MapType type) =>
|
||||||
|
type is MapType.QuestLog or MapType.GatheringLog or MapType.FlagMarker or MapType.Bozja
|
||||||
|
or MapType.MobHunt or MapType.SharedFate or MapType.Teleport or MapType.Treasure;
|
||||||
|
|
||||||
private void OnOpenMapHook(AgentMap* agent, OpenMapInfo* mapInfo) =>
|
private void OnOpenMapHook(AgentMap* agent, OpenMapInfo* mapInfo) =>
|
||||||
HookSafety.ExecuteSafe(() =>
|
HookSafety.ExecuteSafe(() =>
|
||||||
{
|
{
|
||||||
|
// MUST run before Original: cancel any in-progress silent refresh so OnFrameworkUpdate won't call Hide()
|
||||||
|
// after the game opens the map. Also set flags for OnShowHook pass-through.
|
||||||
|
if (IsUserInitiatedMapOpen(mapInfo->Type))
|
||||||
|
{
|
||||||
|
SuppressSilentRefreshForUserMapOpen();
|
||||||
|
System.MapWindow.ProcessingCommand = true;
|
||||||
|
_userOpenedMapFramesRemaining = 30; // Persists after ProcessingCommand cleared by MapWindow.OnOpen
|
||||||
|
}
|
||||||
|
|
||||||
openMapHook!.Original(agent, mapInfo);
|
openMapHook!.Original(agent, mapInfo);
|
||||||
|
|
||||||
switch (mapInfo->Type)
|
switch (mapInfo->Type)
|
||||||
@@ -492,7 +577,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;
|
||||||
|
|||||||
@@ -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 (0–100+).
|
||||||
|
/// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Interface.Textures.TextureWraps;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
|
using KamiLib.Configuration;
|
||||||
|
using Lumina.Excel.Sheets;
|
||||||
|
using Mappy.Classes.SelectionWindowComponents;
|
||||||
|
|
||||||
|
namespace Mappy.Data;
|
||||||
|
|
||||||
|
public unsafe record MapNote(uint Territory, uint Map, float X, float Y, string Title, string Description)
|
||||||
|
{
|
||||||
|
public IDalamudTextureWrap? GetMapTexture() => MapDrawableOption.GetMapTexture(Map);
|
||||||
|
|
||||||
|
public Map GetMap() => Service.DataManager.GetExcelSheet<Map>().GetRow(Map);
|
||||||
|
|
||||||
|
public TerritoryType GetTerritoryType() => Service.DataManager.GetExcelSheet<TerritoryType>().GetRow(Territory);
|
||||||
|
|
||||||
|
public string GetIdString() => $"{Territory}_{Map}_{X}_{Y}_{Title.GetHashCode()}";
|
||||||
|
|
||||||
|
/// <summary>Tooltip text for map/minimap: Title with Description on second line if present.</summary>
|
||||||
|
public string GetTooltipText()
|
||||||
|
{
|
||||||
|
var t = Title ?? "";
|
||||||
|
var d = Description ?? "";
|
||||||
|
return string.IsNullOrWhiteSpace(d) ? t : $"{t}\n{d}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 GetCoordinate() => new(X, Y);
|
||||||
|
|
||||||
|
/// <summary>Returns the stored map coordinates for display (same space as Place Note/Flag).</summary>
|
||||||
|
public Vector2 GetMapCoordinate() => new(X, Y);
|
||||||
|
|
||||||
|
public void Focus()
|
||||||
|
{
|
||||||
|
System.SystemConfig.FollowPlayer = false;
|
||||||
|
System.IntegrationsController.OpenMap(Map);
|
||||||
|
System.MapRenderer.CenterOnCoordinate(GetCoordinate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MapNoteConfig
|
||||||
|
{
|
||||||
|
public List<MapNote> Notes { get; set; } = [];
|
||||||
|
|
||||||
|
public int MaxNotes { get; set; } = 100;
|
||||||
|
|
||||||
|
public static MapNoteConfig Load() => Service.PluginInterface.LoadConfigFile<MapNoteConfig>("MapNotes.data.json");
|
||||||
|
|
||||||
|
public void Save() => Service.PluginInterface.SaveConfigFile("MapNotes.data.json", System.MapNoteConfig);
|
||||||
|
|
||||||
|
/// <summary>Finds a note at or near the given map coordinates (same space as Place Note).</summary>
|
||||||
|
/// <param name="threshold">Distance in map coordinate space; typical icon is ~200–500 units, so 500 covers right-clicks on the icon.</param>
|
||||||
|
public MapNote? FindNoteAt(uint territoryId, uint mapId, float x, float y, float threshold = 500f)
|
||||||
|
{
|
||||||
|
MapNote? closest = null;
|
||||||
|
var minDistSq = threshold * threshold;
|
||||||
|
foreach (var n in Notes.Where(n => n.Territory == territoryId && n.Map == mapId)) {
|
||||||
|
var dx = x - n.X;
|
||||||
|
var dy = y - n.Y;
|
||||||
|
var distSq = dx * dx + dy * dy;
|
||||||
|
if (distSq < minDistSq) {
|
||||||
|
minDistSq = distSq;
|
||||||
|
closest = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return closest;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -112,10 +112,60 @@ public class SystemConfig : CharacterConfiguration
|
|||||||
public bool MinimapShowQuestDirectionArrow = true;
|
public bool MinimapShowQuestDirectionArrow = true;
|
||||||
/// <summary>Show direction arrows for nearby FATEs on the minimap (purple, matching FATE marker).</summary>
|
/// <summary>Show direction arrows for nearby FATEs on the minimap (purple, matching FATE marker).</summary>
|
||||||
public bool MinimapShowFateDirectionArrows = true;
|
public bool MinimapShowFateDirectionArrows = true;
|
||||||
|
/// <summary>Show a red direction arrow pointing toward the player flag when it's off the minimap.</summary>
|
||||||
|
public bool MinimapShowFlagDirectionArrow = true;
|
||||||
/// <summary>Show quest objective area radius circles on the minimap (same as Area Map).</summary>
|
/// <summary>Show quest objective area radius circles on the minimap (same as Area Map).</summary>
|
||||||
public bool MinimapShowQuestAreaRadius = true;
|
public bool MinimapShowQuestAreaRadius = true;
|
||||||
/// <summary>When true, minimap hides with the game GUI (dialogue, interaction, nameplates off). When false, minimap stays visible during dialogue and object interaction.</summary>
|
/// <summary>When true, minimap hides with the game GUI (dialogue, interaction, nameplates off). When false, minimap stays visible during dialogue and object interaction.</summary>
|
||||||
public bool MinimapHideWithGameGui = false;
|
public bool MinimapHideWithGameGui = false;
|
||||||
|
/// <summary>Show Player/NPC tracking (other players, enemies, bosses, etc.) on the minimap, matching the main map display.</summary>
|
||||||
|
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>
|
||||||
|
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]
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -160,6 +175,7 @@ public unsafe partial class MapRenderer : IDisposable
|
|||||||
if (string.IsNullOrEmpty(idStr)) return false;
|
if (string.IsNullOrEmpty(idStr)) return false;
|
||||||
|
|
||||||
// Try several path conventions so the minimap can show without ever requiring the user to open the Area Map.
|
// Try several path conventions so the minimap can show without ever requiring the user to open the Area Map.
|
||||||
|
// Prefer TextureProvider.GetFromGame (same as Area Map) so path resolution and mods match; fall back to Lumina GetTexFile.
|
||||||
var fileName = idStr.Replace("/", "");
|
var fileName = idStr.Replace("/", "");
|
||||||
var pathsToTry = new[]
|
var pathsToTry = new[]
|
||||||
{
|
{
|
||||||
@@ -170,17 +186,24 @@ public unsafe partial class MapRenderer : IDisposable
|
|||||||
$"ui/uld/areamap/{fileName}.tex",
|
$"ui/uld/areamap/{fileName}.tex",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
string? gamePath = null;
|
||||||
IDalamudTextureWrap? texture = null;
|
IDalamudTextureWrap? texture = null;
|
||||||
foreach (var path in pathsToTry) {
|
foreach (var path in pathsToTry) {
|
||||||
|
var wrap = Service.TextureProvider.GetFromGame(path).GetWrapOrDefault();
|
||||||
|
if (wrap is not null && wrap.Handle != IntPtr.Zero && wrap.Size.X > 0 && wrap.Size.Y > 0) {
|
||||||
|
gamePath = path;
|
||||||
|
break;
|
||||||
|
}
|
||||||
texture = LoadSingleTexture(path);
|
texture = LoadSingleTexture(path);
|
||||||
if (texture is not null) break;
|
if (texture is not null) break;
|
||||||
}
|
}
|
||||||
if (texture is null) return false;
|
|
||||||
|
if (gamePath is null && texture is null) return false;
|
||||||
|
|
||||||
TrimMinimapCacheToLimit();
|
TrimMinimapCacheToLimit();
|
||||||
var entry = _minimapCache[mapId] = new MinimapCacheEntry();
|
var entry = _minimapCache[mapId] = new MinimapCacheEntry();
|
||||||
entry.PathKey = $"lumina:{mapId}";
|
entry.PathKey = gamePath is not null ? $"game:{gamePath}" : $"lumina:{mapId}";
|
||||||
entry.Texture = texture;
|
entry.Texture = texture; // null when using game path (looked up each frame)
|
||||||
entry.ScaleFactor = map.SizeFactor / 100f;
|
entry.ScaleFactor = map.SizeFactor / 100f;
|
||||||
entry.OffsetX = map.OffsetX;
|
entry.OffsetX = map.OffsetX;
|
||||||
entry.OffsetY = map.OffsetY;
|
entry.OffsetY = map.OffsetY;
|
||||||
@@ -214,7 +237,7 @@ public unsafe partial class MapRenderer : IDisposable
|
|||||||
// Markers are drawn from whatever the game has already populated (e.g. after opening the map once).
|
// Markers are drawn from whatever the game has already populated (e.g. after opening the map once).
|
||||||
|
|
||||||
// When the game has the current map loaded (area map open or just closed), update our cache for this map.
|
// When the game has the current map loaded (area map open or just closed), update our cache for this map.
|
||||||
if (agent->SelectedMapId == currentMapId && agent->SelectedMapPath.Length > 0) {
|
if (agent->SelectedMapId == currentMapId && agent->SelectedMapPath.Length > 0 && agent->SelectedMapBgPath.Length > 0) {
|
||||||
var bgPath = $"{agent->SelectedMapBgPath}.tex";
|
var bgPath = $"{agent->SelectedMapBgPath}.tex";
|
||||||
var fgPath = $"{agent->SelectedMapPath}.tex";
|
var fgPath = $"{agent->SelectedMapPath}.tex";
|
||||||
var pathKey = bgPath + "|" + fgPath;
|
var pathKey = bgPath + "|" + fgPath;
|
||||||
@@ -236,14 +259,23 @@ public unsafe partial class MapRenderer : IDisposable
|
|||||||
TryEnsureLuminaCacheFor(currentMapId);
|
TryEnsureLuminaCacheFor(currentMapId);
|
||||||
|
|
||||||
// Draw from cache if we have it for the current map.
|
// Draw from cache if we have it for the current map.
|
||||||
if (!_minimapCache.TryGetValue(currentMapId, out var cached) || cached.Texture is null)
|
if (!_minimapCache.TryGetValue(currentMapId, out var cached))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Resolve texture: use cached texture, or for "game:" path look up via TextureProvider each frame (same as Area Map).
|
||||||
|
IDalamudTextureWrap? textureToDraw = cached.Texture;
|
||||||
|
if (textureToDraw is null && cached.PathKey.StartsWith("game:", StringComparison.Ordinal)) {
|
||||||
|
var path = cached.PathKey.Substring(5);
|
||||||
|
textureToDraw = Service.TextureProvider.GetFromGame(path).GetWrapOrDefault();
|
||||||
|
}
|
||||||
|
if (textureToDraw is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Use the size passed by the minimap window (window size) so zoom/center is stable.
|
// Use the size passed by the minimap window (window size) so zoom/center is stable.
|
||||||
if (size.X <= 0 || size.Y <= 0) return;
|
if (size.X <= 0 || size.Y <= 0) return;
|
||||||
|
|
||||||
var zoom = Math.Clamp(System.SystemConfig.MinimapZoom, 0.03f, 0.112f);
|
var zoom = Math.Clamp(System.SystemConfig.MinimapZoom, 0.03f, 0.112f);
|
||||||
var mapSize = cached.Texture.Size.X;
|
var mapSize = textureToDraw.Size.X;
|
||||||
// Scale so the map COVERS the view at zoom=1 (use max so no black bands at max zoom out).
|
// Scale so the map COVERS the view at zoom=1 (use max so no black bands at max zoom out).
|
||||||
var fitScale = Math.Max(size.X, size.Y) / mapSize;
|
var fitScale = Math.Max(size.X, size.Y) / mapSize;
|
||||||
var scale = fitScale / zoom;
|
var scale = fitScale / zoom;
|
||||||
@@ -255,11 +287,11 @@ public unsafe partial class MapRenderer : IDisposable
|
|||||||
var drawOffset = (-playerCoord + new Vector2(cached.OffsetX, cached.OffsetY)) * cached.ScaleFactor;
|
var drawOffset = (-playerCoord + new Vector2(cached.OffsetX, cached.OffsetY)) * cached.ScaleFactor;
|
||||||
|
|
||||||
var centerOffset = size / 2.0f;
|
var centerOffset = size / 2.0f;
|
||||||
var mapCenterOffset = (cached.Texture.Size / 2f) * scale;
|
var mapCenterOffset = (textureToDraw.Size / 2f) * scale;
|
||||||
var drawPosition = centerOffset - mapCenterOffset + drawOffset * scale;
|
var drawPosition = centerOffset - mapCenterOffset + drawOffset * scale;
|
||||||
|
|
||||||
// Clamp so the map always fills the view (no black), but when zoomed in allow full pan so the player tracks.
|
// Clamp so the map always fills the view (no black), but when zoomed in allow full pan so the player tracks.
|
||||||
var texSize = cached.Texture.Size * scale;
|
var texSize = textureToDraw.Size * scale;
|
||||||
if (texSize.X > size.X && texSize.Y > size.Y) {
|
if (texSize.X > size.X && texSize.Y > size.Y) {
|
||||||
// Zoomed in: allow full pan range so the map can shift in any direction and the player stays at center.
|
// Zoomed in: allow full pan range so the map can shift in any direction and the player stays at center.
|
||||||
drawPosition.X = Math.Clamp(drawPosition.X, size.X - texSize.X, texSize.X - size.X);
|
drawPosition.X = Math.Clamp(drawPosition.X, size.X - texSize.X, texSize.X - size.X);
|
||||||
@@ -272,7 +304,7 @@ public unsafe partial class MapRenderer : IDisposable
|
|||||||
|
|
||||||
// Content top-left in screen space so player is drawn at the true center of the minimap (not offset by title bar).
|
// Content top-left in screen space so player is drawn at the true center of the minimap (not offset by title bar).
|
||||||
var contentTopLeft = ImGui.GetCursorScreenPos();
|
var contentTopLeft = ImGui.GetCursorScreenPos();
|
||||||
DrawMinimapCachedTextureAt(drawPosition, scale, cached.Texture);
|
DrawMinimapCachedTextureAt(drawPosition, scale, textureToDraw);
|
||||||
var centerScreen = contentTopLeft + centerOffset;
|
var centerScreen = contentTopLeft + centerOffset;
|
||||||
// Draw cone under markers so quest/FATE/POI markers stay visible on top of the cone.
|
// Draw cone under markers so quest/FATE/POI markers stay visible on top of the cone.
|
||||||
DrawMinimapConeAtCenter(centerScreen, scale);
|
DrawMinimapConeAtCenter(centerScreen, scale);
|
||||||
@@ -282,6 +314,8 @@ public unsafe partial class MapRenderer : IDisposable
|
|||||||
DrawMinimapQuestDirectionArrow(contentTopLeft, drawPosition, scale, size, centerOffset, cached.OffsetX, cached.OffsetY, cached.ScaleFactor, currentMapId);
|
DrawMinimapQuestDirectionArrow(contentTopLeft, drawPosition, scale, size, centerOffset, cached.OffsetX, cached.OffsetY, cached.ScaleFactor, currentMapId);
|
||||||
if (System.SystemConfig.MinimapShowFateDirectionArrows)
|
if (System.SystemConfig.MinimapShowFateDirectionArrows)
|
||||||
DrawMinimapFateDirectionArrows(contentTopLeft, drawPosition, scale, size, centerOffset, cached.OffsetX, cached.OffsetY, cached.ScaleFactor);
|
DrawMinimapFateDirectionArrows(contentTopLeft, drawPosition, scale, size, centerOffset, cached.OffsetX, cached.OffsetY, cached.ScaleFactor);
|
||||||
|
if (System.SystemConfig.MinimapShowFlagDirectionArrow)
|
||||||
|
DrawMinimapFlagDirectionArrow(contentTopLeft, drawPosition, scale, size, centerOffset, cached.OffsetX, cached.OffsetY, cached.ScaleFactor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TrimMinimapCacheToLimit()
|
private void TrimMinimapCacheToLimit()
|
||||||
@@ -344,13 +378,23 @@ public unsafe partial class MapRenderer : IDisposable
|
|||||||
|
|
||||||
private static TexFile? GetTexFile(string rawPath)
|
private static TexFile? GetTexFile(string rawPath)
|
||||||
{
|
{
|
||||||
var path = Service.TextureSubstitutionProvider.GetSubstitutedPath(rawPath);
|
if (string.IsNullOrWhiteSpace(rawPath)) return null;
|
||||||
|
string path;
|
||||||
|
try {
|
||||||
|
path = Service.TextureSubstitutionProvider.GetSubstitutedPath(rawPath);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(path)) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
if (Path.IsPathRooted(path)) {
|
if (Path.IsPathRooted(path)) {
|
||||||
return Service.DataManager.GameData.GetFileFromDisk<TexFile>(path);
|
return Service.DataManager.GameData.GetFileFromDisk<TexFile>(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Service.DataManager.GetFile<TexFile>(path);
|
return Service.DataManager.GetFile<TexFile>(path);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawMapMarkers()
|
private void DrawMapMarkers()
|
||||||
@@ -364,6 +408,8 @@ public unsafe partial class MapRenderer : IDisposable
|
|||||||
DrawFieldMarkers();
|
DrawFieldMarkers();
|
||||||
DrawPlayer();
|
DrawPlayer();
|
||||||
DrawStaticTextMarkers();
|
DrawStaticTextMarkers();
|
||||||
|
DrawMapNotes();
|
||||||
|
DrawMovementTrail();
|
||||||
DrawFlag();
|
DrawFlag();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Linq;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
@@ -18,6 +19,52 @@ namespace Mappy.MapRenderer;
|
|||||||
|
|
||||||
public partial class MapRenderer
|
public partial class MapRenderer
|
||||||
{
|
{
|
||||||
|
private unsafe void DrawMinimapGameObjects(
|
||||||
|
Vector2 contentTopLeft,
|
||||||
|
Func<float, float, Vector2> texToContent,
|
||||||
|
Vector2 size,
|
||||||
|
float scaleFactor,
|
||||||
|
float offsetX,
|
||||||
|
float offsetY)
|
||||||
|
{
|
||||||
|
if (!System.SystemConfig.MinimapShowPlayersAndNpcs) return;
|
||||||
|
if (Service.ObjectTable is not { LocalPlayer: { } player }) return;
|
||||||
|
|
||||||
|
foreach (var obj in Service.ObjectTable.Reverse())
|
||||||
|
{
|
||||||
|
if (!obj.IsTargetable) continue;
|
||||||
|
if (GroupManager.Instance()->MainGroup.IsEntityIdInParty(obj.EntityId)) continue;
|
||||||
|
if (GroupManager.Instance()->MainGroup.IsEntityIdInAlliance(obj.EntityId)) continue;
|
||||||
|
if (Vector3.Distance(obj.Position, player.Position) >= 150.0f) continue;
|
||||||
|
|
||||||
|
var iconId = obj.ObjectKind switch
|
||||||
|
{
|
||||||
|
ObjectKind.Player when System.SystemConfig.ShowPlayers => System.SystemConfig.MinimapOtherPlayerIconId,
|
||||||
|
ObjectKind.BattleNpc when IsBoss(obj) && obj.TargetObject is null => 60402u,
|
||||||
|
ObjectKind.BattleNpc when IsBoss(obj) && obj.TargetObject is not null => 60401u,
|
||||||
|
ObjectKind.BattleNpc when obj is { SubKind: (int)BattleNpcSubKind.Enemy, TargetObject: not null } => 60422u,
|
||||||
|
ObjectKind.BattleNpc when obj is { SubKind: (int)BattleNpcSubKind.Enemy, TargetObject: null } => 60424u,
|
||||||
|
ObjectKind.BattleNpc when obj.SubKind == (int)BattleNpcSubKind.Pet => 60961u,
|
||||||
|
ObjectKind.Treasure => 60003u,
|
||||||
|
ObjectKind.GatheringPoint => System.GatheringPointIconCache.GetValue(obj.BaseId),
|
||||||
|
ObjectKind.EventObj when IsAetherCurrent(obj) => 60653u,
|
||||||
|
_ => 0u
|
||||||
|
};
|
||||||
|
|
||||||
|
if (iconId is 0) continue;
|
||||||
|
if (System.IconConfig.IconSettingMap.TryGetValue(iconId, out var setting) && setting.Hide) continue;
|
||||||
|
|
||||||
|
var pos = new Vector2(obj.Position.X, obj.Position.Z);
|
||||||
|
var tx = 1024.0f + (pos.X - offsetX) * scaleFactor;
|
||||||
|
var ty = 1024.0f + (pos.Y - offsetY) * scaleFactor;
|
||||||
|
var contentPos = texToContent(tx, ty);
|
||||||
|
if (!IsInMinimapBounds(contentPos, size, MinimapBoundsMargin)) continue;
|
||||||
|
|
||||||
|
var tooltip = GetTooltipForGameObject(obj);
|
||||||
|
DrawMinimapIcon(iconId, contentPos + contentTopLeft, sizeScale: 1.5f, tooltip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private unsafe void DrawGameObjects()
|
private unsafe void DrawGameObjects()
|
||||||
{
|
{
|
||||||
if (AgentMap.Instance()->SelectedMapId != AgentMap.Instance()->CurrentMapId) return;
|
if (AgentMap.Instance()->SelectedMapId != AgentMap.Instance()->CurrentMapId) return;
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
|
using Mappy.Classes;
|
||||||
|
using Mappy.Data;
|
||||||
|
|
||||||
|
namespace Mappy.MapRenderer;
|
||||||
|
|
||||||
|
public partial class MapRenderer
|
||||||
|
{
|
||||||
|
private unsafe void DrawMapNotes()
|
||||||
|
{
|
||||||
|
var agent = AgentMap.Instance();
|
||||||
|
var territoryId = agent->SelectedTerritoryId;
|
||||||
|
var mapId = agent->SelectedMapId;
|
||||||
|
|
||||||
|
foreach (var note in System.MapNoteConfig.Notes.Where(n => n.Territory == territoryId && n.Map == mapId).ToList()) {
|
||||||
|
var pos = new Vector2(note.X, note.Y) * Scale * DrawHelpers.GetMapScaleFactor() + DrawHelpers.GetCombinedOffsetVector() * Scale;
|
||||||
|
|
||||||
|
DrawHelpers.DrawMapMarker(new MarkerInfo
|
||||||
|
{
|
||||||
|
Position = pos,
|
||||||
|
Offset = DrawPosition,
|
||||||
|
Scale = Scale,
|
||||||
|
IconId = DrawHelpers.MapNoteIconId,
|
||||||
|
PrimaryText = () => note.Title ?? "",
|
||||||
|
SecondaryText = () => string.IsNullOrWhiteSpace(note.Description)
|
||||||
|
? note.GetTerritoryType().PlaceNameZone.Value.Name.ExtractText()
|
||||||
|
: $"{note.Description}\n{note.GetTerritoryType().PlaceNameZone.Value.Name.ExtractText()}",
|
||||||
|
OnLeftClicked = () => note.Focus(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
|||||||
using FFXIVClientStructs.Interop;
|
using FFXIVClientStructs.Interop;
|
||||||
using Lumina.Excel.Sheets;
|
using Lumina.Excel.Sheets;
|
||||||
using Mappy.Classes;
|
using Mappy.Classes;
|
||||||
|
using Mappy.Data;
|
||||||
using Mappy.Extensions;
|
using Mappy.Extensions;
|
||||||
using KamiLib.Extensions;
|
using KamiLib.Extensions;
|
||||||
using FieldMarkerSheet = Lumina.Excel.Sheets.FieldMarker;
|
using FieldMarkerSheet = Lumina.Excel.Sheets.FieldMarker;
|
||||||
@@ -55,8 +56,14 @@ public partial class MapRenderer
|
|||||||
DrawMinimapGatheringMarkers(contentTopLeft, TexToContent, size);
|
DrawMinimapGatheringMarkers(contentTopLeft, TexToContent, size);
|
||||||
// Party / alliance members (same marker as main map)
|
// Party / alliance members (same marker as main map)
|
||||||
DrawMinimapGroupMembers(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY);
|
DrawMinimapGroupMembers(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY);
|
||||||
|
// Player / NPC tracking (other players, enemies, bosses, etc.)
|
||||||
|
DrawMinimapGameObjects(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY);
|
||||||
// User flag
|
// User flag
|
||||||
DrawMinimapFlag(contentTopLeft, TexToContent, scaleFactor, offsetX, offsetY);
|
DrawMinimapFlag(contentTopLeft, TexToContent, scaleFactor, offsetX, offsetY);
|
||||||
|
// User map notes
|
||||||
|
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)
|
||||||
@@ -150,14 +157,7 @@ public partial class MapRenderer
|
|||||||
list.Add((tx, ty, tooltip, iconId));
|
list.Add((tx, ty, tooltip, iconId));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (agent->FlagMarkerCount > 0) {
|
// Flag excluded: drawn separately with red arrow via DrawMinimapFlagDirectionArrow
|
||||||
ref var flag = ref agent->FlagMapMarkers[0];
|
|
||||||
if (flag.TerritoryId == agent->CurrentMapId || flag.TerritoryId == agent->CurrentTerritoryId) {
|
|
||||||
var tooltip = System.TooltipCache.GetValue(flag.MapMarker.IconId);
|
|
||||||
if (string.IsNullOrEmpty(tooltip)) tooltip = "Flag";
|
|
||||||
AddIfNew(1024.0f + (flag.XFloat - offsetX) * scaleFactor, 1024.0f + (flag.YFloat - offsetY) * scaleFactor, tooltip, flag.MapMarker.IconId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (agent->TempMapMarkerCount > 0) {
|
if (agent->TempMapMarkerCount > 0) {
|
||||||
var span = new Span<TempMapMarker>(Unsafe.AsPointer(ref agent->TempMapMarkers[0]), agent->TempMapMarkerCount);
|
var span = new Span<TempMapMarker>(Unsafe.AsPointer(ref agent->TempMapMarkers[0]), agent->TempMapMarkerCount);
|
||||||
@@ -290,6 +290,65 @@ public partial class MapRenderer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Draws a red direction arrow at the edge of the minimap pointing toward the player flag. Only shown when the flag is off the minimap.</summary>
|
||||||
|
private unsafe void DrawMinimapFlagDirectionArrow(
|
||||||
|
Vector2 contentTopLeft,
|
||||||
|
Vector2 drawPosition,
|
||||||
|
float scale,
|
||||||
|
Vector2 size,
|
||||||
|
Vector2 centerOffset,
|
||||||
|
float offsetX,
|
||||||
|
float offsetY,
|
||||||
|
float scaleFactor)
|
||||||
|
{
|
||||||
|
var agent = AgentMap.Instance();
|
||||||
|
if (agent->FlagMarkerCount is 0) return;
|
||||||
|
ref var flag = ref agent->FlagMapMarkers[0];
|
||||||
|
if (flag.TerritoryId != agent->CurrentTerritoryId || flag.MapId != agent->CurrentMapId) return;
|
||||||
|
|
||||||
|
var radius = Math.Min(size.X, size.Y) * 0.5f;
|
||||||
|
var arrowDist = radius - 4f;
|
||||||
|
var centerScreen = contentTopLeft + centerOffset;
|
||||||
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
|
||||||
|
var tx = 1024.0f + (flag.XFloat - offsetX) * scaleFactor;
|
||||||
|
var ty = 1024.0f + (flag.YFloat - offsetY) * scaleFactor;
|
||||||
|
var targetInContent = drawPosition + new Vector2(tx, ty) * scale;
|
||||||
|
var toTarget = targetInContent - centerOffset;
|
||||||
|
var distToTarget = toTarget.Length();
|
||||||
|
if (distToTarget < 0.01f) return;
|
||||||
|
if (distToTarget <= radius) return; // Flag visible on minimap, no arrow
|
||||||
|
|
||||||
|
const float arrowSize = 20f;
|
||||||
|
const float baseHalf = 8f;
|
||||||
|
const float headDepth = 5f;
|
||||||
|
var colorHead = ImGui.GetColorU32(new Vector4(0.92f, 0.2f, 0.2f, 0.95f));
|
||||||
|
var colorOutline = ImGui.GetColorU32(new Vector4(0.45f, 0.08f, 0.08f, 1f));
|
||||||
|
|
||||||
|
var direction = toTarget / distToTarget;
|
||||||
|
var arrowPos = centerScreen + direction * arrowDist;
|
||||||
|
var cos = MathF.Cos(MathF.Atan2(direction.Y, direction.X));
|
||||||
|
var sin = MathF.Sin(MathF.Atan2(direction.Y, direction.X));
|
||||||
|
var perpX = -sin;
|
||||||
|
var perpY = cos;
|
||||||
|
var tipScreen = arrowPos + new Vector2(cos * arrowSize, sin * arrowSize);
|
||||||
|
var baseBack = arrowPos - new Vector2(cos * headDepth, sin * headDepth);
|
||||||
|
var base1Screen = baseBack + new Vector2(perpX * baseHalf, perpY * baseHalf);
|
||||||
|
var base2Screen = baseBack - new Vector2(perpX * baseHalf, perpY * baseHalf);
|
||||||
|
|
||||||
|
drawList.AddTriangleFilled(tipScreen, base1Screen, base2Screen, colorHead);
|
||||||
|
drawList.AddTriangle(tipScreen, base1Screen, base2Screen, colorOutline, 2f);
|
||||||
|
|
||||||
|
var tooltip = System.TooltipCache.GetValue(flag.MapMarker.IconId);
|
||||||
|
if (string.IsNullOrEmpty(tooltip)) tooltip = "Flag";
|
||||||
|
var minX = Math.Min(tipScreen.X, Math.Min(base1Screen.X, base2Screen.X)) - 4f;
|
||||||
|
var minY = Math.Min(tipScreen.Y, Math.Min(base1Screen.Y, base2Screen.Y)) - 4f;
|
||||||
|
var maxX = Math.Max(tipScreen.X, Math.Max(base1Screen.X, base2Screen.X)) + 4f;
|
||||||
|
var maxY = Math.Max(tipScreen.Y, Math.Max(base1Screen.Y, base2Screen.Y)) + 4f;
|
||||||
|
if (ImGui.IsMouseHoveringRect(new Vector2(minX, minY), new Vector2(maxX, maxY)))
|
||||||
|
ImGui.SetTooltip(tooltip);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsInMinimapBounds(Vector2 contentPos, Vector2 size, float margin)
|
private static bool IsInMinimapBounds(Vector2 contentPos, Vector2 size, float margin)
|
||||||
{
|
{
|
||||||
// Use a large margin so we don't cull markers that are panned slightly off (zoomed in).
|
// Use a large margin so we don't cull markers that are panned slightly off (zoomed in).
|
||||||
@@ -317,6 +376,9 @@ public partial class MapRenderer
|
|||||||
/// <summary>Cached quest/objective (temp) markers per map; populated during silent refresh on quest accept/turn-in/objective update.</summary>
|
/// <summary>Cached quest/objective (temp) markers per map; populated during silent refresh on quest accept/turn-in/objective update.</summary>
|
||||||
private static readonly Dictionary<uint, List<CachedTempMarker>> TempMarkerCache = new();
|
private static readonly Dictionary<uint, List<CachedTempMarker>> TempMarkerCache = new();
|
||||||
|
|
||||||
|
/// <summary>Clear the temp marker cache for a map so stale markers (e.g. from a turned-in quest) are not drawn until we refresh.</summary>
|
||||||
|
public static void InvalidateTempMarkerCache(uint mapId) => TempMarkerCache.Remove(mapId);
|
||||||
|
|
||||||
/// <summary>Cached non-FATE event markers per map; populated during silent refresh.</summary>
|
/// <summary>Cached non-FATE event markers per map; populated during silent refresh.</summary>
|
||||||
private static readonly Dictionary<uint, List<CachedEventMarker>> EventMarkerCache = new();
|
private static readonly Dictionary<uint, List<CachedEventMarker>> EventMarkerCache = new();
|
||||||
|
|
||||||
@@ -669,6 +731,57 @@ 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)
|
||||||
|
{
|
||||||
|
var agent = AgentMap.Instance();
|
||||||
|
var territoryId = agent->CurrentTerritoryId;
|
||||||
|
var mapId = agent->CurrentMapId;
|
||||||
|
|
||||||
|
foreach (var note in System.MapNoteConfig.Notes.Where(n => n.Territory == territoryId && n.Map == mapId)) {
|
||||||
|
if (System.IconConfig.IconSettingMap.TryGetValue(DrawHelpers.MapNoteIconId, out var setting) && setting.Hide) continue;
|
||||||
|
|
||||||
|
var tx = 1024.0f + (note.X - offsetX) * scaleFactor;
|
||||||
|
var ty = 1024.0f + (note.Y - offsetY) * scaleFactor;
|
||||||
|
var contentPos = texToContent(tx, ty);
|
||||||
|
if (!IsInMinimapBounds(contentPos, size, MinimapBoundsMargin)) continue;
|
||||||
|
|
||||||
|
var centerScreen = contentPos + contentTopLeft;
|
||||||
|
var iconSize = new Vector2(22f, 28f) * (MinimapIconScaleFromConfig * 0.75f);
|
||||||
|
var col = setting?.Color ?? Vector4.One;
|
||||||
|
DrawHelpers.DrawMapNoteIconCentered(centerScreen, iconSize, col);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(note.GetTooltipText()) && ImGui.IsMouseHoveringRect(centerScreen - iconSize / 2f, centerScreen + iconSize / 2f))
|
||||||
|
ImGui.SetTooltip(note.GetTooltipText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private unsafe void DrawMinimapFlag(Vector2 contentTopLeft, Func<float, float, Vector2> texToContent, float scaleFactor, float offsetX, float offsetY)
|
private unsafe void DrawMinimapFlag(Vector2 contentTopLeft, Func<float, float, Vector2> texToContent, 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
@@ -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.1</Version>
|
<Version>1.0.0.20</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>
|
||||||
|
|||||||
@@ -28,9 +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.MovementTrailConfig = new MovementTrailConfig();
|
||||||
|
|
||||||
System.Teleporter = new Teleporter(Service.PluginInterface);
|
System.Teleporter = new Teleporter(Service.PluginInterface);
|
||||||
|
|
||||||
@@ -86,6 +91,14 @@ public sealed class MappyPlugin : IDalamudPlugin
|
|||||||
DisableDelegate = _ => System.WindowManager.GetWindow<FlagHistoryWindow>()?.Close(),
|
DisableDelegate = _ => System.WindowManager.GetWindow<FlagHistoryWindow>()?.Close(),
|
||||||
ToggleDelegate = _ => System.WindowManager.GetWindow<FlagHistoryWindow>()?.UnCollapseOrToggle(),
|
ToggleDelegate = _ => System.WindowManager.GetWindow<FlagHistoryWindow>()?.UnCollapseOrToggle(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
System.CommandManager.RegisterCommand(new ToggleCommandHandler
|
||||||
|
{
|
||||||
|
BaseActivationPath = "/notelist",
|
||||||
|
EnableDelegate = _ => System.WindowManager.OpenOrCreateUnique<MapNoteListWindow>(WindowFlags.OpenImmediately | WindowFlags.RequireLoggedIn),
|
||||||
|
DisableDelegate = _ => System.WindowManager.GetWindow<MapNoteListWindow>()?.Close(),
|
||||||
|
ToggleDelegate = _ => System.WindowManager.GetWindow<MapNoteListWindow>()?.UnCollapseOrToggle(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void OpenMapWindow() => AgentMap.Instance()->Show();
|
private unsafe void OpenMapWindow() => AgentMap.Instance()->Show();
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ public static class System
|
|||||||
public static SystemConfig SystemConfig { get; set; }
|
public static SystemConfig SystemConfig { get; set; }
|
||||||
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 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; }
|
||||||
@@ -43,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; }
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
@@ -280,19 +292,136 @@ public class MinimapOptionsTab : ITabItem
|
|||||||
configChanged |= ImGui.Checkbox("Show FATE Direction Arrows", ref System.SystemConfig.MinimapShowFateDirectionArrows);
|
configChanged |= ImGui.Checkbox("Show FATE Direction Arrows", ref System.SystemConfig.MinimapShowFateDirectionArrows);
|
||||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||||
ImGui.SetTooltip("Show purple arrows at the edge of the minimap pointing toward nearby FATEs.");
|
ImGui.SetTooltip("Show purple arrows at the edge of the minimap pointing toward nearby FATEs.");
|
||||||
|
configChanged |= ImGui.Checkbox("Show Flag Direction Arrow", ref System.SystemConfig.MinimapShowFlagDirectionArrow);
|
||||||
|
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||||
|
ImGui.SetTooltip("Show a red arrow at the edge of the minimap pointing toward your placed flag.");
|
||||||
configChanged |= ImGui.Checkbox("Show Quest Area Radius", ref System.SystemConfig.MinimapShowQuestAreaRadius);
|
configChanged |= ImGui.Checkbox("Show Quest Area Radius", ref System.SystemConfig.MinimapShowQuestAreaRadius);
|
||||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||||
ImGui.SetTooltip("Show quest objective area circles on the minimap (same as on the Area Map).");
|
ImGui.SetTooltip("Show quest objective area circles on the minimap (same as on the Area Map).");
|
||||||
|
configChanged |= ImGui.Checkbox("Show Players and NPCs", ref System.SystemConfig.MinimapShowPlayersAndNpcs);
|
||||||
|
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||||
|
ImGui.SetTooltip("Show Player/NPC tracking on the minimap (other players, enemies, bosses, etc.), matching the main map display.");
|
||||||
|
var otherPlayerIcon = (int)System.SystemConfig.MinimapOtherPlayerIconId;
|
||||||
|
if (ImGui.DragInt("Other Player Icon (minimap)", ref otherPlayerIcon, 1.0f, 60000, 61000, "%d")) {
|
||||||
|
System.SystemConfig.MinimapOtherPlayerIconId = (uint)Math.Clamp(otherPlayerIcon, 60000, 61000);
|
||||||
|
configChanged = true;
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||||
|
ImGui.SetTooltip("Icon ID for other players on the minimap (default 60403). Must be distinct from party marker (60421).");
|
||||||
configChanged |= ImGui.Checkbox("Hide Minimap With Game GUI", ref System.SystemConfig.MinimapHideWithGameGui);
|
configChanged |= ImGui.Checkbox("Hide Minimap With Game GUI", ref System.SystemConfig.MinimapHideWithGameGui);
|
||||||
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
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
|
using KamiLib.Window;
|
||||||
|
using Mappy.Data;
|
||||||
|
|
||||||
|
namespace Mappy.Windows;
|
||||||
|
|
||||||
|
public class MapNoteListWindow : Window
|
||||||
|
{
|
||||||
|
private static float NoteElementHeight => 95.0f * ImGuiHelpers.GlobalScale;
|
||||||
|
|
||||||
|
public MapNoteListWindow() : base("HSMappy Note List Window", new Vector2(400.0f, 400.0f))
|
||||||
|
{
|
||||||
|
AdditionalInfoTooltip = "Shows user-placed map notes for the current map";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override unsafe void DrawContents()
|
||||||
|
{
|
||||||
|
var agent = AgentMap.Instance();
|
||||||
|
var territoryId = agent->SelectedTerritoryId;
|
||||||
|
var mapId = agent->SelectedMapId;
|
||||||
|
var notesForMap = System.MapNoteConfig.Notes
|
||||||
|
.Where(n => n.Territory == territoryId && n.Map == mapId)
|
||||||
|
.ToImmutableList();
|
||||||
|
|
||||||
|
ImGuiClip.ClippedDraw(notesForMap, DrawNote, NoteElementHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawNote(MapNote note)
|
||||||
|
{
|
||||||
|
using var id = ImRaii.PushId(note.GetIdString());
|
||||||
|
|
||||||
|
using (ImRaii.Child("note_container", new Vector2(ImGui.GetContentRegionAvail().X, NoteElementHeight - ImGui.GetStyle().FramePadding.Y * 2.0f))) {
|
||||||
|
using (ImRaii.Child("note_image_container", new Vector2(155.0f * ImGuiHelpers.GlobalScale, ImGui.GetContentRegionAvail().Y))) {
|
||||||
|
DrawNoteImage(note);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
using (ImRaii.Child("note_contents_container", ImGui.GetContentRegionAvail())) {
|
||||||
|
var buttonHeight = 28f * ImGuiHelpers.GlobalScale;
|
||||||
|
var textHeight = ImGui.GetContentRegionAvail().Y - buttonHeight;
|
||||||
|
using (ImRaii.Child("note_text_area", new Vector2(ImGui.GetContentRegionAvail().X, textHeight))) {
|
||||||
|
DrawNoteData(note);
|
||||||
|
}
|
||||||
|
DrawButtons(note);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawNoteImage(MapNote note)
|
||||||
|
{
|
||||||
|
var texture = note.GetMapTexture();
|
||||||
|
if (texture is not null) {
|
||||||
|
ImGui.Image(texture.Handle, ImGui.GetContentRegionAvail(), new Vector2(0.15f, 0.15f), new Vector2(0.85f, 0.85f));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ImGuiHelpers.ScaledDummy(ImGui.GetContentRegionAvail());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawNoteData(MapNote note)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(note.Title ?? "");
|
||||||
|
if (!string.IsNullOrWhiteSpace(note.Description)) {
|
||||||
|
ImGui.TextColored(KnownColor.Gray.Vector().Lighten(0.20f), note.Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TextColored(KnownColor.Gray.Vector().Lighten(0.20f), note.GetMap().PlaceName.Value.Name.ExtractText());
|
||||||
|
|
||||||
|
var coord = note.GetMapCoordinate();
|
||||||
|
var zoneName = note.GetTerritoryType().PlaceNameZone.Value.Name.ExtractText();
|
||||||
|
ImGui.TextColored(KnownColor.Gray.Vector(), $"{zoneName} • {coord.X:F1}, {coord.Y:F1}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawButtons(MapNote note)
|
||||||
|
{
|
||||||
|
var buttonSize = ImGuiHelpers.ScaledVector2(80.0f, 24.0f);
|
||||||
|
|
||||||
|
ImGui.SetCursorPos(new Vector2(0.0f, ImGui.GetContentRegionMax().Y - buttonSize.Y));
|
||||||
|
if (ImGui.Button("Focus", buttonSize)) {
|
||||||
|
note.Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button("Remove", buttonSize)) {
|
||||||
|
System.MapNoteConfig.Notes.Remove(note);
|
||||||
|
System.MapNoteConfig.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnClose()
|
||||||
|
{
|
||||||
|
System.WindowManager.RemoveWindow(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,13 @@ namespace Mappy.Windows;
|
|||||||
|
|
||||||
public class MapWindow : Window
|
public class MapWindow : Window
|
||||||
{
|
{
|
||||||
|
public static (uint Territory, uint Map, float X, float Y)? PendingMapNotePosition;
|
||||||
|
private static string _pendingNoteTitle = string.Empty;
|
||||||
|
private static string _pendingNoteDescription = string.Empty;
|
||||||
|
private static bool _requestOpenAddNotePopup;
|
||||||
|
|
||||||
|
public static void RequestOpenAddNotePopup() => _requestOpenAddNotePopup = true;
|
||||||
|
|
||||||
public Vector2 MapDrawOffset { get; private set; }
|
public Vector2 MapDrawOffset { get; private set; }
|
||||||
public HoverFlags HoveredFlags { get; private set; }
|
public HoverFlags HoveredFlags { get; private set; }
|
||||||
public bool ProcessingCommand { get; set; }
|
public bool ProcessingCommand { get; set; }
|
||||||
@@ -48,7 +55,6 @@ public class MapWindow : Window
|
|||||||
|
|
||||||
public override unsafe void PreOpenCheck()
|
public override unsafe void PreOpenCheck()
|
||||||
{
|
{
|
||||||
// If you managed to open the window while the agent says it should be closed
|
|
||||||
if (System.MapWindow.IsOpen && AgentMap.Instance()->AddonId is 0)
|
if (System.MapWindow.IsOpen && AgentMap.Instance()->AddonId is 0)
|
||||||
{
|
{
|
||||||
Service.Log.Debug("[OnShow] MapWindow can not be open now.");
|
Service.Log.Debug("[OnShow] MapWindow can not be open now.");
|
||||||
@@ -244,6 +250,49 @@ public class MapWindow : Window
|
|||||||
|
|
||||||
// Draw Context Menu
|
// Draw Context Menu
|
||||||
mapContextMenu.Draw(MapDrawOffset);
|
mapContextMenu.Draw(MapDrawOffset);
|
||||||
|
|
||||||
|
DrawAddMapNotePopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawAddMapNotePopup()
|
||||||
|
{
|
||||||
|
if (_requestOpenAddNotePopup) {
|
||||||
|
ImGui.OpenPopup("AddMapNote");
|
||||||
|
_requestOpenAddNotePopup = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ImGui.BeginPopup("AddMapNote")) return;
|
||||||
|
|
||||||
|
ImGui.Text("Note Title:");
|
||||||
|
ImGui.SetNextItemWidth(250f * ImGuiHelpers.GlobalScale);
|
||||||
|
ImGui.InputText("##notetitle", ref _pendingNoteTitle, 256);
|
||||||
|
|
||||||
|
ImGui.Text("Note Description:");
|
||||||
|
ImGui.SetNextItemWidth(250f * ImGuiHelpers.GlobalScale);
|
||||||
|
ImGui.InputText("##notedesc", ref _pendingNoteDescription, 512);
|
||||||
|
|
||||||
|
if (ImGui.Button("Cancel")) {
|
||||||
|
PendingMapNotePosition = null;
|
||||||
|
_pendingNoteTitle = string.Empty;
|
||||||
|
_pendingNoteDescription = string.Empty;
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button("Add")) {
|
||||||
|
if (PendingMapNotePosition is { } pos && !string.IsNullOrWhiteSpace(_pendingNoteTitle)) {
|
||||||
|
System.MapNoteConfig.Notes.Add(new MapNote(pos.Territory, pos.Map, pos.X, pos.Y, _pendingNoteTitle.Trim(), _pendingNoteDescription?.Trim() ?? ""));
|
||||||
|
if (System.MapNoteConfig.Notes.Count > System.MapNoteConfig.MaxNotes) {
|
||||||
|
System.MapNoteConfig.Notes.RemoveAt(0);
|
||||||
|
}
|
||||||
|
System.MapNoteConfig.Save();
|
||||||
|
}
|
||||||
|
PendingMapNotePosition = null;
|
||||||
|
_pendingNoteTitle = string.Empty;
|
||||||
|
_pendingNoteDescription = string.Empty;
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void UpdateStyle()
|
private unsafe void UpdateStyle()
|
||||||
|
|||||||
+323
-14
@@ -1,16 +1,25 @@
|
|||||||
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;
|
||||||
|
|
||||||
public class MinimapWindow : Window
|
public class MinimapWindow : Window
|
||||||
{
|
{
|
||||||
|
private bool _wasLoggedIn;
|
||||||
|
|
||||||
public MinimapWindow() : base("HSMappy Minimap###HSMappyMinimap", new Vector2(200.0f, 200.0f))
|
public MinimapWindow() : base("HSMappy Minimap###HSMappyMinimap", new Vector2(200.0f, 200.0f))
|
||||||
{
|
{
|
||||||
DisableWindowSounds = true;
|
DisableWindowSounds = true;
|
||||||
@@ -22,12 +31,35 @@ public class MinimapWindow : Window
|
|||||||
|
|
||||||
public override void PreOpenCheck()
|
public override void PreOpenCheck()
|
||||||
{
|
{
|
||||||
if (Service.ClientState is { IsLoggedIn: false } or { IsPvP: true })
|
var isLoggedIn = Service.ClientState is { IsLoggedIn: true, IsPvP: false };
|
||||||
|
if (!isLoggedIn)
|
||||||
|
{
|
||||||
IsOpen = false;
|
IsOpen = false;
|
||||||
|
_wasLoggedIn = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore minimap when transitioning from login screen to in-game (ShowMinimap persists in config)
|
||||||
|
if (!_wasLoggedIn && System.SystemConfig.ShowMinimap)
|
||||||
|
UnCollapseOrShow();
|
||||||
|
_wasLoggedIn = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
@@ -51,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()
|
||||||
@@ -101,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 @@
|
|||||||
[{"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.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.1","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.1/latest.zip","IsHide":false,"IsTestingExclusive":false,"DownloadLinkTesting":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.1/latest.zip","DownloadLinkUpdate":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.1/latest.zip","LastUpdate":"1772140521"}]
|
[{"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.20: Fix marker refresh sometimes not running when accepting a quest or advancing a quest step. 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.20","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.20/latest.zip","IsHide":false,"IsTestingExclusive":false,"DownloadLinkTesting":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.20/latest.zip","DownloadLinkUpdate":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.20/latest.zip","LastUpdate":"1772591258"}]
|
||||||
|
|||||||
Reference in New Issue
Block a user