Files
Radicade/Tools/august-client-triage.ps1
2026-10-16 23:33:13 -07:00

432 lines
18 KiB
PowerShell

<#
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)
}