Radicade Public Code

This commit is contained in:
JogmansonReal
2026-10-16 23:33:13 -07:00
commit 55175acdf6
832 changed files with 363261 additions and 0 deletions
+608
View File
@@ -0,0 +1,608 @@
<#
Read-only triage for a Radicade-patched September 2023 Rec Room install
(game build 20230831, folder 2023-09-06T06-53-47Z, Unity 2020.3.33f1).
It also answers the two questions a boot that never reaches the login screen
raises on a machine somebody else cannot look at:
* WHICH server does this client's own data point at? The metadata literal
table is read directly, because a byte search cannot tell the live host
from the dead copy the patch leaves behind. A client whose literals say
api.recflare.net is running another project's patch, whatever the plugin
config says.
* WHAT has this Windows user talked to before? BepInEx, config, caches and
the last log are read, because %LOCALAPPDATA%Low\Against Gravity\Rec Room
belongs to the user rather than to a game copy and is shared by every
install on the PC.
Usage:
powershell -ExecutionPolicy Bypass -File september-client-triage.ps1
powershell -ExecutionPolicy Bypass -File september-client-triage.ps1 -GameDir "C:\path\to\2023-09-06T06-53-47Z"
It only reads files. It writes september-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.
The single failure this script exists for:
[Error :InteropManager] Failed to generate Il2Cpp interop assemblies:
... System.FormatException: Invalid or corrupt metadata (magic number check failed)
Rec Room does not ship a standard IL2CPP metadata header. Its global-metadata.dat
begins with a per-release scrambled 256-byte block, so LibCpp2IL's magic check
(0xFAB11BAF) rejects it. The working install keeps a normalized copy
(global-metadata.interop.dat, magic af 1b b1 fa) and points BepInEx at that copy
instead - either through a patched loader (BepInEx 6.0.0-be.697) or through the
[IL2CPP] GlobalMetadataPath setting (newer BepInEx builds).
A stock BepInEx core has neither, so interop generation dies, no interop
assemblies are written, and the chainloader then fails with
"Could not load file or assembly 'UnityEngine.CoreModule'".
#>
[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-Bytes {
param([string]$Path)
# The leading comma matters: without it PowerShell unrolls the array into the pipeline
# and the caller receives one object per byte. On the 33 MB global-metadata.dat that is
# 33 million boxed objects and the script appears to hang for minutes.
try { return ,([System.IO.File]::ReadAllBytes($Path)) }
catch { return $null }
}
# True when the file holds the literal as UTF-16 or as UTF-8/ASCII.
function Test-Literal {
param([byte[]]$Bytes, [string]$Literal)
if (-not $Bytes) { return $false }
foreach ($needle in @([System.Text.Encoding]::Unicode.GetBytes($Literal),
[System.Text.Encoding]::UTF8.GetBytes($Literal))) {
$limit = $Bytes.Length - $needle.Length
for ($i = 0; $i -le $limit; $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 }
}
}
return $false
}
function Get-FirstBytes {
param([string]$Path, [int]$Count = 4)
$bytes = Get-Bytes $Path
if (-not $bytes) { return 'unreadable' }
return (($bytes[0..([Math]::Min($Count, $bytes.Length) - 1)] | ForEach-Object { $_.ToString('x2') }) -join ' ')
}
function Get-TextIfExists {
param([string]$Path)
if (-not (Test-Path -LiteralPath $Path)) { return $null }
try { return (Get-Content -LiteralPath $Path -Raw -ErrorAction Stop) } catch { return $null }
}
function Get-MatchCount {
param([string]$Text, [string]$Pattern)
if (-not $Text) { return 0 }
return ([regex]::Matches($Text, $Pattern)).Count
}
# The patch keeps the original literal blob verbatim and appends the rewritten literals, so
# a byte search finds BOTH the dead and the live copy of every host (the reference file still
# contains "ns.rec.net" as dead bytes) and cannot tell them apart. Only the literal table
# says which string the runtime actually resolves, so read it: for 20230831 its four header
# fields sit at byte 0x74 (table offset), 0x24 (table size), 0xA0 (data offset), 0x3C (data
# size) - the same positions tools/patch-0831-client-metadata.py rewrites.
function Get-MetadataHostCounts {
param([string]$Path, [int]$Top = 25)
$bytes = Get-Bytes $Path
if (-not $bytes -or $bytes.Length -lt 0x100) { return $null }
$tableOff = [BitConverter]::ToUInt32($bytes, 0x74)
$tableSize = [BitConverter]::ToUInt32($bytes, 0x24)
$dataOff = [BitConverter]::ToUInt32($bytes, 0xA0)
$dataSize = [BitConverter]::ToUInt32($bytes, 0x3C)
if ($tableSize -eq 0 -or ($tableOff + $tableSize) -gt $bytes.Length) { return $null }
if (($dataOff + $dataSize) -gt $bytes.Length) { return $null }
$counts = @{}
$records = [int]($tableSize / 8)
for ($i = 0; $i -lt $records; $i++) {
$pos = [int]$tableOff + ($i * 8)
$len = [BitConverter]::ToUInt32($bytes, $pos)
$idx = [BitConverter]::ToInt32($bytes, $pos + 4)
if ($len -eq 0 -or $len -gt 512 -or $idx -lt 0) { continue }
$start = [int]$dataOff + $idx
if (($start + $len) -gt $bytes.Length) { continue }
# Only http(s) literals are interesting, and UTF-8 will not start one with any
# other byte - skipping the rest keeps this loop off the slow path.
if ($bytes[$start] -ne 0x68) { continue }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, [int]$len)
$m = [regex]::Match($text, '^https?://(?<host>[^/?#]+)')
if (-not $m.Success) { continue }
$hostName = $m.Groups['host'].Value.ToLowerInvariant()
if ($counts.ContainsKey($hostName)) { $counts[$hostName]++ } else { $counts[$hostName] = 1 }
}
return @($counts.GetEnumerator() |
ForEach-Object { [pscustomobject]@{ Host = $_.Key; Count = $_.Value } } |
Sort-Object -Property Count, Host -Descending |
Select-Object -First $Top)
}
# --- locate the install -------------------------------------------------------
if (-not $GameDir) {
$candidates = @(
(Join-Path $env:USERPROFILE 'Downloads\2023-09-06T06-53-47Z'),
(Join-Path $env:USERPROFILE 'Desktop\2023-09-06T06-53-47Z'),
'C:\2023-09-06T06-53-47Z'
)
$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 'GameAssembly.dll') } |
ForEach-Object { $_.FullName })
}
foreach ($c in $candidates) {
if ($c -and (Test-Path -LiteralPath (Join-Path $c 'GameAssembly.dll'))) { $GameDir = $c; break }
}
}
if (-not $GameDir) { $GameDir = Read-Host 'Path to the Rec Room folder that contains GameAssembly.dll' }
$GameDir = $GameDir.TrimEnd('\')
if (-not $OutFile) {
if ($PSScriptRoot) { $OutFile = Join-Path $PSScriptRoot 'september-client-report.txt' }
else { $OutFile = Join-Path (Get-Location) 'september-client-report.txt' }
}
# The data folder is renamed in some distributions (RecRoom_Data -> Radicade_Data),
# so find it instead of assuming. BepInEx resolves {GameDataPath} at runtime and does
# not care about the name.
$dataDir = $null
foreach ($name in @('RecRoom_Data', 'Radicade_Data', 'Recroom_Data')) {
$p = Join-Path $GameDir $name
if (Test-Path -LiteralPath (Join-Path $p 'il2cpp_data')) { $dataDir = $p; break }
}
if (-not $dataDir) {
$dataDir = Get-ChildItem -LiteralPath $GameDir -Directory -ErrorAction SilentlyContinue |
Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'il2cpp_data') } |
Select-Object -First 1 -ExpandProperty FullName
}
$metadataDir = if ($dataDir) { Join-Path $dataDir 'il2cpp_data\Metadata' } else { $null }
$metadata = if ($metadataDir) { Join-Path $metadataDir 'global-metadata.dat' } else { $null }
$interopDat = if ($metadataDir) { Join-Path $metadataDir 'global-metadata.interop.dat' } else { $null }
$pristineDat = if ($metadataDir) { Join-Path $metadataDir 'global-metadata.dat.orig' } else { $null }
$bepInEx = Join-Path $GameDir 'BepInEx'
$coreDir = Join-Path $bepInEx 'core'
$loader = Join-Path $coreDir 'BepInEx.Unity.IL2CPP.dll'
$configFile = Join-Path $bepInEx 'config\BepInEx.cfg'
$interopDir = Join-Path $bepInEx 'interop'
$unityLibs = Join-Path $bepInEx 'unity-libs'
$plugin = Join-Path $bepInEx 'plugins\RadicadePatch.dll'
$bepLog = Join-Path $bepInEx 'LogOutput.log'
$changelog = Join-Path $GameDir 'changelog.txt'
$assembly = Join-Path $GameDir 'GameAssembly.dll'
$winhttp = Join-Path $GameDir 'winhttp.dll'
$doorstop = Join-Path $GameDir 'doorstop_config.ini'
$markerFile = if ($dataDir) { Join-Path $dataDir 'radicade-patch.json' } else { $null }
# Per Windows user, NOT per game copy: every install on this PC shares it.
$profileDir = Join-Path $env:USERPROFILE 'AppData\LocalLow\Against Gravity\Rec Room'
# Reference values from the install that logs in and loads rooms.
$expected = @{
AssemblySize = 167873024
AssemblyMd5 = '54f26d061797ab615688d62108195fa7'
MetadataSize = 33774004
MetadataMd5 = 'ce5097af6072c4e474f02704f020e94f'
InteropSize = 32115816
InteropMd5 = '4c6548745a2855fc9ab1c5fbaea639d2'
LoaderSize = 60928
LoaderMd5 = '9430bb84349d52b753a181788a86c5e2'
WinhttpMd5 = 'b2a4d0cb04bf8f5a27ccab237ecc2586'
PluginSize = 96768
PluginMd5 = '64e4045f09107f5525764b83c6f4ba2b'
InteropCount = 332
InteropHash = 'f4651f741ef8c3f61cd261bfb1e02e72'
MagicScrambled = 'e0 eb 02 00'
MagicStandard = 'af 1b b1 fa'
}
# --- header ------------------------------------------------------------------
Write-Both 'Radicade September 2023 client triage (game build 20230831)'
Write-Both ("generated : {0}" -f (Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))
Write-Both ("game folder : {0}" -f $GameDir)
Write-Both ("data folder : {0}" -f $(if ($dataDir) { Split-Path $dataDir -Leaf } else { 'NOT FOUND' }))
Write-Both ("powershell : {0}" -f $PSVersionTable.PSVersion)
Write-Both ''
# --- game build ---------------------------------------------------------------
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)
if ($info.Length -eq $expected.AssemblySize -and $md5 -eq $expected.AssemblyMd5) {
Add-Verdict 'OK' 'GameAssembly.dll is the reference September build'
} elseif ($info.Length -eq $expected.AssemblySize) {
Add-Verdict 'WARN' 'GameAssembly.dll has the reference size but a different hash - verify the build'
} else {
Add-Verdict 'MISSING' ("GameAssembly.dll is {0} b, the reference build is {1} b - this patch stack only works on that exact build" -f $info.Length, $expected.AssemblySize)
}
} else {
Add-Verdict 'MISSING' 'GameAssembly.dll - this is not a Rec Room install folder?'
}
Write-Both ''
# --- IL2CPP metadata ----------------------------------------------------------
Write-Both '== IL2CPP metadata =='
if ($metadata -and (Test-Path -LiteralPath $metadata)) {
$info = Get-Item -LiteralPath $metadata
$md5 = Get-Md5 $metadata
$magic = Get-FirstBytes $metadata
Write-Both ("global-metadata.dat: {0} b md5 {1} magic {2}" -f $info.Length, $md5, $magic)
if ($magic -ne $expected.MagicScrambled) {
Add-Verdict 'WARN' ("global-metadata.dat magic is {0}, expected the scrambled Rec Room header {1} - unusual for this build" -f $magic, $expected.MagicScrambled)
}
if ($info.Length -eq $expected.MetadataSize -and $md5 -eq $expected.MetadataMd5) {
Add-Verdict 'OK' 'global-metadata.dat matches the reference host-literal patch'
} else {
Add-Verdict 'WARN' 'global-metadata.dat is not the reference file - the live hosts below say what it is really patched for'
}
# A byte search cannot answer "which server does this client use": the patch keeps the
# original blob verbatim and appends the rewritten literals, so the dead copy of every
# host is still in the file (the reference metadata still contains "ns.rec.net"). Read
# the literal table instead - that is what the runtime resolves.
$hosts = Get-MetadataHostCounts -Path $metadata
if ($hosts) {
Write-Both ' live service hosts this client resolves (from the literal table, dead copies ignored):'
foreach ($h in $hosts) { Write-Both (" {0,4}x {1}" -f $h.Count, $h.Host) }
$foreign = @($hosts | Where-Object { $_.Host -match 'recflare|viverec|trollroom|meownet' })
# Only a PORTED loopback host is the baked-server tell: this build carries a bare
# "localhost" literal of its own, so that one must not raise anything.
$loopback = @($hosts | Where-Object { $_.Host -match '^(localhost|127\.0\.0\.1):[0-9]+$' })
$stock = @($hosts | Where-Object { $_.Host -match '(^|\.)rec\.net$' })
if ($foreign.Count -gt 0) {
$names = ($foreign | ForEach-Object { $_.Host }) -join ', '
Add-Verdict 'MISSING' ("this client's own literals resolve {0} - that is another private server's patch. No plugin setting can redirect a host the client resolves itself, so install this bundle's Metadata folder over <Data>\il2cpp_data\Metadata." -f $names)
} elseif ($loopback.Count -gt 0) {
Add-Verdict 'WARN' 'the metadata is patched for a machine-local address: the server has to run on THIS machine, or take the Metadata folder from the bundle instead'
} elseif ($stock.Count -gt 0 -and $md5 -ne $expected.MetadataMd5) {
Add-Verdict 'MISSING' 'the client still resolves the official RecNet hosts: this metadata was never patched for a private server'
}
} else {
Add-Verdict 'WARN' 'could not read the metadata literal table - unexpected layout for this build'
}
# The marker is what the patch tool wrote; the plugin reads the same file at boot.
if ($markerFile -and (Test-Path -LiteralPath $markerFile)) {
$markerText = Get-TextIfExists $markerFile
$patchedUrl = if ($markerText -match '"serverUrl"\s*:\s*"([^"]+)"') { $Matches[1] } else { '?' }
Write-Both ("radicade-patch.json: serverUrl {0}" -f $patchedUrl)
if ($patchedUrl -match 'localhost|127\.0\.0\.1') {
Add-Verdict 'MISSING' ("this install was metadata-patched for {0} (an earlier pack), so its bootstrap host is a machine-local address and nothing is listening there. Take global-metadata.dat and global-metadata.interop.dat from the bundle." -f $patchedUrl)
}
} else {
Add-Verdict 'WARN' 'no radicade-patch.json in the data folder: nothing records which server this install was patched for'
}
} else {
Add-Verdict 'MISSING' ("global-metadata.dat not found under {0}" -f $metadataDir)
}
if ($pristineDat -and (Test-Path -LiteralPath $pristineDat)) {
$info = Get-Item -LiteralPath $pristineDat
Write-Both ("global-metadata.dat.orig: {0} b magic {1}" -f $info.Length, (Get-FirstBytes $pristineDat))
}
# The normalized copy is what interop generation must be pointed at.
if ($interopDat -and (Test-Path -LiteralPath $interopDat)) {
$info = Get-Item -LiteralPath $interopDat
$md5 = Get-Md5 $interopDat
$magic = Get-FirstBytes $interopDat
Write-Both ("global-metadata.interop.dat: {0} b md5 {1} magic {2}" -f $info.Length, $md5, $magic)
if ($magic -eq $expected.MagicStandard) {
Add-Verdict 'OK' 'global-metadata.interop.dat carries the standard header LibCpp2IL requires'
} else {
Add-Verdict 'MISSING' ("global-metadata.interop.dat magic is {0}, expected {1} - interop generation cannot read this file" -f $magic, $expected.MagicStandard)
}
if ($info.Length -ne $expected.InteropSize -or $md5 -ne $expected.InteropMd5) {
Add-Verdict 'WARN' 'normalized metadata is not the reference file - harmless if interop assemblies are already built, otherwise regenerate it'
}
} else {
Add-Verdict 'MISSING' 'global-metadata.interop.dat is absent: nothing can normalize the header for interop generation'
}
Write-Both ''
# --- BepInEx loader -----------------------------------------------------------
Write-Both '== BepInEx loader =='
$beVersion = $null
foreach ($candidate in @($bepLog, $changelog)) {
$text = Get-TextIfExists $candidate
if ($text -and $text -match 'BepInEx (\d+\.\d+\.\d+-be\.\d+)') { $beVersion = $Matches[1]; break }
}
if ($beVersion) {
Write-Both ("BepInEx version : {0} (from {1})" -f $beVersion, (Split-Path $candidate -Leaf))
} else {
Write-Both 'BepInEx version : unknown (no LogOutput.log or changelog.txt yet)'
}
$loaderExists = Test-Path -LiteralPath $loader
$loaderPatched = $false
if ($loaderExists) {
$info = Get-Item -LiteralPath $loader
$md5 = Get-Md5 $loader
$bytes = Get-Bytes $loader
$loaderPatched = Test-Literal $bytes 'global-metadata.interop.dat'
Write-Both ("BepInEx.Unity.IL2CPP.dll: {0} b md5 {1} reads {2}" -f $info.Length, $md5,
$(if ($loaderPatched) { 'global-metadata.interop.dat (patched)' } else { 'global-metadata.dat (stock)' }))
if ($md5 -eq $expected.LoaderMd5) {
Add-Verdict 'OK' 'patched be.697 loader installed'
} elseif ($loaderPatched) {
Add-Verdict 'OK' 'loader reads the normalized metadata, but it is not the reference loader - a different BepInEx build'
} else {
Add-Verdict 'MISSING' 'loader reads global-metadata.dat: this is a stock BepInEx core and it cannot parse this game metadata'
}
} else {
Add-Verdict 'MISSING' 'BepInEx/core/BepInEx.Unity.IL2CPP.dll - BepInEx is not installed'
}
# Newer BepInEx builds expose the same fix as a config setting instead of a patched binary.
$configText = Get-TextIfExists $configFile
$metadataOverride = $null
if ($configText -and $configText -match '(?m)^\s*GlobalMetadataPath\s*=\s*(.+?)\s*$') { $metadataOverride = $Matches[1] }
if ($metadataOverride) {
$overrideMatches = $metadataOverride -match 'interop\.dat'
Write-Both ("[IL2CPP] GlobalMetadataPath = {0}" -f $metadataOverride)
if ($metadataOverride -match '\{GameRootPath\}|\{GameDataPath\}') {
# BepInEx joins this onto the game root; only the {GameDataPath} form is right.
if ($overrideMatches) {
Add-Verdict 'OK' 'BepInEx is configured to normalize metadata through the config setting'
} else {
Add-Verdict 'MISSING' 'GlobalMetadataPath points at the un-normalized global-metadata.dat'
}
} else {
Add-Verdict 'WARN' 'GlobalMetadataPath is set without a {GameDataPath} placeholder - confirm the path resolves'
}
} else {
$needsIt = $loaderExists -and -not $loaderPatched
if ($needsIt) {
Add-Verdict 'MISSING' 'no [IL2CPP] GlobalMetadataPath override in BepInEx/config/BepInEx.cfg - this is the fix for a stock loader'
} elseif (-not $loaderExists) {
Add-Verdict 'WARN' 'BepInEx/config/BepInEx.cfg has no GlobalMetadataPath (fine on the patched be.697 loader)'
} else {
Write-Both '[IL2CPP] GlobalMetadataPath: not set (not required: the loader is patched)'
}
}
Write-Both ''
# --- interop assemblies -------------------------------------------------------
Write-Both '== interop assemblies =='
if (Test-Path -LiteralPath $interopDir) {
$dlls = Get-ChildItem -LiteralPath $interopDir -Filter '*.dll' -File -ErrorAction SilentlyContinue
$count = @($dlls).Count
$hashFile = Join-Path $interopDir 'assembly-hash.txt'
$hashText = Get-TextIfExists $hashFile
Write-Both ("BepInEx/interop : {0} .dll files" -f $count)
if ($hashText) { Write-Both ("assembly-hash.txt : {0}{1}" -f $hashText.Trim(), $(if ($hashText.Trim() -eq $expected.InteropHash) { ' (matches the reference install)' } else { ' (different from the reference install - interop will be regenerated)' })) }
else { Write-Both 'assembly-hash.txt : missing' }
if ($count -ge $expected.InteropCount) {
Add-Verdict 'OK' ("interop assemblies are present ({0} .dll >= {1})" -f $count, $expected.InteropCount)
} elseif ($count -gt 0) {
Add-Verdict 'MISSING' ("only {0} interop assemblies (expected {1}) - a failed generation left the folder half-built" -f $count, $expected.InteropCount)
} else {
Add-Verdict 'MISSING' 'BepInEx/interop is empty - interop generation never completed'
}
} else {
Add-Verdict 'MISSING' 'BepInEx/interop does not exist - on a fresh start BepInEx tries to generate it, which needs the normalized metadata'
}
if (Test-Path -LiteralPath $unityLibs) {
$libCount = @(Get-ChildItem -LiteralPath $unityLibs -Filter '*.dll' -File).Count
Write-Both ("BepInEx/unity-libs : {0} .dll files" -f $libCount)
} else {
Add-Verdict 'WARN' 'BepInEx/unity-libs is missing - BepInEx will download it (needs internet) before generating interop'
}
Write-Both ''
# --- plugin and host pieces ---------------------------------------------------
Write-Both '== Radicade pieces =='
if (Test-Path -LiteralPath $plugin) {
$info = Get-Item -LiteralPath $plugin
$md5 = Get-Md5 $plugin
Write-Both ("RadicadePatch.dll: {0} b md5 {1}" -f $info.Length, $md5)
if ($md5 -eq $expected.PluginMd5) {
Add-Verdict 'OK' 'reference September client plugin'
} else {
Add-Verdict 'WARN' 'RadicadePatch.dll is a different build - fine if it is one you built yourself'
}
} else {
Add-Verdict 'MISSING' 'BepInEx/plugins/RadicadePatch.dll - nothing will patch the client'
}
if (Test-Path -LiteralPath $winhttp) {
$md5 = Get-Md5 $winhttp
if ($md5 -eq $expected.WinhttpMd5) { Add-Verdict 'OK' 'winhttp.dll is Doorstop 4.3.0 (reference)' }
else { Add-Verdict 'WARN' 'winhttp.dll is not the reference Doorstop - confirm nothing else owns the proxy dll' }
} else {
Add-Verdict 'MISSING' 'winhttp.dll - without it BepInEx never starts'
}
if (Test-Path -LiteralPath $doorstop) {
$text = Get-TextIfExists $doorstop
$target = if ($text -match '(?m)^\s*target_assembly\s*=\s*(.+?)\s*$') { $Matches[1] } else { '?' }
Write-Both ("doorstop target_assembly: {0}" -f $target)
if ($target -notmatch 'BepInEx\\core\\BepInEx\.Unity\.IL2CPP\.dll|BepInEx/core/BepInEx\.Unity\.IL2CPP\.dll') {
Add-Verdict 'MISSING' 'doorstop_config.ini does not target the IL2CPP chainloader'
}
} else {
Add-Verdict 'MISSING' 'doorstop_config.ini is missing'
}
Write-Both ''
# --- per-user client data -----------------------------------------------------
# %LOCALAPPDATA%Low\Against Gravity\Rec Room belongs to the Windows user, not to a game
# copy: every install on the PC shares it. A nameserver answer or a config cached there by
# another private server therefore outlives the folder it came from, which is how a client
# whose own metadata says one server starts asking a completely different one for its
# gameconfigs.
Write-Both '== per-user client data (shared by every install of this game) =='
if (Test-Path -LiteralPath $profileDir) {
Write-Both (" folder: {0}" -f $profileDir)
foreach ($name in @('Library', 'appstate.raw', 'Player.log', 'Player-prev.log')) {
$p = Join-Path $profileDir $name
if (Test-Path -LiteralPath $p) {
$info = Get-Item -LiteralPath $p
Write-Both (" {0,-16} {1,12} b last write {2}" -f $name, $info.Length, $info.LastWriteTime.ToString('yyyy-MM-dd HH:mm'))
}
}
foreach ($name in @('HTTPCache', 'Cookies', 'AnalyticsCache')) {
$p = Join-Path $profileDir $name
if (Test-Path -LiteralPath $p) {
$n = @(Get-ChildItem -LiteralPath $p -Recurse -File -ErrorAction SilentlyContinue).Count
Write-Both (" {0,-16} {1} file(s)" -f $name, $n)
}
}
$talkedTo = @{}
foreach ($name in @('Library', 'Player.log')) {
$text = Get-TextIfExists (Join-Path $profileDir $name)
if (-not $text) { continue }
foreach ($m in [regex]::Matches($text, 'https?://(?<host>[a-z0-9.-]+)')) {
$h = $m.Groups['host'].Value.ToLowerInvariant()
if ($talkedTo.ContainsKey($h)) { $talkedTo[$h]++ } else { $talkedTo[$h] = 1 }
}
}
if ($talkedTo.Count -gt 0) {
Write-Both ' hosts this profile has seen (cached responses and the last run):'
foreach ($h in @($talkedTo.GetEnumerator() |
ForEach-Object { [pscustomobject]@{ Host = $_.Key; Count = $_.Value } } |
Sort-Object -Property Count, Host -Descending |
Select-Object -First 25)) {
Write-Both (" {0,4}x {1}" -f $h.Count, $h.Host)
}
$stale = @($talkedTo.Keys | Where-Object { $_ -match 'recflare|viverec|trollroom|meownet' } | Sort-Object)
if ($stale.Count -gt 0) {
Write-Both (" note: this profile's cache still names another private server ({0}). Harmless while the client dials Radicade, but rename this folder before a fresh attempt so nothing stale can be replayed." -f ($stale -join ', '))
}
}
} else {
Write-Both (" {0} not found - nothing cached for this Windows user yet" -f $profileDir)
}
Write-Both ''
# --- what the last run said ---------------------------------------------------
Write-Both '== last BepInEx log =='
$logText = Get-TextIfExists $bepLog
if ($logText) {
$markers = [ordered]@{
'magic number check failed' = 'LibCpp2IL rejected the metadata header'
'Failed to generate Il2Cpp interop' = 'interop generation aborted'
'UnityEngine.CoreModule' = 'chainloader could not load Unity assemblies'
'Chainloader startup complete' = 'plugin loaded'
'Done!' = 'Il2CppInteropGen finished'
'TlsFatalAlert' = 'a TLS handshake was aborted (certificate refused)'
'user_canceled' = 'the embedded verifyer refused the certificate - host not on TLS:AllowedHosts'
'wantsToQuit answered false' = 'the anti-cheat shutdown was vetoed: the boot is stuck, it did not crash'
'Exception: Aborted' = 'the anti-cheat shutdown aborted the session'
'metadata was patched for' = 'the [Meta] warning fired (metadata and Connection:ServerUrl disagree)'
}
foreach ($k in $markers.Keys) {
$n = Get-MatchCount $logText ([regex]::Escape($k))
if ($n -gt 0) { Write-Both ("{0,4}x {1,-34} ({2})" -f $n, $k, $markers[$k]) }
}
if ((Get-MatchCount $logText 'magic number check failed') -gt 0 -and
(Get-MatchCount $logText 'Chainloader startup complete') -eq 0) {
Add-Verdict 'MISSING' 'the last run died in interop generation: BepInEx is pointed at the un-normalized metadata'
} elseif ((Get-MatchCount $logText 'Chainloader startup complete') -gt 0) {
Add-Verdict 'OK' 'the last run completed the chainloader'
}
# Reaching another private server is fatal, not cosmetic: nothing this client asks for
# exists there, so the boot never gets its configs and the anti-cheat path takes the
# session down.
$dialled = @()
foreach ($pat in @('recflare\.net', 'viverec\.com', 'trollroom', 'meownet')) {
if ((Get-MatchCount $logText $pat) -gt 0) { $dialled += ($pat -replace '\\', '') }
}
if ($dialled.Count -gt 0) {
Add-Verdict 'MISSING' ("the last run dialled another private server ({0}): this client's service bases are not Radicade's. Copy the bundle's Metadata folder over this install and rename %LOCALAPPDATA%Low\Against Gravity\Rec Room before the next launch." -f ($dialled -join ', '))
}
if ((Get-MatchCount $logText 'metadata was patched for') -gt 0) {
Add-Verdict 'WARN' 'the metadata marker and Connection:ServerUrl disagree - see the [Meta] line in the log'
}
$versionLine = ($logText -split "`n" | Where-Object { $_ -match 'BepInEx \d+\.\d+\.\d+-be\.\d+' } | Select-Object -First 1)
if ($versionLine) { Write-Both ("header: {0}" -f $versionLine.Trim()) }
} else {
Add-Verdict 'WARN' 'BepInEx/LogOutput.log not found - launch the game once so the log exists'
}
Write-Both ''
# --- verdict ------------------------------------------------------------------
Write-Both '== verdict =='
Write-Both ("missing: {0} warnings: {1}" -f $script:Failures, $script:Warnings)
if ($script:Failures -eq 0) {
Write-Both 'Every required piece is in place.'
} else {
Write-Both 'The MISSING lines above are the whole problem; fix them in that order.'
}
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, $_)
}
exit $(if ($script:Failures -gt 0) { 1 } else { 0 })