mirror of
https://github.com/MikuLeaks/MikuSB.git
synced 2026-06-04 20:13:57 +00:00
358 lines
12 KiB
C#
358 lines
12 KiB
C#
using System.Collections;
|
|
using System.ComponentModel;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using MikuSB.Util;
|
|
|
|
namespace MikuSB.Loader;
|
|
|
|
public static class GameLaunchService
|
|
{
|
|
public static int Launch(params string[] extraGameArguments)
|
|
{
|
|
ConfigManager.LoadConfig();
|
|
var options = LaunchOptions.FromConfig(extraGameArguments);
|
|
return Launch(options);
|
|
}
|
|
|
|
public static int Launch(LaunchOptions options)
|
|
{
|
|
var startupInfo = new STARTUPINFOW
|
|
{
|
|
cb = Marshal.SizeOf<STARTUPINFOW>()
|
|
};
|
|
|
|
var commandLine = BuildCommandLine(options.GamePath, options.GameArguments);
|
|
var workingDirectory = options.WorkingDirectory ?? Path.GetDirectoryName(options.GamePath)
|
|
?? throw new InvalidOperationException("Unable to determine working directory.");
|
|
|
|
var environment = BuildEnvironmentBlock(options.EnvironmentVariables);
|
|
try
|
|
{
|
|
if (!CreateProcessW(
|
|
lpApplicationName: options.GamePath,
|
|
lpCommandLine: commandLine,
|
|
lpProcessAttributes: IntPtr.Zero,
|
|
lpThreadAttributes: IntPtr.Zero,
|
|
bInheritHandles: false,
|
|
dwCreationFlags: CreationFlags.CREATE_SUSPENDED | CreationFlags.CREATE_UNICODE_ENVIRONMENT,
|
|
lpEnvironment: environment,
|
|
lpCurrentDirectory: workingDirectory,
|
|
lpStartupInfo: ref startupInfo,
|
|
lpProcessInformation: out var processInfo))
|
|
{
|
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to create game process.");
|
|
}
|
|
|
|
try
|
|
{
|
|
foreach (var patchPath in options.PatchPaths)
|
|
InjectDll(processInfo.hProcess, patchPath);
|
|
|
|
if (ResumeThread(processInfo.hThread) == uint.MaxValue)
|
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to resume game process.");
|
|
|
|
return processInfo.dwProcessId;
|
|
}
|
|
finally
|
|
{
|
|
CloseHandle(processInfo.hThread);
|
|
CloseHandle(processInfo.hProcess);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (environment != IntPtr.Zero)
|
|
Marshal.FreeHGlobal(environment);
|
|
}
|
|
}
|
|
|
|
private static void InjectDll(IntPtr processHandle, string dllPath)
|
|
{
|
|
var dllBytes = Encoding.Unicode.GetBytes(dllPath + '\0');
|
|
var remoteBuffer = VirtualAllocEx(
|
|
processHandle,
|
|
IntPtr.Zero,
|
|
(nuint)dllBytes.Length,
|
|
AllocationType.Commit | AllocationType.Reserve,
|
|
MemoryProtection.ReadWrite);
|
|
|
|
if (remoteBuffer == IntPtr.Zero)
|
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "VirtualAllocEx failed.");
|
|
|
|
try
|
|
{
|
|
if (!WriteProcessMemory(processHandle, remoteBuffer, dllBytes, dllBytes.Length, out var written) ||
|
|
written.ToInt64() != dllBytes.Length)
|
|
{
|
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "WriteProcessMemory failed.");
|
|
}
|
|
|
|
var kernel32 = GetModuleHandleW("kernel32.dll");
|
|
if (kernel32 == IntPtr.Zero)
|
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "GetModuleHandleW(kernel32.dll) failed.");
|
|
|
|
var loadLibrary = GetProcAddress(kernel32, "LoadLibraryW");
|
|
if (loadLibrary == IntPtr.Zero)
|
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "GetProcAddress(LoadLibraryW) failed.");
|
|
|
|
var remoteThread = CreateRemoteThread(
|
|
processHandle,
|
|
IntPtr.Zero,
|
|
0,
|
|
loadLibrary,
|
|
remoteBuffer,
|
|
0,
|
|
out _);
|
|
|
|
if (remoteThread == IntPtr.Zero)
|
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateRemoteThread failed.");
|
|
|
|
try
|
|
{
|
|
var waitResult = WaitForSingleObject(remoteThread, 10_000);
|
|
if (waitResult != 0)
|
|
throw new Win32Exception($"Remote LoadLibraryW timed out or failed: {waitResult}");
|
|
}
|
|
finally
|
|
{
|
|
CloseHandle(remoteThread);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
VirtualFreeEx(processHandle, remoteBuffer, 0, FreeType.Release);
|
|
}
|
|
}
|
|
|
|
private static string BuildCommandLine(string exePath, IReadOnlyList<string> gameArgs)
|
|
{
|
|
var parts = new List<string> { Quote(exePath) };
|
|
parts.AddRange(gameArgs.Select(Quote));
|
|
return string.Join(' ', parts);
|
|
}
|
|
|
|
private static IntPtr BuildEnvironmentBlock(IReadOnlyDictionary<string, string> variables)
|
|
{
|
|
if (variables.Count == 0)
|
|
return IntPtr.Zero;
|
|
|
|
var merged = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
|
|
merged[(string)entry.Key] = entry.Value?.ToString() ?? string.Empty;
|
|
|
|
foreach (var pair in variables)
|
|
merged[pair.Key] = pair.Value;
|
|
|
|
var payload = string.Join('\0', merged.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
|
.Select(x => $"{x.Key}={x.Value}")) + "\0\0";
|
|
|
|
return Marshal.StringToHGlobalUni(payload);
|
|
}
|
|
|
|
private static string Quote(string value)
|
|
{
|
|
if (value.Length == 0)
|
|
return "\"\"";
|
|
|
|
if (!value.Any(char.IsWhiteSpace) && !value.Contains('"'))
|
|
return value;
|
|
|
|
return "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
|
|
}
|
|
|
|
[Flags]
|
|
private enum CreationFlags : uint
|
|
{
|
|
CREATE_SUSPENDED = 0x00000004,
|
|
CREATE_UNICODE_ENVIRONMENT = 0x00000400
|
|
}
|
|
|
|
[Flags]
|
|
private enum AllocationType : uint
|
|
{
|
|
Commit = 0x1000,
|
|
Reserve = 0x2000
|
|
}
|
|
|
|
[Flags]
|
|
private enum MemoryProtection : uint
|
|
{
|
|
ReadWrite = 0x04
|
|
}
|
|
|
|
[Flags]
|
|
private enum FreeType : uint
|
|
{
|
|
Release = 0x8000
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
private struct STARTUPINFOW
|
|
{
|
|
public int cb;
|
|
public string? lpReserved;
|
|
public string? lpDesktop;
|
|
public string? lpTitle;
|
|
public int dwX;
|
|
public int dwY;
|
|
public int dwXSize;
|
|
public int dwYSize;
|
|
public int dwXCountChars;
|
|
public int dwYCountChars;
|
|
public int dwFillAttribute;
|
|
public int dwFlags;
|
|
public short wShowWindow;
|
|
public short cbReserved2;
|
|
public IntPtr lpReserved2;
|
|
public IntPtr hStdInput;
|
|
public IntPtr hStdOutput;
|
|
public IntPtr hStdError;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct PROCESS_INFORMATION
|
|
{
|
|
public IntPtr hProcess;
|
|
public IntPtr hThread;
|
|
public int dwProcessId;
|
|
public int dwThreadId;
|
|
}
|
|
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
private static extern bool CreateProcessW(
|
|
string? lpApplicationName,
|
|
string lpCommandLine,
|
|
IntPtr lpProcessAttributes,
|
|
IntPtr lpThreadAttributes,
|
|
bool bInheritHandles,
|
|
CreationFlags dwCreationFlags,
|
|
IntPtr lpEnvironment,
|
|
string? lpCurrentDirectory,
|
|
ref STARTUPINFOW lpStartupInfo,
|
|
out PROCESS_INFORMATION lpProcessInformation);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
private static extern IntPtr VirtualAllocEx(
|
|
IntPtr hProcess,
|
|
IntPtr lpAddress,
|
|
nuint dwSize,
|
|
AllocationType flAllocationType,
|
|
MemoryProtection flProtect);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
private static extern bool VirtualFreeEx(
|
|
IntPtr hProcess,
|
|
IntPtr lpAddress,
|
|
nuint dwSize,
|
|
FreeType dwFreeType);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
private static extern bool WriteProcessMemory(
|
|
IntPtr hProcess,
|
|
IntPtr lpBaseAddress,
|
|
byte[] lpBuffer,
|
|
int nSize,
|
|
out IntPtr lpNumberOfBytesWritten);
|
|
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
private static extern IntPtr GetModuleHandleW(string lpModuleName);
|
|
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
|
|
private static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
private static extern IntPtr CreateRemoteThread(
|
|
IntPtr hProcess,
|
|
IntPtr lpThreadAttributes,
|
|
nuint dwStackSize,
|
|
IntPtr lpStartAddress,
|
|
IntPtr lpParameter,
|
|
uint dwCreationFlags,
|
|
out int lpThreadId);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
private static extern uint ResumeThread(IntPtr hThread);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
private static extern bool CloseHandle(IntPtr hObject);
|
|
}
|
|
|
|
public sealed class LaunchOptions
|
|
{
|
|
public required string GamePath { get; init; }
|
|
public required IReadOnlyList<string> PatchPaths { get; init; }
|
|
public string? WorkingDirectory { get; init; }
|
|
public required IReadOnlyList<string> GameArguments { get; init; }
|
|
public required IReadOnlyDictionary<string, string> EnvironmentVariables { get; init; }
|
|
|
|
public static LaunchOptions FromConfig(IEnumerable<string>? extraGameArguments = null)
|
|
{
|
|
var config = ConfigManager.Config;
|
|
var gamePath = ResolvePath(config.Loader.GamePath, AppContext.BaseDirectory);
|
|
var patchPaths = ResolvePatchPaths(config.Loader.PatchPaths, AppContext.BaseDirectory);
|
|
var gameArgs = new List<string>(config.Loader.Arguments ?? []);
|
|
if (extraGameArguments is not null)
|
|
gameArgs.AddRange(extraGameArguments.Where(x => !string.IsNullOrWhiteSpace(x)));
|
|
|
|
var env = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
if (config.Loader.SetAllProxy && config.Proxy.Enabled)
|
|
env["ALL_PROXY"] = $"socks5h://127.0.0.1:{config.Proxy.Port}";
|
|
|
|
if (string.IsNullOrWhiteSpace(gamePath))
|
|
throw new InvalidOperationException("Loader.GamePath is not configured.");
|
|
if (!File.Exists(gamePath))
|
|
throw new FileNotFoundException("Game executable not found.", gamePath);
|
|
if (patchPaths.Count == 0)
|
|
throw new InvalidOperationException("At least one patch path is required.");
|
|
|
|
foreach (var patchPath in patchPaths)
|
|
{
|
|
if (!File.Exists(patchPath))
|
|
throw new FileNotFoundException("Patch DLL not found.", patchPath);
|
|
}
|
|
|
|
var workingDirectory = Path.GetDirectoryName(gamePath);
|
|
if (string.IsNullOrWhiteSpace(workingDirectory) || !Directory.Exists(workingDirectory))
|
|
throw new DirectoryNotFoundException($"Working directory not found: {workingDirectory}");
|
|
|
|
return new LaunchOptions
|
|
{
|
|
GamePath = Path.GetFullPath(gamePath),
|
|
PatchPaths = patchPaths,
|
|
WorkingDirectory = Path.GetFullPath(workingDirectory),
|
|
GameArguments = gameArgs,
|
|
EnvironmentVariables = env
|
|
};
|
|
}
|
|
|
|
private static string? ResolvePath(string? value, string baseDirectory)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
return null;
|
|
|
|
return Path.IsPathRooted(value)
|
|
? value
|
|
: Path.GetFullPath(Path.Combine(baseDirectory, value));
|
|
}
|
|
|
|
private static List<string> ResolvePatchPaths(IEnumerable<string>? values, string baseDirectory)
|
|
{
|
|
var result = new List<string>();
|
|
if (values is null)
|
|
return result;
|
|
|
|
foreach (var value in values)
|
|
{
|
|
var resolved = ResolvePath(value, baseDirectory);
|
|
if (!string.IsNullOrWhiteSpace(resolved))
|
|
result.Add(resolved);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|