diff --git a/Mappy/Classes/MapWindowComponents/MapContextMenu.cs b/Mappy/Classes/MapWindowComponents/MapContextMenu.cs
index e368804..3278ebd 100644
--- a/Mappy/Classes/MapWindowComponents/MapContextMenu.cs
+++ b/Mappy/Classes/MapWindowComponents/MapContextMenu.cs
@@ -36,6 +36,10 @@ public unsafe class MapContextMenu
}
}
+ if (ImGui.MenuItem("Clear Movement Trail")) {
+ System.MovementTrailConfig.Clear();
+ }
+
if (ImGui.MenuItem("Place Flag")) {
AgentMap.Instance()->FlagMarkerCount = 0;
AgentMap.Instance()->SetFlagMapMarker(AgentMap.Instance()->SelectedTerritoryId, AgentMap.Instance()->SelectedMapId, scaledResult.X, scaledResult.Y);
diff --git a/Mappy/Controllers/IntegrationsController.cs b/Mappy/Controllers/IntegrationsController.cs
index a1a6b39..98c14ff 100644
--- a/Mappy/Controllers/IntegrationsController.cs
+++ b/Mappy/Controllers/IntegrationsController.cs
@@ -173,6 +173,18 @@ public unsafe class IntegrationsController : IDisposable
// so quest turn-in and objective progression during suppression still trigger refresh when suppression ends.
_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 */ }
+ }
}
/// Build a string of (QuestId, Sequence) for each active quest so we can detect step advances.
diff --git a/Mappy/Data/MovementTrailConfig.cs b/Mappy/Data/MovementTrailConfig.cs
new file mode 100644
index 0000000..df8b9dd
--- /dev/null
+++ b/Mappy/Data/MovementTrailConfig.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+
+namespace Mappy.Data;
+
+/// Single point on the movement trail (Carbonite-style).
+public record MovementTrailPoint(uint Territory, uint Map, float X, float Y, double TimeStamp);
+
+/// Config and storage for the movement trail (where you've been on the map).
+public class MovementTrailConfig
+{
+ private readonly List _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 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;
+ }
+
+ /// Record a new trail point if the player has moved enough. Call from Framework.Update.
+ 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;
+ }
+ }
+
+ /// Get points for the current map that haven't faded yet.
+ public IEnumerable 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();
+ }
+}
diff --git a/Mappy/Data/SystemConfig.cs b/Mappy/Data/SystemConfig.cs
index 3019f02..227400b 100644
--- a/Mappy/Data/SystemConfig.cs
+++ b/Mappy/Data/SystemConfig.cs
@@ -123,6 +123,16 @@ public class SystemConfig : CharacterConfiguration
/// Icon ID for other players on the minimap (default 60403, distinct from party 60421). Override if you prefer a different look.
public uint MinimapOtherPlayerIconId = 60403;
+ // Movement Trail (Carbonite-style: show where you've been)
+ /// Draw a red trail of dots on the map showing where you've been.
+ public bool ShowMovementTrail = false;
+ /// Minimum distance (world units) before adding a new trail point.
+ public float MovementTrailMinDistance = 2f;
+ /// How long (seconds) trail points stay visible before fading out.
+ public float MovementTrailFadeTimeSeconds = 60f;
+ /// Maximum number of trail points to keep.
+ public int MovementTrailMaxPoints = 100;
+
// Do not persist this setting
[JsonIgnore]
public bool DebugMode = false;
diff --git a/Mappy/MapRenderer/MapRenderer.Core.cs b/Mappy/MapRenderer/MapRenderer.Core.cs
index 55171a7..85aa7ce 100644
--- a/Mappy/MapRenderer/MapRenderer.Core.cs
+++ b/Mappy/MapRenderer/MapRenderer.Core.cs
@@ -377,6 +377,7 @@ public unsafe partial class MapRenderer : IDisposable
DrawPlayer();
DrawStaticTextMarkers();
DrawMapNotes();
+ DrawMovementTrail();
DrawFlag();
}
}
\ No newline at end of file
diff --git a/Mappy/MapRenderer/MapRenderer.MinimapMarkers.cs b/Mappy/MapRenderer/MapRenderer.MinimapMarkers.cs
index 7ff8a2d..a472c3f 100644
--- a/Mappy/MapRenderer/MapRenderer.MinimapMarkers.cs
+++ b/Mappy/MapRenderer/MapRenderer.MinimapMarkers.cs
@@ -62,6 +62,8 @@ public partial class MapRenderer
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.)
DrawMinimapTempMarkers(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY, scale);
// Field markers (waymarks)
@@ -729,6 +731,33 @@ public partial class MapRenderer
}
}
+ private unsafe void DrawMinimapMovementTrail(Vector2 contentTopLeft, Func 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 texToContent, Vector2 size, float scaleFactor, float offsetX, float offsetY)
{
var agent = AgentMap.Instance();
diff --git a/Mappy/MapRenderer/MapRenderer.MovementTrail.cs b/Mappy/MapRenderer/MapRenderer.MovementTrail.cs
new file mode 100644
index 0000000..68fbff6
--- /dev/null
+++ b/Mappy/MapRenderer/MapRenderer.MovementTrail.cs
@@ -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));
+ }
+ }
+}
diff --git a/Mappy/Mappy.csproj b/Mappy/Mappy.csproj
index cc6eeee..445cae7 100644
--- a/Mappy/Mappy.csproj
+++ b/Mappy/Mappy.csproj
@@ -4,7 +4,7 @@
HSMappy
HSMappy
Knack117
- 1.0.0.13
+ 1.0.0.14
A more versatile in-game map.
Replaces the in-game map with an ImGui implementation with several additional features. Fork with minimap improvements, quest radius on minimap, and more.
http://brassnet.ddns.net:33983/KnackAtNite/HSMappy
diff --git a/Mappy/MappyPlugin.cs b/Mappy/MappyPlugin.cs
index c189b0b..905cdbb 100644
--- a/Mappy/MappyPlugin.cs
+++ b/Mappy/MappyPlugin.cs
@@ -32,6 +32,7 @@ public sealed class MappyPlugin : IDalamudPlugin
System.IconConfig = IconConfig.Load();
System.FlagConfig = FlagConfig.Load();
System.MapNoteConfig = MapNoteConfig.Load();
+ System.MovementTrailConfig = new MovementTrailConfig();
System.Teleporter = new Teleporter(Service.PluginInterface);
diff --git a/Mappy/System.cs b/Mappy/System.cs
index 71d482d..298670d 100644
--- a/Mappy/System.cs
+++ b/Mappy/System.cs
@@ -18,6 +18,7 @@ public static class System
public static IconConfig IconConfig { 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 MapWindow MapWindow { get; set; }
public static MinimapWindow MinimapWindow { get; set; }
diff --git a/Mappy/Windows/ConfigurationWindow.cs b/Mappy/Windows/ConfigurationWindow.cs
index cc57a64..1381c1d 100644
--- a/Mappy/Windows/ConfigurationWindow.cs
+++ b/Mappy/Windows/ConfigurationWindow.cs
@@ -80,6 +80,18 @@ public class MapFunctionsTab : ITabItem
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");
using (ImRaii.PushIndent()) {
configChanged |= ImGui.Checkbox("Show Misc Tooltips", ref System.SystemConfig.ShowMiscTooltips);
diff --git a/Mappy/pluginmaster.json b/Mappy/pluginmaster.json
index 9880775..f459354 100644
--- a/Mappy/pluginmaster.json
+++ b/Mappy/pluginmaster.json
@@ -1 +1 @@
-[{"Author":"Knack117","Name":"HSMappy","Punchline":"A more versatile in-game map.","Description":"Replaces the in-game map with an ImGui implementation with several additional features. Fork with minimap improvements, quest radius on minimap, white gradient player cone, and more.","Changelog":"1.0.0.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"}]