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..7d5d880 --- /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 + * - StartResizableWindowHookAsync + */ + + // -> 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 > exeDirLen) + { + 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. + /// 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. + /// + /// + /// 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..945da6d 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. + /// 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. + /// + /// + /// 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);