From 99d5e44c23c70ebf7eb8905fe22fa11c2a75def7 Mon Sep 17 00:00:00 2001 From: srkizer Date: Wed, 10 Apr 2024 07:48:40 +0900 Subject: [PATCH 1/2] Sanitize IME strings to UCS-2 (#1758) Our ImGui has ImWChar defined as ushort, and it does not support UCS-2. ImGui would filter the input characters to UCS-2 range, but our IME implementation would forcefully set the buffer for the text input. This filters all candidate and composition strings to fit in UCS-2 range, notifying the user that some candidates are not properly supported. --- Dalamud/Interface/Internal/DalamudIme.cs | 56 +++++++++++++++++++++--- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 2b0da5aca..35e33fbfb 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -14,6 +14,7 @@ using System.Text.Unicode; using Dalamud.Game; using Dalamud.Game.Text; using Dalamud.Hooking.WndProcHook; +using Dalamud.Interface.Colors; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.ManagedFontAtlas.Internals; @@ -82,7 +83,7 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService private readonly ImGuiSetPlatformImeDataDelegate setPlatformImeDataDelegate; /// The candidates. - private readonly List candidateStrings = new(); + private readonly List<(string String, bool Supported)> candidateStrings = new(); /// The selected imm component. private string compositionString = string.Empty; @@ -242,6 +243,42 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService } } + private static (string String, bool Supported) ToUcs2(char* data, int nc = -1) + { + if (nc == -1) + { + nc = 0; + while (data[nc] != 0) + nc++; + } + + var supported = true; + var sb = new StringBuilder(); + sb.EnsureCapacity(nc); + for (var i = 0; i < nc; i++) + { + if (char.IsHighSurrogate(data[i]) && i + 1 < nc && char.IsLowSurrogate(data[i + 1])) + { + // Surrogate pair is found, but only UCS-2 characters are supported. Skip the next low surrogate. + sb.Append('\xFFFD'); + supported = false; + i++; + } + else if (char.IsSurrogate(data[i]) || !Rune.IsValid(data[i])) + { + // Lone surrogate pair, or an invalid codepoint. + sb.Append('\xFFFD'); + supported = false; + } + else + { + sb.Append(data[i]); + } + } + + return (sb.ToString(), supported); + } + private static string ImmGetCompositionString(HIMC hImc, uint comp) { var numBytes = ImmGetCompositionStringW(hImc, comp, null, 0); @@ -250,7 +287,8 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService var data = stackalloc char[numBytes / 2]; _ = ImmGetCompositionStringW(hImc, comp, data, (uint)numBytes); - return new(data, 0, numBytes / 2); + + return ToUcs2(data, numBytes / 2).String; } private void ReleaseUnmanagedResources() @@ -623,8 +661,8 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService (int)candlist.dwPageStart, (int)Math.Min(candlist.dwCount - candlist.dwPageStart, candlist.dwPageSize))) { - this.candidateStrings.Add(new((char*)(pStorage + candlist.dwOffset[i]))); - this.ReflectCharacterEncounters(this.candidateStrings[^1]); + this.candidateStrings.Add(ToUcs2((char*)(pStorage + candlist.dwOffset[i]))); + this.ReflectCharacterEncounters(this.candidateStrings[^1].String); } } @@ -783,7 +821,15 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService if (selected) color = ImGui.GetColorU32(ImGuiCol.NavHighlight); - drawList.AddText(cursor, color, $"{i + 1}. {ime.candidateStrings[i]}"); + var s = $"{i + 1}. {ime.candidateStrings[i].String}"; + drawList.AddText(cursor, color, s); + if (!ime.candidateStrings[i].Supported) + { + var pos = cursor + ImGui.CalcTextSize(s) with { Y = 0 } + + new Vector2(4 * ImGuiHelpers.GlobalScale, 0); + drawList.AddText(pos, ImGui.GetColorU32(ImGuiColors.DalamudRed), " (x)"); + } + cursor.Y += candTextSize.Y + spaceY; } From de6dcb8b530e8e114c4e96fb5b146dae13403628 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Tue, 9 Apr 2024 15:49:37 -0700 Subject: [PATCH 2/2] Add some small map helpers (#1756) * feat: Add new `.GetMapCoordinates` extension method - Used to easily resolve player-friendly map coordinates for any GameObject. * feat: Add MapID to ClientState - Provides easy access to the player's current map ID --- Dalamud/Game/ClientState/ClientState.cs | 15 +++++++++++ Dalamud/Plugin/Services/IClientState.cs | 5 ++++ Dalamud/Utility/MapUtil.cs | 34 +++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index a67a0abb6..2e8d128c3 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -12,6 +12,8 @@ using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; + using Lumina.Excel.GeneratedSheets; using Action = System.Action; @@ -89,6 +91,16 @@ internal sealed class ClientState : IInternalDisposableService, IClientState /// public ushort TerritoryType { get; private set; } + /// + public unsafe uint MapId + { + get + { + var agentMap = AgentMap.Instance(); + return agentMap != null ? AgentMap.Instance()->CurrentMapId : 0; + } + } + /// public PlayerCharacter? LocalPlayer => Service.GetNullable()?[0] as PlayerCharacter; @@ -237,6 +249,9 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat /// public ushort TerritoryType => this.clientStateService.TerritoryType; + + /// + public uint MapId => this.clientStateService.MapId; /// public PlayerCharacter? LocalPlayer => this.clientStateService.LocalPlayer; diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index 652a6c888..50e31fad0 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -48,6 +48,11 @@ public interface IClientState /// Gets the current Territory the player resides in. /// public ushort TerritoryType { get; } + + /// + /// Gets the current Map the player resides in. + /// + public uint MapId { get; } /// /// Gets the local player character, if one is present. diff --git a/Dalamud/Utility/MapUtil.cs b/Dalamud/Utility/MapUtil.cs index b4bbe1038..2ed3dfad1 100644 --- a/Dalamud/Utility/MapUtil.cs +++ b/Dalamud/Utility/MapUtil.cs @@ -1,5 +1,11 @@ using System.Numerics; +using Dalamud.Data; +using Dalamud.Game.ClientState.Objects.Types; + +using FFXIVClientStructs.FFXIV.Client.UI.Agent; + +using Lumina; using Lumina.Excel.GeneratedSheets; namespace Dalamud.Utility; @@ -128,4 +134,32 @@ public static class MapUtil { return WorldToMap(worldCoordinates, map.OffsetX, map.OffsetY, map.SizeFactor); } + + /// + /// Extension method to get the current position of a GameObject in Map Coordinates (visible to players in the + /// minimap or chat). A Z (height) value will always be returned, even on maps that do not natively show one. + /// + /// Thrown if ClientState is unavailable. + /// The GameObject to get the position for. + /// Whether to "correct" a Z offset to sane values for maps that don't have one. + /// A Vector3 that represents the X (east/west), Y (north/south), and Z (height) position of this object. + public static unsafe Vector3 GetMapCoordinates(this GameObject go, bool correctZOffset = false) + { + var agentMap = AgentMap.Instance(); + + if (agentMap == null || agentMap->CurrentMapId == 0) + throw new InvalidOperationException("Could not determine active map - data may not be loaded yet?"); + + var territoryTransient = Service.Get() + .GetExcelSheet()! + .GetRow(agentMap->CurrentTerritoryId); + + return WorldToMap( + go.Position, + agentMap->CurrentOffsetX, + agentMap->CurrentOffsetY, + territoryTransient?.OffsetZ ?? 0, + (uint)agentMap->CurrentMapSizeFactor, + correctZOffset); + } }