<# Read-only triage for a Radicade-patched August 2023 Rec Room install. Usage: powershell -ExecutionPolicy Bypass -File august-client-triage.ps1 powershell -ExecutionPolicy Bypass -File august-client-triage.ps1 -GameDir "C:\path\to\2023-08-29T06-53-33Z" It only reads files and opens two TCP sockets to localhost. It writes august-client-report.txt next to this script (override with -OutFile). Send that file back verbatim; it names exactly which piece of the stack is missing. #> [CmdletBinding()] param( [string]$GameDir, [string]$OutFile ) $ErrorActionPreference = 'Continue' $script:Report = New-Object System.Collections.Generic.List[string] $script:Failures = 0 $script:Warnings = 0 function Add-Line { param([string]$Text = '') $script:Report.Add($Text) } function Write-Both { param([string]$Text = '') Write-Host $Text Add-Line $Text } function Add-Verdict { param( [ValidateSet('OK', 'WARN', 'MISSING')][string]$State, [string]$Text ) if ($State -eq 'MISSING') { $script:Failures++ } if ($State -eq 'WARN') { $script:Warnings++ } Add-Line ("[{0,-7}] {1}" -f $State, $Text) if ($State -ne 'OK') { Write-Host ("[{0}] {1}" -f $State, $Text) } } function Get-Md5 { param([string]$Path) try { return (Get-FileHash -LiteralPath $Path -Algorithm MD5).Hash.ToLower() } catch { return 'unreadable' } } function Get-ByteString { param([string]$Path) try { $bytes = [System.IO.File]::ReadAllBytes($Path) return [System.Text.Encoding]::GetEncoding(28591).GetString($bytes) } catch { return $null } } function Test-Utf16Literal { param([string]$Path, [string]$Literal) try { $bytes = [System.IO.File]::ReadAllBytes($Path) $needle = [System.Text.Encoding]::Unicode.GetBytes($Literal) for ($i = 0; $i -le ($bytes.Length - $needle.Length); $i++) { if ($bytes[$i] -ne $needle[0]) { continue } $hit = $true for ($j = 1; $j -lt $needle.Length; $j++) { if ($bytes[$i + $j] -ne $needle[$j]) { $hit = $false; break } } if ($hit) { return $true } } } catch { } return $false } function Test-Tcp { param([int]$Port) $client = New-Object System.Net.Sockets.TcpClient try { $task = $client.ConnectAsync('127.0.0.1', $Port) if ($task.Wait(1500) -and $client.Connected) { return $true } return $false } catch { return $false } finally { $client.Close() } } function Get-LogMarkers { param([string]$Path, [string[]]$Markers) $result = @{} foreach ($m in $Markers) { $result[$m] = 0 } if (-not (Test-Path -LiteralPath $Path)) { return $result } try { $text = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop foreach ($m in $Markers) { $result[$m] = ([regex]::Matches($text, [regex]::Escape($m))).Count } } catch { } return $result } # --- locate the install ------------------------------------------------------- if (-not $GameDir) { $candidates = @( (Join-Path $env:USERPROFILE 'Downloads\2023-08-29T06-53-33Z'), (Join-Path $env:USERPROFILE 'Desktop\2023-08-29T06-53-33Z'), 'C:\2023-08-29T06-53-33Z' ) $downloads = Join-Path $env:USERPROFILE 'Downloads' if (Test-Path -LiteralPath $downloads) { $candidates += (Get-ChildItem -LiteralPath $downloads -Directory -ErrorAction SilentlyContinue | Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'RecRoom.exe') } | ForEach-Object { $_.FullName }) } foreach ($c in $candidates) { if ($c -and (Test-Path -LiteralPath (Join-Path $c 'RecRoom.exe'))) { $GameDir = $c; break } } } if (-not $GameDir) { $GameDir = Read-Host 'Path to the Rec Room folder that contains RecRoom.exe' } $GameDir = $GameDir.TrimEnd('\') if (-not $OutFile) { if ($PSScriptRoot) { $OutFile = Join-Path $PSScriptRoot 'august-client-report.txt' } else { $OutFile = Join-Path (Get-Location) 'august-client-report.txt' } } $playerLog = Join-Path $env:USERPROFILE 'AppData\LocalLow\Against Gravity\Rec Room\Player.log' $bepLog = Join-Path $GameDir 'BepInEx\LogOutput.log' $metadata = Join-Path $GameDir 'Recroom_Data\il2cpp_data\Metadata\global-metadata.dat' $interopDat = Join-Path $GameDir 'Recroom_Data\il2cpp_data\Metadata\global-metadata.interop.dat' $bootConfig = Join-Path $GameDir 'Recroom_Data\boot.config' $patchJson = Join-Path $GameDir 'Recroom_Data\radicade-patch.json' $winhttp = Join-Path $GameDir 'winhttp.dll' $doorstop = Join-Path $GameDir 'doorstop_config.ini' $loader = Join-Path $GameDir 'BepInEx\core\BepInEx.Unity.IL2CPP.dll' $plugin = Join-Path $GameDir 'BepInEx\plugins\RadicadeAugustClient.dll' $assembly = Join-Path $GameDir 'GameAssembly.dll' $interopDir = Join-Path $GameDir 'BepInEx\interop' # Expected values taken from the reference install that logs in and loads rooms. $expected = @{ MetadataSize = 38654794 MetadataMd5 = 'cc9c32014b5fc3dd1a2a8fb12376be15' InteropSize = 38654794 InteropMd5 = '4a1a28078f957af470f3b31951b32648' BootSize = 173 BootMd5 = 'dc2e5f7af7d5df736a4cc3477b4b1a1f' WinhttpSize = 26112 WinhttpMd5 = 'b2a4d0cb04bf8f5a27ccab237ecc2586' LoaderSize = 60928 LoaderMd5 = '9430bb84349d52b753a181788a86c5e2' PluginSize = 62464 PluginMd5 = '8718a45dd42d7cb72b3e46e04b744d91' InteropCount = 335 AssemblySize = 167390208 } # --- header ------------------------------------------------------------------ Write-Both 'Radicade August 2023 client triage' Write-Both ("generated : {0}" -f (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')) Write-Both ("game folder : {0}" -f $GameDir) Write-Both ("powershell : {0}" -f $PSVersionTable.PSVersion) Write-Both '' Write-Both 'NOTE: "Failed to load Type RecRoom.Analytics.Statsig.StatsigDebugOverrides" in' Write-Both 'Player.log is NORMAL. It appears in a completely unpatched launch of this' Write-Both 'build and in every working install. It is not a symptom of anything.' Write-Both '' # --- install identity -------------------------------------------------------- Write-Both '== game build ==' if (Test-Path -LiteralPath $assembly) { $info = Get-Item -LiteralPath $assembly $md5 = Get-Md5 $assembly Write-Both ("GameAssembly.dll : {0} b md5 {1}" -f $info.Length, $md5) Write-Both (" file version : {0}" -f $info.VersionInfo.FileVersion) if ($info.Length -eq $expected.AssemblySize) { Add-Verdict 'OK' ('GameAssembly.dll size matches the reference build ({0} b)' -f $expected.AssemblySize) } else { Add-Verdict 'WARN' ("GameAssembly.dll is {0} b, the reference build is {1} b - a different build will not accept this patch stack" -f $info.Length, $expected.AssemblySize) } } else { Add-Verdict 'MISSING' 'GameAssembly.dll - this is not a Rec Room install folder?' } if (Test-Path -LiteralPath $playerLog) { $firstLine = (Get-Content -LiteralPath $playerLog -TotalCount 1 -ErrorAction SilentlyContinue) Write-Both ("Player.log line 1 : {0}" -f $firstLine) if ($firstLine -notmatch '2020\.3\.33f1') { Add-Verdict 'WARN' 'Player.log does not report Unity 2020.3.33f1 - unexpected engine build' } } else { Add-Verdict 'WARN' ('Player.log not found at {0}' -f $playerLog) } Write-Both '' # --- the patched data files -------------------------------------------------- Write-Both '== patched data files ==' if (Test-Path -LiteralPath $metadata) { $info = Get-Item -LiteralPath $metadata $md5 = Get-Md5 $metadata $text = Get-ByteString $metadata $local = if ($text) { ([regex]::Matches($text, 'localhost:3030')).Count } else { -1 } $recnet = if ($text) { ([regex]::Matches($text, 'rec\.net')).Count } else { -1 } Write-Both ("global-metadata.dat : {0} b md5 {1}" -f $info.Length, $md5) Write-Both (" localhost:3030 : {0} occurrence(s) (reference: 28)" -f $local) Write-Both (" rec.net literals : {0} occurrence(s) (reference: 50)" -f $recnet) if ($info.Length -eq $expected.MetadataSize -and $md5 -eq $expected.MetadataMd5) { Add-Verdict 'OK' 'global-metadata.dat is the Radicade-patched file' } elseif ($local -eq 0) { Add-Verdict 'MISSING' 'global-metadata.dat is UNPATCHED - the client points at the official servers, the plugin cannot fix that' } else { Add-Verdict 'WARN' 'global-metadata.dat differs from the reference file (size or md5)' } } else { Add-Verdict 'MISSING' 'Recroom_Data\il2cpp_data\Metadata\global-metadata.dat' } $metadataOrig = $metadata + '.orig' if (Test-Path -LiteralPath $metadataOrig) { $origInfo = Get-Item -LiteralPath $metadataOrig Add-Line (" global-metadata.dat.orig: {0} b (a 32039456 b copy is the pristine vanilla file the patcher backed up)" -f $origInfo.Length) } if (Test-Path -LiteralPath $interopDat) { $info = Get-Item -LiteralPath $interopDat $md5 = Get-Md5 $interopDat if ($info.Length -eq $expected.InteropSize -and $md5 -eq $expected.InteropMd5) { Add-Verdict 'OK' 'global-metadata.interop.dat matches the reference' } else { Add-Verdict 'WARN' ("global-metadata.interop.dat is {0} b md5 {1} (reference {2} b {3})" -f $info.Length, $md5, $expected.InteropSize, $expected.InteropMd5) } } else { Add-Verdict 'MISSING' 'Recroom_Data\il2cpp_data\Metadata\global-metadata.interop.dat - the patched BepInEx loader needs it to build interop assemblies' } if (Test-Path -LiteralPath $bootConfig) { $info = Get-Item -LiteralPath $bootConfig $md5 = Get-Md5 $bootConfig Write-Both ("boot.config : {0} b md5 {1}" -f $info.Length, $md5) Write-Both (' content : ' + ((Get-Content -LiteralPath $bootConfig) -join ' | ')) if ($info.Length -eq $expected.BootSize -and $md5 -eq $expected.BootMd5) { Add-Verdict 'OK' 'boot.config is the Radicade variant' } else { Add-Verdict 'WARN' 'boot.config differs from the reference (vr-enabled=0 / player-connection-mode=Listen)' } } else { Add-Verdict 'MISSING' 'Recroom_Data\boot.config' } if (Test-Path -LiteralPath $patchJson) { Add-Line 'radicade-patch.json :' foreach ($line in (Get-Content -LiteralPath $patchJson)) { Add-Line (' ' + $line) } Add-Verdict 'OK' 'Recroom_Data\radicade-patch.json present - the metadata was patched on this machine' } else { Add-Verdict 'MISSING' 'Recroom_Data\radicade-patch.json - no evidence this install was ever patched' } Write-Both '' # --- loader stack ------------------------------------------------------------ Write-Both '== loader stack ==' if (Test-Path -LiteralPath $winhttp) { $info = Get-Item -LiteralPath $winhttp $md5 = Get-Md5 $winhttp if ($info.Length -eq $expected.WinhttpSize -and $md5 -eq $expected.WinhttpMd5) { Add-Verdict 'OK' 'winhttp.dll is the Doorstop 4.3.0 proxy' } else { Add-Verdict 'WARN' ("winhttp.dll is {0} b md5 {1} (reference {2} b {3}) - another proxy or a disabled/renamed copy?" -f $info.Length, $md5, $expected.WinhttpSize, $expected.WinhttpMd5) } } else { Add-Verdict 'MISSING' 'winhttp.dll - without the Doorstop proxy nothing loads BepInEx' if (Test-Path -LiteralPath (Join-Path $GameDir 'winhttp.dll.baseline-disabled')) { Add-Verdict 'MISSING' 'winhttp.dll is renamed to winhttp.dll.baseline-disabled - rename it back' } } if (Test-Path -LiteralPath $doorstop) { $cfg = Get-Content -LiteralPath $doorstop -Raw if ($cfg -match 'enabled\s*=\s*true' -or $cfg -match '^\s*enabled\s*=\s*1') { Add-Verdict 'OK' 'doorstop_config.ini has enabled = true' } else { Add-Verdict 'MISSING' 'doorstop_config.ini does not set enabled = true' } if ($cfg -match 'BepInEx\\core\\BepInEx\.Unity\.IL2CPP\.dll') { Add-Verdict 'OK' 'doorstop_config.ini targets BepInEx\core\BepInEx.Unity.IL2CPP.dll' } else { Add-Verdict 'WARN' 'doorstop_config.ini target_assembly is not the IL2CPP preloader' } } else { Add-Verdict 'MISSING' 'doorstop_config.ini' } if (Test-Path -LiteralPath (Join-Path $GameDir '.doorstop_version')) { Add-Line (' .doorstop_version : ' + (Get-Content -LiteralPath (Join-Path $GameDir '.doorstop_version') -Raw).Trim()) } if (Test-Path -LiteralPath $loader) { $info = Get-Item -LiteralPath $loader $md5 = Get-Md5 $loader $patched = Test-Utf16Literal -Path $loader -Literal 'global-metadata.interop.dat' if ($info.Length -eq $expected.LoaderSize -and $md5 -eq $expected.LoaderMd5) { Add-Verdict 'OK' 'BepInEx.Unity.IL2CPP.dll is the patched loader (reads global-metadata.interop.dat)' } elseif (-not $patched) { Add-Verdict 'MISSING' 'BepInEx.Unity.IL2CPP.dll is UNPATCHED - Cpp2IL will reject this build''s metadata header' } else { Add-Verdict 'WARN' ("BepInEx.Unity.IL2CPP.dll is {0} b md5 {1} (reference {2} b {3})" -f $info.Length, $md5, $expected.LoaderSize, $expected.LoaderMd5) } } else { Add-Verdict 'MISSING' 'BepInEx\core\BepInEx.Unity.IL2CPP.dll' } if (Test-Path -LiteralPath $interopDir) { $count = (Get-ChildItem -LiteralPath $interopDir -File -ErrorAction SilentlyContinue).Count if ($count -ge $expected.InteropCount) { Add-Verdict 'OK' ("BepInEx\interop holds {0} assemblies" -f $count) } else { Add-Verdict 'WARN' ("BepInEx\interop holds {0} assemblies (reference {1}) - generation may have failed halfway" -f $count, $expected.InteropCount) } } else { Add-Verdict 'MISSING' 'BepInEx\interop - the game''s managed proxy assemblies are absent' } if (Test-Path -LiteralPath $plugin) { $info = Get-Item -LiteralPath $plugin $md5 = Get-Md5 $plugin if ($info.Length -eq $expected.PluginSize -and $md5 -eq $expected.PluginMd5) { Add-Verdict 'OK' 'RadicadeAugustClient.dll matches the reference plugin build' } else { Add-Verdict 'WARN' ("RadicadeAugustClient.dll is {0} b md5 {1} (reference {2} b {3}) - a different build" -f $info.Length, $md5, $expected.PluginSize, $expected.PluginMd5) } } else { Add-Verdict 'MISSING' 'BepInEx\plugins\RadicadeAugustClient.dll' } $otherPlugins = Get-ChildItem -LiteralPath (Join-Path $GameDir 'BepInEx\plugins') -File -Filter *.dll -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne 'RadicadeAugustClient.dll' } foreach ($p in $otherPlugins) { Add-Verdict 'WARN' ('extra plugin in BepInEx\plugins: {0} ({1} b) - it may not be August-compatible' -f $p.Name, $p.Length) } Write-Both '' # --- server reachability ----------------------------------------------------- Write-Both '== local server ==' $https = Test-Tcp -Port 3030 $http = Test-Tcp -Port 3031 if ($https) { Add-Verdict 'OK' 'something is listening on https://127.0.0.1:3030' } else { Add-Verdict 'MISSING' 'nothing is listening on port 3030 - the client is hardwired to https://localhost:3030, so the Radicade server must run on THIS machine' } if ($http) { Add-Verdict 'OK' 'something is listening on http://127.0.0.1:3031' } else { Add-Verdict 'WARN' 'nothing is listening on port 3031' } Write-Both '' # --- logs -------------------------------------------------------------------- Write-Both '== BepInEx log ==' if (Test-Path -LiteralPath $bepLog) { $markers = Get-LogMarkers -Path $bepLog -Markers @( 'Chainloader startup complete', 'Radicade August Client', 'Bypassing native launch validator', 'global-metadata.interop.dat', 'Cpp2IL', '[Error', '[Warning' ) foreach ($k in $markers.Keys) { Add-Line (" {0,-38}: {1}" -f $k, $markers[$k]) } if ($markers['Chainloader startup complete'] -gt 0) { Add-Verdict 'OK' 'BepInEx chainloader came up' } else { Add-Verdict 'MISSING' 'BepInEx chainloader never completed - read the log tail below' } if ($markers['Radicade August Client'] -gt 0) { Add-Verdict 'OK' 'the Radicade plugin logged under its own name' } else { Add-Verdict 'MISSING' 'the Radicade plugin never logged - it did not load' } Add-Line '' Add-Line ' last 25 lines:' foreach ($line in (Get-Content -LiteralPath $bepLog -Tail 25 -ErrorAction SilentlyContinue)) { Add-Line (' ' + $line) } } else { Add-Verdict 'MISSING' ('BepInEx\LogOutput.log not found at {0} - BepInEx never started' -f $bepLog) } Write-Both '' Write-Both '== Player.log ==' if (Test-Path -LiteralPath $playerLog) { $markers = Get-LogMarkers -Path $playerLog -Markers @( 'Failed to load Type "RecRoom.Analytics.Statsig.StatsigDebugOverrides"', 'Could not resolve host', "Could not resolve host 'ns.rec.net'", 'Launch validation failed', 'FailedToSpawnPlayer', 'Error downloading save data', 'ns.rec.net', 'localhost:3030' ) foreach ($k in $markers.Keys) { Add-Line (" {0,-72}: {1}" -f $k, $markers[$k]) } if ($markers["Could not resolve host 'ns.rec.net'"] -gt 0 -or $markers['ns.rec.net'] -gt 0) { Add-Verdict 'MISSING' 'the client still talks to ns.rec.net - the metadata host literals are unpatched (or the plugin''s nameserver redirect never installed)' } elseif ($markers['localhost:3030'] -gt 0) { Add-Verdict 'OK' 'the client''s requests go to localhost:3030' } if ($markers['Launch validation failed'] -gt 0) { Add-Verdict 'MISSING' 'the native launch validator rejected the install and no bypass ran' } Add-Line '' Add-Line ' last 25 lines:' foreach ($line in (Get-Content -LiteralPath $playerLog -Tail 25 -ErrorAction SilentlyContinue)) { Add-Line (' ' + $line) } } else { Add-Verdict 'MISSING' ('Player.log not found at {0}' -f $playerLog) } # --- verdict ----------------------------------------------------------------- Write-Both '' Write-Both ('RESULT: {0} missing piece(s), {1} warning(s)' -f $script:Failures, $script:Warnings) if ($script:Failures -eq 0) { Write-Both 'Every required piece is present and the local server answers. If the game still' Write-Both 'fails, the report above (BepInEx log tail + Player.log tail) names the next cause.' } else { Write-Both 'The [MISSING] lines above are the install steps that did not happen.' } Write-Both '' Write-Both 'Reminder: the StatsigDebugOverrides line is expected noise in every launch.' try { Set-Content -LiteralPath $OutFile -Value ($script:Report -join "`r`n") -Encoding UTF8 Write-Host '' Write-Host ("Report written to {0}" -f $OutFile) } catch { Write-Host ("Could not write {0}: {1}" -f $OutFile, $_.Exception.Message) }