Skip to content

Commit 37e0a69

Browse files
committed
Implement override for Game Launch API
1 parent 33a5aee commit 37e0a69

File tree

3 files changed

+228
-3
lines changed

3 files changed

+228
-3
lines changed
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
using Hi3Helper.Plugin.Core.Management.PresetConfig;
2+
using Hi3Helper.Plugin.Core.Utility;
3+
using Hi3Helper.Plugin.HBR.Management;
4+
using System;
5+
using System.Diagnostics;
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.IO;
8+
using System.Linq;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
12+
namespace Hi3Helper.Plugin.HBR;
13+
14+
public partial class Seraphim
15+
{
16+
/// <inheritdoc/>
17+
public override async Task<bool> LaunchGameFromGameManagerCoreAsync(GameManagerExtension.RunGameFromGameManagerContext context, string? startArgument, bool isRunBoosted, ProcessPriorityClass processPriority, CancellationToken token)
18+
{
19+
if (!TryGetGameProcessFromContext(context, startArgument, out Process? process))
20+
{
21+
return false;
22+
}
23+
24+
using (process)
25+
{
26+
process.Start();
27+
process.PriorityBoostEnabled = isRunBoosted;
28+
process.PriorityClass = processPriority;
29+
30+
CancellationTokenSource gameLogReaderCts = new CancellationTokenSource();
31+
CancellationTokenSource coopCts = CancellationTokenSource.CreateLinkedTokenSource(token, gameLogReaderCts.Token);
32+
33+
// Run game log reader (Create a new thread)
34+
_ = ReadGameLog(context, coopCts.Token);
35+
36+
// ReSharper disable once PossiblyMistakenUseOfCancellationToken
37+
await process.WaitForExitAsync(token);
38+
await gameLogReaderCts.CancelAsync();
39+
40+
return true;
41+
}
42+
}
43+
44+
/// <inheritdoc/>
45+
public override bool IsGameRunningCore(GameManagerExtension.RunGameFromGameManagerContext context, out bool isGameRunning)
46+
{
47+
isGameRunning = false;
48+
if (!TryGetGameExecutablePath(context, out string? gameExecutablePath))
49+
{
50+
return false;
51+
}
52+
53+
using Process? process = FindExecutableProcess(gameExecutablePath);
54+
isGameRunning = process != null;
55+
56+
return true;
57+
}
58+
59+
/// <inheritdoc/>
60+
public override async Task<bool> WaitRunningGameCoreAsync(GameManagerExtension.RunGameFromGameManagerContext context, CancellationToken token)
61+
{
62+
if (!TryGetGameExecutablePath(context, out string? gameExecutablePath))
63+
{
64+
return false;
65+
}
66+
67+
using Process? process = FindExecutableProcess(gameExecutablePath);
68+
if (process == null)
69+
{
70+
return true;
71+
}
72+
73+
await process.WaitForExitAsync(token);
74+
return true;
75+
}
76+
77+
/// <inheritdoc/>
78+
public override bool KillRunningGameCore(GameManagerExtension.RunGameFromGameManagerContext context, out bool wasGameRunning)
79+
{
80+
wasGameRunning = false;
81+
if (!TryGetGameExecutablePath(context, out string? gameExecutablePath))
82+
{
83+
return false;
84+
}
85+
86+
using Process? process = FindExecutableProcess(gameExecutablePath);
87+
if (process == null)
88+
{
89+
return true;
90+
}
91+
92+
wasGameRunning = true;
93+
process.Kill();
94+
return true;
95+
}
96+
97+
private static Process? FindExecutableProcess(string executablePath)
98+
{
99+
ReadOnlySpan<char> executableDirPath = Path.GetDirectoryName(executablePath.AsSpan());
100+
string executableName = Path.GetFileNameWithoutExtension(executablePath);
101+
102+
Process[] processes = Process.GetProcessesByName(executableName);
103+
Process? returnProcess = null;
104+
105+
foreach (Process process in processes)
106+
{
107+
if (process.MainModule?.FileName.StartsWith(executableDirPath, StringComparison.OrdinalIgnoreCase) ?? false)
108+
{
109+
returnProcess = process;
110+
break;
111+
}
112+
}
113+
114+
if (returnProcess == null)
115+
{
116+
return null;
117+
}
118+
119+
foreach (var process in processes.Where(x => x != returnProcess))
120+
{
121+
process.Dispose();
122+
}
123+
124+
return returnProcess;
125+
}
126+
127+
private static bool TryGetGameExecutablePath(GameManagerExtension.RunGameFromGameManagerContext context, [NotNullWhen(true)] out string? gameExecutablePath)
128+
{
129+
gameExecutablePath = null;
130+
if (context is not { GameManager: HBRGameManager hbrGameManager, PresetConfig: PluginPresetConfigBase presetConfig })
131+
{
132+
return false;
133+
}
134+
135+
hbrGameManager.GetGamePath(out string? gamePath);
136+
presetConfig.comGet_GameExecutableName(out string executablePath);
137+
138+
gamePath?.NormalizePathInplace();
139+
executablePath.NormalizePathInplace();
140+
141+
if (string.IsNullOrEmpty(gamePath))
142+
{
143+
return false;
144+
}
145+
146+
gameExecutablePath = Path.Combine(gamePath, executablePath);
147+
return File.Exists(gameExecutablePath);
148+
}
149+
150+
private static bool TryGetGameProcessFromContext(GameManagerExtension.RunGameFromGameManagerContext context, string? startArgument, [NotNullWhen(true)] out Process? process)
151+
{
152+
process = null;
153+
if (!TryGetGameExecutablePath(context, out string? gameExecutablePath))
154+
{
155+
return false;
156+
}
157+
158+
ProcessStartInfo startInfo = string.IsNullOrEmpty(startArgument) ?
159+
new ProcessStartInfo(gameExecutablePath) :
160+
new ProcessStartInfo(gameExecutablePath, startArgument);
161+
162+
process = new Process
163+
{
164+
StartInfo = startInfo
165+
};
166+
return true;
167+
}
168+
169+
private static async Task ReadGameLog(GameManagerExtension.RunGameFromGameManagerContext context, CancellationToken token)
170+
{
171+
if (context is not { PresetConfig: PluginPresetConfigBase presetConfig })
172+
{
173+
return;
174+
}
175+
176+
presetConfig.comGet_GameAppDataPath(out string gameAppDataPath);
177+
presetConfig.comGet_GameLogFileName(out string gameLogFileName);
178+
179+
if (string.IsNullOrEmpty(gameAppDataPath) ||
180+
string.IsNullOrEmpty(gameLogFileName))
181+
{
182+
return;
183+
}
184+
185+
string gameLogPath = Path.Combine(gameAppDataPath, gameLogFileName);
186+
187+
int retry = 5;
188+
while (!File.Exists(gameLogPath) && retry >= 0)
189+
{
190+
// Delays for 5 seconds to wait the game log existence
191+
await Task.Delay(1000, token);
192+
--retry;
193+
}
194+
195+
if (retry <= 0)
196+
{
197+
return;
198+
}
199+
200+
var printCallback = context.PrintGameLogCallback;
201+
202+
await using FileStream fileStream = File.Open(gameLogPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
203+
using StreamReader reader = new StreamReader(fileStream);
204+
205+
while (!token.IsCancellationRequested)
206+
{
207+
while (await reader.ReadLineAsync(token) is { } line)
208+
{
209+
PassStringLineToCallback(printCallback, line);
210+
}
211+
212+
await Task.Delay(250, token);
213+
}
214+
215+
return;
216+
217+
static unsafe void PassStringLineToCallback(GameManagerExtension.PrintGameLog? invoke, string line)
218+
{
219+
char* lineP = line.GetPinnableStringPointer();
220+
int lineLen = line.Length;
221+
222+
invoke?.Invoke(lineP, lineLen, false);
223+
}
224+
}
225+
}

Hi3Helper.Plugin.HBR/Exports.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ namespace Hi3Helper.Plugin.HBR;
1212
/// NOTE FOR DEVELOPERS:<br/>
1313
/// The export class name can be anything you want. In this example, we use "Seraphim" as a name to the weapon used by the characters.
1414
/// </summary>
15-
public class Seraphim : SharedStatic<Seraphim> // 2025-08-18: We use generic version of SharedStatic<T> to add support for game launch API.
16-
// Though, the devs can still use the old SharedStatic without any compatibility issue.
15+
public partial class Seraphim : SharedStatic<Seraphim> // 2025-08-18: We use generic version of SharedStatic<T> to add support for game launch API.
16+
// Though, the devs can still use the old SharedStatic without any compatibility issue.
1717
{
1818
static Seraphim() => Load<HBRPlugin>(!RuntimeFeature.IsDynamicCodeCompiled ? new Core.Management.GameVersion(0, 8, 2, 0) : default); // Loads the IPlugin instance as HBRPlugin.
1919

0 commit comments

Comments
 (0)