using System; using System.Collections.Generic; using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; using KamiLib.Window; using Lumina.Excel.Sheets; using Mappy.Controllers; using Mappy.Data; using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Map = Lumina.Excel.Sheets.Map; namespace Mappy.Windows; public class MinimapWindow : Window { private bool _wasLoggedIn; public MinimapWindow() : base("HSMappy Minimap###HSMappyMinimap", new Vector2(200.0f, 200.0f)) { DisableWindowSounds = true; Flags |= ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoScrollWithMouse; } public override bool DrawConditions() => IntegrationsController.ShouldShowMinimap() && System.SystemConfig.ShowMinimap; public override void PreOpenCheck() { 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() { var agent = AgentMap.Instance(); // Try loading from Lumina first so minimap can show without ever opening the area map if (!System.MapRenderer.HasMinimapCacheFor(agent->CurrentMapId) && agent->SelectedMapId != agent->CurrentMapId) System.MapRenderer.TryEnsureLuminaCacheFor(agent->CurrentMapId); var mapLoaded = agent->SelectedMapId == agent->CurrentMapId || System.MapRenderer.HasMinimapCacheFor(agent->CurrentMapId); if (!mapLoaded) { // Map data could not be loaded for this area (no Lumina path matched). Minimap shows automatically when data is available. const string hint = "Map unavailable for this area."; var textSize = ImGui.CalcTextSize(hint); var pos = (ImGui.GetWindowSize() - textSize) * 0.5f; ImGui.SetCursorPos(new Vector2(Math.Max(0, pos.X), Math.Max(20, pos.Y))); ImGui.TextColored(new Vector4(0.7f, 0.7f, 0.7f, 0.9f), hint); UpdateStyle(); UpdateSizePosition(); return; } UpdateStyle(); UpdateSizePosition(); // Compensate for window padding (minimap gets zero padding from plugin) var padding = ImGui.GetStyle().WindowPadding; ImGui.SetCursorPos(new Vector2(-padding.X, -padding.Y)); // Use actual window size so content scales when resized var contentSize = ImGui.GetContentRegionAvail(); if (contentSize.X <= 0 || contentSize.Y <= 0) return; var totalWidth = contentSize.X; var totalHeight = contentSize.Y; // Compute bar heights: top bar fits server info on one line; bottom bar = coord text + 2px float topBarHeight = 0f; float bottomBarHeight = 0f; if (System.SystemConfig.MinimapShowMapInfoBar) topBarHeight = ComputeMapInfoBarHeight(totalWidth); if (System.SystemConfig.MinimapShowCoordinateBar) bottomBarHeight = ComputeCoordinateBarHeight(totalWidth, totalHeight, topBarHeight); var minimapSide = Math.Min(totalWidth, totalHeight - topBarHeight - bottomBarHeight); minimapSide = Math.Max(1f, minimapSide); var scale = minimapSide / 200f; // Top bar (outside edge, above minimap) if (System.SystemConfig.MinimapShowMapInfoBar && topBarHeight > 0) { DrawMapInfoBar(totalWidth, topBarHeight, scale); } // Minimap (square, sized to fit available space) var minimapSize = new Vector2(minimapSide, minimapSide); using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, System.SystemConfig.MinimapOpacity)) using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, 0f)) using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, Vector2.Zero)) using (var child = ImRaii.Child("minimap_render", minimapSize, false, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)) { if (child) { System.MapRenderer.DrawMinimapContents(minimapSize); if (ImGui.IsItemHovered()) { var io = ImGui.GetIO(); var wheel = io.MouseWheel; if (wheel != 0) { var zoom = System.SystemConfig.MinimapZoom; zoom -= wheel * 0.012f; System.SystemConfig.MinimapZoom = Math.Clamp(zoom, 0.03f, 0.112f); SystemConfig.Save(); } io.MouseWheel = 0f; io.MouseWheelH = 0f; } } } // Bottom bar (outside edge, below minimap) if (System.SystemConfig.MinimapShowCoordinateBar && bottomBarHeight > 0) { DrawCoordinateBar(totalWidth, bottomBarHeight, scale); } // Restore default padding for the next window is done in plugin Draw callback (PopStyleVar after all windows). } public override void OnOpen() { ImGui.SetWindowPos(System.SystemConfig.MinimapPosition); var size = GetMinimapWindowSize(); ImGui.SetWindowSize(size); } private unsafe float ComputeMapInfoBarHeight(float width) { var (line1, line2) = GetMapInfoLines(); if (string.IsNullOrEmpty(line1) && string.IsNullOrEmpty(line2)) return 0f; var fontScale = ComputeMapInfoFontScale(width, line1, line2); ImGui.SetWindowFontScale(fontScale); var h1 = string.IsNullOrEmpty(line1) ? 0f : ImGui.CalcTextSize(line1).Y; var h2 = string.IsNullOrEmpty(line2) ? 0f : ImGui.CalcTextSize(line2).Y; var lineSpacing = line1.Length > 0 && line2.Length > 0 ? 2f : 0f; ImGui.SetWindowFontScale(1f); return h1 + h2 + lineSpacing + 6f * ImGuiHelpers.GlobalScale; } private static float ComputeMapInfoFontScale(float width, string line1, string line2) { var horzPad = 8f * ImGuiHelpers.GlobalScale; var maxWidth = Math.Max(1f, width - horzPad); ImGui.SetWindowFontScale(1f); var w1 = string.IsNullOrEmpty(line1) ? 0f : ImGui.CalcTextSize(line1).X; var w2 = string.IsNullOrEmpty(line2) ? 0f : ImGui.CalcTextSize(line2).X; var maxLineWidth = Math.Max(w1, w2); if (maxLineWidth <= 0) return 1f; var autoScale = Math.Clamp(maxWidth / maxLineWidth, 0.5f, 1f); var mult = Math.Clamp(System.SystemConfig.MinimapMapInfoFontScale, 0.5f, 2f); return Math.Clamp(autoScale * mult, 0.3f, 1.5f); } private unsafe (string line1, string line2) GetMapInfoLines() { var agent = AgentMap.Instance(); var currentMapId = agent->CurrentMapId; if (currentMapId == 0) return (string.Empty, string.Empty); var mapData = Service.DataManager.GetExcelSheet().GetRow(currentMapId); if (mapData.RowId == 0) return (string.Empty, string.Empty); var rawParts = new string?[4]; if (System.SystemConfig.ShowRegionLabel) rawParts[0] = mapData.PlaceNameRegion.Value.Name.ExtractText(); if (System.SystemConfig.ShowMapLabel) rawParts[1] = mapData.PlaceName.Value.Name.ExtractText(); if (agent->CurrentMapId == currentMapId) { if (TerritoryInfo.Instance()->AreaPlaceNameId is not 0 && System.SystemConfig.ShowAreaLabel) { var areaLabel = Service.DataManager.GetExcelSheet().GetRow(TerritoryInfo.Instance()->AreaPlaceNameId); rawParts[2] = areaLabel.Name.ExtractText(); } if (TerritoryInfo.Instance()->SubAreaPlaceNameId is not 0 && System.SystemConfig.ShowSubAreaLabel) { var subAreaLabel = Service.DataManager.GetExcelSheet().GetRow(TerritoryInfo.Instance()->SubAreaPlaceNameId); rawParts[3] = subAreaLabel.Name.ExtractText(); } } var order = System.SystemConfig.MinimapMapInfoOrder; if (order.Length != 4) order = [0, 1, 2, 3]; var ordered = new List(); foreach (var idx in order) { var i = Math.Clamp(idx, 0, 3); if (rawParts[i] is { } s) ordered.Add(s); } var line1 = ordered.Count >= 2 ? string.Join(" - ", ordered[0], ordered[1]) : (ordered.Count == 1 ? ordered[0] : string.Empty); var line2 = ordered.Count >= 4 ? string.Join(" - ", ordered[2], ordered[3]) : (ordered.Count == 3 ? ordered[2] : string.Empty); return (line1, line2); } private static IDisposable? PushMinimapFont(int fontType) { return fontType switch { 1 when System.MinimapAxis12FontHandle != null => System.MinimapAxis12FontHandle.Push(), 2 when System.MinimapAxis18FontHandle != null => System.MinimapAxis18FontHandle.Push(), _ => null }; } private unsafe void DrawMapInfoBar(float width, float height, float _) { using var childBg = ImRaii.PushColor(ImGuiCol.ChildBg, System.SystemConfig.MinimapMapInfoBarBackground); using var child = ImRaii.Child("minimap_mapinfo_bar", new Vector2(width, height), false, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse); if (!child) return; var (line1, line2) = GetMapInfoLines(); if (string.IsNullOrEmpty(line1) && string.IsNullOrEmpty(line2)) return; using var _font = PushMinimapFont(System.SystemConfig.MinimapMapInfoFontType); var fontScale = ComputeMapInfoFontScale(width, line1, line2); ImGui.SetWindowFontScale(fontScale); try { var color = System.SystemConfig.MinimapMapInfoColor; var totalHeight = 0f; if (line1.Length > 0) totalHeight += ImGui.CalcTextSize(line1).Y; if (line2.Length > 0) totalHeight += (line1.Length > 0 ? 2f : 0f) + ImGui.CalcTextSize(line2).Y; var y = (height - totalHeight) * 0.5f; if (line1.Length > 0) { var sz = ImGui.CalcTextSize(line1); ImGui.SetCursorPos(new Vector2((width - sz.X) * 0.5f, y)); ImGui.TextColored(color, line1); y += sz.Y + 2f; } if (line2.Length > 0) { var sz = ImGui.CalcTextSize(line2); ImGui.SetCursorPos(new Vector2((width - sz.X) * 0.5f, y)); ImGui.TextColored(color, line2); } } finally { ImGui.SetWindowFontScale(1f); } } private float ComputeCoordinateBarHeight(float totalWidth, float totalHeight, float topBarHeight) { var showCoords = System.SystemConfig.MinimapCoordBarShowCoordinates; var showTime = System.SystemConfig.MinimapCoordBarShowTime; var showRepair = System.SystemConfig.MinimapCoordBarShowRepairPercent; if (!showCoords && !showTime && !showRepair) return 0f; var minimapSide = Math.Min(totalWidth, totalHeight - topBarHeight - 20f); minimapSide = Math.Max(1f, minimapSide); var baseScale = Math.Clamp(minimapSide / 200f, 0.15f, 1f); var mult = Math.Clamp(System.SystemConfig.MinimapCoordBarFontScale, 0.5f, 2f); var fontScale = Math.Clamp(baseScale * mult, 0.15f, 1.5f); ImGui.SetWindowFontScale(fontScale); var h = 0f; if (showCoords) h = Math.Max(h, ImGui.CalcTextSize(" 99.9 99.9 ").Y); if (showTime) h = Math.Max(h, ImGui.CalcTextSize(DateTime.Now.ToString("h:mm tt")).Y); if (showRepair) h = Math.Max(h, ImGui.CalcTextSize("100%").Y); ImGui.SetWindowFontScale(1f); return h + 8f * ImGuiHelpers.GlobalScale; } private void DrawCoordinateBar(float width, float height, float scale) { var showCoords = System.SystemConfig.MinimapCoordBarShowCoordinates; var showTime = System.SystemConfig.MinimapCoordBarShowTime; var showRepair = System.SystemConfig.MinimapCoordBarShowRepairPercent; if (!showCoords && !showTime && !showRepair) return; using var childBg = ImRaii.PushColor(ImGuiCol.ChildBg, System.SystemConfig.MinimapCoordBarBackground); using var child = ImRaii.Child("minimap_coord_bar", new Vector2(width, height), false, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse); if (!child) return; var mult = Math.Clamp(System.SystemConfig.MinimapCoordBarFontScale, 0.5f, 2f); var fontScale = Math.Clamp(scale * mult, 0.15f, 1.5f); ImGui.SetWindowFontScale(fontScale); using var _font = PushMinimapFont(System.SystemConfig.MinimapCoordBarFontType); try { var color = System.SystemConfig.MinimapCoordBarColor; var horzPad = 6f * ImGuiHelpers.GlobalScale; var lineHeight = ImGui.GetTextLineHeight(); var yPos = Math.Max(0f, (height - lineHeight) * 0.5f - 2f); var gap = 8f * ImGuiHelpers.GlobalScale; var order = System.SystemConfig.MinimapBottomBarOrder; if (order.Length != 3) order = [0, 1, 2]; var texts = new Dictionary(); if (showCoords && System.MapRenderer.GetCurrentMinimapTransform() is { } transform) { var pos = Service.ObjectTable.LocalPlayer?.Position ?? Vector3.Zero; var mapCoord = MapUtil.WorldToMap(new Vector2(pos.X, pos.Z), transform.Item1, transform.Item2, transform.Item3); texts[0] = $"{mapCoord.X:F1} {mapCoord.Y:F1}"; } if (showRepair) texts[1] = $"{EquipmentConditionHelper.GetLowestConditionPercent():F0}%"; if (showTime) texts[2] = DateTime.Now.ToString("h:mm tt"); var visibleOrder = new List(); foreach (var idx in order) { var i = Math.Clamp(idx, 0, 2); if (texts.TryGetValue(i, out var t)) visibleOrder.Add(t); } var cursorX = horzPad; for (var j = 0; j < visibleOrder.Count; j++) { var text = visibleOrder[j]; var sz = ImGui.CalcTextSize(text); var isLast = j == visibleOrder.Count - 1; var x = isLast ? width - horzPad - sz.X : cursorX; ImGui.SetCursorPos(new Vector2(x, yPos)); ImGui.TextColored(color, text); if (!isLast) cursorX += sz.X + gap; } } finally { ImGui.SetWindowFontScale(1f); } } private void UpdateStyle() { if (System.SystemConfig.MinimapLockPosition) Flags |= ImGuiWindowFlags.NoMove; else Flags &= ~ImGuiWindowFlags.NoMove; } private unsafe Vector2 GetMinimapWindowSize() { var minimapSizePx = System.SystemConfig.MinimapSize; var topBarHeight = System.SystemConfig.MinimapShowMapInfoBar ? ComputeMapInfoBarHeight(minimapSizePx) : 0f; var bottomBarHeight = System.SystemConfig.MinimapShowCoordinateBar ? ComputeCoordinateBarHeight(minimapSizePx, minimapSizePx + topBarHeight + 24f, topBarHeight) : 0f; var totalHeight = minimapSizePx + topBarHeight + bottomBarHeight; return new Vector2(minimapSizePx, totalHeight); } private void UpdateSizePosition() { var config = System.SystemConfig; var windowPosition = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); var expectedSize = GetMinimapWindowSize(); // Size is config-only (set in Mappy settings); always apply config size to window. if (Math.Abs(windowSize.X - expectedSize.X) > 0.1f || Math.Abs(windowSize.Y - expectedSize.Y) > 0.1f) ImGui.SetWindowSize(expectedSize); if (!ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows)) { // Not focused: apply config position to window if (windowPosition != config.MinimapPosition) ImGui.SetWindowPos(config.MinimapPosition); } else { // Focused: save window position to config (size is changed only via settings) if (config.MinimapPosition != windowPosition) { config.MinimapPosition = windowPosition; SystemConfig.Save(); } } } }