<# 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?://(?[^/?#]+)') 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 \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?://(?[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 })