diff --git a/launcher/Backend/AppState.cs b/launcher/Backend/AppState.cs index 33351a0..0b978eb 100644 --- a/launcher/Backend/AppState.cs +++ b/launcher/Backend/AppState.cs @@ -79,6 +79,17 @@ public sealed class AppState [JsonPropertyName("launchInScreenMode")] public bool LaunchInScreenMode { get; set; } + /// When true, the patched ClientMod skips BouncyCastle's + /// TLS chain validation — needed only for self-signed certs on a + /// LAN-only / dev tunnel. Localtunnel + Tunnelto both terminate + /// TLS with valid certs, so the default is off. Leaving it on + /// triggers a CLR detour-compile crash (0x80131506) on the + /// december build — see project_build_layout_per_branch + + /// MonoMod's CompileMethodHook comment in + /// DorkNet.ClientMod/Mod.cs. + [JsonPropertyName("enableTlsTrustBypass")] + public bool EnableTlsTrustBypass { get; set; } + private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true, diff --git a/launcher/Backend/ClientPatcher.cs b/launcher/Backend/ClientPatcher.cs index 9b9f52d..7986b00 100644 --- a/launcher/Backend/ClientPatcher.cs +++ b/launcher/Backend/ClientPatcher.cs @@ -59,10 +59,11 @@ public sealed class ClientPatcher string photonRegion, string apexHost, bool singleOrigin = false, + bool enableTlsTrustBypass = false, CancellationToken ct = default) { return Task.Run(() => ApplyCore(patcherDir, recRoomDataPath, - photonAppId, photonVoiceAppId, photonRegion, apexHost, singleOrigin), ct); + photonAppId, photonVoiceAppId, photonRegion, apexHost, singleOrigin, enableTlsTrustBypass), ct); } private PatchResult ApplyCore( @@ -72,7 +73,8 @@ public sealed class ClientPatcher string photonVoiceAppId, string photonRegion, string apexHost, - bool singleOrigin) + bool singleOrigin, + bool enableTlsTrustBypass) { var log = new System.Text.StringBuilder(); try @@ -220,7 +222,8 @@ public sealed class ClientPatcher .Replace("{PHOTON_VOICE_APPID}", string.IsNullOrEmpty(photonVoiceAppId) ? photonAppId ?? "" : photonVoiceAppId) .Replace("{PHOTON_REGION}", string.IsNullOrEmpty(photonRegion) ? "us" : photonRegion) - .Replace("{SINGLE_ORIGIN_BASE_URL}", singleOrigin ? $"https://{apexHost}" : ""); + .Replace("{SINGLE_ORIGIN_BASE_URL}", singleOrigin ? $"https://{apexHost}" : "") + .Replace("{ENABLE_TLS_TRUST_BYPASS}", enableTlsTrustBypass ? "true" : "false"); File.WriteAllText(configDest, rendered); log.AppendLine($"[patch] wrote config: {Path.GetFileName(configDest)}"); } diff --git a/launcher/Backend/RecRoomLauncher.cs b/launcher/Backend/RecRoomLauncher.cs index 0c375bf..2a8f36b 100644 --- a/launcher/Backend/RecRoomLauncher.cs +++ b/launcher/Backend/RecRoomLauncher.cs @@ -4,15 +4,32 @@ using System.IO; namespace DorkNet.Launcher.Backend; /// Launches the user's already-patched Rec Room install -/// directly via Recroom_Release.exe. Never goes through Steam — -/// DorkNet users may not own the game on Steam. +/// directly. Never goes through Steam — DorkNet users may not own +/// the game on Steam. +/// +/// 2020.03 vs 2020.12 build layout: +/// +/// 2020.03 (pre-EAC): single binary Recroom_Release.exe +/// alongside Recroom_Release_Data/. Run that directly. +/// 2020.12+ (EAC): the real Unity binary is RecRoom.exe +/// with its data in RecRoom_Data/; Recroom_Release.exe +/// is the Easy Anti-Cheat launcher wrapper that Steam invokes. Run +/// RecRoom.exe directly to skip EAC entirely — EAC never +/// loads into the game process, no binary patching required (this is +/// the same approach tools/remove-eac.ps1 documents on the +/// per-version branch). +/// +/// Detection: an EasyAntiCheat/ folder next to the binaries +/// flags the EAC layout. Falls back to whichever exe exists if the +/// heuristic is ambiguous. public static class RecRoomLauncher { - /// The picked install path is the Recroom_Release_Data - /// folder; the executable lives at <parent>\Recroom_Release.exe. - /// Returns true if Process.Start succeeded. - /// passes Rec Room's +forcemode:screen arg so the game opens - /// in 2D desktop mode instead of trying to bind a headset. + /// The picked install path is the *_Data folder; + /// the executable lives at its parent. Returns true if + /// succeeded. + /// passes Rec Room's + /// +forcemode:screen arg so the game opens in 2D desktop + /// mode instead of trying to bind a headset. public static bool TryLaunch(string recRoomDataPath, bool screenMode, out string error) { error = ""; @@ -27,13 +44,15 @@ public static class RecRoomLauncher error = $"Couldn't resolve the parent directory of '{recRoomDataPath}'."; return false; } - var exe = Path.Combine(parent, "Recroom_Release.exe"); - if (!File.Exists(exe)) + + var exe = PickExecutable(parent); + if (exe is null) { - error = $"Recroom_Release.exe not found at '{exe}'. " + - "Pick the *_Data folder that sits next to it."; + error = $"Neither RecRoom.exe nor Recroom_Release.exe found next to '{recRoomDataPath}'. " + + "Pick the *_Data folder that sits beside the game binary."; return false; } + try { Process.Start(new ProcessStartInfo @@ -51,4 +70,24 @@ public static class RecRoomLauncher return false; } } + + /// Pick the right game exe for the install. Prefer + /// RecRoom.exe on EAC builds (so we skip the EAC launcher + /// wrapper). Fall back to whichever single binary exists when only + /// one is present. + private static string? PickExecutable(string parent) + { + var recRoomExe = Path.Combine(parent, "RecRoom.exe"); + var legacyExe = Path.Combine(parent, "Recroom_Release.exe"); + var hasEac = Directory.Exists(Path.Combine(parent, "EasyAntiCheat")); + + // EAC build: RecRoom.exe is the actual game; Recroom_Release.exe + // is the EAC launcher wrapper. Use the former — it never loads + // EAC into the process. + if (hasEac && File.Exists(recRoomExe)) return recRoomExe; + // Only-one-present cases. + if (File.Exists(legacyExe)) return legacyExe; + if (File.Exists(recRoomExe)) return recRoomExe; + return null; + } } diff --git a/launcher/MainWindow.xaml b/launcher/MainWindow.xaml index 2b9988c..8e3eecc 100644 --- a/launcher/MainWindow.xaml +++ b/launcher/MainWindow.xaml @@ -967,6 +967,14 @@ + + diff --git a/launcher/MainWindow.xaml.cs b/launcher/MainWindow.xaml.cs index ebf2385..ff8846c 100644 --- a/launcher/MainWindow.xaml.cs +++ b/launcher/MainWindow.xaml.cs @@ -202,6 +202,8 @@ public partial class MainWindow : Window HostScreenModeChk.IsChecked = _state.LaunchInScreenMode; if (JoinScreenModeChk is not null) JoinScreenModeChk.IsChecked = _state.LaunchInScreenMode; + if (TlsTrustBypassChk is not null) + TlsTrustBypassChk.IsChecked = _state.EnableTlsTrustBypass; // Hosting mode radios — reflect persisted choice on both the // main HostView and the setup wizard's matching step. @@ -721,7 +723,8 @@ public partial class MainWindow : Window var patch = await _patcher.ApplyAsync( patcherDir, _state.RecRoomPath!, _state.PhotonAppId, _state.PhotonVoiceAppId, _state.PhotonRegion, _hostApex, - singleOrigin: _state.HostingMode == HostingMode.SingleOriginTunnel); + singleOrigin: _state.HostingMode == HostingMode.SingleOriginTunnel, + enableTlsTrustBypass: _state.EnableTlsTrustBypass); if (!patch.Ok) { FailStepWithFriendly(activeStep, ErrorTranslator.TranslateMessage(patch.Log)); @@ -1013,7 +1016,8 @@ public partial class MainWindow : Window var patch = await _patcher.ApplyAsync( patcherDir, _state.RecRoomPath!, payload.PhotonAppId, payload.PhotonVoiceAppId, payload.PhotonRegion, payload.Host, - singleOrigin: payload.SingleOrigin); + singleOrigin: payload.SingleOrigin, + enableTlsTrustBypass: _state.EnableTlsTrustBypass); if (!patch.Ok) { FailStepWithFriendly(activeStep, ErrorTranslator.TranslateMessage(patch.Log)); @@ -1066,6 +1070,13 @@ public partial class MainWindow : Window _state.Save(); } + private void OnTlsTrustBypassChanged(object sender, RoutedEventArgs e) + { + if (_state is null) return; + _state.EnableTlsTrustBypass = (sender as CheckBox)?.IsChecked == true; + _state.Save(); + } + private void EnsureRemoteWildcardApex() { if (!string.IsNullOrWhiteSpace(_state.RemoteWildcardApex)) return;