Skip to content

Commit ddd22f7

Browse files
gablmneon-nyan
authored andcommitted
Detach ResizableWindowHook implementation for plugins (#3)
* Adds support for custom Resizable Window Hooks - Begins update 3 of the standard - Fixes locale code not being visible outside the class * Fix issues in comments * Fix incorrect length variable
1 parent f5ca147 commit ddd22f7

File tree

4 files changed

+246
-3
lines changed

4 files changed

+246
-3
lines changed

SharedStatic.V1Ext.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public class SharedStaticV1Ext : SharedStatic
1616
internal unsafe delegate HResult GetCurrentDiscordPresenceInfoDelegate(void* presetConfigP,
1717
DiscordPresenceInfo** presenceInfoP);
1818

19+
// Update3
20+
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);
1921
}
2022

2123
/// <summary>
@@ -39,6 +41,7 @@ static SharedStaticV1Ext()
3941
*/
4042
InitExtension_Update1Exports();
4143
InitExtension_Update2Exports();
44+
InitExtension_Update3Exports();
4245
}
4346

4447
/// <summary>

SharedStatic.V1Ext_Update3.cs

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
using Hi3Helper.Plugin.Core.Management;
2+
using Hi3Helper.Plugin.Core.Management.PresetConfig;
3+
using Hi3Helper.Plugin.Core.Utility;
4+
using Microsoft.Extensions.Logging;
5+
using System;
6+
using System.Runtime.CompilerServices;
7+
using System.Runtime.InteropServices;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
11+
using static Hi3Helper.Plugin.Core.Utility.GameManagerExtension;
12+
13+
#if !MANUALCOM
14+
using System.Runtime.InteropServices.Marshalling;
15+
#endif
16+
17+
namespace Hi3Helper.Plugin.Core;
18+
19+
public partial class SharedStaticV1Ext<T>
20+
{
21+
private static void InitExtension_Update3Exports()
22+
{
23+
/* ----------------------------------------------------------------------
24+
* Update 3 Feature Sets
25+
* ----------------------------------------------------------------------
26+
* This feature sets includes the following feature:
27+
* - Game Launch
28+
* - StartResizableWindowHookAsync
29+
*/
30+
31+
// -> Plugin Async Resizable Window Hook Callback for Specific Game Region based on its IGameManager instance.
32+
TryRegisterApiExport<StartResizableWindowHookAsyncDelegate>("StartResizableWindowHookAsync", StartResizableWindowHookAsync);
33+
}
34+
35+
#region ABI Proxies
36+
/// <summary>
37+
/// This method is an ABI proxy function between the PInvoke Export and the actual plugin's method.<br/>
38+
/// See the documentation for <see cref="SharedStaticV1Ext{T}.StartResizableWindowHookAsync(RunGameFromGameManagerContext, string?, int, int, string?, CancellationToken)"/> method for more information.
39+
/// </summary>
40+
private static unsafe HResult StartResizableWindowHookAsync(nint gameManagerP,
41+
nint presetConfigP,
42+
nint exeName,
43+
int exeNameLen,
44+
int height,
45+
int width,
46+
nint exeDir,
47+
int exeDirLen,
48+
ref Guid cancelToken,
49+
out nint taskResult)
50+
{
51+
taskResult = nint.Zero;
52+
try
53+
{
54+
#if MANUALCOM
55+
IGameManager? gameManager = ComWrappers.ComInterfaceDispatch.GetInstance<IGameManager>((ComWrappers.ComInterfaceDispatch*)gameManagerP);
56+
IPluginPresetConfig? presetConfig = ComWrappers.ComInterfaceDispatch.GetInstance<IPluginPresetConfig>((ComWrappers.ComInterfaceDispatch*)presetConfigP);
57+
#else
58+
IGameManager? gameManager = ComInterfaceMarshaller<IGameManager>.ConvertToManaged((void*)gameManagerP);
59+
IPluginPresetConfig? presetConfig = ComInterfaceMarshaller<IPluginPresetConfig>.ConvertToManaged((void*)presetConfigP);
60+
#endif
61+
62+
if (ThisExtensionExport == null)
63+
{
64+
throw new NullReferenceException("The ThisPluginExport field is null!");
65+
}
66+
67+
if (gameManager == null)
68+
{
69+
throw new NullReferenceException("Cannot cast IGameManager from the pointer, hence it gives null!");
70+
}
71+
72+
if (presetConfig == null)
73+
{
74+
throw new NullReferenceException("Cannot cast IPluginPresetConfig from the pointer, hence it gives null!");
75+
}
76+
77+
CancellationTokenSource? cts = null;
78+
if (Unsafe.IsNullRef(ref cancelToken))
79+
{
80+
cts = ComCancellationTokenVault.RegisterToken(in cancelToken);
81+
}
82+
83+
RunGameFromGameManagerContext context = new()
84+
{
85+
GameManager = gameManager,
86+
PresetConfig = presetConfig,
87+
Plugin = null!,
88+
PrintGameLogCallback = null!,
89+
PluginHandle = nint.Zero
90+
};
91+
92+
string? executableName = null;
93+
if (exeNameLen > 0)
94+
{
95+
char* exeNameP = (char*)exeName;
96+
ReadOnlySpan<char> executableNameSpan = Mem.CreateSpanFromNullTerminated<char>(exeNameP);
97+
if (executableNameSpan.Length > exeNameLen)
98+
{
99+
executableNameSpan = executableNameSpan[..exeNameLen];
100+
}
101+
102+
executableName = executableNameSpan.IsEmpty ? null : executableNameSpan.ToString();
103+
}
104+
105+
string? executableDirectory = null;
106+
if (exeDirLen > 0)
107+
{
108+
char* exeDirP = (char*)exeDir;
109+
ReadOnlySpan<char> executableDirectorySpan = Mem.CreateSpanFromNullTerminated<char>(exeDirP);
110+
if (executableDirectorySpan.Length > exeDirLen)
111+
{
112+
executableDirectorySpan = executableDirectorySpan[..exeDirLen];
113+
}
114+
115+
executableDirectory = executableDirectorySpan.IsEmpty ? null : executableDirectorySpan.ToString();
116+
}
117+
118+
(bool isSupported, Task<bool> task) = ThisExtensionExport
119+
.StartResizableWindowHookAsync(context,
120+
executableName,
121+
height == int.MinValue ? null : height,
122+
width == int.MinValue ? null : width,
123+
executableDirectory,
124+
cts?.Token ?? CancellationToken.None);
125+
126+
taskResult = task.AsResult();
127+
return isSupported;
128+
}
129+
catch (Exception ex)
130+
{
131+
// ignored
132+
InstanceLogger.LogError(ex, "An error has occurred while trying to call StartResizableWindowHookAsync() from the plugin!");
133+
return Marshal.GetHRForException(ex);
134+
}
135+
}
136+
#endregion
137+
138+
#region Core Methods
139+
/// <summary>
140+
/// Asynchronously hook to the game process making the window resizable and wait until the game exit.
141+
/// </summary>
142+
/// <param name="context">The context to launch the game from <see cref="IGameManager"/>.</param>
143+
/// <param name="executableName">The name of the game executable.</param>
144+
/// <param name="height">Height of the host screen.</param>
145+
/// <param name="width">Width of the host screen.</param>
146+
/// <param name="executableDirectory">The path to the directory where the game executable is located.</param>
147+
/// <param name="token">
148+
/// Cancellation token to pass into the plugin's game launch mechanism.<br/>
149+
/// If cancellation is requested, it will cancel the awaiting but not killing the game process.
150+
/// </param>
151+
/// <returns>
152+
/// Returns <c>IsSupported.false</c> if the plugin's API Standard is equal or lower than v0.1.3 or if this method isn't overriden.<br/>
153+
/// Otherwise, <c>IsSupported.true</c> if the plugin supports game launch mechanism and this method.
154+
/// </returns>
155+
protected virtual (bool IsSupported, Task<bool> Task) StartResizableWindowHookAsync(
156+
RunGameFromGameManagerContext context,
157+
string? executableName,
158+
int? height,
159+
int? width,
160+
string? executableDirectory,
161+
CancellationToken token)
162+
{
163+
return (false, Task.FromResult(false));
164+
}
165+
#endregion
166+
}

