8 Commits

8 changed files with 130 additions and 23 deletions
+27 -4
View File
@@ -159,10 +159,15 @@ 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)
}
_lastQuestCount = questCount; _lastQuestCount = questCount;
_lastTempMarkerCount = tempCount; _lastTempMarkerCount = tempCount;
_lastQuestSequenceSnapshot = sequenceSnapshot; _lastQuestSequenceSnapshot = sequenceSnapshot;
} 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> /// <summary>Build a string of (QuestId, Sequence) for each active quest so we can detect step advances.</summary>
@@ -184,9 +189,11 @@ 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>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. Cancels any in-progress silent refresh so we don't Hide() the map the user just opened.</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 +211,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();
@@ -258,13 +268,18 @@ public unsafe class IntegrationsController : IDisposable
System.MapWindow.Close(); System.MapWindow.Close();
} }
if (!ShouldShowMap()) // Allow map when user explicitly opened via Duty List / gathering / flag / teleport
if (!ShouldShowMap() && !System.MapWindow.ProcessingCommand)
{ {
Service.Log.Debug("[OnShow] Condition to open map is rejected, aborting."); Service.Log.Debug("[OnShow] Condition to open map is rejected, aborting.");
return; return;
} }
if (AgentMap.Instance()->AddonId is not 0 && // When user opened via Duty List (quest/gathering/flag/teleport), SelectedMapId != CurrentMapId is expected
// (viewing quest map, not current zone). Don't Hide() - let the map show.
var userRequestedMap = System.MapWindow.ProcessingCommand;
if (!userRequestedMap &&
AgentMap.Instance()->AddonId is not 0 &&
AgentMap.Instance()->CurrentMapId != AgentMap.Instance()->SelectedMapId) AgentMap.Instance()->CurrentMapId != AgentMap.Instance()->SelectedMapId)
{ {
if (!System.SystemConfig.KeepOpen) if (!System.SystemConfig.KeepOpen)
@@ -285,9 +300,17 @@ 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(() =>
{ {
// Set before Original so ShowMap (if called synchronously) can bypass ShouldShowMap rejection
if (IsUserInitiatedMapOpen(mapInfo->Type))
System.MapWindow.ProcessingCommand = true;
openMapHook!.Original(agent, mapInfo); openMapHook!.Original(agent, mapInfo);
switch (mapInfo->Type) switch (mapInfo->Type)
+2
View File
@@ -112,6 +112,8 @@ 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>
+15 -3
View File
@@ -214,7 +214,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;
@@ -282,6 +282,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 +346,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()
@@ -150,14 +150,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 +283,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 +369,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();
+1 -1
View File
@@ -4,7 +4,7 @@
<Name>HSMappy</Name> <Name>HSMappy</Name>
<InternalName>HSMappy</InternalName> <InternalName>HSMappy</InternalName>
<Author>Knack117</Author> <Author>Knack117</Author>
<Version>1.0.0.1</Version> <Version>1.0.0.9</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>
+3
View File
@@ -280,6 +280,9 @@ 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).");
+13 -1
View File
@@ -11,6 +11,8 @@ 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,8 +24,18 @@ 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()
+1 -1
View File
@@ -1 +1 @@
[{"Author":"Knack117","Name":"HSMappy","Punchline":"A more versatile in-game map.","Description":"Replaces the in-game map with an ImGui implementation with several additional features. Fork with minimap improvements, quest radius on minimap, white gradient player cone, and more.","Changelog":"1.0.0.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.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.9","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.9/latest.zip","IsHide":false,"IsTestingExclusive":false,"DownloadLinkTesting":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.9/latest.zip","DownloadLinkUpdate":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.9/latest.zip","LastUpdate":"1772233604"}]