using System; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using InterprocessLib; using Microsoft.CodeAnalysis; using Renderite.Shared; using ResoniteBetterIMESupport.Shared; using UnityEngine.InputSystem; using UnityEngine.InputSystem.LowLevel; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("blhsrwznrghfzpr")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("3.0.5.0")] [assembly: AssemblyInformationalVersion("3.0.5+f44c2dc2e57068838c00482e416693c9da2d9b35")] [assembly: AssemblyProduct("ResoniteBetterIMESupport.Renderer")] [assembly: AssemblyTitle("ResoniteBetterIMESupport.Renderer")] [assembly: AssemblyVersion("3.0.5.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class IsUnmanagedAttribute : Attribute { } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ResoniteBetterIMESupport.Shared { internal static class ImeInterprocessChannel { public const string OwnerId = "ResoniteBetterIMESupport"; public const string QueueName = "ResoniteBetterIMESupport.Ime"; public const string MessageId = "ImeComposition"; } internal sealed class ImeInterprocessMessage : IMemoryPackable { public string Composition = string.Empty; public int CompositionCursor = -1; public bool HasCommittedResult; public void Pack(ref MemoryPacker packer) { ((MemoryPacker)(ref packer)).Write(Composition); ((MemoryPacker)(ref packer)).Write(CompositionCursor); ((MemoryPacker)(ref packer)).Write(HasCommittedResult); } public void Unpack(ref MemoryUnpacker unpacker) { ((MemoryUnpacker)(ref unpacker)).Read(ref Composition); ((MemoryUnpacker)(ref unpacker)).Read(ref CompositionCursor); ((MemoryUnpacker)(ref unpacker)).Read(ref HasCommittedResult); } public override string ToString() { return $"ImeInterprocessMessage:CompositionLength={Composition.Length},CompositionCursor={CompositionCursor},HasCommittedResult={HasCommittedResult}"; } } internal static class ImePluginConfig { private const string DebugSection = "Debug"; private const string EnableDebugLoggingKey = "EnableDebugLogging"; private const string EnableDebugLoggingDescription = "Enable verbose IME debug logging."; public static ConfigEntry BindEnableDebugLogging(ConfigFile config) { return config.Bind("Debug", "EnableDebugLogging", false, "Enable verbose IME debug logging."); } } } namespace ResoniteBetterIMESupport.Renderer { internal static class KeyboardDriverIMEPatch { public sealed class DriverState { public string ImeComposition = string.Empty; public bool KeyboardInputActive; public Action? CompositionHandler; } private static readonly ConditionalWeakTable States = new ConditionalWeakTable(); private static bool _messengerIdentityLogged; private static Messenger? _messenger; public static Type KeyboardDriverType => AccessTools.TypeByName("KeyboardDriver") ?? throw new InvalidOperationException("KeyboardDriver type was not found."); public static DriverState GetState(object driver) { return States.GetOrCreateValue(driver); } public static void InitializeMessaging() { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Expected O, but got Unknown if (_messenger == null) { _messenger = new Messenger("ResoniteBetterIMESupport", false, "ResoniteBetterIMESupport.Ime", (IMemoryPackerEntityPool)null, 1048576L); LogMessengerIdentityOnce(); } } public static void DisposeMessaging() { Messenger? messenger = _messenger; if (messenger != null) { messenger.Dispose(); } _messenger = null; } public static void SyncConfigEntry(ConfigEntry configEntry) where T : unmanaged { InitializeMessaging(); BepInExExtensions.SyncConfigEntry(_messenger, configEntry); } public static void Subscribe(object driver) { object driver2 = driver; Keyboard current = Keyboard.current; if (current == null) { RendererPlugin.Logger.LogWarning((object)"Keyboard.current is null. IME composition support was not attached."); return; } DriverState state = GetState(driver2); if (state.CompositionHandler == null) { InitializeMessaging(); state.CompositionHandler = delegate(IMECompositionString composition) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) OnIMECompositionChange(driver2, composition); }; current.onIMECompositionChange += state.CompositionHandler; } } public static void HandleKeyboardInputActive(object driver, bool keyboardInputActive) { DriverState state = GetState(driver); bool keyboardInputActive2 = state.KeyboardInputActive; state.KeyboardInputActive = keyboardInputActive; if (keyboardInputActive) { if (!keyboardInputActive2) { DebugLog("Keyboard input became active. Reinitializing IME state."); ClearComposition(state); } return; } if (!keyboardInputActive2 || state.ImeComposition.Length == 0) { ClearComposition(state); return; } DebugLog("Keyboard input became inactive. Canceling composition=\"" + EscapeForLog(state.ImeComposition) + "\""); if (!TrySendComposition(string.Empty, -1)) { DebugLog("Composition clear send failed."); } ClearComposition(state); } public static bool ShouldAllowTextInput(object driver, char value) { DriverState state = GetState(driver); if (state.ImeComposition.Length == 0) { return true; } DebugLog($"Suppressed text input during composition: char=0x{(int)value:X4}, composition=\"{EscapeForLog(state.ImeComposition)}\""); return false; } private static void OnIMECompositionChange(object driver, IMECompositionString composition) { string text = ((object)(IMECompositionString)(ref composition)).ToString(); DriverState state = GetState(driver); int compositionCursor = -1; bool flag = false; if (WindowsImeContextReader.TryGetCursorPosition(text, out int cursorPosition, out bool hasCommittedResult, out string _, out string diagnostic)) { compositionCursor = cursorPosition; } flag = hasCommittedResult; DebugLog("OnIMECompositionChange: composition=\"" + EscapeForLog(text) + "\", previous=\"" + EscapeForLog(state.ImeComposition) + "\", windowsIme=" + diagnostic); if (!TrySendComposition(text, compositionCursor, flag)) { DebugLog("Composition update send failed."); return; } state.ImeComposition = text; if (text.Length == 0) { ClearComposition(state); } } private static bool TrySendComposition(string composition, int compositionCursor, bool hasCommittedResult = false) { try { InitializeMessaging(); _messenger.SendObject("ImeComposition", new ImeInterprocessMessage { Composition = composition, CompositionCursor = compositionCursor, HasCommittedResult = hasCommittedResult }); return true; } catch (Exception ex) { DebugLog("Interprocess send threw " + ex.GetType().Name + ": " + EscapeForLog(ex.Message)); DisposeMessaging(); return false; } } private static void LogMessengerIdentityOnce() { if (!_messengerIdentityLogged) { _messengerIdentityLogged = true; DebugLog("Renderer IME sender: ownerId=\"ResoniteBetterIMESupport\", messageId=\"ImeComposition\", queueName=\"ResoniteBetterIMESupport.Ime\""); } } private static void ClearComposition(DriverState state) { state.ImeComposition = string.Empty; } private static string EscapeForLog(string value) { return value.Replace("\\", "\\\\").Replace("\r", "\\r").Replace("\n", "\\n") .Replace("\t", "\\t"); } private static void DebugLog(string message) { RendererPlugin.LogDebugIme(message); } } [BepInPlugin("dev.blhsrwznrghfzpr.ResoniteBetterIMESupport.Renderer", "ResoniteBetterIMESupport.Renderer", "3.0.5")] public sealed class RendererPlugin : BaseUnityPlugin { public const string PluginGuid = "dev.blhsrwznrghfzpr.ResoniteBetterIMESupport.Renderer"; public const string PluginName = "ResoniteBetterIMESupport.Renderer"; public const string PluginVersion = "3.0.5"; internal static ManualLogSource Logger; private static ConfigEntry _enableDebugLogging; private void Awake() { //IL_002f: Unknown result type (might be due to invalid IL or missing references) Logger = ((BaseUnityPlugin)this).Logger; _enableDebugLogging = ImePluginConfig.BindEnableDebugLogging(((BaseUnityPlugin)this).Config); KeyboardDriverIMEPatch.InitializeMessaging(); KeyboardDriverIMEPatch.SyncConfigEntry(_enableDebugLogging); new Harmony("dev.blhsrwznrghfzpr.ResoniteBetterIMESupport.Renderer").PatchAll(Assembly.GetExecutingAssembly()); Logger.LogInfo((object)"ResoniteBetterIMESupport.Renderer loaded."); } private void OnDestroy() { KeyboardDriverIMEPatch.DisposeMessaging(); } internal static void LogDebugIme(string message) { if (_enableDebugLogging.Value) { Logger.LogInfo((object)("[IME debug] " + message)); } } } internal static class WindowsImeContextReader { private const int GCS_COMPSTR = 8; private const int GCS_CURSORPOS = 128; private const int GCS_RESULTSTR = 2048; private const int IMM_ERROR_NODATA = -1; private const int IMM_ERROR_GENERAL = -2; public static bool TryGetCursorPosition(string unityComposition, out int cursorPosition, out bool hasCommittedResult, out string committedResult, out string diagnostic) { cursorPosition = -1; hasCommittedResult = false; committedResult = string.Empty; diagnostic = "unavailable"; if (!IsWindows()) { diagnostic = "not-windows"; return false; } try { return TryGetCursorPositionFromImm32(unityComposition, out cursorPosition, out hasCommittedResult, out committedResult, out diagnostic); } catch (Exception ex) { cursorPosition = -1; hasCommittedResult = false; committedResult = string.Empty; diagnostic = "native-ime-unavailable, exception=" + ex.GetType().Name + ", message=\"" + EscapeForLog(ex.Message) + "\""; return false; } } private static bool TryGetCursorPositionFromImm32(string unityComposition, out int cursorPosition, out bool hasCommittedResult, out string committedResult, out string diagnostic) { cursorPosition = -1; hasCommittedResult = false; committedResult = string.Empty; diagnostic = "unavailable"; IntPtr intPtr = GetActiveWindow(); string arg = "active"; if (intPtr == IntPtr.Zero) { intPtr = GetForegroundWindow(); arg = "foreground"; } if (intPtr == IntPtr.Zero) { diagnostic = "no-hwnd"; return false; } IntPtr intPtr2 = ImmGetContext(intPtr); if (intPtr2 == IntPtr.Zero) { diagnostic = $"no-himc, hwndSource={arg}, hwnd=0x{intPtr.ToInt64():X}"; return false; } try { int status; string text = TryGetCompositionString(intPtr2, 8, out status); int status2; string text2 = TryGetCompositionString(intPtr2, 2048, out status2); committedResult = text2 ?? string.Empty; hasCommittedResult = committedResult.Length > 0; int num = ImmGetCompositionStringW(intPtr2, 128, IntPtr.Zero, 0); if (num == -1 || num == -2) { diagnostic = string.Format("cursor={0}, compStringStatus={1}, compString=\"{2}\", resultStatus={3}, resultString=\"{4}\", hasResult={5}", FormatImmValue(num), FormatImmValue(status), EscapeForLog(text ?? ""), FormatImmValue(status2), EscapeForLog(committedResult), hasCommittedResult); return false; } int num2 = num & 0xFFFF; bool flag = text == null || text == unityComposition; diagnostic = string.Format("cursor={0}, compStringStatus={1}, compString=\"{2}\", resultStatus={3}, resultString=\"{4}\", hasResult={5}, unityMatch={6}", num2, FormatImmValue(status), EscapeForLog(text ?? ""), FormatImmValue(status2), EscapeForLog(committedResult), hasCommittedResult, flag); if (!flag) { return false; } cursorPosition = num2; return true; } finally { TryReleaseContext(intPtr, intPtr2); } } private static void TryReleaseContext(IntPtr hwnd, IntPtr himc) { try { ImmReleaseContext(hwnd, himc); } catch { } } private static string? TryGetCompositionString(IntPtr himc, int index, out int status) { int num = (status = ImmGetCompositionStringW(himc, index, IntPtr.Zero, 0)); if (num <= 0) { if (num != 0) { return null; } return string.Empty; } IntPtr intPtr = Marshal.AllocHGlobal(num); try { int num2 = (status = ImmGetCompositionStringW(himc, index, intPtr, num)); return (num2 > 0) ? Marshal.PtrToStringUni(intPtr, num2 / 2) : ((num2 == 0) ? string.Empty : null); } finally { Marshal.FreeHGlobal(intPtr); } } private static bool IsWindows() { if (Environment.OSVersion.Platform != PlatformID.Win32NT && Environment.OSVersion.Platform != 0 && Environment.OSVersion.Platform != PlatformID.Win32Windows) { return Environment.OSVersion.Platform == PlatformID.WinCE; } return true; } private static string FormatImmValue(int value) { return value switch { -1 => "IMM_ERROR_NODATA", -2 => "IMM_ERROR_GENERAL", _ => value.ToString(), }; } private static string EscapeForLog(string value) { return value.Replace("\\", "\\\\").Replace("\r", "\\r").Replace("\n", "\\n") .Replace("\t", "\\t"); } [DllImport("user32.dll")] private static extern IntPtr GetActiveWindow(); [DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow(); [DllImport("imm32.dll")] private static extern IntPtr ImmGetContext(IntPtr hWnd); [DllImport("imm32.dll")] private static extern bool ImmReleaseContext(IntPtr hWnd, IntPtr hIMC); [DllImport("imm32.dll", CharSet = CharSet.Unicode)] private static extern int ImmGetCompositionStringW(IntPtr hIMC, int dwIndex, IntPtr lpBuf, int dwBufLen); } } namespace ResoniteBetterIMESupport.Renderer.Patches { [HarmonyPatch] internal static class KeyboardDriverStartPatch { private static MethodBase TargetMethod() { return AccessTools.Method(KeyboardDriverIMEPatch.KeyboardDriverType, "Start", (Type[])null, (Type[])null); } private static void Postfix(object __instance) { KeyboardDriverIMEPatch.Subscribe(__instance); } } [HarmonyPatch] internal static class KeyboardDriverTextInputPatch { private static MethodBase TargetMethod() { return AccessTools.Method(KeyboardDriverIMEPatch.KeyboardDriverType, "Current_onTextInput", (Type[])null, (Type[])null); } private static bool Prefix(object __instance, char obj) { return KeyboardDriverIMEPatch.ShouldAllowTextInput(__instance, obj); } } [HarmonyPatch] internal static class KeyboardDriverHandleOutputStatePatch { private static MethodBase TargetMethod() { return AccessTools.Method(KeyboardDriverIMEPatch.KeyboardDriverType, "HandleOutputState", (Type[])null, (Type[])null); } private static void Postfix(object __instance, OutputState output) { KeyboardDriverIMEPatch.HandleKeyboardInputActive(__instance, output.keyboardInputActive); } } }