using System; 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.Classes; using Mappy.Data; using SeString = Dalamud.Game.Text.SeStringHandling.SeString; namespace Mappy.Classes; public class MarkerInfo { public required Vector2 Position { get; set; } public required Vector2 Offset { get; set; } public required float Scale { get; set; } public uint? ObjectiveId { get; init; } public uint? DataId { get; set; } public MarkerType MarkerType { get; set; } public uint IconId { get; set; } public Func? PrimaryText { get; set; } public Func? SecondaryText { get; set; } public float? Radius { get; set; } public Vector4 RadiusColor { get; set; } = KnownColor.CornflowerBlue.Vector(); public Vector4 RadiusOutlineColor { get; set; } = KnownColor.CornflowerBlue.Vector(); public Action? OnRightClicked { get; set; } public Action? OnLeftClicked { get; set; } public bool IsDynamicMarker { get; init; } } public static class DrawHelpers { private static bool DebugMode => System.SystemConfig.DebugMode; public const uint QuestionMarkIcon = 60071; /// Sentinel IconId for user-placed map notes (custom-drawn white page with writing). public const uint MapNoteIconId = 65000; /// /// Offset Vector of SelectedX, SelectedY, scaled with SelectedSizeFactor /// public static Vector2 GetMapOffsetVector() => GetRawMapOffsetVector() * GetMapScaleFactor(); /// /// Unscaled Vector of SelectedX, SelectedY /// public static unsafe Vector2 GetRawMapOffsetVector() => new(AgentMap.Instance()->SelectedOffsetX, AgentMap.Instance()->SelectedOffsetY); /// /// Selected Scale Factor /// public static unsafe float GetMapScaleFactor() => AgentMap.Instance()->SelectedMapSizeFactorFloat; /// /// 1024 vector, center offset vector /// public static Vector2 GetMapCenterOffsetVector() => new(1024.0f, 1024.0f); /// /// Offset for the top left corner of the drawn map /// public static Vector2 GetCombinedOffsetVector() => -GetMapOffsetVector() + GetMapCenterOffsetVector(); public static void DrawMapMarker(MarkerInfo markerInfo) { if (markerInfo.IconId is 0) return; // Don't draw markers that are positioned off the map texture if (markerInfo.Position.X < 0.0f || markerInfo.Position.X > 2048.0f * markerInfo.Scale || markerInfo.Position.Y < 0.0f || markerInfo.Position.Y > 2048.0f * markerInfo.Scale) return; markerInfo.IconId = markerInfo.IconId switch { // Translate circle markers that don't have icons, into [?] icon >= 60483 and <= 60494 => QuestionMarkIcon, // Translate Gemstone Trader Icon into smaller version... why square, why. 60091 => 61731, // Leave all other icons as they were _ => markerInfo.IconId, }; if (DebugMode) { markerInfo.SecondaryText = markerInfo.PrimaryText; markerInfo.PrimaryText = () => $"[Debug] IconId: {markerInfo.IconId}"; } // If this is the first time we have seen this iconId, save it if (System.IconConfig.IconSettingMap.TryAdd(markerInfo.IconId, new IconSetting { IconId = markerInfo.IconId, })) { System.IconConfig.Save(); } // If this icon is disabled, don't even process it if (System.IconConfig.IconSettingMap[markerInfo.IconId] is { Hide: true }) { return; } // Only process modules for Dynamic Markers if (markerInfo.IsDynamicMarker) { foreach (var module in System.Modules) { if (module.ProcessMarker(markerInfo)) { break; } } } DrawRadiusUnderlay(markerInfo); DrawIcon(markerInfo); ProcessInteractions(markerInfo); DrawTooltip(markerInfo); } private static unsafe void DrawRadiusUnderlay(MarkerInfo markerInfo) { if (markerInfo is not { Radius: { } markerRadius and > 1.0f }) return; var center = markerInfo.Position + markerInfo.Offset + ImGui.GetWindowPos(); DrawRadiusCircle(center, markerRadius, markerInfo.Scale, AgentMap.Instance()->SelectedMapSizeFactorFloat, markerInfo.RadiusColor with { W = System.SystemConfig.AreaColor.W }, markerInfo.RadiusOutlineColor with { W = System.SystemConfig.AreaOutlineColor.W }); } /// /// Draw the quest/area radius circle using the same formula as the area map. /// Used by both the area map (DrawRadiusUnderlay) and the minimap so behavior is identical. /// public static unsafe void DrawRadiusCircle(Vector2 centerScreen, float markerRadius, float mapScale, float sizeFactor, Vector4? fillColor = null, Vector4? outlineColor = null) { if (markerRadius <= 1.0f) return; var radiusPixels = markerRadius * mapScale * sizeFactor; if (radiusPixels < 0.5f) return; var fill = ImGui.GetColorU32(fillColor ?? System.SystemConfig.AreaColor); var outline = ImGui.GetColorU32(outlineColor ?? System.SystemConfig.AreaOutlineColor); var drawList = ImGui.GetWindowDrawList(); drawList.AddCircleFilled(centerScreen, radiusPixels, fill); drawList.AddCircle(centerScreen, radiusPixels, outline, 0, 3.0f); } private static void DrawIcon(MarkerInfo markerInfo) { var scale = System.SystemConfig.ScaleWithZoom ? markerInfo.Scale : 1.0f; var iconScale = System.SystemConfig.IconScale; if (markerInfo.IconId is 60401 or 60402) { scale *= 2.0f; } // Fixed scale not supported for map region markers if (IsRegionIcon(markerInfo.IconId)) { scale = markerInfo.Scale; iconScale = 0.42f; } var setting = System.IconConfig.IconSettingMap[markerInfo.IconId]; var sizeMultiplier = scale * iconScale * setting.Scale; 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)) { foreach (var y in Enumerable.Range(-1, 3)) { ImGui.GetWindowDrawList().AddRect(cursorScreenPos + new Vector2(x, y), cursorScreenPos + iconSize, ImGui.GetColorU32(KnownColor.White.Vector()), 3.0f); } } ImGui.GetWindowDrawList().AddRect(cursorScreenPos, cursorScreenPos + iconSize, ImGui.GetColorU32(KnownColor.Red.Vector()), 3.0f); } } public static void DrawText(MarkerInfo markerInfo, SeString text) => DrawText(markerInfo, text.ToString()); public static void DrawText(MarkerInfo markerInfo, string text) { using var largeFont = System.LargeAxisFontHandle.Push(); ImGui.SetWindowFontScale(markerInfo.Scale); var textSize = ImGui.CalcTextSize(text); var drawPosition = markerInfo.Position + markerInfo.Offset + ImGui.GetWindowPos() - textSize / 2.0f; drawPosition = new Vector2(MathF.Round(drawPosition.X), MathF.Round(drawPosition.Y)); if (System.SystemConfig.DebugMode) { ImGui.GetWindowDrawList().AddCircleFilled(markerInfo.Position + markerInfo.Offset + ImGui.GetWindowPos(), 5.0f, ImGui.GetColorU32(KnownColor.Red.Vector())); ImGui.GetWindowDrawList().AddRect(drawPosition, drawPosition + textSize, ImGui.GetColorU32(KnownColor.Green.Vector()), 3.0f); } foreach (var x in Enumerable.Range(-1, 3)) { foreach (var y in Enumerable.Range(-1, 3)) { if (x is 0 && y is 0) continue; ImGui.SetCursorScreenPos(drawPosition + new Vector2(x, y)); ImGui.TextColored(KnownColor.Black.Vector(), text); } } ImGui.SetCursorScreenPos(drawPosition); ImGui.TextColored(KnownColor.White.Vector(), text); ImGui.SetWindowFontScale(1.0f); } private static void ProcessInteractions(MarkerInfo markerInfo) { if (System.IconConfig.IconSettingMap[markerInfo.IconId] is not { AllowClick: true }) return; if (markerInfo is { OnRightClicked: { } rightClickAction } && ImGui.IsItemClicked(ImGuiMouseButton.Right)) { rightClickAction.Invoke(); } if (markerInfo is { OnLeftClicked: { } leftClickAction } && ImGui.IsItemClicked(ImGuiMouseButton.Left)) { leftClickAction.Invoke(); } } private static unsafe void DrawTooltip(MarkerInfo markerInfo) { if (System.IconConfig.IconSettingMap[markerInfo.IconId] is { AllowTooltip: false } && !DebugMode) { return; } var isActivatedViaRadius = false; if (markerInfo is { Radius: { } sameRadius and > 1.0f }) { var center = markerInfo.Position + markerInfo.Offset + ImGui.GetWindowPos(); var radius = sameRadius * markerInfo.Scale * AgentMap.Instance()->SelectedMapSizeFactorFloat; if (Vector2.Distance(ImGui.GetMousePos() - System.MapWindow.MapDrawOffset + ImGui.GetWindowPos(), center) <= radius && System.MapWindow.HoveredFlags.Any()) { isActivatedViaRadius = true; } } if (isActivatedViaRadius || ImGui.IsItemHovered()) { 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); var cursorPosition = ImGui.GetCursorPos(); ImGui.Text(primaryText); if (markerInfo.SecondaryText?.Invoke() is { Length: > 0 } secondaryText) { ImGui.SameLine(); ImGui.SetCursorPos(cursorPosition); ImGuiTweaks.TextColoredUnformatted(KnownColor.Gray.Vector(), $"\n{secondaryText}"); } } } } /// Draw a custom "white page with writing" icon for map notes, centered at the given position. public static void DrawMapNoteIconCentered(Vector2 center, Vector2 size, Vector4 tint) => DrawMapNoteIcon(center - size * 0.5f, size, tint); /// Draw a custom "white page with writing" icon for map notes. 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 { 60091 => true, _ when IsRegionIcon(iconId) => true, _ => false, }; public static bool IsRegionIcon(uint iconId) => iconId switch { >= 63200 and < 63900 => true, >= 62620 and < 62800 => true, _ => false, }; }