mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
609 lines
30 KiB
PowerShell
609 lines
30 KiB
PowerShell
<#
|
|
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 })
|