Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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;
|
||||
|
||||
/// <summary>Sentinel IconId for user-placed map notes (custom-drawn white page with writing).</summary>
|
||||
public const uint MapNoteIconId = 65000;
|
||||
|
||||
/// <summary>
|
||||
/// Offset Vector of SelectedX, SelectedY, scaled with SelectedSizeFactor
|
||||
/// </summary>
|
||||
@@ -143,9 +146,7 @@ public static class DrawHelpers
|
||||
|
||||
private static void DrawIcon(MarkerInfo markerInfo)
|
||||
{
|
||||
var texture = Service.TextureProvider.GetFromGameIcon(markerInfo.IconId).GetWrapOrEmpty();
|
||||
var scale = System.SystemConfig.ScaleWithZoom ? markerInfo.Scale : 1.0f;
|
||||
|
||||
var iconScale = System.SystemConfig.IconScale;
|
||||
|
||||
if (markerInfo.IconId is 60401 or 60402) {
|
||||
@@ -158,11 +159,27 @@ public static class DrawHelpers
|
||||
iconScale = 0.42f;
|
||||
}
|
||||
|
||||
ImGui.SetCursorPos(markerInfo.Position + markerInfo.Offset - texture.Size * iconScale / 2.0f * scale * System.IconConfig.IconSettingMap[markerInfo.IconId].Scale);
|
||||
var cursorScreenPos = ImGui.GetCursorScreenPos();
|
||||
var iconSize = texture.Size * scale * iconScale * System.IconConfig.IconSettingMap[markerInfo.IconId].Scale;
|
||||
var setting = System.IconConfig.IconSettingMap[markerInfo.IconId];
|
||||
var sizeMultiplier = scale * iconScale * setting.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) {
|
||||
foreach (var x in Enumerable.Range(-1, 3)) {
|
||||
@@ -241,7 +258,14 @@ public static class DrawHelpers
|
||||
if (markerInfo.PrimaryText?.Invoke() is { Length: > 0 } primaryText) {
|
||||
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.SameLine();
|
||||
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) =>
|
||||
iconId switch
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Numerics;
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
@@ -17,14 +17,26 @@ public unsafe class MapContextMenu
|
||||
|
||||
if (!contextMenu) return;
|
||||
|
||||
if (ImGui.MenuItem("Place Flag")) {
|
||||
var cursorPosition = ImGui.GetMousePosOnOpeningCurrentPopup(); // Get initial cursor position (screen relative)
|
||||
var mapChildOffset = mapDrawOffset; // Get the screen position we started drawing the map at
|
||||
var mapDrawPositionOffset = System.MapRenderer.DrawPosition; // Get the map texture top left offset vector
|
||||
var textureClickLocation = (cursorPosition - mapChildOffset - mapDrawPositionOffset) / MapRenderer.MapRenderer.Scale; // Math
|
||||
var result = textureClickLocation - new Vector2(1024.0f, 1024.0f); // One of our vectors made the map centered, undo it.
|
||||
var scaledResult = result / DrawHelpers.GetMapScaleFactor() + DrawHelpers.GetRawMapOffsetVector(); // Apply offset x/y and scalefactor
|
||||
var cursorPosition = ImGui.GetMousePosOnOpeningCurrentPopup();
|
||||
var textureClickLocation = (cursorPosition - mapDrawOffset - System.MapRenderer.DrawPosition) / MapRenderer.MapRenderer.Scale;
|
||||
var result = textureClickLocation - new Vector2(1024.0f, 1024.0f);
|
||||
var scaledResult = result / DrawHelpers.GetMapScaleFactor() + DrawHelpers.GetRawMapOffsetVector();
|
||||
var noteAtCursor = System.MapNoteConfig.FindNoteAt(
|
||||
AgentMap.Instance()->SelectedTerritoryId, AgentMap.Instance()->SelectedMapId, scaledResult.X, scaledResult.Y);
|
||||
|
||||
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("Place Flag")) {
|
||||
AgentMap.Instance()->FlagMarkerCount = 0;
|
||||
AgentMap.Instance()->SetFlagMapMarker(AgentMap.Instance()->SelectedTerritoryId, AgentMap.Instance()->SelectedMapId, scaledResult.X, scaledResult.Y);
|
||||
AgentChatLog.Instance()->InsertTextCommandParam(1048, false);
|
||||
@@ -65,5 +77,9 @@ public unsafe class MapContextMenu
|
||||
if (ImGui.MenuItem("Open Flag List", false, System.WindowManager.GetWindow<FlagHistoryWindow>() is null)) {
|
||||
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;
|
||||
}
|
||||
|
||||
// If the window actually considered closed by the agent.
|
||||
if (AgentMap.Instance()->AddonId is 0)
|
||||
{
|
||||
System.WindowManager.GetWindow<MapWindow>()?.Close();
|
||||
|
||||
@@ -35,6 +35,8 @@ public unsafe class IntegrationsController : IDisposable
|
||||
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>
|
||||
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>
|
||||
public static bool SilentRefreshInProgress { get; private set; }
|
||||
@@ -142,6 +144,9 @@ public unsafe class IntegrationsController : IDisposable
|
||||
if (_suppressSilentRefreshFramesRemaining > 0) {
|
||||
_suppressSilentRefreshFramesRemaining--;
|
||||
}
|
||||
if (_userOpenedMapFramesRemaining > 0) {
|
||||
_userOpenedMapFramesRemaining--;
|
||||
}
|
||||
var skipQuestTempRefresh = _suppressSilentRefreshFramesRemaining > 0;
|
||||
|
||||
var questCount = GetActiveQuestCount();
|
||||
@@ -159,10 +164,15 @@ public unsafe class IntegrationsController : IDisposable
|
||||
RequestSilentRefresh(); // objectives added (e.g. new quest)
|
||||
if (_lastQuestSequenceSnapshot.Length > 0 && sequenceSnapshot != _lastQuestSequenceSnapshot)
|
||||
RequestSilentRefresh(); // quest step advanced (multi-step objective)
|
||||
}
|
||||
_lastQuestCount = questCount;
|
||||
_lastTempMarkerCount = tempCount;
|
||||
_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>
|
||||
@@ -184,9 +194,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). 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()
|
||||
{
|
||||
_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
|
||||
}
|
||||
|
||||
@@ -204,6 +216,9 @@ public unsafe class IntegrationsController : IDisposable
|
||||
var currentMapId = agent->CurrentMapId;
|
||||
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;
|
||||
agent->OpenMapByMapId(currentMapId, 0, true);
|
||||
agent->ResetMapMarkers();
|
||||
@@ -251,8 +266,19 @@ public unsafe class IntegrationsController : IDisposable
|
||||
{
|
||||
Service.Log.Verbose("[OnShow] Beginning Show");
|
||||
|
||||
// If you managed to open the window while the agent says it should be closed
|
||||
if (System.MapWindow.IsOpen && AgentMap.Instance()->AddonId is 0)
|
||||
// When user just opened via Duty List / gathering / flag / teleport, pass through immediately.
|
||||
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.");
|
||||
System.MapWindow.Close();
|
||||
@@ -264,15 +290,10 @@ public unsafe class IntegrationsController : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
if (AgentMap.Instance()->AddonId is not 0 &&
|
||||
AgentMap.Instance()->CurrentMapId != AgentMap.Instance()->SelectedMapId)
|
||||
// CurrentMapId != SelectedMapId = viewing quest map in different zone; pass through, don't Hide()
|
||||
if (addonId is not 0 && currentMapId != selectedMapId)
|
||||
{
|
||||
if (!System.SystemConfig.KeepOpen)
|
||||
{
|
||||
AgentMap.Instance()->Hide();
|
||||
}
|
||||
|
||||
Service.Log.Verbose("[OnShow] Vanilla tried to return to current map, aborted.");
|
||||
showMapHook!.Original(agent, a1, a2);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -285,9 +306,22 @@ public unsafe class IntegrationsController : IDisposable
|
||||
showMapHook!.Original(agent, a1, a2);
|
||||
}, 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) =>
|
||||
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);
|
||||
|
||||
switch (mapInfo->Type)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -112,10 +112,16 @@ public class SystemConfig : CharacterConfiguration
|
||||
public bool MinimapShowQuestDirectionArrow = true;
|
||||
/// <summary>Show direction arrows for nearby FATEs on the minimap (purple, matching FATE marker).</summary>
|
||||
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>
|
||||
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>
|
||||
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;
|
||||
|
||||
// Do not persist this setting
|
||||
[JsonIgnore]
|
||||
|
||||
@@ -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).
|
||||
|
||||
// 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 fgPath = $"{agent->SelectedMapPath}.tex";
|
||||
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);
|
||||
if (System.SystemConfig.MinimapShowFateDirectionArrows)
|
||||
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()
|
||||
@@ -344,13 +346,23 @@ public unsafe partial class MapRenderer : IDisposable
|
||||
|
||||
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)) {
|
||||
return Service.DataManager.GameData.GetFileFromDisk<TexFile>(path);
|
||||
}
|
||||
|
||||
return Service.DataManager.GetFile<TexFile>(path);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawMapMarkers()
|
||||
@@ -364,6 +376,7 @@ public unsafe partial class MapRenderer : IDisposable
|
||||
DrawFieldMarkers();
|
||||
DrawPlayer();
|
||||
DrawStaticTextMarkers();
|
||||
DrawMapNotes();
|
||||
DrawFlag();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
@@ -18,6 +19,52 @@ namespace Mappy.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()
|
||||
{
|
||||
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 Lumina.Excel.Sheets;
|
||||
using Mappy.Classes;
|
||||
using Mappy.Data;
|
||||
using Mappy.Extensions;
|
||||
using KamiLib.Extensions;
|
||||
using FieldMarkerSheet = Lumina.Excel.Sheets.FieldMarker;
|
||||
@@ -55,8 +56,12 @@ public partial class MapRenderer
|
||||
DrawMinimapGatheringMarkers(contentTopLeft, TexToContent, size);
|
||||
// Party / alliance members (same marker as main map)
|
||||
DrawMinimapGroupMembers(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY);
|
||||
// Player / NPC tracking (other players, enemies, bosses, etc.)
|
||||
DrawMinimapGameObjects(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY);
|
||||
// User flag
|
||||
DrawMinimapFlag(contentTopLeft, TexToContent, scaleFactor, offsetX, offsetY);
|
||||
// User map notes
|
||||
DrawMinimapMapNotes(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY);
|
||||
// Temporary (quest objectives, etc.)
|
||||
DrawMinimapTempMarkers(contentTopLeft, TexToContent, size, scaleFactor, offsetX, offsetY, scale);
|
||||
// Field markers (waymarks)
|
||||
@@ -150,14 +155,7 @@ public partial class MapRenderer
|
||||
list.Add((tx, ty, tooltip, iconId));
|
||||
}
|
||||
|
||||
if (agent->FlagMarkerCount > 0) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
// Flag excluded: drawn separately with red arrow via DrawMinimapFlagDirectionArrow
|
||||
|
||||
if (agent->TempMapMarkerCount > 0) {
|
||||
var span = new Span<TempMapMarker>(Unsafe.AsPointer(ref agent->TempMapMarkers[0]), agent->TempMapMarkerCount);
|
||||
@@ -290,6 +288,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)
|
||||
{
|
||||
// Use a large margin so we don't cull markers that are panned slightly off (zoomed in).
|
||||
@@ -317,6 +374,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>
|
||||
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>
|
||||
private static readonly Dictionary<uint, List<CachedEventMarker>> EventMarkerCache = new();
|
||||
|
||||
@@ -669,6 +729,30 @@ public partial class MapRenderer
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var agent = AgentMap.Instance();
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<Name>HSMappy</Name>
|
||||
<InternalName>HSMappy</InternalName>
|
||||
<Author>Knack117</Author>
|
||||
<Version>1.0.0.1</Version>
|
||||
<Version>1.0.0.13</Version>
|
||||
<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>
|
||||
<RepoUrl>http://brassnet.ddns.net:33983/KnackAtNite/HSMappy</RepoUrl>
|
||||
|
||||
@@ -31,6 +31,7 @@ public sealed class MappyPlugin : IDalamudPlugin
|
||||
System.SystemConfig = SystemConfig.Load();
|
||||
System.IconConfig = IconConfig.Load();
|
||||
System.FlagConfig = FlagConfig.Load();
|
||||
System.MapNoteConfig = MapNoteConfig.Load();
|
||||
|
||||
System.Teleporter = new Teleporter(Service.PluginInterface);
|
||||
|
||||
@@ -86,6 +87,14 @@ public sealed class MappyPlugin : IDalamudPlugin
|
||||
DisableDelegate = _ => System.WindowManager.GetWindow<FlagHistoryWindow>()?.Close(),
|
||||
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();
|
||||
|
||||
@@ -17,6 +17,7 @@ public static class System
|
||||
public static SystemConfig SystemConfig { get; set; }
|
||||
public static IconConfig IconConfig { get; set; }
|
||||
public static FlagConfig FlagConfig { get; set; }
|
||||
public static MapNoteConfig MapNoteConfig { get; set; }
|
||||
public static WindowManager WindowManager { get; set; }
|
||||
public static MapWindow MapWindow { get; set; }
|
||||
public static MinimapWindow MinimapWindow { get; set; }
|
||||
|
||||
@@ -280,9 +280,22 @@ public class MinimapOptionsTab : ITabItem
|
||||
configChanged |= ImGui.Checkbox("Show FATE Direction Arrows", ref System.SystemConfig.MinimapShowFateDirectionArrows);
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||
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);
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||
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);
|
||||
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.");
|
||||
|
||||
@@ -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 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 HoverFlags HoveredFlags { get; private set; }
|
||||
public bool ProcessingCommand { get; set; }
|
||||
@@ -48,7 +55,6 @@ public class MapWindow : Window
|
||||
|
||||
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)
|
||||
{
|
||||
Service.Log.Debug("[OnShow] MapWindow can not be open now.");
|
||||
@@ -244,6 +250,49 @@ public class MapWindow : Window
|
||||
|
||||
// Draw Context Menu
|
||||
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()
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace Mappy.Windows;
|
||||
|
||||
public class MinimapWindow : Window
|
||||
{
|
||||
private bool _wasLoggedIn;
|
||||
|
||||
public MinimapWindow() : base("HSMappy Minimap###HSMappyMinimap", new Vector2(200.0f, 200.0f))
|
||||
{
|
||||
DisableWindowSounds = true;
|
||||
@@ -22,8 +24,18 @@ public class MinimapWindow : Window
|
||||
|
||||
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;
|
||||
_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()
|
||||
|
||||
@@ -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.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"}]
|
||||
|
||||
Reference in New Issue
Block a user