diff --git a/launcher/Backend/ClientPatcher.cs b/launcher/Backend/ClientPatcher.cs index 830c166..8af57ba 100644 --- a/launcher/Backend/ClientPatcher.cs +++ b/launcher/Backend/ClientPatcher.cs @@ -24,7 +24,14 @@ namespace DorkNet.Launcher.Backend; /// "plugin_dest": "MelonLoader/Mods", // optional — defaults to "MelonLoader/Mods" /// "config_template": "dorknet-clientmod.json.template", // optional — rendered with {HOST}/{PHOTON_APPID}/{PHOTON_VOICE_APPID} /// "config_dest": "MelonLoader/UserData/dorknet-clientmod.json", // optional — defaults shown -/// "old_plugin_paths": ["BepInEx/plugins/DorkNet.ClientPatch.dll"] // optional — deleted before install +/// "old_plugin_paths": ["BepInEx/plugins/DorkNet.ClientPatch.dll"], // optional — deleted before install +/// "steam_stub": { // optional — replaces Valve steam_api64.dll +/// "api_dll": "steam_api64.dll", // file in zip +/// "api_dest": "Recroom_Release_Data/Plugins/steam_api64.dll", +/// "api_backup_suffix": ".steam-original", // original is renamed to dll + suffix +/// "appid_file": "steam_appid.txt", // file in zip (or omit if appid_value set) +/// "appid_dest": "steam_appid.txt" // relative to Rec Room root +/// } /// } /// /// Config template placeholders: {HOST}, {PHOTON_APPID}, @@ -119,7 +126,68 @@ public sealed class ClientPatcher File.Copy(dllSrc, dllDest, overwrite: true); log.AppendLine($"[patch] installed plugin: {Path.GetFileName(manifest.PluginDll)} → {pluginDestDir}"); - // 4. Render the config template. + // 4. Replace Valve's steam_api64.dll with the bundled stub + // (Goldberg emulator) so the unwrapped exe runs without + // Steam loaded. Backs up the original next to the dest + // DLL with the configured suffix so users can revert. + if (manifest.SteamStub is { } stub && !string.IsNullOrEmpty(stub.ApiDll)) + { + var stubSrc = Path.Combine(patcherDir, stub.ApiDll); + if (!File.Exists(stubSrc)) + return PatchResult.Failure($"steam_stub.api_dll listed but missing on disk: {stubSrc}"); + if (string.IsNullOrEmpty(stub.ApiDest)) + return PatchResult.Failure("steam_stub.api_dest is required when steam_stub is set."); + + var dllTarget = Path.Combine(recRoomRoot, stub.ApiDest.Replace('/', Path.DirectorySeparatorChar)); + var dllTargetDir = Path.GetDirectoryName(dllTarget); + if (!string.IsNullOrEmpty(dllTargetDir)) Directory.CreateDirectory(dllTargetDir); + + // Back up the original once. If a .steam-original already + // exists we assume a previous patch ran — leave it alone + // rather than clobber the actual Valve DLL with our stub. + var backupSuffix = string.IsNullOrEmpty(stub.ApiBackupSuffix) + ? ".steam-original" : stub.ApiBackupSuffix; + var backupPath = dllTarget + backupSuffix; + if (File.Exists(dllTarget) && !File.Exists(backupPath)) + { + File.Move(dllTarget, backupPath); + log.AppendLine($"[patch] backed up Valve steam_api64.dll → {Path.GetFileName(backupPath)}"); + } + File.Copy(stubSrc, dllTarget, overwrite: true); + log.AppendLine($"[patch] installed Steam stub: {Path.GetFileName(dllTarget)}"); + + // appid_dest is required; appid source can be a literal + // value or a file in the zip — file wins if both set. + if (!string.IsNullOrEmpty(stub.AppidDest)) + { + var appidTarget = Path.Combine(recRoomRoot, + stub.AppidDest.Replace('/', Path.DirectorySeparatorChar)); + var appidTargetDir = Path.GetDirectoryName(appidTarget); + if (!string.IsNullOrEmpty(appidTargetDir)) Directory.CreateDirectory(appidTargetDir); + + string appidContent; + if (!string.IsNullOrEmpty(stub.AppidFile)) + { + var appidSrc = Path.Combine(patcherDir, stub.AppidFile); + if (!File.Exists(appidSrc)) + return PatchResult.Failure($"steam_stub.appid_file listed but missing on disk: {appidSrc}"); + appidContent = File.ReadAllText(appidSrc).Trim(); + } + else if (!string.IsNullOrEmpty(stub.AppidValue)) + { + appidContent = stub.AppidValue.Trim(); + } + else + { + return PatchResult.Failure( + "steam_stub.appid_dest set without appid_file or appid_value to source the value from."); + } + File.WriteAllText(appidTarget, appidContent); + log.AppendLine($"[patch] wrote {Path.GetFileName(appidTarget)} ({appidContent})"); + } + } + + // 5. Render the config template. if (!string.IsNullOrEmpty(manifest.ConfigTemplate)) { var templatePath = Path.Combine(patcherDir, manifest.ConfigTemplate); @@ -191,6 +259,17 @@ public sealed class ClientPatcher [JsonPropertyName("config_template")] public string? ConfigTemplate { get; set; } [JsonPropertyName("config_dest")] public string? ConfigDest { get; set; } [JsonPropertyName("old_plugin_paths")] public string[]? OldPluginPaths { get; set; } + [JsonPropertyName("steam_stub")] public SteamStubBlock? SteamStub { get; set; } + } + + private sealed class SteamStubBlock + { + [JsonPropertyName("api_dll")] public string? ApiDll { get; set; } + [JsonPropertyName("api_dest")] public string? ApiDest { get; set; } + [JsonPropertyName("api_backup_suffix")] public string? ApiBackupSuffix { get; set; } + [JsonPropertyName("appid_file")] public string? AppidFile { get; set; } + [JsonPropertyName("appid_value")] public string? AppidValue { get; set; } + [JsonPropertyName("appid_dest")] public string? AppidDest { get; set; } } }