SharedStatic.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,9 @@ static unsafe SharedStatic()
101101
internal static Uri? ProxyHost;
102102
internal static string? ProxyUsername;
103103
internal static string? ProxyPassword;
104-
internal static string PluginLocaleCode = "en-us";
105-
106-
public static readonly GameVersion LibraryStandardVersion = new(0, 1, 2, 0);
104+
105+
public static string PluginLocaleCode { get; internal set; } = "en-us";
106+
public static readonly GameVersion LibraryStandardVersion = new(0, 1, 3, 0);
107107
public static readonly ILogger InstanceLogger = new SharedLogger();
108108

109109
#if DEBUG

Utility/GameManagerExtension.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,80 @@ public static bool KillRunningGame(this RunGameFromGameManag
332332
return true;
333333
}
334334

335+
/// <summary>
336+
/// Asynchronously hook to the game process making the window resizable and wait until the game exit.
337+
/// </summary>
338+
/// <param name="context">The context to launch the game from <see cref="IGameManager"/>.</param>
339+
/// <param name="executableName">The name of the game executable.</param>
340+
/// <param name="height">Height of the host screen.</param>
341+
/// <param name="width">Width of the host screen.</param>
342+
/// <param name="executableDirectory">The path to the directory where the game executable is located.</param>
343+
/// <param name="token">
344+
/// Cancellation token to pass into the plugin's game launch mechanism.<br/>
345+
/// If cancellation is requested, it will cancel the awaiting but not killing the game process.
346+
/// </param>
347+
/// <returns>
348+
/// Returns <c>IsSupported.false</c> if the plugin's API Standard is equal or lower than v0.1.3 or if this method isn't overriden.<br/>
349+
/// Otherwise, <c>IsSupported.true</c> if the plugin supports game launch mechanism and this method.
350+
/// </returns>
351+
public static async Task<(bool IsSuccess, Exception? Error)>
352+
StartResizableWindowHookAsync(this RunGameFromGameManagerContext context,
353+
string? executableName = null,
354+
int? height = null,
355+
int? width = null,
356+
string? executableDirectory = null,
357+
CancellationToken token = default)
358+
{
359+
ArgumentNullException.ThrowIfNull(context, nameof(context));
360+
if (!context.PluginHandle.TryGetExport("StartResizableWindowHookAsync", out SharedStaticV1Ext.StartResizableWindowHookAsyncDelegate startResizableWindowHookAsyncCallback))
361+
{
362+
return (false, new NotSupportedException("Plugin doesn't have StartResizableWindowHookAsync export in its API definition!"));
363+
}
364+
365+
nint gameManagerP = GetPointerFromInterface(context.GameManager);
366+
nint presetConfigP = GetPointerFromInterface(context.PresetConfig);
367+
368+
if (gameManagerP == nint.Zero)
369+
{
370+
return (false, new COMException("Cannot cast IGameManager interface to pointer!"));
371+
}
372+
373+
if (presetConfigP == nint.Zero)
374+
{
375+
return (false, new COMException("Cannot cast IPluginPresetConfig interface to pointer!"));
376+
}
377+
378+
nint exeNameP = executableName.GetPinnableStringPointerSafe();
379+
int exeNameLen = executableName?.Length ?? 0;
380+
381+
nint exeDirP = executableDirectory.GetPinnableStringPointerSafe();
382+
int exeDirLen = executableDirectory?.Length ?? 0;
383+
384+
Guid cancelTokenGuid = Guid.CreateVersion7();
385+
int hResult = startResizableWindowHookAsyncCallback(gameManagerP,
386+
presetConfigP,
387+
exeNameP,
388+
exeNameLen,
389+
height ?? int.MinValue,
390+
width ?? int.MinValue,
391+
exeDirP,
392+
exeDirLen,
393+
ref cancelTokenGuid,
394+
out nint taskResult);
395+
396+
if (taskResult == nint.Zero)
397+
{
398+
return (false, new NullReferenceException("ComAsyncResult pointer in taskReturn argument shouldn't return a null pointer!"));
399+
}
400+
401+
if (hResult != 0)
402+
{
403+
return (false, Marshal.GetExceptionForHR(hResult));
404+
}
405+
406+
return await ExecuteSuccessAsyncTask(context.Plugin, taskResult, cancelTokenGuid, token);
407+
}
408+
335409
private static unsafe nint GetPointerFromInterface<T>(this T interfaceSource)
336410
where T : class
337411
=> (nint)ComInterfaceMarshaller<T>.ConvertToUnmanaged(interfaceSource);

0 commit comments

Comments
 (0)