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;