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() }; 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 gameArgs) { var parts = new List { Quote(exePath) }; parts.AddRange(gameArgs.Select(Quote)); return string.Join(' ', parts); } private static IntPtr BuildEnvironmentBlock(IReadOnlyDictionary variables) { if (variables.Count == 0) return IntPtr.Zero; var merged = new Dictionary(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 PatchPaths { get; init; } public string? WorkingDirectory { get; init; } public required IReadOnlyList GameArguments { get; init; } public required IReadOnlyDictionary EnvironmentVariables { get; init; } public static LaunchOptions FromConfig(IEnumerable? 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(config.Loader.Arguments ?? []); if (extraGameArguments is not null) gameArgs.AddRange(extraGameArguments.Where(x => !string.IsNullOrWhiteSpace(x))); var env = new Dictionary(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 ResolvePatchPaths(IEnumerable? values, string baseDirectory) { var result = new List(); 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; } }