From 6ea20a8773163de4abf11e1a40510b89f3eff22e Mon Sep 17 00:00:00 2001 From: Gabriel Lima <44784408+gablm@users.noreply.github.com> Date: Sun, 30 Nov 2025 01:04:47 +0000 Subject: [PATCH 1/3] Adds support for custom Resizable Window Hooks - Begins update 3 of the standard - Fixes locale code not being visible outside the class --- SharedStatic.V1Ext.cs | 3 + SharedStatic.V1Ext_Update3.cs | 166 ++++++++++++++++++++++++++++++++ SharedStatic.cs | 6 +- Utility/GameManagerExtension.cs | 74 ++++++++++++++ 4 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 SharedStatic.V1Ext_Update3.cs diff --git a/SharedStatic.V1Ext.cs b/SharedStatic.V1Ext.cs index 1cbf40e..b5bf5f0 100644 --- a/SharedStatic.V1Ext.cs +++ b/SharedStatic.V1Ext.cs @@ -16,6 +16,8 @@ public class SharedStaticV1Ext : SharedStatic internal unsafe delegate HResult GetCurrentDiscordPresenceInfoDelegate(void* presetConfigP, DiscordPresenceInfo** presenceInfoP); + // Update3 + internal delegate HResult StartResizableWindowHookAsyncDelegate(nint gameManagerP, nint presetConfigP, nint executableName, int executableNameLen, int height, int width, nint executableDirectory, int executableDirectoryLen, ref Guid cancelToken, out nint taskResult); } /// @@ -39,6 +41,7 @@ static SharedStaticV1Ext() */ InitExtension_Update1Exports(); InitExtension_Update2Exports(); + InitExtension_Update3Exports(); } /// diff --git a/SharedStatic.V1Ext_Update3.cs b/SharedStatic.V1Ext_Update3.cs new file mode 100644 index 0000000..67163a5 --- /dev/null +++ b/SharedStatic.V1Ext_Update3.cs @@ -0,0 +1,166 @@ +using Hi3Helper.Plugin.Core.Management; +using Hi3Helper.Plugin.Core.Management.PresetConfig; +using Hi3Helper.Plugin.Core.Utility; +using Microsoft.Extensions.Logging; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +using static Hi3Helper.Plugin.Core.Utility.GameManagerExtension; + +#if !MANUALCOM +using System.Runtime.InteropServices.Marshalling; +#endif + +namespace Hi3Helper.Plugin.Core; + +public partial class SharedStaticV1Ext +{ + private static void InitExtension_Update3Exports() + { + /* ---------------------------------------------------------------------- + * Update 3 Feature Sets + * ---------------------------------------------------------------------- + * This feature sets includes the following feature: + * - Game Launch + * - StartResizableWindowHook + */ + + // -> Plugin Async Resizable Window Hook Callback for Specific Game Region based on its IGameManager instance. + TryRegisterApiExport("StartResizableWindowHookAsync", StartResizableWindowHookAsync); + } + + #region ABI Proxies + /// + /// This method is an ABI proxy function between the PInvoke Export and the actual plugin's method.
+ /// See the documentation for method for more information. + ///
+ private static unsafe HResult StartResizableWindowHookAsync(nint gameManagerP, + nint presetConfigP, + nint exeName, + int exeNameLen, + int height, + int width, + nint exeDir, + int exeDirLen, + ref Guid cancelToken, + out nint taskResult) + { + taskResult = nint.Zero; + try + { +#if MANUALCOM + IGameManager? gameManager = ComWrappers.ComInterfaceDispatch.GetInstance((ComWrappers.ComInterfaceDispatch*)gameManagerP); + IPluginPresetConfig? presetConfig = ComWrappers.ComInterfaceDispatch.GetInstance((ComWrappers.ComInterfaceDispatch*)presetConfigP); +#else + IGameManager? gameManager = ComInterfaceMarshaller.ConvertToManaged((void*)gameManagerP); + IPluginPresetConfig? presetConfig = ComInterfaceMarshaller.ConvertToManaged((void*)presetConfigP); +#endif + + if (ThisExtensionExport == null) + { + throw new NullReferenceException("The ThisPluginExport field is null!"); + } + + if (gameManager == null) + { + throw new NullReferenceException("Cannot cast IGameManager from the pointer, hence it gives null!"); + } + + if (presetConfig == null) + { + throw new NullReferenceException("Cannot cast IPluginPresetConfig from the pointer, hence it gives null!"); + } + + CancellationTokenSource? cts = null; + if (Unsafe.IsNullRef(ref cancelToken)) + { + cts = ComCancellationTokenVault.RegisterToken(in cancelToken); + } + + RunGameFromGameManagerContext context = new() + { + GameManager = gameManager, + PresetConfig = presetConfig, + Plugin = null!, + PrintGameLogCallback = null!, + PluginHandle = nint.Zero + }; + + string? executableName = null; + if (exeNameLen > 0) + { + char* exeNameP = (char*)exeName; + ReadOnlySpan executableNameSpan = Mem.CreateSpanFromNullTerminated(exeNameP); + if (executableNameSpan.Length > exeNameLen) + { + executableNameSpan = executableNameSpan[..exeNameLen]; + } + + executableName = executableNameSpan.IsEmpty ? null : executableNameSpan.ToString(); + } + + string? executableDirectory = null; + if (exeDirLen > 0) + { + char* exeDirP = (char*)exeDir; + ReadOnlySpan executableDirectorySpan = Mem.CreateSpanFromNullTerminated(exeDirP); + if (executableDirectorySpan.Length > exeNameLen) + { + executableDirectorySpan = executableDirectorySpan[..exeDirLen]; + } + + executableDirectory = executableDirectorySpan.IsEmpty ? null : executableDirectorySpan.ToString(); + } + + (bool isSupported, Task task) = ThisExtensionExport + .StartResizableWindowHookAsync(context, + executableName, + height == int.MinValue ? null : height, + width == int.MinValue ? null : width, + executableDirectory, + cts?.Token ?? CancellationToken.None); + + taskResult = task.AsResult(); + return isSupported; + } + catch (Exception ex) + { + // ignored + InstanceLogger.LogError(ex, "An error has occurred while trying to call StartResizableWindowHookAsync() from the plugin!"); + return Marshal.GetHRForException(ex); + } + } + #endregion + + #region Core Methods + /// + /// Asynchronously hook to the game process making the window resizable and wait until the game exit. + /// + /// The context to launch the game from . + /// The name of the game executable. + /// Height of the host screen. + /// Height of the host screen. + /// The path where the game executable is located. + /// + /// Cancellation token to pass into the plugin's game launch mechanism.
+ /// If cancellation is requested, it will cancel the awaiting but not killing the game process. + /// + /// + /// Returns IsSupported.false if the plugin's API Standard is equal or lower than v0.1.3 or if this method isn't overriden.
+ /// Otherwise, IsSupported.true if the plugin supports game launch mechanism and this method. + ///
+ protected virtual (bool IsSupported, Task Task) StartResizableWindowHookAsync( + RunGameFromGameManagerContext context, + string? executableName, + int? height, + int? width, + string? executableDirectory, + CancellationToken token) + { + return (false, Task.FromResult(false)); + } + #endregion +} diff --git a/SharedStatic.cs b/SharedStatic.cs index 8aa10f4..ecaa785 100644 --- a/SharedStatic.cs +++ b/SharedStatic.cs @@ -101,9 +101,9 @@ static unsafe SharedStatic() internal static Uri? ProxyHost; internal static string? ProxyUsername; internal static string? ProxyPassword; - internal static string PluginLocaleCode = "en-us"; - - public static readonly GameVersion LibraryStandardVersion = new(0, 1, 2, 0); + + public static string PluginLocaleCode { get; internal set; } = "en-us"; + public static readonly GameVersion LibraryStandardVersion = new(0, 1, 3, 0); public static readonly ILogger InstanceLogger = new SharedLogger(); #if DEBUG diff --git a/Utility/GameManagerExtension.cs b/Utility/GameManagerExtension.cs index a08ed45..f28829f 100644 --- a/Utility/GameManagerExtension.cs +++ b/Utility/GameManagerExtension.cs @@ -332,6 +332,80 @@ public static bool KillRunningGame(this RunGameFromGameManag return true; } + /// + /// Asynchronously hook to the game process making the window resizable and wait until the game exit. + /// + /// The context to launch the game from . + /// The name of the game executable. + /// Height of the host screen. + /// Height of the host screen. + /// The path where the game executable is located. + /// + /// Cancellation token to pass into the plugin's game launch mechanism.
+ /// If cancellation is requested, it will cancel the awaiting but not killing the game process. + /// + /// + /// Returns IsSupported.false if the plugin's API Standard is equal or lower than v0.1.3 or if this method isn't overriden.
+ /// Otherwise, IsSupported.true if the plugin supports game launch mechanism and this method. + ///
+ public static async Task<(bool IsSuccess, Exception? Error)> + StartResizableWindowHookAsync(this RunGameFromGameManagerContext context, + string? executableName = null, + int? height = null, + int? width = null, + string? executableDirectory = null, + CancellationToken token = default) + { + ArgumentNullException.ThrowIfNull(context, nameof(context)); + if (!context.PluginHandle.TryGetExport("StartResizableWindowHookAsync", out SharedStaticV1Ext.StartResizableWindowHookAsyncDelegate startResizableWindowHookAsyncCallback)) + { + return (false, new NotSupportedException("Plugin doesn't have StartResizableWindowHookAsync export in its API definition!")); + } + + nint gameManagerP = GetPointerFromInterface(context.GameManager); + nint presetConfigP = GetPointerFromInterface(context.PresetConfig); + + if (gameManagerP == nint.Zero) + { + return (false, new COMException("Cannot cast IGameManager interface to pointer!")); + } + + if (presetConfigP == nint.Zero) + { + return (false, new COMException("Cannot cast IPluginPresetConfig interface to pointer!")); + } + + nint exeNameP = executableName.GetPinnableStringPointerSafe(); + int exeNameLen = executableName?.Length ?? 0; + + nint exeDirP = executableDirectory.GetPinnableStringPointerSafe(); + int exeDirLen = executableDirectory?.Length ?? 0; + + Guid cancelTokenGuid = Guid.CreateVersion7(); + int hResult = startResizableWindowHookAsyncCallback(gameManagerP, + presetConfigP, + exeNameP, + exeNameLen, + height ?? int.MinValue, + width ?? int.MinValue, + exeDirP, + exeDirLen, + ref cancelTokenGuid, + out nint taskResult); + + if (taskResult == nint.Zero) + { + return (false, new NullReferenceException("ComAsyncResult pointer in taskReturn argument shouldn't return a null pointer!")); + } + + if (hResult != 0) + { + return (false, Marshal.GetExceptionForHR(hResult)); + } + + return await ExecuteSuccessAsyncTask(context.Plugin, taskResult, cancelTokenGuid, token); + } + private static unsafe nint GetPointerFromInterface(this T interfaceSource) where T : class => (nint)ComInterfaceMarshaller.ConvertToUnmanaged(interfaceSource); From 1b409aa1f82c321240e4cf3c70fa6545cfe87665 Mon Sep 17 00:00:00 2001 From: Gabriel Lima <44784408+gablm@users.noreply.github.com> Date: Sun, 30 Nov 2025 11:59:09 +0000 Subject: [PATCH 2/3] Fix issues in comments --- SharedStatic.V1Ext_Update3.cs | 6 +++--- Utility/GameManagerExtension.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/SharedStatic.V1Ext_Update3.cs b/SharedStatic.V1Ext_Update3.cs index 67163a5..3cd9427 100644 --- a/SharedStatic.V1Ext_Update3.cs +++ b/SharedStatic.V1Ext_Update3.cs @@ -25,7 +25,7 @@ private static void InitExtension_Update3Exports() * ---------------------------------------------------------------------- * This feature sets includes the following feature: * - Game Launch - * - StartResizableWindowHook + * - StartResizableWindowHookAsync */ // -> Plugin Async Resizable Window Hook Callback for Specific Game Region based on its IGameManager instance. @@ -142,8 +142,8 @@ private static unsafe HResult StartResizableWindowHookAsync(nint gameManager /// The context to launch the game from . /// The name of the game executable. /// Height of the host screen. - /// Height of the host screen. - /// The path where the game executable is located. + /// Width of the host screen. + /// The path to the directory where the game executable is located. /// /// Cancellation token to pass into the plugin's game launch mechanism.
/// If cancellation is requested, it will cancel the awaiting but not killing the game process. diff --git a/Utility/GameManagerExtension.cs b/Utility/GameManagerExtension.cs index f28829f..945da6d 100644 --- a/Utility/GameManagerExtension.cs +++ b/Utility/GameManagerExtension.cs @@ -338,8 +338,8 @@ public static bool KillRunningGame(this RunGameFromGameManag /// The context to launch the game from . /// The name of the game executable. /// Height of the host screen. - /// Height of the host screen. - /// The path where the game executable is located. + /// Width of the host screen. + /// The path to the directory where the game executable is located. /// /// Cancellation token to pass into the plugin's game launch mechanism.
/// If cancellation is requested, it will cancel the awaiting but not killing the game process. From 441b9fcaf3677442accd1bada3830deb2cf66bee Mon Sep 17 00:00:00 2001 From: Gabriel Lima <44784408+gablm@users.noreply.github.com> Date: Sun, 30 Nov 2025 12:30:04 +0000 Subject: [PATCH 3/3] Fix incorrect length variable --- SharedStatic.V1Ext_Update3.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SharedStatic.V1Ext_Update3.cs b/SharedStatic.V1Ext_Update3.cs index 3cd9427..7d5d880 100644 --- a/SharedStatic.V1Ext_Update3.cs +++ b/SharedStatic.V1Ext_Update3.cs @@ -107,7 +107,7 @@ private static unsafe HResult StartResizableWindowHookAsync(nint gameManager { char* exeDirP = (char*)exeDir; ReadOnlySpan executableDirectorySpan = Mem.CreateSpanFromNullTerminated(exeDirP); - if (executableDirectorySpan.Length > exeNameLen) + if (executableDirectorySpan.Length > exeDirLen) { executableDirectorySpan = executableDirectorySpan[..exeDirLen]; }