v1.0.0.14: Movement Trail (Carbonite-style) - red dots show where you've been

Made-with: Cursor
This commit is contained in:
2026-03-01 00:57:18 -05:00
parent 542da3a71b
commit b4638eb60b
12 changed files with 195 additions and 2 deletions
@@ -36,6 +36,10 @@ public unsafe class MapContextMenu
} }
} }
if (ImGui.MenuItem("Clear Movement Trail")) {
System.MovementTrailConfig.Clear();
}
if (ImGui.MenuItem("Place Flag")) { if (ImGui.MenuItem("Place Flag")) {
AgentMap.Instance()->FlagMarkerCount = 0; AgentMap.Instance()->FlagMarkerCount = 0;
AgentMap.Instance()->SetFlagMapMarker(AgentMap.Instance()->SelectedTerritoryId, AgentMap.Instance()->SelectedMapId, scaledResult.X, scaledResult.Y); AgentMap.Instance()->SetFlagMapMarker(AgentMap.Instance()->SelectedTerritoryId, AgentMap.Instance()->SelectedMapId, scaledResult.X, scaledResult.Y);
@@ -173,6 +173,18 @@ public unsafe class IntegrationsController : IDisposable
// so quest turn-in and objective progression during suppression still trigger refresh when suppression ends. // so quest turn-in and objective progression during suppression still trigger refresh when suppression ends.
_lastTempMarkerCount = tempCount; _lastTempMarkerCount = tempCount;
} }
// Movement trail: record player position when enabled (Carbonite-style)
if (System.SystemConfig.ShowMovementTrail && Service.ObjectTable.LocalPlayer is { } localPlayer) {
try {
var agent = AgentMap.Instance();
System.MovementTrailConfig.TryAddPoint(
agent->CurrentTerritoryId,
agent->CurrentMapId,
localPlayer.Position.X,
localPlayer.Position.Z);
} catch { /* ignore */ }
}
} }
/// <summary>Build a string of (QuestId, Sequence) for each active quest so we can detect step advances.</summary> /// <summary>Build a string of (QuestId, Sequence) for each active quest so we can detect step advances.</summary>
+82
View File
@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace Mappy.Data;
/// <summary>Single point on the movement trail (Carbonite-style).</summary>
public record MovementTrailPoint(uint Territory, uint Map, float X, float Y, double TimeStamp);
/// <summary>Config and storage for the movement trail (where you've been on the map).</summary>
public class MovementTrailConfig
{
private readonly List<MovementTrailPoint> _points = [];
private int _nextIndex;
private float _lastX = float.MinValue;
private float _lastY = float.MinValue;
private uint _lastTerritory;
public int MaxPoints { get; set; } = 100;
public float MinDistance { get; set; } = 2f;
public float FadeTimeSeconds { get; set; } = 60f;
public Vector4 TrailColor { get; set; } = new(1f, 0f, 0f, 0.9f); // Red like Carbonite
public IReadOnlyList<MovementTrailPoint> Points => _points;
private float EffectiveMinDistance => Mappy.System.SystemConfig?.MovementTrailMinDistance ?? MinDistance;
private float EffectiveFadeTime => Mappy.System.SystemConfig?.MovementTrailFadeTimeSeconds ?? FadeTimeSeconds;
private int EffectiveMaxPoints => Mappy.System.SystemConfig?.MovementTrailMaxPoints ?? MaxPoints;
public void Clear()
{
_points.Clear();
_nextIndex = 0;
_lastX = float.MinValue;
_lastY = float.MinValue;
}
/// <summary>Record a new trail point if the player has moved enough. Call from Framework.Update.</summary>
public void TryAddPoint(uint territory, uint map, float x, float y)
{
// Reset when changing territory
if (territory != _lastTerritory)
{
Clear();
_lastTerritory = territory;
}
var dx = x - _lastX;
var dy = y - _lastY;
var moveDist = MathF.Sqrt(dx * dx + dy * dy);
if (moveDist < EffectiveMinDistance && _lastX > float.MinValue)
return;
_lastX = x;
_lastY = y;
var now = DateTime.UtcNow.Subtract(DateTime.UnixEpoch).TotalSeconds;
var max = EffectiveMaxPoints;
if (_points.Count < max)
{
_points.Add(new MovementTrailPoint(territory, map, x, y, now));
}
else
{
_points[_nextIndex] = new MovementTrailPoint(territory, map, x, y, now);
_nextIndex = (_nextIndex + 1) % max;
}
}
/// <summary>Get points for the current map that haven't faded yet.</summary>
public IEnumerable<MovementTrailPoint> GetVisiblePoints(uint territoryId, uint mapId)
{
var now = DateTime.UtcNow.Subtract(DateTime.UnixEpoch).TotalSeconds;
var fade = EffectiveFadeTime;
return _points
.Where(p => p.Territory == territoryId && p.Map == mapId && (now - p.TimeStamp) < fade)
.ToList();
}
}
+10
View File
@@ -123,6 +123,16 @@ public class SystemConfig : CharacterConfiguration
/// <summary>Icon ID for other players on the minimap (default 60403, distinct from party 60421). Override if you prefer a different look.</summary> /// <summary>Icon ID for other players on the minimap (default 60403, distinct from party 60421). Override if you prefer a different look.</summary>
public uint MinimapOtherPlayerIconId = 60403; public uint MinimapOtherPlayerIconId = 60403;
// 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]
public bool DebugMode = false; public bool DebugMode = false;
+1
View File
@@ -377,6 +377,7 @@ public unsafe partial class MapRenderer : IDisposable
DrawPlayer(); DrawPlayer();
DrawStaticTextMarkers(); DrawStaticTextMarkers();
DrawMapNotes(); DrawMapNotes();
DrawMovementTrail();
DrawFlag(); DrawFlag();
} }
} }
@@ -62,6 +62,8 @@ public partial class MapRenderer
DrawMinimapFlag(contentTopLeft, TexToContent, scaleFactor, offsetX, offsetY); DrawMinimapFlag(contentTopLeft, TexToContent, scaleFactor, offsetX, offsetY);
// User map notes // User map notes
DrawMinimapMapNotes(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY); DrawMinimapMapNotes(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY);
// Movement trail
DrawMinimapMovementTrail(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY);
// Temporary (quest objectives, etc.) // Temporary (quest objectives, etc.)
DrawMinimapTempMarkers(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY, scale); DrawMinimapTempMarkers(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY, scale);
// Field markers (waymarks) // Field markers (waymarks)
@@ -729,6 +731,33 @@ public partial class MapRenderer
} }
} }
private unsafe void DrawMinimapMovementTrail(Vector2 contentTopLeft, Func<float, float, Vector2> texToContent, Vector2 size, float scaleFactor, float offsetX, float offsetY)
{
if (!System.SystemConfig.ShowMovementTrail) return;
var agent = AgentMap.Instance();
var territoryId = agent->CurrentTerritoryId;
var mapId = agent->CurrentMapId;
var now = DateTime.UtcNow.Subtract(DateTime.UnixEpoch).TotalSeconds;
var fadeTime = System.SystemConfig.MovementTrailFadeTimeSeconds;
foreach (var pt in System.MovementTrailConfig.GetVisiblePoints(territoryId, mapId).ToList()) {
var age = now - pt.TimeStamp;
var alpha = (float)((fadeTime - age) / fadeTime * 0.9);
if (alpha <= 0f) continue;
var tx = 1024.0f + (pt.X - offsetX) * scaleFactor;
var ty = 1024.0f + (pt.Y - offsetY) * scaleFactor;
var contentPos = texToContent(tx, ty);
if (!IsInMinimapBounds(contentPos, size, MinimapBoundsMargin)) continue;
var centerScreen = contentPos + contentTopLeft;
var trailSize = 4f * 0.75f;
var color = System.MovementTrailConfig.TrailColor with { W = alpha };
ImGui.GetWindowDrawList().AddCircleFilled(centerScreen, trailSize, ImGui.GetColorU32(color));
}
}
private unsafe void DrawMinimapMapNotes(Vector2 contentTopLeft, Func<float, float, Vector2> texToContent, Vector2 size, float scaleFactor, float offsetX, float offsetY) private unsafe void DrawMinimapMapNotes(Vector2 contentTopLeft, Func<float, float, Vector2> texToContent, Vector2 size, float scaleFactor, float offsetX, float offsetY)
{ {
var agent = AgentMap.Instance(); var agent = AgentMap.Instance();
@@ -0,0 +1,41 @@
using System;
using System.Linq;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Mappy.Classes;
using Mappy.Data;
namespace Mappy.MapRenderer;
public partial class MapRenderer
{
private unsafe void DrawMovementTrail()
{
if (!System.SystemConfig.ShowMovementTrail) return;
var agent = AgentMap.Instance();
var territoryId = agent->SelectedTerritoryId;
var mapId = agent->SelectedMapId;
var now = DateTime.UtcNow.Subtract(DateTime.UnixEpoch).TotalSeconds;
var fadeTime = System.SystemConfig.MovementTrailFadeTimeSeconds;
foreach (var pt in System.MovementTrailConfig.GetVisiblePoints(territoryId, mapId).ToList()) {
var age = now - pt.TimeStamp;
var alpha = (float)((fadeTime - age) / fadeTime * 0.9);
if (alpha <= 0f) continue;
// Same coordinate space as map notes: world X,Z
var pos = new Vector2(pt.X, pt.Y) * Scale * DrawHelpers.GetMapScaleFactor() + DrawHelpers.GetCombinedOffsetVector() * Scale;
var size = Math.Clamp(4 * Scale, 3f, 25f);
var screenPos = ImGui.GetWindowPos() + DrawPosition + pos;
var color = System.MovementTrailConfig.TrailColor with { W = alpha };
var drawList = ImGui.GetWindowDrawList();
drawList.AddCircleFilled(screenPos, size, ImGui.GetColorU32(color));
}
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
<Name>HSMappy</Name> <Name>HSMappy</Name>
<InternalName>HSMappy</InternalName> <InternalName>HSMappy</InternalName>
<Author>Knack117</Author> <Author>Knack117</Author>
<Version>1.0.0.13</Version> <Version>1.0.0.14</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>
+1
View File
@@ -32,6 +32,7 @@ public sealed class MappyPlugin : IDalamudPlugin
System.IconConfig = IconConfig.Load(); System.IconConfig = IconConfig.Load();
System.FlagConfig = FlagConfig.Load(); System.FlagConfig = FlagConfig.Load();
System.MapNoteConfig = MapNoteConfig.Load(); System.MapNoteConfig = MapNoteConfig.Load();
System.MovementTrailConfig = new MovementTrailConfig();
System.Teleporter = new Teleporter(Service.PluginInterface); System.Teleporter = new Teleporter(Service.PluginInterface);
+1
View File
@@ -18,6 +18,7 @@ public static class System
public static IconConfig IconConfig { get; set; } public static IconConfig IconConfig { get; set; }
public static FlagConfig FlagConfig { get; set; } public static FlagConfig FlagConfig { get; set; }
public static MapNoteConfig MapNoteConfig { get; set; } public static MapNoteConfig MapNoteConfig { get; set; }
public static MovementTrailConfig MovementTrailConfig { get; set; }
public static WindowManager WindowManager { get; set; } public static WindowManager WindowManager { get; set; }
public static MapWindow MapWindow { get; set; } public static MapWindow MapWindow { get; set; }
public static MinimapWindow MinimapWindow { get; set; } public static MinimapWindow MinimapWindow { get; set; }
+12
View File
@@ -80,6 +80,18 @@ public class MapFunctionsTab : ITabItem
configChanged |= ImGui.Checkbox("Center on Quest", ref System.SystemConfig.CenterOnQuest); configChanged |= ImGui.Checkbox("Center on Quest", ref System.SystemConfig.CenterOnQuest);
} }
ImGuiTweaks.Header("Movement Trail (Carbonite-style)");
using (ImRaii.PushIndent()) {
configChanged |= ImGui.Checkbox("Show Movement Trail", ref System.SystemConfig.ShowMovementTrail);
ImGui.TextDisabled("Draw a trail of red dots showing where you've been on the map.");
if (System.SystemConfig.ShowMovementTrail) {
configChanged |= ImGui.SliderFloat("Min Distance (world units)", ref System.SystemConfig.MovementTrailMinDistance, 0.5f, 10f);
configChanged |= ImGui.SliderFloat("Fade Time (seconds)", ref System.SystemConfig.MovementTrailFadeTimeSeconds, 10f, 300f);
configChanged |= ImGui.SliderInt("Max Points", ref System.SystemConfig.MovementTrailMaxPoints, 20, 500);
}
ImGuiHelpers.ScaledDummy(5.0f);
}
ImGuiTweaks.Header("Misc Options"); ImGuiTweaks.Header("Misc Options");
using (ImRaii.PushIndent()) { using (ImRaii.PushIndent()) {
configChanged |= ImGui.Checkbox("Show Misc Tooltips", ref System.SystemConfig.ShowMiscTooltips); configChanged |= ImGui.Checkbox("Show Misc Tooltips", ref System.SystemConfig.ShowMiscTooltips);
+1 -1
View File
@@ -1 +1 @@
[{"Author":"Knack117","Name":"HSMappy","Punchline":"A more versatile in-game map.","Description":"Replaces the in-game map with an ImGui implementation with several additional features. Fork with minimap improvements, quest radius on minimap, white gradient player cone, and more.","Changelog":"1.0.0.13: User-placed map notes with Title/Description; custom white-page icon; notes on minimap; Remove Note via context menu; Note List layout fix. 1.0.0.12: Other players on minimap use distinct icon (60403) from party markers. 1.0.0.11: Player/NPC tracking on minimap with Show Players and NPCs toggle. 1.0.0.10: Release build. Suppress silent refresh at start of OnOpenMapHook; remove debug logging. 1.0.0.9: Duty List quest click: don't Hide() when viewing quest map (SelectedMapId != CurrentMapId). 1.0.0.8: Cancel silent refresh when opening map from Duty List so it doesn't immediately close. 1.0.0.7: Duty List quest click opens Area Map even when Hide With Game GUI would block it. 1.0.0.6: Minimap stays open after client restart (restore on login). 1.0.0.5: Fix crash when map texture path is invalid (ArgumentOutOfRangeException in Lumina GetFileHash). 1.0.0.4: Temp marker circle refreshes when quest objective is progressed. 1.0.0.3: Fix marker cache refresh after quest turn-in; invalidate temp cache so old markers don't persist. 1.0.0.2: Red direction arrow on minimap pointing to player flag. 1.0.0.1: Duty List quest click keeps Area Map open; player flags show on minimap. 1.0.0.0: Initial HSMappy release. Minimap: quest radius circle (orange, transparent), tooltip; cone drawn under markers; white gradient cone; /hsmappy commands.","InternalName":"HSMappy","AssemblyVersion":"1.0.0.13","RepoUrl":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy","ApplicableVersion":"any","Tags":["map","mapping","overlay","utility"],"CategoryTags":["jobs"],"DalamudApiLevel":14,"DownloadLinkInstall":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.13/latest.zip","IsHide":false,"IsTestingExclusive":false,"DownloadLinkTesting":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.13/latest.zip","DownloadLinkUpdate":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.13/latest.zip","LastUpdate":"1760400000"}] [{"Author":"Knack117","Name":"HSMappy","Punchline":"A more versatile in-game map.","Description":"Replaces the in-game map with an ImGui implementation with several additional features. Fork with minimap improvements, quest radius on minimap, white gradient player cone, and more.","Changelog":"1.0.0.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.14","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.14/latest.zip","IsHide":false,"IsTestingExclusive":false,"DownloadLinkTesting":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.14/latest.zip","DownloadLinkUpdate":"http://brassnet.ddns.net:33983/KnackAtNite/HSMappy/releases/download/v1.0.0.14/latest.zip","LastUpdate":"1772326614"}]