From b6d88f798a600c5033887f949b2a3d419e12afa9 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 7 Dec 2023 22:41:10 +0900 Subject: [PATCH 01/33] Make CJK imes work better --- Dalamud/Dalamud.cs | 3 +- Dalamud/Game/Gui/Internal/DalamudIME.cs | 301 ---------- Dalamud/Interface/Internal/DalamudIme.cs | 521 ++++++++++++++++++ .../Interface/Internal/DalamudInterface.cs | 10 +- .../Interface/Internal/InterfaceManager.cs | 59 +- .../Internal/Windows/DalamudImeWindow.cs | 223 ++++++++ .../Interface/Internal/Windows/IMEWindow.cs | 120 ---- .../Interface/Internal/WndProcHookManager.cs | 273 +++++++++ Dalamud/Interface/Utility/ImGuiHelpers.cs | 20 + 9 files changed, 1064 insertions(+), 466 deletions(-) delete mode 100644 Dalamud/Game/Gui/Internal/DalamudIME.cs create mode 100644 Dalamud/Interface/Internal/DalamudIme.cs create mode 100644 Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs delete mode 100644 Dalamud/Interface/Internal/Windows/IMEWindow.cs create mode 100644 Dalamud/Interface/Internal/WndProcHookManager.cs diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 9896b87a6..4ab617d0a 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using Dalamud.Common; using Dalamud.Configuration.Internal; using Dalamud.Game; -using Dalamud.Game.Gui.Internal; using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal; using Dalamud.Storage; @@ -178,7 +177,7 @@ internal sealed class Dalamud : IServiceType // this must be done before unloading interface manager, in order to do rebuild // the correct cascaded WndProc (IME -> RawDX11Scene -> Game). Otherwise the game // will not receive any windows messages - Service.GetNullable()?.Dispose(); + Service.GetNullable()?.Dispose(); // this must be done before unloading plugins, or it can cause a race condition // due to rendering happening on another thread, where a plugin might receive diff --git a/Dalamud/Game/Gui/Internal/DalamudIME.cs b/Dalamud/Game/Gui/Internal/DalamudIME.cs deleted file mode 100644 index a9f6991ae..000000000 --- a/Dalamud/Game/Gui/Internal/DalamudIME.cs +++ /dev/null @@ -1,301 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Numerics; -using System.Runtime.InteropServices; -using System.Text; - -using Dalamud.Hooking; -using Dalamud.Interface.Internal; -using Dalamud.Logging.Internal; -using ImGuiNET; -using PInvoke; - -using static Dalamud.NativeFunctions; - -namespace Dalamud.Game.Gui.Internal; - -/// -/// This class handles IME for non-English users. -/// -[ServiceManager.EarlyLoadedService] -internal unsafe class DalamudIME : IDisposable, IServiceType -{ - private static readonly ModuleLog Log = new("IME"); - - private AsmHook imguiTextInputCursorHook; - private Vector2* cursorPos; - - [ServiceManager.ServiceConstructor] - private DalamudIME() - { - } - - /// - /// Gets a value indicating whether the module is enabled. - /// - internal bool IsEnabled { get; private set; } - - /// - /// Gets the index of the first imm candidate in relation to the full list. - /// - internal CandidateList ImmCandNative { get; private set; } = default; - - /// - /// Gets the imm candidates. - /// - internal List ImmCand { get; private set; } = new(); - - /// - /// Gets the selected imm component. - /// - internal string ImmComp { get; private set; } = string.Empty; - - /// - public void Dispose() - { - this.imguiTextInputCursorHook?.Dispose(); - Marshal.FreeHGlobal((IntPtr)this.cursorPos); - } - - /// - /// Processes window messages. - /// - /// Handle of the window. - /// Type of window message. - /// wParam or the pointer to it. - /// lParam or the pointer to it. - /// Return value, if not doing further processing. - public unsafe IntPtr? ProcessWndProcW(IntPtr hWnd, User32.WindowMessage msg, void* wParamPtr, void* lParamPtr) - { - try - { - if (ImGui.GetCurrentContext() != IntPtr.Zero && ImGui.GetIO().WantTextInput) - { - var io = ImGui.GetIO(); - var wmsg = (WindowsMessage)msg; - long wParam = (long)wParamPtr, lParam = (long)lParamPtr; - try - { - wParam = Marshal.ReadInt32((IntPtr)wParamPtr); - } - catch - { - // ignored - } - - try - { - lParam = Marshal.ReadInt32((IntPtr)lParamPtr); - } - catch - { - // ignored - } - - switch (wmsg) - { - case WindowsMessage.WM_IME_NOTIFY: - switch ((IMECommand)(IntPtr)wParam) - { - case IMECommand.ChangeCandidate: - this.ToggleWindow(true); - this.LoadCand(hWnd); - break; - case IMECommand.OpenCandidate: - this.ToggleWindow(true); - this.ImmCandNative = default; - // this.ImmCand.Clear(); - break; - - case IMECommand.CloseCandidate: - this.ToggleWindow(false); - this.ImmCandNative = default; - // this.ImmCand.Clear(); - break; - - default: - break; - } - - break; - case WindowsMessage.WM_IME_COMPOSITION: - if (((long)(IMEComposition.CompStr | IMEComposition.CompAttr | IMEComposition.CompClause | - IMEComposition.CompReadAttr | IMEComposition.CompReadClause | IMEComposition.CompReadStr) & (long)(IntPtr)lParam) > 0) - { - var hIMC = ImmGetContext(hWnd); - if (hIMC == IntPtr.Zero) - return IntPtr.Zero; - - var dwSize = ImmGetCompositionStringW(hIMC, IMEComposition.CompStr, IntPtr.Zero, 0); - var unmanagedPointer = Marshal.AllocHGlobal((int)dwSize); - ImmGetCompositionStringW(hIMC, IMEComposition.CompStr, unmanagedPointer, (uint)dwSize); - - var bytes = new byte[dwSize]; - Marshal.Copy(unmanagedPointer, bytes, 0, (int)dwSize); - Marshal.FreeHGlobal(unmanagedPointer); - - var lpstr = Encoding.Unicode.GetString(bytes); - this.ImmComp = lpstr; - if (lpstr == string.Empty) - { - this.ToggleWindow(false); - } - else - { - this.LoadCand(hWnd); - } - } - - if (((long)(IntPtr)lParam & (long)IMEComposition.ResultStr) > 0) - { - var hIMC = ImmGetContext(hWnd); - if (hIMC == IntPtr.Zero) - return IntPtr.Zero; - - var dwSize = ImmGetCompositionStringW(hIMC, IMEComposition.ResultStr, IntPtr.Zero, 0); - var unmanagedPointer = Marshal.AllocHGlobal((int)dwSize); - ImmGetCompositionStringW(hIMC, IMEComposition.ResultStr, unmanagedPointer, (uint)dwSize); - - var bytes = new byte[dwSize]; - Marshal.Copy(unmanagedPointer, bytes, 0, (int)dwSize); - Marshal.FreeHGlobal(unmanagedPointer); - - var lpstr = Encoding.Unicode.GetString(bytes); - io.AddInputCharactersUTF8(lpstr); - - this.ImmComp = string.Empty; - this.ImmCandNative = default; - this.ImmCand.Clear(); - this.ToggleWindow(false); - } - - break; - - default: - break; - } - } - } - catch (Exception ex) - { - Log.Error(ex, "Prevented a crash in an IME hook"); - } - - return null; - } - - /// - /// Get the position of the cursor. - /// - /// The position of the cursor. - internal Vector2 GetCursorPos() - { - return new Vector2(this.cursorPos->X, this.cursorPos->Y); - } - - private unsafe void LoadCand(IntPtr hWnd) - { - if (hWnd == IntPtr.Zero) - return; - - var hImc = ImmGetContext(hWnd); - if (hImc == IntPtr.Zero) - return; - - var size = ImmGetCandidateListW(hImc, 0, IntPtr.Zero, 0); - if (size == 0) - return; - - var candlistPtr = Marshal.AllocHGlobal((int)size); - size = ImmGetCandidateListW(hImc, 0, candlistPtr, (uint)size); - - var candlist = this.ImmCandNative = Marshal.PtrToStructure(candlistPtr); - var pageSize = candlist.PageSize; - var candCount = candlist.Count; - - if (pageSize > 0 && candCount > 1) - { - var dwOffsets = new int[candCount]; - for (var i = 0; i < candCount; i++) - { - dwOffsets[i] = Marshal.ReadInt32(candlistPtr + ((i + 6) * sizeof(int))); - } - - var pageStart = candlist.PageStart; - - var cand = new string[pageSize]; - this.ImmCand.Clear(); - - for (var i = 0; i < pageSize; i++) - { - var offStart = dwOffsets[i + pageStart]; - var offEnd = i + pageStart + 1 < candCount ? dwOffsets[i + pageStart + 1] : size; - - var pStrStart = candlistPtr + (int)offStart; - var pStrEnd = candlistPtr + (int)offEnd; - - var len = (int)(pStrEnd.ToInt64() - pStrStart.ToInt64()); - if (len > 0) - { - var candBytes = new byte[len]; - Marshal.Copy(pStrStart, candBytes, 0, len); - - var candStr = Encoding.Unicode.GetString(candBytes); - cand[i] = candStr; - - this.ImmCand.Add(candStr); - } - } - - Marshal.FreeHGlobal(candlistPtr); - } - } - - [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")] - private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) - { - try - { - var module = Process.GetCurrentProcess().Modules.Cast().First(m => m.ModuleName == "cimgui.dll"); - var scanner = new SigScanner(module); - var cursorDrawingPtr = scanner.ScanModule("F3 0F 11 75 ?? 0F 28 CF"); - Log.Debug($"Found cursorDrawingPtr at {cursorDrawingPtr:X}"); - - this.cursorPos = (Vector2*)Marshal.AllocHGlobal(sizeof(Vector2)); - this.cursorPos->X = 0f; - this.cursorPos->Y = 0f; - - var asm = new[] - { - "use64", - $"push rax", - $"mov rax, {(IntPtr)this.cursorPos + sizeof(float)}", - $"movss [rax],xmm7", - $"mov rax, {(IntPtr)this.cursorPos}", - $"movss [rax],xmm6", - $"pop rax", - }; - - Log.Debug($"Asm Code:\n{string.Join("\n", asm)}"); - this.imguiTextInputCursorHook = new AsmHook(cursorDrawingPtr, asm, "ImguiTextInputCursorHook"); - this.imguiTextInputCursorHook?.Enable(); - - this.IsEnabled = true; - Log.Information("Enabled!"); - } - catch (Exception ex) - { - Log.Information(ex, "Enable failed"); - } - } - - private void ToggleWindow(bool visible) - { - if (visible) - Service.GetNullable()?.OpenImeWindow(); - else - Service.GetNullable()?.CloseImeWindow(); - } -} diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs new file mode 100644 index 000000000..1fc70b0f6 --- /dev/null +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -0,0 +1,521 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +using Dalamud.Game.Text; +using Dalamud.Interface.Utility; +using Dalamud.Logging.Internal; + +using ImGuiNET; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Interface.Internal; + +/// +/// This class handles IME for non-English users. +/// +[ServiceManager.EarlyLoadedService] +internal sealed unsafe class DalamudIme : IDisposable, IServiceType +{ + private static readonly ModuleLog Log = new("IME"); + + private readonly ImGuiSetPlatformImeDataDelegate setPlatformImeDataDelegate; + + [ServiceManager.ServiceConstructor] + private DalamudIme() => this.setPlatformImeDataDelegate = this.ImGuiSetPlatformImeData; + + /// + /// Finalizes an instance of the class. + /// + ~DalamudIme() => this.ReleaseUnmanagedResources(); + + private delegate void ImGuiSetPlatformImeDataDelegate(ImGuiViewportPtr viewport, ImGuiPlatformImeDataPtr data); + + /// + /// Gets a value indicating whether to display the cursor in input text. This also deals with blinking. + /// + internal static bool ShowCursorInInputText + { + get + { + if (!ImGui.GetIO().ConfigInputTextCursorBlink) + return true; + ref var textState = ref TextState; + if (textState.Id == 0 || (textState.Flags & ImGuiInputTextFlags.ReadOnly) != 0) + return true; + if (textState.CursorAnim <= 0) + return true; + return textState.CursorAnim % 1.2f <= 0.8f; + } + } + + /// + /// Gets the cursor position, in screen coordinates. + /// + internal Vector2 CursorPos { get; private set; } + + /// + /// Gets the associated viewport. + /// + internal ImGuiViewportPtr AssociatedViewport { get; private set; } + + /// + /// Gets the index of the first imm candidate in relation to the full list. + /// + internal CANDIDATELIST ImmCandNative { get; private set; } + + /// + /// Gets the imm candidates. + /// + internal List ImmCand { get; private set; } = new(); + + /// + /// Gets the selected imm component. + /// + internal string ImmComp { get; private set; } = string.Empty; + + /// + /// Gets the partial conversion from-range. + /// + internal int PartialConversionFrom { get; private set; } + + /// + /// Gets the partial conversion to-range. + /// + internal int PartialConversionTo { get; private set; } + + /// + /// Gets the cursor offset in the composition string. + /// + internal int CompositionCursorOffset { get; private set; } + + /// + /// Gets a value indicating whether to display partial conversion status. + /// + internal bool ShowPartialConversion => this.PartialConversionFrom != 0 || + this.PartialConversionTo != this.ImmComp.Length; + + /// + /// Gets the input mode icon from . + /// + internal string? InputModeIcon { get; private set; } + + private static ref ImGuiInputTextState TextState => ref *(ImGuiInputTextState*)(ImGui.GetCurrentContext() + 0x4588); + + /// + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + /// + /// Processes window messages. + /// + /// The arguments. + public void ProcessImeMessage(ref WndProcHookManager.WndProcOverrideEventArgs args) + { + if (!ImGuiHelpers.IsImGuiInitialized) + return; + + // Are we not the target of text input? + if (!ImGui.GetIO().WantTextInput) + return; + + var hImc = ImmGetContext(args.Hwnd); + if (hImc == nint.Zero) + return; + + try + { + var invalidTarget = TextState.Id == 0 || (TextState.Flags & ImGuiInputTextFlags.ReadOnly) != 0; + + switch (args.Message) + { + case WM.WM_IME_NOTIFY when (nint)args.WParam is IMN.IMN_OPENCANDIDATE or IMN.IMN_CLOSECANDIDATE or IMN.IMN_CHANGECANDIDATE: + this.UpdateImeWindowStatus(hImc); + args.SuppressAndReturn(0); + break; + + case WM.WM_IME_STARTCOMPOSITION: + args.SuppressAndReturn(0); + break; + + case WM.WM_IME_COMPOSITION: + if (invalidTarget) + ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); + else + this.ReplaceCompositionString(hImc, (uint)args.LParam); + + // Log.Verbose($"{nameof(WM.WM_IME_COMPOSITION)}({(nint)args.LParam:X}): {this.ImmComp}"); + args.SuppressAndReturn(0); + break; + + case WM.WM_IME_ENDCOMPOSITION: + // Log.Verbose($"{nameof(WM.WM_IME_ENDCOMPOSITION)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); + args.SuppressAndReturn(0); + break; + + case WM.WM_IME_CONTROL: + // Log.Verbose($"{nameof(WM.WM_IME_CONTROL)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); + args.SuppressAndReturn(0); + break; + + case WM.WM_IME_REQUEST: + // Log.Verbose($"{nameof(WM.WM_IME_REQUEST)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); + args.SuppressAndReturn(0); + break; + + case WM.WM_IME_SETCONTEXT: + // Hide candidate and composition windows. + args.LParam = (LPARAM)((nint)args.LParam & ~(ISC_SHOWUICOMPOSITIONWINDOW | 0xF)); + + // Log.Verbose($"{nameof(WM.WM_IME_SETCONTEXT)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); + args.SuppressWithDefault(); + break; + + case WM.WM_IME_NOTIFY: + // Log.Verbose($"{nameof(WM.WM_IME_NOTIFY)}({(nint)args.WParam:X}): {this.ImmComp}"); + break; + } + + this.UpdateInputLanguage(hImc); + } + finally + { + ImmReleaseContext(args.Hwnd, hImc); + } + } + + private static string ImmGetCompositionString(HIMC hImc, uint comp) + { + var numBytes = ImmGetCompositionStringW(hImc, comp, null, 0); + if (numBytes == 0) + return string.Empty; + + var data = stackalloc char[numBytes / 2]; + _ = ImmGetCompositionStringW(hImc, comp, data, (uint)numBytes); + return new(data, 0, numBytes / 2); + } + + private void ReleaseUnmanagedResources() => ImGui.GetIO().SetPlatformImeDataFn = nint.Zero; + + private void UpdateInputLanguage(HIMC hImc) + { + uint conv, sent; + ImmGetConversionStatus(hImc, &conv, &sent); + var lang = GetKeyboardLayout(0); + var open = ImmGetOpenStatus(hImc) != false; + + // Log.Verbose($"{nameof(this.UpdateInputLanguage)}: conv={conv:X} sent={sent:X} open={open} lang={lang:X}"); + + var native = (conv & 1) != 0; + var katakana = (conv & 2) != 0; + var fullwidth = (conv & 8) != 0; + switch (lang & 0x3F) + { + case LANG.LANG_KOREAN: + if (native) + this.InputModeIcon = "\uE025"; + else if (fullwidth) + this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumeric}"; + else + this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumericHalfWidth}"; + break; + + case LANG.LANG_JAPANESE: + // wtf + // see the function called from: 48 8b 0d ?? ?? ?? ?? e8 ?? ?? ?? ?? 8b d8 e9 ?? 00 00 0 + if (open && native && katakana && fullwidth) + this.InputModeIcon = $"{(char)SeIconChar.ImeKatakana}"; + else if (open && native && katakana) + this.InputModeIcon = $"{(char)SeIconChar.ImeKatakanaHalfWidth}"; + else if (open && native) + this.InputModeIcon = $"{(char)SeIconChar.ImeHiragana}"; + else if (open && fullwidth) + this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumeric}"; + else + this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumericHalfWidth}"; + break; + + case LANG.LANG_CHINESE: + // TODO: does Chinese IME also need "open" check? + if (native) + this.InputModeIcon = "\uE026"; + else + this.InputModeIcon = "\uE027"; + break; + + default: + this.InputModeIcon = null; + break; + } + + this.UpdateImeWindowStatus(hImc); + } + + private void ReplaceCompositionString(HIMC hImc, uint comp) + { + ref var textState = ref TextState; + var finalCommit = (comp & GCS.GCS_RESULTSTR) != 0; + + ref var s = ref textState.Stb.SelectStart; + ref var e = ref textState.Stb.SelectEnd; + ref var c = ref textState.Stb.Cursor; + s = Math.Clamp(s, 0, textState.CurLenW); + e = Math.Clamp(e, 0, textState.CurLenW); + c = Math.Clamp(c, 0, textState.CurLenW); + if (s == e) + s = e = c; + if (s > e) + (s, e) = (e, s); + + var newString = finalCommit + ? ImmGetCompositionString(hImc, GCS.GCS_RESULTSTR) + : ImmGetCompositionString(hImc, GCS.GCS_COMPSTR); + + if (s != e) + textState.DeleteChars(s, e - s); + textState.InsertChars(s, newString); + + if (finalCommit) + s = e = s + newString.Length; + else + e = s + newString.Length; + + this.ImmComp = finalCommit ? string.Empty : newString; + + this.CompositionCursorOffset = + finalCommit + ? 0 + : ImmGetCompositionStringW(hImc, GCS.GCS_CURSORPOS, null, 0); + + if (finalCommit) + { + this.PartialConversionFrom = this.PartialConversionTo = 0; + } + else if ((comp & GCS.GCS_COMPATTR) != 0) + { + var attrLength = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, null, 0); + var attrPtr = stackalloc byte[attrLength]; + var attr = new Span(attrPtr, Math.Min(this.ImmComp.Length, attrLength)); + _ = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, attrPtr, (uint)attrLength); + var l = 0; + while (l < attr.Length && attr[l] is not ATTR_TARGET_CONVERTED and not ATTR_TARGET_NOTCONVERTED) + l++; + + var r = l; + while (r < attr.Length && attr[r] is ATTR_TARGET_CONVERTED or ATTR_TARGET_NOTCONVERTED) + r++; + + if (r == 0 || l == this.ImmComp.Length) + (l, r) = (0, this.ImmComp.Length); + + (this.PartialConversionFrom, this.PartialConversionTo) = (l, r); + } + else + { + this.PartialConversionFrom = 0; + this.PartialConversionTo = this.ImmComp.Length; + } + + // Put the cursor at the beginning, so that the candidate window appears aligned with the text. + c = s; + this.UpdateImeWindowStatus(hImc); + } + + private void ClearState() + { + this.ImmComp = string.Empty; + this.PartialConversionFrom = this.PartialConversionTo = 0; + this.UpdateImeWindowStatus(default); + + ref var textState = ref TextState; + textState.Stb.Cursor = textState.Stb.SelectStart = textState.Stb.SelectEnd; + } + + private void LoadCand(HIMC hImc) + { + this.ImmCand.Clear(); + this.ImmCandNative = default; + + if (hImc == default) + return; + + var size = (int)ImmGetCandidateListW(hImc, 0, null, 0); + if (size == 0) + return; + + var pStorage = stackalloc byte[size]; + if (size != ImmGetCandidateListW(hImc, 0, (CANDIDATELIST*)pStorage, (uint)size)) + return; + + ref var candlist = ref *(CANDIDATELIST*)pStorage; + this.ImmCandNative = candlist; + + if (candlist.dwPageSize == 0 || candlist.dwCount == 0) + return; + + foreach (var i in Enumerable.Range( + (int)candlist.dwPageStart, + (int)Math.Min(candlist.dwCount - candlist.dwPageStart, candlist.dwPageSize))) + { + this.ImmCand.Add(new((char*)(pStorage + candlist.dwOffset[i]))); + } + } + + private void UpdateImeWindowStatus(HIMC hImc) + { + if (Service.GetNullable() is not { } di) + return; + + this.LoadCand(hImc); + if (this.ImmCand.Count != 0 || this.ShowPartialConversion || this.InputModeIcon != default) + di.OpenImeWindow(); + else + di.CloseImeWindow(); + } + + private void ImGuiSetPlatformImeData(ImGuiViewportPtr viewport, ImGuiPlatformImeDataPtr data) + { + this.CursorPos = data.InputPos; + if (data.WantVisible) + { + this.AssociatedViewport = viewport; + } + else + { + this.AssociatedViewport = default; + this.ClearState(); + } + } + + [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")] + private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) => + ImGui.GetIO().SetPlatformImeDataFn = Marshal.GetFunctionPointerForDelegate(this.setPlatformImeDataDelegate); + + /// + /// Ported from imstb_textedit.h. + /// + [StructLayout(LayoutKind.Sequential, Size = 0xE2C)] + private struct StbTextEditState + { + /// + /// Position of the text cursor within the string. + /// + public int Cursor; + + /// + /// Selection start point. + /// + public int SelectStart; + + /// + /// selection start and end point in characters; if equal, no selection. + /// + /// + /// Note that start may be less than or greater than end (e.g. when dragging the mouse, + /// start is where the initial click was, and you can drag in either direction.) + /// + public int SelectEnd; + + /// + /// Each text field keeps its own insert mode state. + /// To keep an app-wide insert mode, copy this value in/out of the app state. + /// + public byte InsertMode; + + /// + /// Page size in number of row. + /// This value MUST be set to >0 for pageup or pagedown in multilines documents. + /// + public int RowCountPerPage; + + // Remainder is stb-private data. + } + + [StructLayout(LayoutKind.Sequential)] + private struct ImGuiInputTextState + { + public uint Id; + public int CurLenW; + public int CurLenA; + public ImVector TextWRaw; + public ImVector TextARaw; + public ImVector InitialTextARaw; + public bool TextAIsValid; + public int BufCapacityA; + public float ScrollX; + public StbTextEditState Stb; + public float CursorAnim; + public bool CursorFollow; + public bool SelectedAllMouseLock; + public bool Edited; + public ImGuiInputTextFlags Flags; + + public ImVectorWrapper TextW => new((ImVector*)Unsafe.AsPointer(ref this.TextWRaw)); + + public ImVectorWrapper TextA => new((ImVector*)Unsafe.AsPointer(ref this.TextWRaw)); + + public ImVectorWrapper InitialTextA => new((ImVector*)Unsafe.AsPointer(ref this.TextWRaw)); + + // See imgui_widgets.cpp: STB_TEXTEDIT_DELETECHARS + public void DeleteChars(int pos, int n) + { + var dst = this.TextW.Data + pos; + + // We maintain our buffer length in both UTF-8 and wchar formats + this.Edited = true; + this.CurLenA -= Encoding.UTF8.GetByteCount(dst, n); + this.CurLenW -= n; + + // Offset remaining text (FIXME-OPT: Use memmove) + var src = this.TextW.Data + pos + n; + int i; + for (i = 0; src[i] != 0; i++) + dst[i] = src[i]; + dst[i] = '\0'; + } + + // See imgui_widgets.cpp: STB_TEXTEDIT_INSERTCHARS + public bool InsertChars(int pos, ReadOnlySpan newText) + { + var isResizable = (this.Flags & ImGuiInputTextFlags.CallbackResize) != 0; + var textLen = this.CurLenW; + Debug.Assert(pos <= textLen, "pos <= text_len"); + + var newTextLenUtf8 = Encoding.UTF8.GetByteCount(newText); + if (!isResizable && newTextLenUtf8 + this.CurLenA + 1 > this.BufCapacityA) + return false; + + // Grow internal buffer if needed + if (newText.Length + textLen + 1 > this.TextW.Length) + { + if (!isResizable) + return false; + + Debug.Assert(textLen < this.TextW.Length, "text_len < this.TextW.Length"); + this.TextW.Resize(textLen + Math.Clamp(newText.Length * 4, 32, Math.Max(256, newText.Length)) + 1); + } + + var text = this.TextW.DataSpan; + if (pos != textLen) + text.Slice(pos, textLen - pos).CopyTo(text[(pos + newText.Length)..]); + newText.CopyTo(text[pos..]); + + this.Edited = true; + this.CurLenW += newText.Length; + this.CurLenA += newTextLenUtf8; + this.TextW[this.CurLenW] = '\0'; + + return true; + } + } +} diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 1dcc5c0c7..95415659b 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -59,7 +59,7 @@ internal class DalamudInterface : IDisposable, IServiceType private readonly ComponentDemoWindow componentDemoWindow; private readonly DataWindow dataWindow; private readonly GamepadModeNotifierWindow gamepadModeNotifierWindow; - private readonly ImeWindow imeWindow; + private readonly DalamudImeWindow imeWindow; private readonly ConsoleWindow consoleWindow; private readonly PluginStatWindow pluginStatWindow; private readonly PluginInstallerWindow pluginWindow; @@ -111,7 +111,7 @@ internal class DalamudInterface : IDisposable, IServiceType this.componentDemoWindow = new ComponentDemoWindow() { IsOpen = false }; this.dataWindow = new DataWindow() { IsOpen = false }; this.gamepadModeNotifierWindow = new GamepadModeNotifierWindow() { IsOpen = false }; - this.imeWindow = new ImeWindow() { IsOpen = false }; + this.imeWindow = new DalamudImeWindow() { IsOpen = false }; this.consoleWindow = new ConsoleWindow(configuration) { IsOpen = configuration.LogOpenAtStartup }; this.pluginStatWindow = new PluginStatWindow() { IsOpen = false }; this.pluginWindow = new PluginInstallerWindow(pluginImageCache, configuration) { IsOpen = false }; @@ -256,7 +256,7 @@ internal class DalamudInterface : IDisposable, IServiceType public void OpenGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.IsOpen = true; /// - /// Opens the . + /// Opens the . /// public void OpenImeWindow() => this.imeWindow.IsOpen = true; @@ -356,7 +356,7 @@ internal class DalamudInterface : IDisposable, IServiceType #region Close /// - /// Closes the . + /// Closes the . /// public void CloseImeWindow() => this.imeWindow.IsOpen = false; @@ -408,7 +408,7 @@ internal class DalamudInterface : IDisposable, IServiceType public void ToggleGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.Toggle(); /// - /// Toggles the . + /// Toggles the . /// public void ToggleImeWindow() => this.imeWindow.Toggle(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 1b12fd853..d7ab5ba9d 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -12,7 +12,6 @@ using Dalamud.Configuration.Internal; using Dalamud.Game; using Dalamud.Game.ClientState.GamePad; using Dalamud.Game.ClientState.Keys; -using Dalamud.Game.Gui.Internal; using Dalamud.Game.Internal.DXGI; using Dalamud.Hooking; using Dalamud.Interface.GameFonts; @@ -73,12 +72,16 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly WndProcHookManager wndProcHookManager = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly DalamudIme dalamudIme = Service.Get(); private readonly ManualResetEvent fontBuildSignal; private readonly SwapChainVtableResolver address; - private readonly Hook dispatchMessageWHook; private readonly Hook setCursorHook; - private Hook processMessageHook; private RawDX11Scene? scene; private Hook? presentHook; @@ -92,8 +95,6 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceConstructor] private InterfaceManager() { - this.dispatchMessageWHook = Hook.FromImport( - null, "user32.dll", "DispatchMessageW", 0, this.DispatchMessageWDetour); this.setCursorHook = Hook.FromImport( null, "user32.dll", "SetCursor", 0, this.SetCursorDetour); @@ -111,12 +112,6 @@ internal class InterfaceManager : IDisposable, IServiceType [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate IntPtr SetCursorDelegate(IntPtr hCursor); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate IntPtr DispatchMessageWDelegate(ref User32.MSG msg); - - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate IntPtr ProcessMessageDelegate(IntPtr hWnd, uint msg, ulong wParam, ulong lParam, IntPtr handeled); - /// /// This event gets called each frame to facilitate ImGui drawing. /// @@ -236,10 +231,9 @@ internal class InterfaceManager : IDisposable, IServiceType this.setCursorHook.Dispose(); this.presentHook?.Dispose(); this.resizeBuffersHook?.Dispose(); - this.dispatchMessageWHook.Dispose(); - this.processMessageHook?.Dispose(); }).Wait(); + this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; this.scene?.Dispose(); } @@ -660,6 +654,20 @@ internal class InterfaceManager : IDisposable, IServiceType this.scene = newScene; Service.Provide(new(this)); + + this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc; + } + + private unsafe void WndProcHookManagerOnPreWndProc(ref WndProcHookManager.WndProcOverrideEventArgs args) + { + var r = this.scene?.ProcessWndProcW(args.Hwnd, (User32.WindowMessage)args.Message, args.WParam, args.LParam); + if (r is not null) + { + args.ReturnValue = r.Value; + args.SuppressCall = true; + } + + this.dalamudIme.ProcessImeMessage(ref args); } /* @@ -1095,15 +1103,9 @@ internal class InterfaceManager : IDisposable, IServiceType Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}"); - var wndProcAddress = sigScanner.ScanText("E8 ?? ?? ?? ?? 80 7C 24 ?? ?? 74 ?? B8"); - Log.Verbose($"WndProc address 0x{wndProcAddress.ToInt64():X}"); - this.processMessageHook = Hook.FromAddress(wndProcAddress, this.ProcessMessageDetour); - this.setCursorHook.Enable(); this.presentHook.Enable(); this.resizeBuffersHook.Enable(); - this.dispatchMessageWHook.Enable(); - this.processMessageHook.Enable(); }); } @@ -1124,25 +1126,6 @@ internal class InterfaceManager : IDisposable, IServiceType this.isRebuildingFonts = false; } - private unsafe IntPtr ProcessMessageDetour(IntPtr hWnd, uint msg, ulong wParam, ulong lParam, IntPtr handeled) - { - var ime = Service.GetNullable(); - var res = ime?.ProcessWndProcW(hWnd, (User32.WindowMessage)msg, (void*)wParam, (void*)lParam); - return this.processMessageHook.Original(hWnd, msg, wParam, lParam, handeled); - } - - private unsafe IntPtr DispatchMessageWDetour(ref User32.MSG msg) - { - if (msg.hwnd == this.GameWindowHandle && this.scene != null) - { - var res = this.scene.ProcessWndProcW(msg.hwnd, msg.message, (void*)msg.wParam, (void*)msg.lParam); - if (res != null) - return res.Value; - } - - return this.dispatchMessageWHook.IsDisposed ? User32.DispatchMessage(ref msg) : this.dispatchMessageWHook.Original(ref msg); - } - private IntPtr ResizeBuffersDetour(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) { #if DEBUG diff --git a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs new file mode 100644 index 000000000..1819ed819 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs @@ -0,0 +1,223 @@ +using System.Numerics; + +using Dalamud.Interface.Windowing; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows; + +/// +/// A window for displaying IME details. +/// +internal unsafe class DalamudImeWindow : Window +{ + private const int ImePageSize = 9; + + /// + /// Initializes a new instance of the class. + /// + public DalamudImeWindow() + : base( + "Dalamud IME", + ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoBackground) + { + this.Size = default(Vector2); + + this.RespectCloseHotkey = false; + } + + /// + public override void Draw() + { + } + + /// + public override void PostDraw() + { + if (Service.GetNullable() is not { } ime) + return; + + var viewport = ime.AssociatedViewport; + if (viewport.NativePtr is null) + return; + + var drawCand = ime.ImmCand.Count != 0; + var drawConv = drawCand || ime.ShowPartialConversion; + var drawIme = ime.InputModeIcon != null; + + var pad = ImGui.GetStyle().WindowPadding; + var candTextSize = ImGui.CalcTextSize(ime.ImmComp == string.Empty ? " " : ime.ImmComp); + + var native = ime.ImmCandNative; + var totalIndex = native.dwSelection + 1; + var totalSize = native.dwCount; + + var pageStart = native.dwPageStart; + var pageIndex = (pageStart / ImePageSize) + 1; + var pageCount = (totalSize / ImePageSize) + 1; + var pageInfo = $"{totalIndex}/{totalSize} ({pageIndex}/{pageCount})"; + + // Calc the window size. + var maxTextWidth = 0f; + for (var i = 0; i < ime.ImmCand.Count; i++) + { + var textSize = ImGui.CalcTextSize($"{i + 1}. {ime.ImmCand[i]}"); + maxTextWidth = maxTextWidth > textSize.X ? maxTextWidth : textSize.X; + } + + maxTextWidth = maxTextWidth > ImGui.CalcTextSize(pageInfo).X ? maxTextWidth : ImGui.CalcTextSize(pageInfo).X; + maxTextWidth = maxTextWidth > ImGui.CalcTextSize(ime.ImmComp).X + ? maxTextWidth + : ImGui.CalcTextSize(ime.ImmComp).X; + + var numEntries = (drawCand ? ime.ImmCand.Count + 1 : 0) + 1 + (drawIme ? 1 : 0); + var spaceY = ImGui.GetStyle().ItemSpacing.Y; + var imeWindowHeight = (spaceY * (numEntries - 1)) + (candTextSize.Y * numEntries); + var windowSize = new Vector2(maxTextWidth, imeWindowHeight) + (pad * 2); + + // 1. Figure out the expanding direction. + var expandUpward = ime.CursorPos.Y + windowSize.Y > viewport.WorkPos.Y + viewport.WorkSize.Y; + var windowPos = ime.CursorPos - pad; + if (expandUpward) + { + windowPos.Y -= windowSize.Y - candTextSize.Y - (pad.Y * 2); + if (drawIme) + windowPos.Y += candTextSize.Y + spaceY; + } + else + { + if (drawIme) + windowPos.Y -= candTextSize.Y + spaceY; + } + + // 2. Contain within the viewport. Do not use clamp, as the target window might be too small. + if (windowPos.X < viewport.WorkPos.X) + windowPos.X = viewport.WorkPos.X; + else if (windowPos.X + windowSize.X > viewport.WorkPos.X + viewport.WorkSize.X) + windowPos.X = (viewport.WorkPos.X + viewport.WorkSize.X) - windowSize.X; + if (windowPos.Y < viewport.WorkPos.Y) + windowPos.Y = viewport.WorkPos.Y; + else if (windowPos.Y + windowSize.Y > viewport.WorkPos.Y + viewport.WorkSize.Y) + windowPos.Y = (viewport.WorkPos.Y + viewport.WorkSize.Y) - windowSize.Y; + + var cursor = windowPos + pad; + + // Draw the ime window. + var drawList = ImGui.GetForegroundDrawList(viewport); + + // Draw the background rect for candidates. + if (drawCand) + { + Vector2 candRectLt, candRectRb; + if (!expandUpward) + { + candRectLt = windowPos + candTextSize with { X = 0 } + pad with { X = 0 }; + candRectRb = windowPos + windowSize; + if (drawIme) + candRectLt.Y += spaceY + candTextSize.Y; + } + else + { + candRectLt = windowPos; + candRectRb = windowPos + (windowSize - candTextSize with { X = 0 } - pad with { X = 0 }); + if (drawIme) + candRectRb.Y -= spaceY + candTextSize.Y; + } + + drawList.AddRectFilled( + candRectLt, + candRectRb, + ImGui.GetColorU32(ImGuiCol.WindowBg), + ImGui.GetStyle().WindowRounding); + } + + if (!expandUpward && drawIme) + { + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.InputModeIcon); + cursor.Y += candTextSize.Y + spaceY; + } + + if (!expandUpward && drawConv) + { + DrawTextBeingConverted(); + cursor.Y += candTextSize.Y + spaceY; + + // Add a separator. + drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator)); + } + + if (drawCand) + { + // Add the candidate words. + for (var i = 0; i < ime.ImmCand.Count; i++) + { + var selected = i == (native.dwSelection % ImePageSize); + var color = ImGui.GetColorU32(ImGuiCol.Text); + if (selected) + color = ImGui.GetColorU32(ImGuiCol.NavHighlight); + + drawList.AddText(cursor, color, $"{i + 1}. {ime.ImmCand[i]}"); + cursor.Y += candTextSize.Y + spaceY; + } + + // Add a separator + drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator)); + + // Add the pages infomation. + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), pageInfo); + cursor.Y += candTextSize.Y + spaceY; + } + + if (expandUpward && drawConv) + { + // Add a separator. + drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator)); + + DrawTextBeingConverted(); + cursor.Y += candTextSize.Y + spaceY; + } + + if (expandUpward && drawIme) + { + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.InputModeIcon); + } + + return; + + void DrawTextBeingConverted() + { + // Draw the text background. + drawList.AddRectFilled( + cursor - (pad / 2), + cursor + candTextSize + (pad / 2), + ImGui.GetColorU32(ImGuiCol.WindowBg)); + + // If only a part of the full text is marked for conversion, then draw background for the part being edited. + if (ime.PartialConversionFrom != 0 || ime.PartialConversionTo != ime.ImmComp.Length) + { + var part1 = ime.ImmComp[..ime.PartialConversionFrom]; + var part2 = ime.ImmComp[..ime.PartialConversionTo]; + var size1 = ImGui.CalcTextSize(part1); + var size2 = ImGui.CalcTextSize(part2); + drawList.AddRectFilled( + cursor + size1 with { Y = 0 }, + cursor + size2, + ImGui.GetColorU32(ImGuiCol.TextSelectedBg)); + } + + // Add the text being converted. + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.ImmComp); + + // Draw the caret inside the composition string. + if (DalamudIme.ShowCursorInInputText) + { + var partBeforeCaret = ime.ImmComp[..ime.CompositionCursorOffset]; + var sizeBeforeCaret = ImGui.CalcTextSize(partBeforeCaret); + drawList.AddLine( + cursor + sizeBeforeCaret with { Y = 0 }, + cursor + sizeBeforeCaret, + ImGui.GetColorU32(ImGuiCol.Text)); + } + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/IMEWindow.cs b/Dalamud/Interface/Internal/Windows/IMEWindow.cs deleted file mode 100644 index 80e03caf3..000000000 --- a/Dalamud/Interface/Internal/Windows/IMEWindow.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System.Numerics; - -using Dalamud.Game.ClientState.Keys; -using Dalamud.Game.Gui.Internal; -using Dalamud.Interface.Windowing; -using ImGuiNET; - -namespace Dalamud.Interface.Internal.Windows; - -/// -/// A window for displaying IME details. -/// -internal unsafe class ImeWindow : Window -{ - private const int ImePageSize = 9; - - /// - /// Initializes a new instance of the class. - /// - public ImeWindow() - : base("Dalamud IME", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoBackground) - { - this.Size = new Vector2(100, 200); - this.SizeCondition = ImGuiCond.FirstUseEver; - - this.RespectCloseHotkey = false; - } - - /// - public override void Draw() - { - if (this.IsOpen && Service.Get()[VirtualKey.SHIFT]) Service.Get().CloseImeWindow(); - var ime = Service.GetNullable(); - - if (ime == null || !ime.IsEnabled) - { - ImGui.Text("IME is unavailable."); - return; - } - - // ImGui.Text($"{ime.GetCursorPos()}"); - // ImGui.Text($"{ImGui.GetWindowViewport().WorkSize}"); - } - - /// - public override void PostDraw() - { - if (this.IsOpen && Service.Get()[VirtualKey.SHIFT]) Service.Get().CloseImeWindow(); - var ime = Service.GetNullable(); - - if (ime == null || !ime.IsEnabled) - return; - - var maxTextWidth = 0f; - var textHeight = ImGui.CalcTextSize(ime.ImmComp).Y; - - var native = ime.ImmCandNative; - var totalIndex = native.Selection + 1; - var totalSize = native.Count; - - var pageStart = native.PageStart; - var pageIndex = (pageStart / ImePageSize) + 1; - var pageCount = (totalSize / ImePageSize) + 1; - var pageInfo = $"{totalIndex}/{totalSize} ({pageIndex}/{pageCount})"; - - // Calc the window size - for (var i = 0; i < ime.ImmCand.Count; i++) - { - var textSize = ImGui.CalcTextSize($"{i + 1}. {ime.ImmCand[i]}"); - maxTextWidth = maxTextWidth > textSize.X ? maxTextWidth : textSize.X; - } - - maxTextWidth = maxTextWidth > ImGui.CalcTextSize(pageInfo).X ? maxTextWidth : ImGui.CalcTextSize(pageInfo).X; - maxTextWidth = maxTextWidth > ImGui.CalcTextSize(ime.ImmComp).X ? maxTextWidth : ImGui.CalcTextSize(ime.ImmComp).X; - - var imeWindowWidth = maxTextWidth + (2 * ImGui.GetStyle().WindowPadding.X); - var imeWindowHeight = (textHeight * (ime.ImmCand.Count + 2)) + (5 * (ime.ImmCand.Count - 1)) + (2 * ImGui.GetStyle().WindowPadding.Y); - - // Calc the window pos - var cursorPos = ime.GetCursorPos(); - var imeWindowMinPos = new Vector2(cursorPos.X, cursorPos.Y); - var imeWindowMaxPos = new Vector2(imeWindowMinPos.X + imeWindowWidth, imeWindowMinPos.Y + imeWindowHeight); - var gameWindowSize = ImGui.GetWindowViewport().WorkSize; - - var offset = new Vector2( - imeWindowMaxPos.X - gameWindowSize.X > 0 ? imeWindowMaxPos.X - gameWindowSize.X : 0, - imeWindowMaxPos.Y - gameWindowSize.Y > 0 ? imeWindowMaxPos.Y - gameWindowSize.Y : 0); - imeWindowMinPos -= offset; - imeWindowMaxPos -= offset; - - var nextDrawPosY = imeWindowMinPos.Y; - var drawAreaPosX = imeWindowMinPos.X + ImGui.GetStyle().WindowPadding.X; - - // Draw the ime window - var drawList = ImGui.GetForegroundDrawList(); - // Draw the background rect - drawList.AddRectFilled(imeWindowMinPos, imeWindowMaxPos, ImGui.GetColorU32(ImGuiCol.WindowBg), ImGui.GetStyle().WindowRounding); - // Add component text - drawList.AddText(new Vector2(drawAreaPosX, nextDrawPosY), ImGui.GetColorU32(ImGuiCol.Text), ime.ImmComp); - nextDrawPosY += textHeight + ImGui.GetStyle().ItemSpacing.Y; - // Add separator - drawList.AddLine(new Vector2(drawAreaPosX, nextDrawPosY), new Vector2(drawAreaPosX + maxTextWidth, nextDrawPosY), ImGui.GetColorU32(ImGuiCol.Separator)); - // Add candidate words - for (var i = 0; i < ime.ImmCand.Count; i++) - { - var selected = i == (native.Selection % ImePageSize); - var color = ImGui.GetColorU32(ImGuiCol.Text); - if (selected) - color = ImGui.GetColorU32(ImGuiCol.NavHighlight); - - drawList.AddText(new Vector2(drawAreaPosX, nextDrawPosY), color, $"{i + 1}. {ime.ImmCand[i]}"); - nextDrawPosY += textHeight + ImGui.GetStyle().ItemSpacing.Y; - } - - // Add separator - drawList.AddLine(new Vector2(drawAreaPosX, nextDrawPosY), new Vector2(drawAreaPosX + maxTextWidth, nextDrawPosY), ImGui.GetColorU32(ImGuiCol.Separator)); - // Add pages infomation - drawList.AddText(new Vector2(drawAreaPosX, nextDrawPosY), ImGui.GetColorU32(ImGuiCol.Text), pageInfo); - } -} diff --git a/Dalamud/Interface/Internal/WndProcHookManager.cs b/Dalamud/Interface/Internal/WndProcHookManager.cs new file mode 100644 index 000000000..fcd90c95a --- /dev/null +++ b/Dalamud/Interface/Internal/WndProcHookManager.cs @@ -0,0 +1,273 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Dalamud.Hooking; +using Dalamud.Interface.Utility; +using Dalamud.Logging.Internal; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Interface.Internal; + +/// +/// A manifestation of "I can't believe this is required". +/// +[ServiceManager.BlockingEarlyLoadedService] +internal sealed class WndProcHookManager : IServiceType, IDisposable +{ + private static readonly ModuleLog Log = new("WPHM"); + + private readonly Hook dispatchMessageWHook; + private readonly Dictionary wndProcNextDict = new(); + private readonly WndProcDelegate wndProcDelegate; + private readonly uint unhookSelfMessage; + private bool disposed; + + [ServiceManager.ServiceConstructor] + private unsafe WndProcHookManager() + { + this.wndProcDelegate = this.WndProcDetour; + this.dispatchMessageWHook = Hook.FromImport( + null, "user32.dll", "DispatchMessageW", 0, this.DispatchMessageWDetour); + this.dispatchMessageWHook.Enable(); + fixed (void* pMessageName = $"{nameof(WndProcHookManager)}.{nameof(this.unhookSelfMessage)}") + this.unhookSelfMessage = RegisterWindowMessageW((ushort*)pMessageName); + } + + /// + /// Finalizes an instance of the class. + /// + ~WndProcHookManager() => this.ReleaseUnmanagedResources(); + + /// + /// Delegate for overriding WndProc. + /// + /// The arguments. + public delegate void WndProcOverrideDelegate(ref WndProcOverrideEventArgs args); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private delegate LRESULT WndProcDelegate(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private delegate nint DispatchMessageWDelegate(ref MSG msg); + + /// + /// Called before WndProc. + /// + public event WndProcOverrideDelegate? PreWndProc; + + /// + /// Called after WndProc. + /// + public event WndProcOverrideDelegate? PostWndProc; + + /// + public void Dispose() + { + this.disposed = true; + this.dispatchMessageWHook.Dispose(); + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + /// + /// Detour for . Used to discover new windows to hook. + /// + /// The message. + /// The original return value. + private unsafe nint DispatchMessageWDetour(ref MSG msg) + { + lock (this.wndProcNextDict) + { + if (!this.disposed && ImGuiHelpers.FindViewportId(msg.hwnd) >= 0 && + !this.wndProcNextDict.ContainsKey(msg.hwnd)) + { + this.wndProcNextDict[msg.hwnd] = SetWindowLongPtrW( + msg.hwnd, + GWLP.GWLP_WNDPROC, + Marshal.GetFunctionPointerForDelegate(this.wndProcDelegate)); + } + } + + return this.dispatchMessageWHook.IsDisposed + ? DispatchMessageW((MSG*)Unsafe.AsPointer(ref msg)) + : this.dispatchMessageWHook.Original(ref msg); + } + + private unsafe LRESULT WndProcDetour(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam) + { + nint nextProc; + lock (this.wndProcNextDict) + { + if (!this.wndProcNextDict.TryGetValue(hwnd, out nextProc)) + { + // Something went wrong; prevent crash. Things will, regardless of the effort, break. + return DefWindowProcW(hwnd, uMsg, wParam, lParam); + } + } + + if (uMsg == this.unhookSelfMessage) + { + // Remove self from the chain. + SetWindowLongPtrW(hwnd, GWLP.GWLP_WNDPROC, nextProc); + lock (this.wndProcNextDict) + this.wndProcNextDict.Remove(hwnd); + + // Even though this message is dedicated for our processing, + // satisfy the expectations by calling the next window procedure. + return CallWindowProcW( + (delegate* unmanaged)nextProc, + hwnd, + uMsg, + wParam, + lParam); + } + + var arg = new WndProcOverrideEventArgs(hwnd, ref uMsg, ref wParam, ref lParam); + try + { + this.PreWndProc?.Invoke(ref arg); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(this.PostWndProc)} error"); + } + + if (!arg.SuppressCall) + { + try + { + arg.ReturnValue = CallWindowProcW( + (delegate* unmanaged)nextProc, + hwnd, + uMsg, + wParam, + lParam); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(CallWindowProcW)} error; probably some other software's fault"); + } + + try + { + this.PostWndProc?.Invoke(ref arg); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(this.PostWndProc)} error"); + } + } + + if (uMsg == WM.WM_NCDESTROY) + { + // The window will cease to exist, once we return. + SetWindowLongPtrW(hwnd, GWLP.GWLP_WNDPROC, nextProc); + lock (this.wndProcNextDict) + this.wndProcNextDict.Remove(hwnd); + } + + return arg.ReturnValue; + } + + private void ReleaseUnmanagedResources() + { + this.disposed = true; + + // As wndProcNextDict will be touched on each SendMessageW call, make a copy of window list first. + HWND[] windows; + lock (this.wndProcNextDict) + windows = this.wndProcNextDict.Keys.ToArray(); + + // Unregister our hook from all the windows we hooked. + foreach (var v in windows) + SendMessageW(v, this.unhookSelfMessage, default, default); + } + + /// + /// Parameters for . + /// + public ref struct WndProcOverrideEventArgs + { + /// + /// The handle of the target window of the message. + /// + public readonly HWND Hwnd; + + /// + /// The message. + /// + public ref uint Message; + + /// + /// The WPARAM. + /// + public ref WPARAM WParam; + + /// + /// The LPARAM. + /// + public ref LPARAM LParam; + + /// + /// Initializes a new instance of the struct. + /// + /// The handle of the target window of the message. + /// The message. + /// The WPARAM. + /// The LPARAM. + public WndProcOverrideEventArgs(HWND hwnd, ref uint msg, ref WPARAM wParam, ref LPARAM lParam) + { + this.Hwnd = hwnd; + this.LParam = ref lParam; + this.WParam = ref wParam; + this.Message = ref msg; + this.ViewportId = ImGuiHelpers.FindViewportId(hwnd); + } + + /// + /// Gets or sets a value indicating whether to suppress calling the next WndProc in the chain.
+ /// Does nothing if changed from . + ///
+ public bool SuppressCall { get; set; } + + /// + /// Gets or sets the return value.
+ /// Has the return value from next window procedure, if accessed from . + ///
+ public LRESULT ReturnValue { get; set; } + + /// + /// Gets the ImGui viewport ID. + /// + public int ViewportId { get; init; } + + /// + /// Gets a value indicating whether this message is for the game window (the first viewport). + /// + public bool IsGameWindow => this.ViewportId == 0; + + /// + /// Sets to true and sets . + /// + /// The new return value. + public void SuppressAndReturn(LRESULT returnValue) + { + this.ReturnValue = returnValue; + this.SuppressCall = true; + } + + /// + /// Sets to true and calls . + /// + public void SuppressWithDefault() + { + this.ReturnValue = DefWindowProcW(this.Hwnd, this.Message, this.WParam, this.LParam); + this.SuppressCall = true; + } + } +} diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 579d93f86..85f81b203 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -426,6 +426,26 @@ public static class ImGuiHelpers /// The pointer. /// Whether it is empty. public static unsafe bool IsNull(this ImFontAtlasPtr ptr) => ptr.NativePtr == null; + + /// + /// Finds the corresponding ImGui viewport ID for the given window handle. + /// + /// The window handle. + /// The viewport ID, or -1 if not found. + internal static unsafe int FindViewportId(nint hwnd) + { + if (!IsImGuiInitialized) + return -1; + + var viewports = new ImVectorWrapper(&ImGui.GetPlatformIO().NativePtr->Viewports); + for (var i = 0; i < viewports.LengthUnsafe; i++) + { + if (viewports.DataUnsafe[i].PlatformHandle == hwnd) + return i; + } + + return -1; + } /// /// Get data needed for each new frame. From e089949a728893474d83d05393df5487ace96c0d Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 02:33:18 +0900 Subject: [PATCH 02/33] fix minor things --- Dalamud/Interface/Internal/DalamudIme.cs | 28 ++++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 1fc70b0f6..6535228a7 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -184,6 +184,13 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType case WM.WM_IME_NOTIFY: // Log.Verbose($"{nameof(WM.WM_IME_NOTIFY)}({(nint)args.WParam:X}): {this.ImmComp}"); break; + + case WM.WM_LBUTTONDOWN: + case WM.WM_RBUTTONDOWN: + case WM.WM_MBUTTONDOWN: + case WM.WM_XBUTTONDOWN: + ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_COMPLETE, 0); + break; } this.UpdateInputLanguage(hImc); @@ -299,9 +306,11 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType if (finalCommit) { - this.PartialConversionFrom = this.PartialConversionTo = 0; + this.ClearState(hImc); + return; } - else if ((comp & GCS.GCS_COMPATTR) != 0) + + if ((comp & GCS.GCS_COMPATTR) != 0) { var attrLength = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, null, 0); var attrPtr = stackalloc byte[attrLength]; @@ -331,14 +340,17 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType this.UpdateImeWindowStatus(hImc); } - private void ClearState() + private void ClearState(HIMC hImc) { this.ImmComp = string.Empty; this.PartialConversionFrom = this.PartialConversionTo = 0; + this.CompositionCursorOffset = 0; this.UpdateImeWindowStatus(default); ref var textState = ref TextState; textState.Stb.Cursor = textState.Stb.SelectStart = textState.Stb.SelectEnd; + + Log.Information($"{nameof(this.ClearState)}"); } private void LoadCand(HIMC hImc) @@ -386,15 +398,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType private void ImGuiSetPlatformImeData(ImGuiViewportPtr viewport, ImGuiPlatformImeDataPtr data) { this.CursorPos = data.InputPos; - if (data.WantVisible) - { - this.AssociatedViewport = viewport; - } - else - { - this.AssociatedViewport = default; - this.ClearState(); - } + this.AssociatedViewport = data.WantVisible ? viewport : default; } [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")] From f03552a2ab51aa3a5c1b24501028beb5127db354 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 02:39:11 +0900 Subject: [PATCH 03/33] Prevent Tab key from breaking input --- Dalamud/Interface/Internal/DalamudIme.cs | 54 +++++++++++++++++------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 6535228a7..718ec53e6 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -55,12 +55,12 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType return textState.CursorAnim % 1.2f <= 0.8f; } } - + /// /// Gets the cursor position, in screen coordinates. /// internal Vector2 CursorPos { get; private set; } - + /// /// Gets the associated viewport. /// @@ -101,7 +101,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType /// internal bool ShowPartialConversion => this.PartialConversionFrom != 0 || this.PartialConversionTo != this.ImmComp.Length; - + /// /// Gets the input mode icon from . /// @@ -139,15 +139,17 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType switch (args.Message) { - case WM.WM_IME_NOTIFY when (nint)args.WParam is IMN.IMN_OPENCANDIDATE or IMN.IMN_CLOSECANDIDATE or IMN.IMN_CHANGECANDIDATE: + case WM.WM_IME_NOTIFY + when (nint)args.WParam is IMN.IMN_OPENCANDIDATE or IMN.IMN_CLOSECANDIDATE + or IMN.IMN_CHANGECANDIDATE: this.UpdateImeWindowStatus(hImc); args.SuppressAndReturn(0); break; - + case WM.WM_IME_STARTCOMPOSITION: args.SuppressAndReturn(0); break; - + case WM.WM_IME_COMPOSITION: if (invalidTarget) ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); @@ -162,12 +164,12 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType // Log.Verbose($"{nameof(WM.WM_IME_ENDCOMPOSITION)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); args.SuppressAndReturn(0); break; - + case WM.WM_IME_CONTROL: // Log.Verbose($"{nameof(WM.WM_IME_CONTROL)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); args.SuppressAndReturn(0); break; - + case WM.WM_IME_REQUEST: // Log.Verbose($"{nameof(WM.WM_IME_REQUEST)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); args.SuppressAndReturn(0); @@ -180,11 +182,31 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType // Log.Verbose($"{nameof(WM.WM_IME_SETCONTEXT)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); args.SuppressWithDefault(); break; - + case WM.WM_IME_NOTIFY: // Log.Verbose($"{nameof(WM.WM_IME_NOTIFY)}({(nint)args.WParam:X}): {this.ImmComp}"); break; + case WM.WM_KEYDOWN when (int)args.WParam is + VK.VK_TAB + or VK.VK_PRIOR + or VK.VK_NEXT + or VK.VK_END + or VK.VK_HOME + or VK.VK_LEFT + or VK.VK_UP + or VK.VK_RIGHT + or VK.VK_DOWN + or VK.VK_RETURN: + if (this.ImmCand.Count != 0) + { + TextState.Stb.SelectStart = TextState.Stb.Cursor = TextState.Stb.SelectEnd; + ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); + args.WParam = VK.VK_PROCESSKEY; + } + + break; + case WM.WM_LBUTTONDOWN: case WM.WM_RBUTTONDOWN: case WM.WM_MBUTTONDOWN: @@ -192,7 +214,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_COMPLETE, 0); break; } - + this.UpdateInputLanguage(hImc); } finally @@ -220,7 +242,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType ImmGetConversionStatus(hImc, &conv, &sent); var lang = GetKeyboardLayout(0); var open = ImmGetOpenStatus(hImc) != false; - + // Log.Verbose($"{nameof(this.UpdateInputLanguage)}: conv={conv:X} sent={sent:X} open={open} lang={lang:X}"); var native = (conv & 1) != 0; @@ -285,8 +307,8 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType (s, e) = (e, s); var newString = finalCommit - ? ImmGetCompositionString(hImc, GCS.GCS_RESULTSTR) - : ImmGetCompositionString(hImc, GCS.GCS_COMPSTR); + ? ImmGetCompositionString(hImc, GCS.GCS_RESULTSTR) + : ImmGetCompositionString(hImc, GCS.GCS_COMPSTR); if (s != e) textState.DeleteChars(s, e - s); @@ -303,13 +325,13 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType finalCommit ? 0 : ImmGetCompositionStringW(hImc, GCS.GCS_CURSORPOS, null, 0); - + if (finalCommit) { this.ClearState(hImc); return; } - + if ((comp & GCS.GCS_COMPATTR) != 0) { var attrLength = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, null, 0); @@ -349,7 +371,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType ref var textState = ref TextState; textState.Stb.Cursor = textState.Stb.SelectStart = textState.Stb.SelectEnd; - + Log.Information($"{nameof(this.ClearState)}"); } From 01b45c98ac0abc12a6c644d61a1a7f82525a5e8e Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 02:42:54 +0900 Subject: [PATCH 04/33] w --- Dalamud/Interface/Internal/DalamudIme.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 718ec53e6..286197590 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -200,8 +200,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType or VK.VK_RETURN: if (this.ImmCand.Count != 0) { - TextState.Stb.SelectStart = TextState.Stb.Cursor = TextState.Stb.SelectEnd; - ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); + this.ClearState(hImc); args.WParam = VK.VK_PROCESSKEY; } @@ -367,6 +366,8 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType this.ImmComp = string.Empty; this.PartialConversionFrom = this.PartialConversionTo = 0; this.CompositionCursorOffset = 0; + TextState.Stb.SelectStart = TextState.Stb.Cursor = TextState.Stb.SelectEnd; + ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); this.UpdateImeWindowStatus(default); ref var textState = ref TextState; From 2c3139d8b7685d16b1db1268361a0176aab6efeb Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 15:41:37 +0900 Subject: [PATCH 05/33] Ensure borders on IME mode foreground icon --- Dalamud/Game/Text/SeIconChar.cs | 32 ++++++++++++++++--- Dalamud/Interface/Internal/DalamudIme.cs | 9 +++--- .../Internal/Windows/DalamudImeWindow.cs | 28 ++++++++++++++++ 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/Dalamud/Game/Text/SeIconChar.cs b/Dalamud/Game/Text/SeIconChar.cs index c1be00613..17924c671 100644 --- a/Dalamud/Game/Text/SeIconChar.cs +++ b/Dalamud/Game/Text/SeIconChar.cs @@ -611,29 +611,51 @@ public enum SeIconChar QuestRepeatable = 0xE0BF, /// - /// The IME hiragana icon unicode character. + /// The [あ] character indicating that the Japanese IME is in full-width Hiragana input mode. /// + /// + /// Half-width Hiragana exists as a Windows API constant, but the feature is unused, or at least unexposed to the end user via the IME. + /// ImeHiragana = 0xE020, /// - /// The IME katakana icon unicode character. + /// The [ア] character indicating that the Japanese IME is in full-width Katakana input mode. /// ImeKatakana = 0xE021, /// - /// The IME alphanumeric icon unicode character. + /// The [A] character indicating that Japanese or Korean IME is in full-width Latin character input mode. /// ImeAlphanumeric = 0xE022, /// - /// The IME katakana half-width icon unicode character. + /// The [_ア] character indicating that the Japanese IME is in half-width Katakana input mode. /// ImeKatakanaHalfWidth = 0xE023, /// - /// The IME alphanumeric half-width icon unicode character. + /// The [_A] character indicating that Japanese or Korean IME is in half-width Latin character input mode. /// ImeAlphanumericHalfWidth = 0xE024, + + /// + /// The [가] character indicating that the Korean IME is in Hangul input mode. + /// + /// + /// Use and for alphanumeric input mode, + /// toggled via Alt+=. + /// + ImeKoreanHangul = 0xE025, + + /// + /// The [中] character indicating that the Chinese IME is in Han character input mode. + /// + ImeChineseHan = 0xE026, + + /// + /// The [英] character indicating that the Chinese IME is in Latin character input mode. + /// + ImeChineseLatin = 0xE027, /// /// The instance (1) icon unicode character. diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 286197590..9e0466e66 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -19,7 +19,7 @@ using static TerraFX.Interop.Windows.Windows; namespace Dalamud.Interface.Internal; /// -/// This class handles IME for non-English users. +/// This class handles CJK IME. /// [ServiceManager.EarlyLoadedService] internal sealed unsafe class DalamudIme : IDisposable, IServiceType @@ -251,7 +251,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { case LANG.LANG_KOREAN: if (native) - this.InputModeIcon = "\uE025"; + this.InputModeIcon = $"{(char)SeIconChar.ImeKoreanHangul}"; else if (fullwidth) this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumeric}"; else @@ -274,11 +274,10 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType break; case LANG.LANG_CHINESE: - // TODO: does Chinese IME also need "open" check? if (native) - this.InputModeIcon = "\uE026"; + this.InputModeIcon = $"{(char)SeIconChar.ImeChineseHan}"; else - this.InputModeIcon = "\uE027"; + this.InputModeIcon = $"{(char)SeIconChar.ImeChineseLatin}"; break; default: diff --git a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs index 1819ed819..7417afd91 100644 --- a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs +++ b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs @@ -133,6 +133,20 @@ internal unsafe class DalamudImeWindow : Window if (!expandUpward && drawIme) { + for (var dx = -2; dx <= 2; dx++) + { + for (var dy = -2; dy <= 2; dy++) + { + if (dx != 0 || dy != 0) + { + drawList.AddText( + cursor + new Vector2(dx, dy), + ImGui.GetColorU32(ImGuiCol.WindowBg), + ime.InputModeIcon); + } + } + } + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.InputModeIcon); cursor.Y += candTextSize.Y + spaceY; } @@ -179,6 +193,20 @@ internal unsafe class DalamudImeWindow : Window if (expandUpward && drawIme) { + for (var dx = -2; dx <= 2; dx++) + { + for (var dy = -2; dy <= 2; dy++) + { + if (dx != 0 || dy != 0) + { + drawList.AddText( + cursor + new Vector2(dx, dy), + ImGui.GetColorU32(ImGuiCol.WindowBg), + ime.InputModeIcon); + } + } + } + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.InputModeIcon); } From 806ecc0faf7b019d3a31093561778c2f4328fb0e Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 15:48:20 +0900 Subject: [PATCH 06/33] Use RenderChar instead of AddText --- Dalamud/Interface/Internal/DalamudIme.cs | 24 +++++++++--------- .../Internal/Windows/DalamudImeWindow.cs | 25 +++++++++++++++---- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 9e0466e66..f44c885ce 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -105,7 +105,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType /// /// Gets the input mode icon from . /// - internal string? InputModeIcon { get; private set; } + internal char InputModeIcon { get; private set; } private static ref ImGuiInputTextState TextState => ref *(ImGuiInputTextState*)(ImGui.GetCurrentContext() + 0x4588); @@ -251,37 +251,37 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { case LANG.LANG_KOREAN: if (native) - this.InputModeIcon = $"{(char)SeIconChar.ImeKoreanHangul}"; + this.InputModeIcon = (char)SeIconChar.ImeKoreanHangul; else if (fullwidth) - this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumeric}"; + this.InputModeIcon = (char)SeIconChar.ImeAlphanumeric; else - this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumericHalfWidth}"; + this.InputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth; break; case LANG.LANG_JAPANESE: // wtf // see the function called from: 48 8b 0d ?? ?? ?? ?? e8 ?? ?? ?? ?? 8b d8 e9 ?? 00 00 0 if (open && native && katakana && fullwidth) - this.InputModeIcon = $"{(char)SeIconChar.ImeKatakana}"; + this.InputModeIcon = (char)SeIconChar.ImeKatakana; else if (open && native && katakana) - this.InputModeIcon = $"{(char)SeIconChar.ImeKatakanaHalfWidth}"; + this.InputModeIcon = (char)SeIconChar.ImeKatakanaHalfWidth; else if (open && native) - this.InputModeIcon = $"{(char)SeIconChar.ImeHiragana}"; + this.InputModeIcon = (char)SeIconChar.ImeHiragana; else if (open && fullwidth) - this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumeric}"; + this.InputModeIcon = (char)SeIconChar.ImeAlphanumeric; else - this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumericHalfWidth}"; + this.InputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth; break; case LANG.LANG_CHINESE: if (native) - this.InputModeIcon = $"{(char)SeIconChar.ImeChineseHan}"; + this.InputModeIcon = (char)SeIconChar.ImeChineseHan; else - this.InputModeIcon = $"{(char)SeIconChar.ImeChineseLatin}"; + this.InputModeIcon = (char)SeIconChar.ImeChineseLatin; break; default: - this.InputModeIcon = null; + this.InputModeIcon = default; break; } diff --git a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs index 7417afd91..ecaa522e5 100644 --- a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs +++ b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs @@ -43,7 +43,8 @@ internal unsafe class DalamudImeWindow : Window var drawCand = ime.ImmCand.Count != 0; var drawConv = drawCand || ime.ShowPartialConversion; - var drawIme = ime.InputModeIcon != null; + var drawIme = ime.InputModeIcon != 0; + var imeIconFont = InterfaceManager.DefaultFont; var pad = ImGui.GetStyle().WindowPadding; var candTextSize = ImGui.CalcTextSize(ime.ImmComp == string.Empty ? " " : ime.ImmComp); @@ -139,7 +140,9 @@ internal unsafe class DalamudImeWindow : Window { if (dx != 0 || dy != 0) { - drawList.AddText( + imeIconFont.RenderChar( + drawList, + imeIconFont.FontSize, cursor + new Vector2(dx, dy), ImGui.GetColorU32(ImGuiCol.WindowBg), ime.InputModeIcon); @@ -147,7 +150,12 @@ internal unsafe class DalamudImeWindow : Window } } - drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.InputModeIcon); + imeIconFont.RenderChar( + drawList, + imeIconFont.FontSize, + cursor, + ImGui.GetColorU32(ImGuiCol.Text), + ime.InputModeIcon); cursor.Y += candTextSize.Y + spaceY; } @@ -199,7 +207,9 @@ internal unsafe class DalamudImeWindow : Window { if (dx != 0 || dy != 0) { - drawList.AddText( + imeIconFont.RenderChar( + drawList, + imeIconFont.FontSize, cursor + new Vector2(dx, dy), ImGui.GetColorU32(ImGuiCol.WindowBg), ime.InputModeIcon); @@ -207,7 +217,12 @@ internal unsafe class DalamudImeWindow : Window } } - drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.InputModeIcon); + imeIconFont.RenderChar( + drawList, + imeIconFont.FontSize, + cursor, + ImGui.GetColorU32(ImGuiCol.Text), + ime.InputModeIcon); } return; From b910ebc014f4ac7fdefa0839d574c0626fde794c Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 10 Dec 2023 22:32:28 +0900 Subject: [PATCH 07/33] Auto-enable fonts depending on the character input --- Dalamud/Interface/Internal/DalamudIme.cs | 68 +++++++++++++++++++ .../Interface/Internal/InterfaceManager.cs | 56 ++++++++++++++- 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index f44c885ce..f8d7fb690 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -5,8 +5,10 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using System.Text.Unicode; using Dalamud.Game.Text; +using Dalamud.Interface.GameFonts; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; @@ -26,6 +28,26 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { private static readonly ModuleLog Log = new("IME"); + private static readonly UnicodeRange[] HanRange = + { + UnicodeRanges.CjkRadicalsSupplement, + UnicodeRanges.CjkSymbolsandPunctuation, + UnicodeRanges.CjkUnifiedIdeographsExtensionA, + UnicodeRanges.CjkUnifiedIdeographs, + UnicodeRanges.CjkCompatibilityIdeographs, + UnicodeRanges.CjkCompatibilityForms, + // No more; Extension B~ are outside BMP range + }; + + private static readonly UnicodeRange[] HangulRange = + { + UnicodeRanges.HangulJamo, + UnicodeRanges.HangulSyllables, + UnicodeRanges.HangulCompatibilityJamo, + UnicodeRanges.HangulJamoExtendedA, + UnicodeRanges.HangulJamoExtendedB, + }; + private readonly ImGuiSetPlatformImeDataDelegate setPlatformImeDataDelegate; [ServiceManager.ServiceConstructor] @@ -38,6 +60,16 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType private delegate void ImGuiSetPlatformImeDataDelegate(ImGuiViewportPtr viewport, ImGuiPlatformImeDataPtr data); + /// + /// Gets a value indicating whether Han(Chinese) input has been detected. + /// + public bool EncounteredHan { get; private set; } + + /// + /// Gets a value indicating whether Hangul(Korean) input has been detected. + /// + public bool EncounteredHangul { get; private set; } + /// /// Gets a value indicating whether to display the cursor in input text. This also deals with blinking. /// @@ -116,6 +148,39 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType GC.SuppressFinalize(this); } + /// + /// Looks for the characters inside and enables fonts accordingly. + /// + /// The string. + public void ReflectCharacterEncounters(string str) + { + foreach (var chr in str) + { + if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) + { + if (Service.Get() + .GetFdtReader(GameFontFamilyAndSize.Axis12) + ?.FindGlyph(chr) is null) + { + if (!this.EncounteredHan) + { + this.EncounteredHan = true; + Service.Get().RebuildFonts(); + } + } + } + + if (HangulRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) + { + if (!this.EncounteredHangul) + { + this.EncounteredHangul = true; + Service.Get().RebuildFonts(); + } + } + } + } + /// /// Processes window messages. /// @@ -308,6 +373,8 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType ? ImmGetCompositionString(hImc, GCS.GCS_RESULTSTR) : ImmGetCompositionString(hImc, GCS.GCS_COMPSTR); + this.ReflectCharacterEncounters(newString); + if (s != e) textState.DeleteChars(s, e - s); textState.InsertChars(s, newString); @@ -402,6 +469,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType (int)Math.Min(candlist.dwCount - candlist.dwPageStart, candlist.dwPageSize))) { this.ImmCand.Add(new((char*)(pStorage + candlist.dwOffset[i]))); + this.ReflectCharacterEncounters(this.ImmCand[^1]); } } diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index d7ab5ba9d..49dfdb248 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using System.Text.Unicode; using System.Threading; using Dalamud.Configuration.Internal; @@ -786,10 +787,22 @@ internal class InterfaceManager : IDisposable, IServiceType var fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKkr-Regular.otf"); if (!File.Exists(fontPathKr)) fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansKR-Regular.otf"); + if (!File.Exists(fontPathKr)) + fontPathKr = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "malgun.ttf"); if (!File.Exists(fontPathKr)) fontPathKr = null; Log.Verbose("[FONT] fontPathKr = {0}", fontPathKr); + var fontPathChs = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msyh.ttc"); + if (!File.Exists(fontPathChs)) + fontPathChs = null; + Log.Verbose("[FONT] fontPathChs = {0}", fontPathChs); + + var fontPathCht = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msjh.ttc"); + if (!File.Exists(fontPathCht)) + fontPathCht = null; + Log.Verbose("[FONT] fontPathChs = {0}", fontPathCht); + // Default font Log.Verbose("[FONT] SetupFonts - Default font"); var fontInfo = new TargetFontModification( @@ -817,7 +830,8 @@ internal class InterfaceManager : IDisposable, IServiceType this.loadedFontInfo[DefaultFont] = fontInfo; } - if (fontPathKr != null && Service.Get().EffectiveLanguage == "ko") + if (fontPathKr != null + && (Service.Get().EffectiveLanguage == "ko" || this.dalamudIme.EncounteredHangul)) { fontConfig.MergeMode = true; fontConfig.GlyphRanges = ioFonts.GetGlyphRangesKorean(); @@ -826,6 +840,46 @@ internal class InterfaceManager : IDisposable, IServiceType fontConfig.MergeMode = false; } + if (fontPathCht != null && Service.Get().EffectiveLanguage == "tw") + { + fontConfig.MergeMode = true; + var rangeHandle = GCHandle.Alloc(new ushort[] + { + (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), + (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), + 0, + }, GCHandleType.Pinned); + garbageList.Add(rangeHandle); + fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); + fontConfig.PixelSnapH = true; + ioFonts.AddFontFromFileTTF(fontPathCht, fontConfig.SizePixels, fontConfig); + fontConfig.MergeMode = false; + } + else if (fontPathChs != null && (Service.Get().EffectiveLanguage == "zh" + || this.dalamudIme.EncounteredHan)) + { + fontConfig.MergeMode = true; + var rangeHandle = GCHandle.Alloc(new ushort[] + { + (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), + (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), + 0, + }, GCHandleType.Pinned); + garbageList.Add(rangeHandle); + fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); + fontConfig.PixelSnapH = true; + ioFonts.AddFontFromFileTTF(fontPathChs, fontConfig.SizePixels, fontConfig); + fontConfig.MergeMode = false; + } + // FontAwesome icon font Log.Verbose("[FONT] SetupFonts - FontAwesome icon font"); { From 4be635be675204da1c0c59c6eb04e08e112754ce Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 17 Dec 2023 12:40:33 +0900 Subject: [PATCH 08/33] Remove ClearState log --- Dalamud/Interface/Internal/DalamudIme.cs | 2 +- Dalamud/Interface/Internal/WndProcHookManager.cs | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index f8d7fb690..b3252546a 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -439,7 +439,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType ref var textState = ref TextState; textState.Stb.Cursor = textState.Stb.SelectStart = textState.Stb.SelectEnd; - Log.Information($"{nameof(this.ClearState)}"); + // Log.Information($"{nameof(this.ClearState)}"); } private void LoadCand(HIMC hImc) diff --git a/Dalamud/Interface/Internal/WndProcHookManager.cs b/Dalamud/Interface/Internal/WndProcHookManager.cs index fcd90c95a..1110ff387 100644 --- a/Dalamud/Interface/Internal/WndProcHookManager.cs +++ b/Dalamud/Interface/Internal/WndProcHookManager.cs @@ -112,19 +112,21 @@ internal sealed class WndProcHookManager : IServiceType, IDisposable if (uMsg == this.unhookSelfMessage) { - // Remove self from the chain. - SetWindowLongPtrW(hwnd, GWLP.GWLP_WNDPROC, nextProc); - lock (this.wndProcNextDict) - this.wndProcNextDict.Remove(hwnd); - // Even though this message is dedicated for our processing, // satisfy the expectations by calling the next window procedure. - return CallWindowProcW( + var rv = CallWindowProcW( (delegate* unmanaged)nextProc, hwnd, uMsg, wParam, lParam); + + // Remove self from the chain. + SetWindowLongPtrW(hwnd, GWLP.GWLP_WNDPROC, nextProc); + lock (this.wndProcNextDict) + this.wndProcNextDict.Remove(hwnd); + + return rv; } var arg = new WndProcOverrideEventArgs(hwnd, ref uMsg, ref wParam, ref lParam); From 0afb3d2c8af5cb538fd88f0d9d113ee21ff036f6 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 17 Dec 2023 13:33:40 +0900 Subject: [PATCH 09/33] Better WndProc handling --- .../Hooking/WndProcHook/WndProcEventArgs.cs | 144 +++++++++ .../WndProcHook/WndProcEventDelegate.cs | 7 + .../Hooking/WndProcHook/WndProcHookManager.cs | 115 ++++++++ Dalamud/Interface/Internal/DalamudIme.cs | 33 ++- .../Interface/Internal/InterfaceManager.cs | 10 +- .../Interface/Internal/WndProcHookManager.cs | 275 ------------------ 6 files changed, 294 insertions(+), 290 deletions(-) create mode 100644 Dalamud/Hooking/WndProcHook/WndProcEventArgs.cs create mode 100644 Dalamud/Hooking/WndProcHook/WndProcEventDelegate.cs create mode 100644 Dalamud/Hooking/WndProcHook/WndProcHookManager.cs delete mode 100644 Dalamud/Interface/Internal/WndProcHookManager.cs diff --git a/Dalamud/Hooking/WndProcHook/WndProcEventArgs.cs b/Dalamud/Hooking/WndProcHook/WndProcEventArgs.cs new file mode 100644 index 000000000..b25df5d14 --- /dev/null +++ b/Dalamud/Hooking/WndProcHook/WndProcEventArgs.cs @@ -0,0 +1,144 @@ +using System.Runtime.InteropServices; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Hooking.WndProcHook; + +/// +/// Event arguments for , +/// and the manager for individual WndProc hook. +/// +internal sealed unsafe class WndProcEventArgs +{ + private readonly WndProcHookManager owner; + private readonly delegate* unmanaged oldWndProcW; + private readonly WndProcDelegate myWndProc; + + private GCHandle gcHandle; + private bool released; + + /// + /// Initializes a new instance of the class. + /// + /// The owner. + /// The handle of the target window of the message. + /// The viewport ID. + internal WndProcEventArgs(WndProcHookManager owner, HWND hwnd, int viewportId) + { + this.Hwnd = hwnd; + this.owner = owner; + this.ViewportId = viewportId; + this.myWndProc = this.WndProcDetour; + this.oldWndProcW = (delegate* unmanaged)SetWindowLongPtrW( + hwnd, + GWLP.GWLP_WNDPROC, + Marshal.GetFunctionPointerForDelegate(this.myWndProc)); + this.gcHandle = GCHandle.Alloc(this); + } + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private delegate LRESULT WndProcDelegate(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam); + + /// + /// Gets the handle of the target window of the message. + /// + public HWND Hwnd { get; } + + /// + /// Gets the ImGui viewport ID. + /// + public int ViewportId { get; } + + /// + /// Gets or sets the message. + /// + public uint Message { get; set; } + + /// + /// Gets or sets the WPARAM. + /// + public WPARAM WParam { get; set; } + + /// + /// Gets or sets the LPARAM. + /// + public LPARAM LParam { get; set; } + + /// + /// Gets or sets a value indicating whether to suppress calling the next WndProc in the chain.
+ /// Does nothing if changed from . + ///
+ public bool SuppressCall { get; set; } + + /// + /// Gets or sets the return value.
+ /// Has the return value from next window procedure, if accessed from . + ///
+ public LRESULT ReturnValue { get; set; } + + /// + /// Sets to true and sets . + /// + /// The new return value. + public void SuppressWithValue(LRESULT returnValue) + { + this.ReturnValue = returnValue; + this.SuppressCall = true; + } + + /// + /// Sets to true and sets from the result of + /// . + /// + public void SuppressWithDefault() + { + this.ReturnValue = DefWindowProcW(this.Hwnd, this.Message, this.WParam, this.LParam); + this.SuppressCall = true; + } + + /// + internal void InternalRelease() + { + if (this.released) + return; + + this.released = true; + SendMessageW(this.Hwnd, WM.WM_NULL, 0, 0); + this.FinalRelease(); + } + + private void FinalRelease() + { + if (!this.gcHandle.IsAllocated) + return; + + this.gcHandle.Free(); + SetWindowLongPtrW(this.Hwnd, GWLP.GWLP_WNDPROC, (nint)this.oldWndProcW); + this.owner.OnHookedWindowRemoved(this); + } + + private LRESULT WndProcDetour(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam) + { + if (hwnd != this.Hwnd) + return CallWindowProcW(this.oldWndProcW, hwnd, uMsg, wParam, lParam); + + this.SuppressCall = false; + this.ReturnValue = 0; + this.Message = uMsg; + this.WParam = wParam; + this.LParam = lParam; + this.owner.InvokePreWndProc(this); + + if (!this.SuppressCall) + this.ReturnValue = CallWindowProcW(this.oldWndProcW, hwnd, uMsg, wParam, lParam); + + this.owner.InvokePostWndProc(this); + + if (uMsg == WM.WM_NCDESTROY || this.released) + this.FinalRelease(); + + return this.ReturnValue; + } +} diff --git a/Dalamud/Hooking/WndProcHook/WndProcEventDelegate.cs b/Dalamud/Hooking/WndProcHook/WndProcEventDelegate.cs new file mode 100644 index 000000000..f753f16cc --- /dev/null +++ b/Dalamud/Hooking/WndProcHook/WndProcEventDelegate.cs @@ -0,0 +1,7 @@ +namespace Dalamud.Hooking.WndProcHook; + +/// +/// Delegate for overriding WndProc. +/// +/// The arguments. +internal delegate void WndProcEventDelegate(WndProcEventArgs args); diff --git a/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs new file mode 100644 index 000000000..00934f27f --- /dev/null +++ b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs @@ -0,0 +1,115 @@ +using System.Collections.Generic; +using System.Runtime.InteropServices; + +using Dalamud.Interface.Utility; +using Dalamud.Logging.Internal; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Hooking.WndProcHook; + +/// +/// Manages WndProc hooks for game main window and extra ImGui viewport windows. +/// +[ServiceManager.BlockingEarlyLoadedService] +internal sealed class WndProcHookManager : IServiceType, IDisposable +{ + private static readonly ModuleLog Log = new(nameof(WndProcHookManager)); + + private readonly Hook dispatchMessageWHook; + private readonly Dictionary wndProcOverrides = new(); + + [ServiceManager.ServiceConstructor] + private unsafe WndProcHookManager() + { + this.dispatchMessageWHook = Hook.FromImport( + null, + "user32.dll", + "DispatchMessageW", + 0, + this.DispatchMessageWDetour); + this.dispatchMessageWHook.Enable(); + } + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private unsafe delegate nint DispatchMessageWDelegate(MSG* msg); + + /// + /// Called before WndProc. + /// + public event WndProcEventDelegate? PreWndProc; + + /// + /// Called after WndProc. + /// + public event WndProcEventDelegate? PostWndProc; + + /// + public void Dispose() + { + this.dispatchMessageWHook.Dispose(); + foreach (var v in this.wndProcOverrides.Values) + v.InternalRelease(); + this.wndProcOverrides.Clear(); + } + + /// + /// Invokes . + /// + /// The arguments. + internal void InvokePreWndProc(WndProcEventArgs args) + { + try + { + this.PreWndProc?.Invoke(args); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(this.PreWndProc)} error"); + } + } + + /// + /// Invokes . + /// + /// The arguments. + internal void InvokePostWndProc(WndProcEventArgs args) + { + try + { + this.PostWndProc?.Invoke(args); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(this.PostWndProc)} error"); + } + } + + /// + /// Removes from the list of known WndProc overrides. + /// + /// Object to remove. + internal void OnHookedWindowRemoved(WndProcEventArgs args) + { + if (!this.dispatchMessageWHook.IsDisposed) + this.wndProcOverrides.Remove(args.Hwnd); + } + + /// + /// Detour for . Used to discover new windows to hook. + /// + /// The message. + /// The original return value. + private unsafe nint DispatchMessageWDetour(MSG* msg) + { + if (!this.wndProcOverrides.ContainsKey(msg->hwnd) + && ImGuiHelpers.FindViewportId(msg->hwnd) is var vpid and >= 0) + { + this.wndProcOverrides[msg->hwnd] = new(this, msg->hwnd, vpid); + } + + return this.dispatchMessageWHook.Original(msg); + } +} diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index b3252546a..9bd9a2498 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -8,6 +8,7 @@ using System.Text; using System.Text.Unicode; using Dalamud.Game.Text; +using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; @@ -77,6 +78,8 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { get { + if (!ImGuiHelpers.IsImGuiInitialized) + return true; if (!ImGui.GetIO().ConfigInputTextCursorBlink) return true; ref var textState = ref TextState; @@ -185,7 +188,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType /// Processes window messages. ///
/// The arguments. - public void ProcessImeMessage(ref WndProcHookManager.WndProcOverrideEventArgs args) + public void ProcessImeMessage(WndProcEventArgs args) { if (!ImGuiHelpers.IsImGuiInitialized) return; @@ -208,11 +211,11 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType when (nint)args.WParam is IMN.IMN_OPENCANDIDATE or IMN.IMN_CLOSECANDIDATE or IMN.IMN_CHANGECANDIDATE: this.UpdateImeWindowStatus(hImc); - args.SuppressAndReturn(0); + args.SuppressWithValue(0); break; case WM.WM_IME_STARTCOMPOSITION: - args.SuppressAndReturn(0); + args.SuppressWithValue(0); break; case WM.WM_IME_COMPOSITION: @@ -222,22 +225,22 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType this.ReplaceCompositionString(hImc, (uint)args.LParam); // Log.Verbose($"{nameof(WM.WM_IME_COMPOSITION)}({(nint)args.LParam:X}): {this.ImmComp}"); - args.SuppressAndReturn(0); + args.SuppressWithValue(0); break; case WM.WM_IME_ENDCOMPOSITION: // Log.Verbose($"{nameof(WM.WM_IME_ENDCOMPOSITION)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); - args.SuppressAndReturn(0); + args.SuppressWithValue(0); break; case WM.WM_IME_CONTROL: // Log.Verbose($"{nameof(WM.WM_IME_CONTROL)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); - args.SuppressAndReturn(0); + args.SuppressWithValue(0); break; case WM.WM_IME_REQUEST: // Log.Verbose($"{nameof(WM.WM_IME_REQUEST)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); - args.SuppressAndReturn(0); + args.SuppressWithValue(0); break; case WM.WM_IME_SETCONTEXT: @@ -298,7 +301,11 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType return new(data, 0, numBytes / 2); } - private void ReleaseUnmanagedResources() => ImGui.GetIO().SetPlatformImeDataFn = nint.Zero; + private void ReleaseUnmanagedResources() + { + if (ImGuiHelpers.IsImGuiInitialized) + ImGui.GetIO().SetPlatformImeDataFn = nint.Zero; + } private void UpdateInputLanguage(HIMC hImc) { @@ -492,8 +499,16 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType } [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")] - private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) => + private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) + { + if (!ImGuiHelpers.IsImGuiInitialized) + { + throw new InvalidOperationException( + $"Expected {nameof(InterfaceManager.InterfaceManagerWithScene)} to have initialized ImGui."); + } + ImGui.GetIO().SetPlatformImeDataFn = Marshal.GetFunctionPointerForDelegate(this.setPlatformImeDataDelegate); + } /// /// Ported from imstb_textedit.h. diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 49dfdb248..48157fa86 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -15,6 +15,7 @@ using Dalamud.Game.ClientState.GamePad; using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Internal.DXGI; using Dalamud.Hooking; +using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; @@ -659,16 +660,13 @@ internal class InterfaceManager : IDisposable, IServiceType this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc; } - private unsafe void WndProcHookManagerOnPreWndProc(ref WndProcHookManager.WndProcOverrideEventArgs args) + private unsafe void WndProcHookManagerOnPreWndProc(WndProcEventArgs args) { var r = this.scene?.ProcessWndProcW(args.Hwnd, (User32.WindowMessage)args.Message, args.WParam, args.LParam); if (r is not null) - { - args.ReturnValue = r.Value; - args.SuppressCall = true; - } + args.SuppressWithValue(r.Value); - this.dalamudIme.ProcessImeMessage(ref args); + this.dalamudIme.ProcessImeMessage(args); } /* diff --git a/Dalamud/Interface/Internal/WndProcHookManager.cs b/Dalamud/Interface/Internal/WndProcHookManager.cs deleted file mode 100644 index 1110ff387..000000000 --- a/Dalamud/Interface/Internal/WndProcHookManager.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -using Dalamud.Hooking; -using Dalamud.Interface.Utility; -using Dalamud.Logging.Internal; - -using TerraFX.Interop.Windows; - -using static TerraFX.Interop.Windows.Windows; - -namespace Dalamud.Interface.Internal; - -/// -/// A manifestation of "I can't believe this is required". -/// -[ServiceManager.BlockingEarlyLoadedService] -internal sealed class WndProcHookManager : IServiceType, IDisposable -{ - private static readonly ModuleLog Log = new("WPHM"); - - private readonly Hook dispatchMessageWHook; - private readonly Dictionary wndProcNextDict = new(); - private readonly WndProcDelegate wndProcDelegate; - private readonly uint unhookSelfMessage; - private bool disposed; - - [ServiceManager.ServiceConstructor] - private unsafe WndProcHookManager() - { - this.wndProcDelegate = this.WndProcDetour; - this.dispatchMessageWHook = Hook.FromImport( - null, "user32.dll", "DispatchMessageW", 0, this.DispatchMessageWDetour); - this.dispatchMessageWHook.Enable(); - fixed (void* pMessageName = $"{nameof(WndProcHookManager)}.{nameof(this.unhookSelfMessage)}") - this.unhookSelfMessage = RegisterWindowMessageW((ushort*)pMessageName); - } - - /// - /// Finalizes an instance of the class. - /// - ~WndProcHookManager() => this.ReleaseUnmanagedResources(); - - /// - /// Delegate for overriding WndProc. - /// - /// The arguments. - public delegate void WndProcOverrideDelegate(ref WndProcOverrideEventArgs args); - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate LRESULT WndProcDelegate(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam); - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate nint DispatchMessageWDelegate(ref MSG msg); - - /// - /// Called before WndProc. - /// - public event WndProcOverrideDelegate? PreWndProc; - - /// - /// Called after WndProc. - /// - public event WndProcOverrideDelegate? PostWndProc; - - /// - public void Dispose() - { - this.disposed = true; - this.dispatchMessageWHook.Dispose(); - this.ReleaseUnmanagedResources(); - GC.SuppressFinalize(this); - } - - /// - /// Detour for . Used to discover new windows to hook. - /// - /// The message. - /// The original return value. - private unsafe nint DispatchMessageWDetour(ref MSG msg) - { - lock (this.wndProcNextDict) - { - if (!this.disposed && ImGuiHelpers.FindViewportId(msg.hwnd) >= 0 && - !this.wndProcNextDict.ContainsKey(msg.hwnd)) - { - this.wndProcNextDict[msg.hwnd] = SetWindowLongPtrW( - msg.hwnd, - GWLP.GWLP_WNDPROC, - Marshal.GetFunctionPointerForDelegate(this.wndProcDelegate)); - } - } - - return this.dispatchMessageWHook.IsDisposed - ? DispatchMessageW((MSG*)Unsafe.AsPointer(ref msg)) - : this.dispatchMessageWHook.Original(ref msg); - } - - private unsafe LRESULT WndProcDetour(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam) - { - nint nextProc; - lock (this.wndProcNextDict) - { - if (!this.wndProcNextDict.TryGetValue(hwnd, out nextProc)) - { - // Something went wrong; prevent crash. Things will, regardless of the effort, break. - return DefWindowProcW(hwnd, uMsg, wParam, lParam); - } - } - - if (uMsg == this.unhookSelfMessage) - { - // Even though this message is dedicated for our processing, - // satisfy the expectations by calling the next window procedure. - var rv = CallWindowProcW( - (delegate* unmanaged)nextProc, - hwnd, - uMsg, - wParam, - lParam); - - // Remove self from the chain. - SetWindowLongPtrW(hwnd, GWLP.GWLP_WNDPROC, nextProc); - lock (this.wndProcNextDict) - this.wndProcNextDict.Remove(hwnd); - - return rv; - } - - var arg = new WndProcOverrideEventArgs(hwnd, ref uMsg, ref wParam, ref lParam); - try - { - this.PreWndProc?.Invoke(ref arg); - } - catch (Exception e) - { - Log.Error(e, $"{nameof(this.PostWndProc)} error"); - } - - if (!arg.SuppressCall) - { - try - { - arg.ReturnValue = CallWindowProcW( - (delegate* unmanaged)nextProc, - hwnd, - uMsg, - wParam, - lParam); - } - catch (Exception e) - { - Log.Error(e, $"{nameof(CallWindowProcW)} error; probably some other software's fault"); - } - - try - { - this.PostWndProc?.Invoke(ref arg); - } - catch (Exception e) - { - Log.Error(e, $"{nameof(this.PostWndProc)} error"); - } - } - - if (uMsg == WM.WM_NCDESTROY) - { - // The window will cease to exist, once we return. - SetWindowLongPtrW(hwnd, GWLP.GWLP_WNDPROC, nextProc); - lock (this.wndProcNextDict) - this.wndProcNextDict.Remove(hwnd); - } - - return arg.ReturnValue; - } - - private void ReleaseUnmanagedResources() - { - this.disposed = true; - - // As wndProcNextDict will be touched on each SendMessageW call, make a copy of window list first. - HWND[] windows; - lock (this.wndProcNextDict) - windows = this.wndProcNextDict.Keys.ToArray(); - - // Unregister our hook from all the windows we hooked. - foreach (var v in windows) - SendMessageW(v, this.unhookSelfMessage, default, default); - } - - /// - /// Parameters for . - /// - public ref struct WndProcOverrideEventArgs - { - /// - /// The handle of the target window of the message. - /// - public readonly HWND Hwnd; - - /// - /// The message. - /// - public ref uint Message; - - /// - /// The WPARAM. - /// - public ref WPARAM WParam; - - /// - /// The LPARAM. - /// - public ref LPARAM LParam; - - /// - /// Initializes a new instance of the struct. - /// - /// The handle of the target window of the message. - /// The message. - /// The WPARAM. - /// The LPARAM. - public WndProcOverrideEventArgs(HWND hwnd, ref uint msg, ref WPARAM wParam, ref LPARAM lParam) - { - this.Hwnd = hwnd; - this.LParam = ref lParam; - this.WParam = ref wParam; - this.Message = ref msg; - this.ViewportId = ImGuiHelpers.FindViewportId(hwnd); - } - - /// - /// Gets or sets a value indicating whether to suppress calling the next WndProc in the chain.
- /// Does nothing if changed from . - ///
- public bool SuppressCall { get; set; } - - /// - /// Gets or sets the return value.
- /// Has the return value from next window procedure, if accessed from . - ///
- public LRESULT ReturnValue { get; set; } - - /// - /// Gets the ImGui viewport ID. - /// - public int ViewportId { get; init; } - - /// - /// Gets a value indicating whether this message is for the game window (the first viewport). - /// - public bool IsGameWindow => this.ViewportId == 0; - - /// - /// Sets to true and sets . - /// - /// The new return value. - public void SuppressAndReturn(LRESULT returnValue) - { - this.ReturnValue = returnValue; - this.SuppressCall = true; - } - - /// - /// Sets to true and calls . - /// - public void SuppressWithDefault() - { - this.ReturnValue = DefWindowProcW(this.Hwnd, this.Message, this.WParam, this.LParam); - this.SuppressCall = true; - } - } -} From 6fefc3bee0692310ed14babecba948e3f9777c69 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 17 Dec 2023 14:09:38 +0900 Subject: [PATCH 10/33] Safer unload --- .../Hooking/WndProcHook/WndProcHookManager.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs index 00934f27f..91020f898 100644 --- a/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs +++ b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Runtime.InteropServices; +using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; @@ -21,6 +22,8 @@ internal sealed class WndProcHookManager : IServiceType, IDisposable private readonly Hook dispatchMessageWHook; private readonly Dictionary wndProcOverrides = new(); + private HWND mainWindowHwnd; + [ServiceManager.ServiceConstructor] private unsafe WndProcHookManager() { @@ -31,6 +34,12 @@ internal sealed class WndProcHookManager : IServiceType, IDisposable 0, this.DispatchMessageWDetour); this.dispatchMessageWHook.Enable(); + + // Capture the game main window handle, + // so that no guarantees would have to be made on the service dispose order. + Service + .GetAsync() + .ContinueWith(r => this.mainWindowHwnd = (HWND)r.Result.Manager.GameWindowHandle); } [UnmanagedFunctionPointer(CallingConvention.StdCall)] @@ -49,7 +58,19 @@ internal sealed class WndProcHookManager : IServiceType, IDisposable /// public void Dispose() { + if (this.dispatchMessageWHook.IsDisposed) + return; + this.dispatchMessageWHook.Dispose(); + + // Ensure that either we're on the main thread, or DispatchMessage is executed at least once. + // The game calls DispatchMessageW only from its main thread, so if we're already on one, + // this line does nothing; if not, it will require a cycle of GetMessage ... DispatchMessageW, + // which at the point of returning from DispatchMessageW(=point of returning from SendMessageW), + // the hook would be guaranteed to be fully disabled and detour delegate would be safe to be released. + SendMessageW(this.mainWindowHwnd, WM.WM_NULL, 0, 0); + + // Now this.wndProcOverrides cannot be touched from other thread. foreach (var v in this.wndProcOverrides.Values) v.InternalRelease(); this.wndProcOverrides.Clear(); From 9ca2d34f956c2a904c325fcb5816b6c00165f1c9 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 19 Dec 2023 09:07:45 +0100 Subject: [PATCH 11/33] Update ClientStructs (#1577) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 3364dfea7..edc754348 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 3364dfea769b79e43aebaa955b6b98ec1d6eb458 +Subproject commit edc754348a3ed8fd49da6695248bfebe7ba89c12 From 6eb8153a99acb83cc321474ac22251b59e076a27 Mon Sep 17 00:00:00 2001 From: srkizer Date: Fri, 22 Dec 2023 10:56:01 +0900 Subject: [PATCH 12/33] Add missing EmptyClipboard (#1584) * Add missing EmptyClipboard * Fix missing GlobalUnlock --- Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index fd07d824f..1746fb1c4 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -131,6 +131,7 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis ptr[str.Length] = default; GlobalUnlock(hMem); + EmptyClipboard(); SetClipboardData(CF.CF_UNICODETEXT, hMem); } catch (Exception e) @@ -158,9 +159,9 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis return this.clipboardData.Data; } + var hMem = (HGLOBAL)GetClipboardData(CF.CF_UNICODETEXT); try { - var hMem = (HGLOBAL)GetClipboardData(CF.CF_UNICODETEXT); if (hMem != default) { var ptr = (char*)GlobalLock(hMem); @@ -191,6 +192,8 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis } finally { + if (hMem != default) + GlobalUnlock(hMem); CloseClipboard(); } From e015da0447ae20216617913d10db77e9bb18bb6e Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 21 Dec 2023 18:10:44 -0800 Subject: [PATCH 13/33] Improve Dalamud ConsoleWindow plugin search (#1582) * Improve Dalamud ConsoleWindow plugin search * Improve Dalamud ConsoleWindow plugin search * Add `no results` message to plugin filter --- Dalamud/Interface/Internal/Windows/ConsoleWindow.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 89dd153cc..770582a30 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -39,6 +39,7 @@ internal class ConsoleWindow : Window, IDisposable private string commandText = string.Empty; private string textFilter = string.Empty; private string selectedSource = "DalamudInternal"; + private string pluginFilter = string.Empty; private bool filterShowUncaughtExceptions; private bool showFilterToolbar; @@ -475,14 +476,24 @@ internal class ConsoleWindow : Window, IDisposable ImGui.TableNextColumn(); ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); - if (ImGui.BeginCombo("##Sources", this.selectedSource)) + if (ImGui.BeginCombo("##Sources", this.selectedSource, ImGuiComboFlags.HeightLarge)) { var sourceNames = Service.Get().InstalledPlugins .Select(p => p.Manifest.InternalName) .OrderBy(s => s) .Prepend("DalamudInternal") + .Where(name => this.pluginFilter is "" || new FuzzyMatcher(this.pluginFilter.ToLowerInvariant(), MatchMode.Fuzzy).Matches(name.ToLowerInvariant()) != 0) .ToList(); + ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); + ImGui.InputTextWithHint("##PluginSearchFilter", "Filter Plugin List", ref this.pluginFilter, 2048); + ImGui.Separator(); + + if (!sourceNames.Any()) + { + ImGui.TextColored(KnownColor.OrangeRed.Vector(), "No Results"); + } + foreach (var selectable in sourceNames) { if (ImGui.Selectable(selectable, this.selectedSource == selectable)) From c0bb3aebc278a983b01bb77ef9cecaecb5efe8bd Mon Sep 17 00:00:00 2001 From: srkizer Date: Sat, 23 Dec 2023 19:24:41 +0900 Subject: [PATCH 14/33] Fix crashes from Ctrl+Z when having IME activated (#1587) --- Dalamud/Interface/Internal/DalamudIme.cs | 166 +++++++++++++++++------ 1 file changed, 122 insertions(+), 44 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 9bd9a2498..70e230c5f 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; @@ -11,7 +12,6 @@ using Dalamud.Game.Text; using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Utility; -using Dalamud.Logging.Internal; using ImGuiNET; @@ -27,7 +27,9 @@ namespace Dalamud.Interface.Internal; [ServiceManager.EarlyLoadedService] internal sealed unsafe class DalamudIme : IDisposable, IServiceType { - private static readonly ModuleLog Log = new("IME"); + private const int ImGuiContextTextStateOffset = 0x4588; + private const int CImGuiStbTextCreateUndoOffset = 0xB57A0; + private const int CImGuiStbTextUndoOffset = 0xB59C0; private static readonly UnicodeRange[] HanRange = { @@ -49,8 +51,40 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType UnicodeRanges.HangulJamoExtendedB, }; + private static readonly delegate* unmanaged + StbTextMakeUndoReplace; + + private static readonly delegate* unmanaged StbTextUndo; + private readonly ImGuiSetPlatformImeDataDelegate setPlatformImeDataDelegate; + private (int Start, int End, int Cursor)? temporaryUndoSelection; + + [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1003:Symbols should be spaced correctly", Justification = ".")] + static DalamudIme() + { + nint cimgui; + try + { + _ = ImGui.GetCurrentContext(); + + cimgui = Process.GetCurrentProcess().Modules.Cast() + .First(x => x.ModuleName == "cimgui.dll") + .BaseAddress; + } + catch + { + return; + } + + StbTextMakeUndoReplace = + (delegate* unmanaged) + (cimgui + CImGuiStbTextCreateUndoOffset); + StbTextUndo = + (delegate* unmanaged) + (cimgui + CImGuiStbTextUndoOffset); + } + [ServiceManager.ServiceConstructor] private DalamudIme() => this.setPlatformImeDataDelegate = this.ImGuiSetPlatformImeData; @@ -82,12 +116,12 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType return true; if (!ImGui.GetIO().ConfigInputTextCursorBlink) return true; - ref var textState = ref TextState; - if (textState.Id == 0 || (textState.Flags & ImGuiInputTextFlags.ReadOnly) != 0) + var textState = TextState; + if (textState->Id == 0 || (textState->Flags & ImGuiInputTextFlags.ReadOnly) != 0) return true; - if (textState.CursorAnim <= 0) + if (textState->CursorAnim <= 0) return true; - return textState.CursorAnim % 1.2f <= 0.8f; + return textState->CursorAnim % 1.2f <= 0.8f; } } @@ -142,7 +176,8 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType ///
internal char InputModeIcon { get; private set; } - private static ref ImGuiInputTextState TextState => ref *(ImGuiInputTextState*)(ImGui.GetCurrentContext() + 0x4588); + private static ImGuiInputTextState* TextState => + (ImGuiInputTextState*)(ImGui.GetCurrentContext() + ImGuiContextTextStateOffset); /// public void Dispose() @@ -203,7 +238,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType try { - var invalidTarget = TextState.Id == 0 || (TextState.Flags & ImGuiInputTextFlags.ReadOnly) != 0; + var invalidTarget = TextState->Id == 0 || (TextState->Flags & ImGuiInputTextFlags.ReadOnly) != 0; switch (args.Message) { @@ -362,41 +397,26 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType private void ReplaceCompositionString(HIMC hImc, uint comp) { - ref var textState = ref TextState; var finalCommit = (comp & GCS.GCS_RESULTSTR) != 0; - - ref var s = ref textState.Stb.SelectStart; - ref var e = ref textState.Stb.SelectEnd; - ref var c = ref textState.Stb.Cursor; - s = Math.Clamp(s, 0, textState.CurLenW); - e = Math.Clamp(e, 0, textState.CurLenW); - c = Math.Clamp(c, 0, textState.CurLenW); - if (s == e) - s = e = c; - if (s > e) - (s, e) = (e, s); - var newString = finalCommit ? ImmGetCompositionString(hImc, GCS.GCS_RESULTSTR) : ImmGetCompositionString(hImc, GCS.GCS_COMPSTR); this.ReflectCharacterEncounters(newString); - if (s != e) - textState.DeleteChars(s, e - s); - textState.InsertChars(s, newString); + if (this.temporaryUndoSelection is not null) + { + TextState->Undo(); + TextState->SelectionTuple = this.temporaryUndoSelection.Value; + this.temporaryUndoSelection = null; + } - if (finalCommit) - s = e = s + newString.Length; - else - e = s + newString.Length; + TextState->SanitizeSelectionRange(); + if (TextState->ReplaceSelectionAndPushUndo(newString)) + this.temporaryUndoSelection = TextState->SelectionTuple; - this.ImmComp = finalCommit ? string.Empty : newString; - - this.CompositionCursorOffset = - finalCommit - ? 0 - : ImmGetCompositionStringW(hImc, GCS.GCS_CURSORPOS, null, 0); + // Put the cursor at the beginning, so that the candidate window appears aligned with the text. + TextState->SetSelectionRange(TextState->SelectionTuple.Start, newString.Length, 0); if (finalCommit) { @@ -404,6 +424,9 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType return; } + this.ImmComp = newString; + this.CompositionCursorOffset = ImmGetCompositionStringW(hImc, GCS.GCS_CURSORPOS, null, 0); + if ((comp & GCS.GCS_COMPATTR) != 0) { var attrLength = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, null, 0); @@ -429,8 +452,6 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType this.PartialConversionTo = this.ImmComp.Length; } - // Put the cursor at the beginning, so that the candidate window appears aligned with the text. - c = s; this.UpdateImeWindowStatus(hImc); } @@ -439,13 +460,11 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType this.ImmComp = string.Empty; this.PartialConversionFrom = this.PartialConversionTo = 0; this.CompositionCursorOffset = 0; - TextState.Stb.SelectStart = TextState.Stb.Cursor = TextState.Stb.SelectEnd; + this.temporaryUndoSelection = null; + TextState->Stb.SelectStart = TextState->Stb.Cursor = TextState->Stb.SelectEnd; ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); this.UpdateImeWindowStatus(default); - ref var textState = ref TextState; - textState.Stb.Cursor = textState.Stb.SelectStart = textState.Stb.SelectEnd; - // Log.Information($"{nameof(this.ClearState)}"); } @@ -498,7 +517,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType this.AssociatedViewport = data.WantVisible ? viewport : default; } - [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")] + [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui context initialization.")] private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) { if (!ImGuiHelpers.IsImGuiInitialized) @@ -569,15 +588,71 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType public bool Edited; public ImGuiInputTextFlags Flags; - public ImVectorWrapper TextW => new((ImVector*)Unsafe.AsPointer(ref this.TextWRaw)); + public ImVectorWrapper TextW => new((ImVector*)&this.ThisPtr->TextWRaw); - public ImVectorWrapper TextA => new((ImVector*)Unsafe.AsPointer(ref this.TextWRaw)); + public (int Start, int End, int Cursor) SelectionTuple + { + get => (this.Stb.SelectStart, this.Stb.SelectEnd, this.Stb.Cursor); + set => (this.Stb.SelectStart, this.Stb.SelectEnd, this.Stb.Cursor) = value; + } - public ImVectorWrapper InitialTextA => new((ImVector*)Unsafe.AsPointer(ref this.TextWRaw)); + private ImGuiInputTextState* ThisPtr => (ImGuiInputTextState*)Unsafe.AsPointer(ref this); + + public void SetSelectionRange(int offset, int length, int relativeCursorOffset) + { + this.Stb.SelectStart = offset; + this.Stb.SelectEnd = offset + length; + if (relativeCursorOffset >= 0) + this.Stb.Cursor = this.Stb.SelectStart + relativeCursorOffset; + else + this.Stb.Cursor = this.Stb.SelectEnd + 1 + relativeCursorOffset; + this.SanitizeSelectionRange(); + } + + public void SanitizeSelectionRange() + { + ref var s = ref this.Stb.SelectStart; + ref var e = ref this.Stb.SelectEnd; + ref var c = ref this.Stb.Cursor; + s = Math.Clamp(s, 0, this.CurLenW); + e = Math.Clamp(e, 0, this.CurLenW); + c = Math.Clamp(c, 0, this.CurLenW); + if (s == e) + s = e = c; + if (s > e) + (s, e) = (e, s); + } + + public void Undo() => StbTextUndo(this.ThisPtr, &this.ThisPtr->Stb); + + public bool MakeUndoReplace(int offset, int oldLength, int newLength) + { + if (oldLength == 0 && newLength == 0) + return false; + + StbTextMakeUndoReplace(this.ThisPtr, &this.ThisPtr->Stb, offset, oldLength, newLength); + return true; + } + + public bool ReplaceSelectionAndPushUndo(ReadOnlySpan newText) + { + var off = this.Stb.SelectStart; + var len = this.Stb.SelectEnd - this.Stb.SelectStart; + return this.MakeUndoReplace(off, len, newText.Length) && this.ReplaceChars(off, len, newText); + } + + public bool ReplaceChars(int pos, int len, ReadOnlySpan newText) + { + this.DeleteChars(pos, len); + return this.InsertChars(pos, newText); + } // See imgui_widgets.cpp: STB_TEXTEDIT_DELETECHARS public void DeleteChars(int pos, int n) { + if (n == 0) + return; + var dst = this.TextW.Data + pos; // We maintain our buffer length in both UTF-8 and wchar formats @@ -596,6 +671,9 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType // See imgui_widgets.cpp: STB_TEXTEDIT_INSERTCHARS public bool InsertChars(int pos, ReadOnlySpan newText) { + if (newText.Length == 0) + return true; + var isResizable = (this.Flags & ImGuiInputTextFlags.CallbackResize) != 0; var textLen = this.CurLenW; Debug.Assert(pos <= textLen, "pos <= text_len"); From b55875255837220c24a115468c5dc160977082d9 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Sat, 23 Dec 2023 02:25:08 -0800 Subject: [PATCH 15/33] chore: Consolidate on ImGuiColors (#1585) Uses ImGuiColors over KnownColor in most places so that themes override things properly. --- Dalamud/Interface/Internal/Windows/ConsoleWindow.cs | 4 ++-- .../Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs | 3 ++- .../Internal/Windows/Data/Widgets/IconBrowserWidget.cs | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 770582a30..53821d9df 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -150,7 +150,7 @@ internal class ConsoleWindow : Window, IDisposable { const string regexErrorString = "Regex Filter Error"; ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X / 2.0f - ImGui.CalcTextSize(regexErrorString).X / 2.0f); - ImGui.TextColored(KnownColor.OrangeRed.Vector(), regexErrorString); + ImGui.TextColored(ImGuiColors.DalamudRed, regexErrorString); } ImGui.BeginChild("scrolling", new Vector2(0, ImGui.GetFrameHeightWithSpacing() - 55 * ImGuiHelpers.GlobalScale), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar); @@ -491,7 +491,7 @@ internal class ConsoleWindow : Window, IDisposable if (!sourceNames.Any()) { - ImGui.TextColored(KnownColor.OrangeRed.Vector(), "No Results"); + ImGui.TextColored(ImGuiColors.DalamudRed, "No Results"); } foreach (var selectable in sourceNames) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 0e654d316..5b2855298 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -3,6 +3,7 @@ using System.Drawing; using System.Linq; using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using ImGuiNET; @@ -130,7 +131,7 @@ public class AddonLifecycleWidget : IDataWindowWidget } else { - var color = receiveEventListener.Hook.IsEnabled ? KnownColor.Green.Vector() : KnownColor.OrangeRed.Vector(); + var color = receiveEventListener.Hook.IsEnabled ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed; var text = receiveEventListener.Hook.IsEnabled ? "Enabled" : "Disabled"; ImGui.TextColored(color, text); } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs index dcae6e689..06c691cc9 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Numerics; using Dalamud.Data; +using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; @@ -154,7 +155,7 @@ public class IconBrowserWidget : IDataWindowWidget this.nullValues.Add(iconId); } - ImGui.GetWindowDrawList().AddRect(cursor, cursor + this.iconSize, ImGui.GetColorU32(KnownColor.White.Vector())); + ImGui.GetWindowDrawList().AddRect(cursor, cursor + this.iconSize, ImGui.GetColorU32(ImGuiColors.DalamudWhite)); } catch (Exception) { From c17897c6fbb0be5c2303e19d3b273284186f9819 Mon Sep 17 00:00:00 2001 From: marzent Date: Sat, 23 Dec 2023 11:27:24 +0100 Subject: [PATCH 16/33] Mark DalamudIme as BlockingEarlyLoadedService (#1579) --- Dalamud/Interface/Internal/DalamudIme.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 70e230c5f..e030b4e50 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -24,7 +24,7 @@ namespace Dalamud.Interface.Internal; /// /// This class handles CJK IME. /// -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal sealed unsafe class DalamudIme : IDisposable, IServiceType { private const int ImGuiContextTextStateOffset = 0x4588; From a6b802b577f9ad3f2a4d3b435310f7a0f9e1aeed Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sun, 24 Dec 2023 23:05:13 +0100 Subject: [PATCH 17/33] Update ClientStructs (#1586) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index edc754348..97b814ca1 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit edc754348a3ed8fd49da6695248bfebe7ba89c12 +Subproject commit 97b814ca15d147911cdac3059623185a57984e0a From 02b1f6e42690d1c053a1c4b243131f97f5d331d2 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 31 Dec 2023 14:30:21 -0800 Subject: [PATCH 18/33] [AddonEventManager] Actually Ensure Thread Safety (#1589) * Actually make AddonEventManager thread safe * Ensure AddonEventHandlers are also thread safe Additionally, use Guid instead of strings * Make DalamudInternalKey readonly * Properly use ConcurrentDict features Fixes GUID not working --- .../Game/Addon/Events/AddonEventListener.cs | 10 ++- .../Game/Addon/Events/AddonEventManager.cs | 79 ++++++++----------- .../Addon/Events/PluginEventController.cs | 12 +-- 3 files changed, 43 insertions(+), 58 deletions(-) diff --git a/Dalamud/Game/Addon/Events/AddonEventListener.cs b/Dalamud/Game/Addon/Events/AddonEventListener.cs index ceac38108..a2498d5a7 100644 --- a/Dalamud/Game/Addon/Events/AddonEventListener.cs +++ b/Dalamud/Game/Addon/Events/AddonEventListener.cs @@ -67,7 +67,10 @@ internal unsafe class AddonEventListener : IDisposable { if (node is null) return; - node->AddEvent(eventType, param, this.eventListener, (AtkResNode*)addon, false); + Service.Get().RunOnFrameworkThread(() => + { + node->AddEvent(eventType, param, this.eventListener, (AtkResNode*)addon, false); + }); } /// @@ -80,7 +83,10 @@ internal unsafe class AddonEventListener : IDisposable { if (node is null) return; - node->RemoveEvent(eventType, param, this.eventListener, false); + Service.Get().RunOnFrameworkThread(() => + { + node->RemoveEvent(eventType, param, this.eventListener, false); + }); } [UnmanagedCallersOnly] diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index af713a771..4231b0d09 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Linq; +using System.Collections.Concurrent; using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -9,7 +8,6 @@ using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; -using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -26,22 +24,19 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// /// PluginName for Dalamud Internal use. /// - public const string DalamudInternalKey = "Dalamud.Internal"; + public static readonly Guid DalamudInternalKey = Guid.NewGuid(); private static readonly ModuleLog Log = new("AddonEventManager"); [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycle = Service.Get(); - [ServiceManager.ServiceDependency] - private readonly Framework framework = Service.Get(); - private readonly AddonLifecycleEventListener finalizeEventListener; private readonly AddonEventManagerAddressResolver address; private readonly Hook onUpdateCursor; - private readonly List pluginEventControllers; + private readonly ConcurrentDictionary pluginEventControllers; private AddonCursorType? cursorOverride; @@ -51,10 +46,8 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType this.address = new AddonEventManagerAddressResolver(); this.address.Setup(sigScanner); - this.pluginEventControllers = new List - { - new(DalamudInternalKey), // Create entry for Dalamud's Internal Use. - }; + this.pluginEventControllers = new ConcurrentDictionary(); + this.pluginEventControllers.TryAdd(DalamudInternalKey, new PluginEventController()); this.cursorOverride = null; @@ -73,7 +66,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType { this.onUpdateCursor.Dispose(); - foreach (var pluginEventController in this.pluginEventControllers) + foreach (var (_, pluginEventController) in this.pluginEventControllers) { pluginEventController.Dispose(); } @@ -90,16 +83,17 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// The event type for this event. /// The handler to call when event is triggered. /// IAddonEventHandle used to remove the event. - internal IAddonEventHandle? AddEvent(string pluginId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) + internal IAddonEventHandle? AddEvent(Guid pluginId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) { - if (!ThreadSafety.IsMainThread) throw new InvalidOperationException("This should be done only from the main thread. Modifying active native code on non-main thread is not supported."); - - if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController) + if (this.pluginEventControllers.TryGetValue(pluginId, out var controller)) { - return eventController.AddEvent(atkUnitBase, atkResNode, eventType, eventHandler); + return controller.AddEvent(atkUnitBase, atkResNode, eventType, eventHandler); + } + else + { + Log.Verbose($"Unable to locate controller for {pluginId}. No event was added."); } - Log.Verbose($"Unable to locate controller for {pluginId}. No event was added."); return null; } @@ -108,13 +102,11 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// /// Unique ID for this plugin. /// The Unique Id for this event. - internal void RemoveEvent(string pluginId, IAddonEventHandle eventHandle) + internal void RemoveEvent(Guid pluginId, IAddonEventHandle eventHandle) { - if (!ThreadSafety.IsMainThread) throw new InvalidOperationException("This should be done only from the main thread. Modifying active native code on non-main thread is not supported."); - - if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController) + if (this.pluginEventControllers.TryGetValue(pluginId, out var controller)) { - eventController.RemoveEvent(eventHandle); + controller.RemoveEvent(eventHandle); } else { @@ -137,33 +129,28 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// Adds a new managed event controller if one doesn't already exist for this pluginId. /// /// Unique ID for this plugin. - internal void AddPluginEventController(string pluginId) + internal void AddPluginEventController(Guid pluginId) { - this.framework.RunOnFrameworkThread(() => - { - if (this.pluginEventControllers.All(entry => entry.PluginId != pluginId)) + this.pluginEventControllers.GetOrAdd( + pluginId, + key => { - Log.Verbose($"Creating new PluginEventController for: {pluginId}"); - this.pluginEventControllers.Add(new PluginEventController(pluginId)); - } - }); + Log.Verbose($"Creating new PluginEventController for: {key}"); + return new PluginEventController(); + }); } /// /// Removes an existing managed event controller for the specified plugin. /// /// Unique ID for this plugin. - internal void RemovePluginEventController(string pluginId) + internal void RemovePluginEventController(Guid pluginId) { - this.framework.RunOnFrameworkThread(() => + if (this.pluginEventControllers.TryRemove(pluginId, out var controller)) { - if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } controller) - { - Log.Verbose($"Removing PluginEventController for: {pluginId}"); - this.pluginEventControllers.Remove(controller); - controller.Dispose(); - } - }); + Log.Verbose($"Removing PluginEventController for: {pluginId}"); + controller.Dispose(); + } } /// @@ -178,7 +165,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType foreach (var pluginList in this.pluginEventControllers) { - pluginList.RemoveForAddon(addonInfo.AddonName); + pluginList.Value.RemoveForAddon(addonInfo.AddonName); } } @@ -234,7 +221,7 @@ internal class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddon { this.plugin = plugin; - this.eventManagerService.AddPluginEventController(plugin.Manifest.WorkingPluginId.ToString()); + this.eventManagerService.AddPluginEventController(plugin.Manifest.WorkingPluginId); } /// @@ -246,16 +233,16 @@ internal class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddon this.eventManagerService.ResetCursor(); } - this.eventManagerService.RemovePluginEventController(this.plugin.Manifest.WorkingPluginId.ToString()); + this.eventManagerService.RemovePluginEventController(this.plugin.Manifest.WorkingPluginId); } /// public IAddonEventHandle? AddEvent(IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) - => this.eventManagerService.AddEvent(this.plugin.Manifest.WorkingPluginId.ToString(), atkUnitBase, atkResNode, eventType, eventHandler); + => this.eventManagerService.AddEvent(this.plugin.Manifest.WorkingPluginId, atkUnitBase, atkResNode, eventType, eventHandler); /// public void RemoveEvent(IAddonEventHandle eventHandle) - => this.eventManagerService.RemoveEvent(this.plugin.Manifest.WorkingPluginId.ToString(), eventHandle); + => this.eventManagerService.RemoveEvent(this.plugin.Manifest.WorkingPluginId, eventHandle); /// public void SetCursor(AddonCursorType cursor) diff --git a/Dalamud/Game/Addon/Events/PluginEventController.cs b/Dalamud/Game/Addon/Events/PluginEventController.cs index 7847dd482..3ba067a6d 100644 --- a/Dalamud/Game/Addon/Events/PluginEventController.cs +++ b/Dalamud/Game/Addon/Events/PluginEventController.cs @@ -19,19 +19,11 @@ internal unsafe class PluginEventController : IDisposable /// /// Initializes a new instance of the class. /// - /// The Unique ID for this plugin. - public PluginEventController(string pluginId) + public PluginEventController() { - this.PluginId = pluginId; - this.EventListener = new AddonEventListener(this.PluginEventListHandler); } - /// - /// Gets the unique ID for this PluginEventList. - /// - public string PluginId { get; init; } - private AddonEventListener EventListener { get; init; } private List Events { get; } = new(); @@ -125,7 +117,7 @@ internal unsafe class PluginEventController : IDisposable if (this.Events.All(registeredEvent => registeredEvent.ParamKey != i)) return i; } - throw new OverflowException($"uint.MaxValue number of ParamKeys used for {this.PluginId}"); + throw new OverflowException($"uint.MaxValue number of ParamKeys used for this event controller."); } /// From 69096c440a8b2bc0e499bd872f86930058e39085 Mon Sep 17 00:00:00 2001 From: marzent Date: Mon, 1 Jan 2024 01:20:00 +0100 Subject: [PATCH 19/33] Allow plugins to load Dalamud dependency assemblies (#1580) --- Dalamud/Plugin/Internal/Loader/PluginLoader.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Dalamud/Plugin/Internal/Loader/PluginLoader.cs b/Dalamud/Plugin/Internal/Loader/PluginLoader.cs index 5c03c32b8..53aec60ef 100644 --- a/Dalamud/Plugin/Internal/Loader/PluginLoader.cs +++ b/Dalamud/Plugin/Internal/Loader/PluginLoader.cs @@ -1,7 +1,7 @@ // Copyright (c) Nate McMaster, Dalamud team. // Licensed under the Apache License, Version 2.0. See License.txt in the Loader root for license information. -using System; +using System.IO; using System.Reflection; using System.Runtime.Loader; @@ -151,6 +151,14 @@ internal class PluginLoader : IDisposable builder.PreferDefaultLoadContextAssembly(assemblyName); } + // This allows plugins to search for dependencies in the Dalamud directory when their assembly + // load would otherwise fail, allowing them to resolve assemblies not already loaded by Dalamud + // itself yet. + builder.AddProbingPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); + + // Also make sure that plugins do not load their own Dalamud assembly. + builder.PreferDefaultLoadContextAssembly(Assembly.GetExecutingAssembly().GetName()); + return builder; } From 01cde50a468ebd80a1d05e336c9e05ef81c597a4 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Mon, 1 Jan 2024 07:11:09 -0800 Subject: [PATCH 20/33] chore: Suppress expected load errors (#1593) - Add new `PluginPreconditionFailedException` to track cases where a plugin could not be loaded due to a precondition not being met. - Make `BannedPluginException` inherit from this - Make `PluginPreconditionFailedException`s show as warnings in the log. --- .../Internal/Exceptions/BannedPluginException.cs | 9 ++------- .../PluginPreconditionFailedException.cs | 16 ++++++++++++++++ Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 15 +++++++++------ 3 files changed, 27 insertions(+), 13 deletions(-) create mode 100644 Dalamud/Plugin/Internal/Exceptions/PluginPreconditionFailedException.cs diff --git a/Dalamud/Plugin/Internal/Exceptions/BannedPluginException.cs b/Dalamud/Plugin/Internal/Exceptions/BannedPluginException.cs index 851e5be33..1119a8c4e 100644 --- a/Dalamud/Plugin/Internal/Exceptions/BannedPluginException.cs +++ b/Dalamud/Plugin/Internal/Exceptions/BannedPluginException.cs @@ -3,19 +3,14 @@ namespace Dalamud.Plugin.Internal.Exceptions; /// /// This represents a banned plugin that attempted an operation. /// -internal class BannedPluginException : PluginException +internal class BannedPluginException : PluginPreconditionFailedException { /// /// Initializes a new instance of the class. /// /// The message describing the invalid operation. public BannedPluginException(string message) + : base(message) { - this.Message = message; } - - /// - /// Gets the message describing the invalid operation. - /// - public override string Message { get; } } diff --git a/Dalamud/Plugin/Internal/Exceptions/PluginPreconditionFailedException.cs b/Dalamud/Plugin/Internal/Exceptions/PluginPreconditionFailedException.cs new file mode 100644 index 000000000..c1bb58d0d --- /dev/null +++ b/Dalamud/Plugin/Internal/Exceptions/PluginPreconditionFailedException.cs @@ -0,0 +1,16 @@ +namespace Dalamud.Plugin.Internal.Exceptions; + +/// +/// An exception to be thrown when policy blocks a plugin from loading. +/// +internal class PluginPreconditionFailedException : InvalidPluginOperationException +{ + /// + /// Initializes a new instance of the class. + /// + /// The message to associate with this exception. + public PluginPreconditionFailedException(string message) + : base(message) + { + } +} diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 91f1625a7..aff9a8b43 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -315,23 +315,23 @@ internal class LocalPlugin : IDisposable } if (pluginManager.IsManifestBanned(this.manifest) && !this.IsDev) - throw new BannedPluginException($"Unable to load {this.Name}, banned"); + throw new BannedPluginException($"Unable to load {this.Name} as it was banned"); if (this.manifest.ApplicableVersion < dalamud.StartInfo.GameVersion) - throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version"); + throw new PluginPreconditionFailedException($"Unable to load {this.Name}, game is newer than applicable version {this.manifest.ApplicableVersion}"); if (this.manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !pluginManager.LoadAllApiLevels) - throw new InvalidPluginOperationException($"Unable to load {this.Name}, incompatible API level"); + throw new PluginPreconditionFailedException($"Unable to load {this.Name}, incompatible API level {this.manifest.DalamudApiLevel}"); // We might want to throw here? if (!this.IsWantedByAnyProfile) Log.Warning("{Name} is loading, but isn't wanted by any profile", this.Name); if (this.IsOrphaned) - throw new InvalidPluginOperationException($"Plugin {this.Name} had no associated repo."); + throw new PluginPreconditionFailedException($"Plugin {this.Name} had no associated repo"); if (!this.CheckPolicy()) - throw new InvalidPluginOperationException("Plugin was not loaded as per policy"); + throw new PluginPreconditionFailedException($"Unable to load {this.Name} as a load policy forbids it"); this.State = PluginState.Loading; Log.Information($"Loading {this.DllFile.Name}"); @@ -439,7 +439,10 @@ internal class LocalPlugin : IDisposable { this.State = PluginState.LoadError; - if (ex is not BannedPluginException) + // If a precondition fails, don't record it as an error, as it isn't really. + if (ex is PluginPreconditionFailedException) + Log.Warning(ex.Message); + else Log.Error(ex, $"Error while loading {this.Name}"); throw; From de53150bd373275047c49fbd31b804f9a8c5ac3d Mon Sep 17 00:00:00 2001 From: srkizer Date: Thu, 4 Jan 2024 02:28:41 +0900 Subject: [PATCH 21/33] Optional recursive dependency pulls and fallback dependency load (#1595) * Optional recursive dependency pulls and fallback dependency load * add api10 todo --------- Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- .../Internal/Loader/AssemblyLoadContextBuilder.cs | 9 ++++++++- Dalamud/Plugin/Internal/Loader/LoaderConfig.cs | 2 +- .../Plugin/Internal/Loader/ManagedLoadContext.cs | 13 ++++++++++++- Dalamud/Plugin/Internal/Loader/PluginLoader.cs | 14 +++++--------- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 14 ++++++++++++-- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs b/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs index b7a2ffe2e..1a6830a3a 100644 --- a/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs +++ b/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs @@ -131,9 +131,16 @@ internal class AssemblyLoadContextBuilder /// or the default app context. /// /// The name of the assembly. + /// Pull assmeblies recursively. /// The builder. - public AssemblyLoadContextBuilder PreferDefaultLoadContextAssembly(AssemblyName assemblyName) + public AssemblyLoadContextBuilder PreferDefaultLoadContextAssembly(AssemblyName assemblyName, bool recursive) { + if (!recursive) + { + this.defaultAssemblies.Add(assemblyName.Name); + return this; + } + var names = new Queue(new[] { assemblyName }); while (names.TryDequeue(out var name)) diff --git a/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs b/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs index d3fcdc99e..0b2150069 100644 --- a/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs +++ b/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs @@ -46,7 +46,7 @@ internal class LoaderConfig /// Gets a list of assemblies which should be unified between the host and the plugin. /// /// what-are-shared-types - public ICollection SharedAssemblies { get; } = new List(); + public ICollection<(AssemblyName Name, bool Recursive)> SharedAssemblies { get; } = new List<(AssemblyName Name, bool Recursive)>(); /// /// Gets or sets a value indicating whether attempt to unify all types from a plugin with the host. diff --git a/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs b/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs index 4bb326ce4..e0629217a 100644 --- a/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs +++ b/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs @@ -194,7 +194,18 @@ internal class ManagedLoadContext : AssemblyLoadContext } } - return null; + // https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/loading-managed#algorithm + // > These assemblies are loaded (load-by-name) as needed by the runtime. + // For load-by-name assembiles, the following will happen in order: + // (1) this.Load will be called. + // (2) AssemblyLoadContext.Default's cache will be referred for lookup. + // (3) Default probing will be done from PLATFORM_RESOURCE_ROOTS and APP_PATHS. + // https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/default-probing#managed-assembly-default-probing + // > TRUSTED_PLATFORM_ASSEMBLIES: List of platform and application assembly file paths. + // > APP_PATHS: is not populated by default and is omitted for most applications. + // If we return null here, if the assembly has not been already loaded, the resolution will fail. + // Therefore as the final attempt, we try loading from the default load context. + return this.defaultLoadContext.LoadFromAssemblyName(assemblyName); } /// diff --git a/Dalamud/Plugin/Internal/Loader/PluginLoader.cs b/Dalamud/Plugin/Internal/Loader/PluginLoader.cs index 53aec60ef..63b47cf17 100644 --- a/Dalamud/Plugin/Internal/Loader/PluginLoader.cs +++ b/Dalamud/Plugin/Internal/Loader/PluginLoader.cs @@ -146,18 +146,14 @@ internal class PluginLoader : IDisposable builder.ShadowCopyNativeLibraries(); } - foreach (var assemblyName in config.SharedAssemblies) + foreach (var (assemblyName, recursive) in config.SharedAssemblies) { - builder.PreferDefaultLoadContextAssembly(assemblyName); + builder.PreferDefaultLoadContextAssembly(assemblyName, recursive); } - // This allows plugins to search for dependencies in the Dalamud directory when their assembly - // load would otherwise fail, allowing them to resolve assemblies not already loaded by Dalamud - // itself yet. - builder.AddProbingPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); - - // Also make sure that plugins do not load their own Dalamud assembly. - builder.PreferDefaultLoadContextAssembly(Assembly.GetExecutingAssembly().GetName()); + // Note: not adding Dalamud path here as a probing path. + // It will be dealt as the last resort from ManagedLoadContext.Load. + // See there for more details. return builder; } diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index aff9a8b43..0ddd4b23e 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -627,8 +627,18 @@ internal class LocalPlugin : IDisposable config.IsUnloadable = true; config.LoadInMemory = true; config.PreferSharedTypes = false; - config.SharedAssemblies.Add(typeof(Lumina.GameData).Assembly.GetName()); - config.SharedAssemblies.Add(typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName()); + + // Pin Lumina and its dependencies recursively (compatibility behavior). + // It currently only pulls in System.* anyway. + // TODO(api10): Remove this. We don't want to pin Lumina anymore, plugins should be able to provide their own. + config.SharedAssemblies.Add((typeof(Lumina.GameData).Assembly.GetName(), true)); + config.SharedAssemblies.Add((typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName(), true)); + + // Make sure that plugins do not load their own Dalamud assembly. + // We do not pin this recursively; if a plugin loads its own assembly of Dalamud, it is always wrong, + // but plugins may load other versions of assemblies that Dalamud depends on. + config.SharedAssemblies.Add((typeof(EntryPoint).Assembly.GetName(), false)); + config.SharedAssemblies.Add((typeof(Common.DalamudStartInfo).Assembly.GetName(), false)); } private void EnsureLoader() From 653dca2feb72ccdb60481aa30d8e6ac360be2ea1 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 5 Jan 2024 20:32:14 +0100 Subject: [PATCH 22/33] Fix verbose log in TextureManager.Dispose (#1596) --- Dalamud/Interface/Internal/TextureManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 40aa72913..9f90ea1ad 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Numerics; @@ -273,7 +273,10 @@ internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITe this.fallbackTextureWrap?.Dispose(); this.framework.Update -= this.FrameworkOnUpdate; - Log.Verbose("Disposing {Num} left behind textures."); + if (this.activeTextures.Count == 0) + return; + + Log.Verbose("Disposing {Num} left behind textures.", this.activeTextures.Count); foreach (var activeTexture in this.activeTextures) { From 84637f6dfa167f70714e781a82840eb976438f00 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Fri, 5 Jan 2024 20:54:12 +0100 Subject: [PATCH 23/33] Update ClientStructs (#1590) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 97b814ca1..07add8584 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 97b814ca15d147911cdac3059623185a57984e0a +Subproject commit 07add8584e6eabb6a8c398b2a7c669cdd607382e From c7c2b2dce1db804e2f7fdd83cc964cdbd6c05951 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Fri, 5 Jan 2024 16:41:26 -0800 Subject: [PATCH 24/33] Revert "Update ClientStructs (#1590)" (#1600) This reverts commit 84637f6dfa167f70714e781a82840eb976438f00. --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 07add8584..97b814ca1 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 07add8584e6eabb6a8c398b2a7c669cdd607382e +Subproject commit 97b814ca15d147911cdac3059623185a57984e0a From 78c0281b9066b36310182d47317be036ea12c199 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sat, 6 Jan 2024 18:38:03 +0100 Subject: [PATCH 25/33] Update ClientStructs (#1599) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 97b814ca1..837c6fafc 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 97b814ca15d147911cdac3059623185a57984e0a +Subproject commit 837c6fafc13fbdda3e13a833b6085e4ce93d19e1 From 767cc49ecb80e29dbdda2fa8329d3c3341c964fe Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sat, 6 Jan 2024 23:16:12 +0100 Subject: [PATCH 26/33] build: 9.0.0.15 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index a870bee17..5844527ee 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.14 + 9.0.0.15 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 3c7900ea13964e7fd45ccb92b384c2a2e467cbd3 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Sat, 13 Jan 2024 10:17:34 -0800 Subject: [PATCH 27/33] Add API Compatibility Checks (#1603) * Add GitHub Action job to check for API compatibility Runs critical path DLLs through `apicompat` tool against currently-live stg build to see what is broken. * Revert CS changes for GH Action positive test-case --- .github/workflows/main.yml | 43 +++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7ada48e50..8a4fdf2e3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,7 +42,48 @@ jobs: with: name: dalamud-artifact path: bin\Release - + + check_api_compat: + name: "Check API Compatibility" + if: ${{ github.event_name == 'pull_request' }} + needs: build + runs-on: windows-latest + steps: + - name: "Install .NET SDK" + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 7 + - name: "Install ApiCompat" + run: | + dotnet tool install -g Microsoft.DotNet.ApiCompat.Tool + - name: "Download Proposed Artifacts" + uses: actions/download-artifact@v2 + with: + name: dalamud-artifact + path: .\right + - name: "Download Live (Stg) Artifacts" + run: | + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip + Expand-Archive -Force latest.zip "left" + - name: "Verify Compatibility" + run: | + $FILES_TO_VALIDATE = "Dalamud.dll","FFXIVClientStructs.dll","Lumina.dll","Lumina.Excel.dll" + + $retcode = 0 + + foreach ($file in $FILES_TO_VALIDATE) { + $testout = "" + Write-Output "::group::=== API COMPATIBILITY CHECK: ${file} ===" + apicompat -l "left\${file}" -r "right\${file}" | Tee-Object -Variable testout + Write-Output "::endgroup::" + if ($testout -ne "APICompat ran successfully without finding any breaking changes.") { + Write-Output "::error::${file} did not pass. Please review it for problems." + $retcode = 1 + } + } + + exit $retcode + deploy_stg: name: Deploy dalamud-distrib staging if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }} From 86b7c29e9445b2bb41abf1ccc2ddc2f3003a2f7a Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Sat, 13 Jan 2024 11:17:26 -0800 Subject: [PATCH 28/33] fix: Make auto-update work again, the lazy way (#1592) * fix: Make auto-update work again, the lazy way. - Move auto-update to run on the first `Notice` message for parity with the welcome message. - Add some logging in a few critical places to make things nicer. * fix overzealous IDE complaints * code-review comments - Remove stray imports that the IDE included - Remove fixme to move auto-updates (for now) * Lazy retry auto-update --- Dalamud/Game/ChatHandlers.cs | 45 ++++++++++++++++++----- Dalamud/Interface/Utility/ImGuiHelpers.cs | 3 +- Dalamud/Plugin/Internal/PluginManager.cs | 8 +++- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 90a399d4c..836fb5ec8 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using CheapLoc; @@ -14,9 +15,9 @@ using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.Internal.Windows.PluginInstaller; +using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; using Dalamud.Utility; -using Serilog; namespace Dalamud.Game; @@ -60,6 +61,8 @@ internal class ChatHandlers : IServiceType // { XivChatType.Echo, Color.Gray }, // }; + private static readonly ModuleLog Log = new("CHATHANDLER"); + private readonly Regex rmtRegex = new( @"4KGOLD|We have sufficient stock|VPK\.OM|[Gg]il for free|[Gg]il [Cc]heap|5GOLD|www\.so9\.com|Fast & Convenient|Cheap & Safety Guarantee|【Code|A O A U E|igfans|4KGOLD\.COM|Cheapest Gil with|pvp and bank on google|Selling Cheap GIL|ff14mogstation\.com|Cheap Gil 1000k|gilsforyou|server 1000K =|gils_selling|E A S Y\.C O M|bonus code|mins delivery guarantee|Sell cheap|Salegm\.com|cheap Mog|Off Code:|FF14Mog.com|使用する5%オ|[Oo][Ff][Ff] [Cc]ode( *)[:;]|offers Fantasia", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -110,6 +113,7 @@ internal class ChatHandlers : IServiceType private bool hasSeenLoadingMsg; private bool startedAutoUpdatingPlugins; + private CancellationTokenSource deferredAutoUpdateCts = new(); [ServiceManager.ServiceConstructor] private ChatHandlers(ChatGui chatGui) @@ -165,16 +169,19 @@ internal class ChatHandlers : IServiceType if (clientState == null) return; - if (type == XivChatType.Notice && !this.hasSeenLoadingMsg) - this.PrintWelcomeMessage(); + if (type == XivChatType.Notice) + { + if (!this.hasSeenLoadingMsg) + this.PrintWelcomeMessage(); + + if (!this.startedAutoUpdatingPlugins) + this.AutoUpdatePluginsWithRetry(); + } // For injections while logged in if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg) this.PrintWelcomeMessage(); - if (!this.startedAutoUpdatingPlugins) - this.AutoUpdatePlugins(); - #if !DEBUG && false if (!this.hasSeenLoadingMsg) return; @@ -264,24 +271,42 @@ internal class ChatHandlers : IServiceType this.hasSeenLoadingMsg = true; } - private void AutoUpdatePlugins() + private void AutoUpdatePluginsWithRetry() + { + var firstAttempt = this.AutoUpdatePlugins(); + if (!firstAttempt) + { + Task.Run(() => + { + Task.Delay(30_000, this.deferredAutoUpdateCts.Token); + this.AutoUpdatePlugins(); + }); + } + } + + private bool AutoUpdatePlugins() { var chatGui = Service.GetNullable(); var pluginManager = Service.GetNullable(); var notifications = Service.GetNullable(); if (chatGui == null || pluginManager == null || notifications == null) - return; + { + Log.Warning("Aborting auto-update because a required service was not loaded."); + return false; + } if (!pluginManager.ReposReady || !pluginManager.InstalledPlugins.Any() || !pluginManager.AvailablePlugins.Any()) { // Plugins aren't ready yet. // TODO: We should retry. This sucks, because it means we won't ever get here again until another notice. - return; + Log.Warning("Aborting auto-update because plugins weren't loaded or ready."); + return false; } this.startedAutoUpdatingPlugins = true; + Log.Debug("Beginning plugin auto-update process..."); Task.Run(() => pluginManager.UpdatePluginsAsync(true, !this.configuration.AutoUpdatePlugins, true)).ContinueWith(task => { this.IsAutoUpdateComplete = true; @@ -320,5 +345,7 @@ internal class ChatHandlers : IServiceType } } }); + + return true; } } diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 85f81b203..ad151ec4e 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -31,7 +31,8 @@ public static class ImGuiHelpers /// This does not necessarily mean you can call drawing functions. /// public static unsafe bool IsImGuiInitialized => - ImGui.GetCurrentContext() is not 0 && ImGui.GetIO().NativePtr is not null; + ImGui.GetCurrentContext() is not (nint)0 // KW: IDEs get mad without the cast, despite being unnecessary + && ImGui.GetIO().NativePtr is not null; /// /// Gets the global Dalamud scale; even available before drawing is ready.
diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 0ef3d49f8..020abf437 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -958,7 +958,7 @@ internal partial class PluginManager : IDisposable, IServiceType autoUpdate ? PluginListInvalidationKind.AutoUpdate : PluginListInvalidationKind.Update, updatedList.Select(x => x.InternalName)); - Log.Information("Plugin update OK."); + Log.Information("Plugin update OK. {updateCount} plugins updated.", updatedList.Length); return updatedList; } @@ -1581,6 +1581,8 @@ internal partial class PluginManager : IDisposable, IServiceType private void DetectAvailablePluginUpdates() { + Log.Debug("Starting plugin update check..."); + lock (this.pluginListLock) { this.updatablePluginsList.Clear(); @@ -1615,10 +1617,12 @@ internal partial class PluginManager : IDisposable, IServiceType } } } + + Log.Debug("Update check found {updateCount} available updates.", this.updatablePluginsList.Count); } private void NotifyAvailablePluginsChanged() - { + { this.DetectAvailablePluginUpdates(); this.OnAvailablePluginsChanged?.InvokeSafely(); From 96ef5e18cd7044d1661c74040eb7708f8874d245 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sat, 13 Jan 2024 20:17:57 +0100 Subject: [PATCH 29/33] build: 9.0.0.16 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 5844527ee..6b2f1300a 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.15 + 9.0.0.16 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From a71b81c82e88f093cbde282f35629472d0567a7e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 13 Jan 2024 18:36:46 +0000 Subject: [PATCH 30/33] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 837c6fafc..89e713c07 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 837c6fafc13fbdda3e13a833b6085e4ce93d19e1 +Subproject commit 89e713c071dae13112550d3e754193704e230b03 From 7d7ab4bc8b488ecb629256d0f7a8804aa7821f90 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 15 Jan 2024 12:11:05 +0000 Subject: [PATCH 31/33] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 89e713c07..0ca14a0a0 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 89e713c071dae13112550d3e754193704e230b03 +Subproject commit 0ca14a0a047d3df403fd9ed1fee7a43de55d1c66 From d71da3f2c0f6eb6caa969fcf13aff2101157cff7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 16 Jan 2024 15:12:45 +0000 Subject: [PATCH 32/33] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 0ca14a0a0..bbc4b9942 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 0ca14a0a047d3df403fd9ed1fee7a43de55d1c66 +Subproject commit bbc4b994254d6913f51da3a20fad9bf4b8c986e5 From 59278224f71a8bf18f7e34c7a4e2521aadf8a12d Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Tue, 16 Jan 2024 20:00:37 +0100 Subject: [PATCH 33/33] build: 9.0.0.17 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 6b2f1300a..ba044a555 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.16 + 9.0.0.17 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion)