From b4638eb60bf8df108237b42cc9f3bc3ee769f152 Mon Sep 17 00:00:00 2001 From: Knack117 Date: Sun, 1 Mar 2026 00:57:18 -0500 Subject: [PATCH] v1.0.0.14: Movement Trail (Carbonite-style) - red dots show where you've been Made-with: Cursor --- .../MapWindowComponents/MapContextMenu.cs | 4 + Mappy/Controllers/IntegrationsController.cs | 12 +++ Mappy/Data/MovementTrailConfig.cs | 82 +++++++++++++++++++ Mappy/Data/SystemConfig.cs | 10 +++ Mappy/MapRenderer/MapRenderer.Core.cs | 1 + .../MapRenderer/MapRenderer.MinimapMarkers.cs | 29 +++++++ .../MapRenderer/MapRenderer.MovementTrail.cs | 41 ++++++++++ Mappy/Mappy.csproj | 2 +- Mappy/MappyPlugin.cs | 1 + Mappy/System.cs | 1 + Mappy/Windows/ConfigurationWindow.cs | 12 +++ Mappy/pluginmaster.json | 2 +- 12 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 Mappy/Data/MovementTrailConfig.cs create mode 100644 Mappy/MapRenderer/MapRenderer.MovementTrail.cs 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"}]