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/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;
}
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);
+ }
}