mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
341 lines
13 KiB
PowerShell
341 lines
13 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Points the Rec Room Studio Unity editor at a Radicade server instead of Rec Room's
|
|
real nameserver.
|
|
|
|
.DESCRIPTION
|
|
Everything the Rec Room Studio editor talks to is discovered from ONE compiled-in
|
|
string: the nameserver base URL inside RecRoom.Studio.Editor.Core.dll
|
|
(shipped default: https://deluxe-backend-test.oldrecroom.com/nameserver).
|
|
The editor GETs that URL and derives every other service base (Auth, API, CDN, ...)
|
|
from the JSON it returns. There is no config file, env var or command-line override
|
|
for it, so pointing RRS at a private server means rewriting that one #US heap string.
|
|
|
|
This patch is done in place and in a size-preserving way:
|
|
* the .NET #US string heap is addressed by offset, and no other entry's offset
|
|
moves because the entry keeps its exact old byte footprint;
|
|
* the freed tail of the entry is zero-filled and is never referenced;
|
|
* the file length is unchanged.
|
|
A one-time backup (<dll>.radicade-orig) is kept, so -Restore is always possible.
|
|
|
|
The backup deliberately lives OUTSIDE the package folder, under
|
|
<ProjectRoot>\radicade-backups\..., because Unity treats everything under
|
|
Library\PackageCache as an immutable package folder and logs
|
|
"asset ... has no meta file, but it's in an immutable folder" for anything
|
|
there it cannot match to a package manifest.
|
|
|
|
The target project is probed for writability first: a Rec Room Studio project sitting
|
|
in a OneDrive folder (or covered by Controlled folder access) rejects every create and
|
|
overwrite, and Unity cannot run from such a tree at all. Copy the project somewhere
|
|
writable and keep working there.
|
|
|
|
.PARAMETER ProjectRoot
|
|
Candidate project roots, tried in order; the first one that exists, holds the studio
|
|
package cache and actually accepts writes is used.
|
|
|
|
.PARAMETER Url
|
|
Nameserver base URL the editor should use.
|
|
|
|
.PARAMETER DllPath
|
|
Patch this DLL directly instead of probing ProjectRoot.
|
|
|
|
.PARAMETER Restore
|
|
Put the backed-up DLL back and exit.
|
|
|
|
.EXAMPLE
|
|
pwsh -File tools/patch-rrs-nameserver.ps1
|
|
pwsh -File tools/patch-rrs-nameserver.ps1 -Url http://127.0.0.1:3031/nameserver
|
|
pwsh -File tools/patch-rrs-nameserver.ps1 -Restore
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[string[]] $ProjectRoot = @(
|
|
'C:\RecRoom\Documents\EggFiles\ProjectTemplate',
|
|
'C:\RecRoom\OneDrive\Documents\EggFiles\ProjectTemplate'
|
|
),
|
|
[string] $Url = 'https://localhost:3030/nameserver',
|
|
[string] $DllPath,
|
|
[switch] $Restore
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
Set-StrictMode -Version Latest
|
|
|
|
# Latin-1 gives a 1:1 byte<->char mapping, so IndexOf() returns byte offsets and the
|
|
# search stays native-fast instead of looping over ~330 KB in PowerShell.
|
|
$latin1 = [System.Text.Encoding]::GetEncoding(28591)
|
|
|
|
# Known shipped values of the literal. The first one is what this template ships; the
|
|
# list keeps the tool working if a future build bakes a different host.
|
|
$knownUrls = @(
|
|
'https://deluxe-backend-test.oldrecroom.com/nameserver'
|
|
)
|
|
|
|
function Write-Status {
|
|
param([string] $Level, [string] $Message)
|
|
Write-Host ("[{0}] {1}" -f $Level, $Message)
|
|
}
|
|
|
|
function Find-EditorCoreDll {
|
|
param([string] $Root, [switch] $Quiet)
|
|
|
|
$pkgRoot = Join-Path $Root 'Library\PackageCache'
|
|
if (-not (Test-Path -LiteralPath $pkgRoot)) {
|
|
if ($Quiet) { return $null }
|
|
throw "No PackageCache under '$Root' - open the project in Unity once so packages get extracted."
|
|
}
|
|
|
|
$candidates = Get-ChildItem -LiteralPath $pkgRoot -Directory -Filter 'com.recroom.studio.build@*' -ErrorAction SilentlyContinue |
|
|
ForEach-Object { Join-Path $_.FullName 'Editor\RecRoom.Studio.Editor.Core.dll' } |
|
|
Where-Object { Test-Path -LiteralPath $_ }
|
|
|
|
if (-not $candidates) {
|
|
if ($Quiet) { return $null }
|
|
throw "RecRoom.Studio.Editor.Core.dll not found under $pkgRoot (looked in com.recroom.studio.build@*)."
|
|
}
|
|
if (@($candidates).Count -gt 1) {
|
|
Write-Status 'WARN' "multiple studio.build packages present, using the first: $($candidates[0])"
|
|
}
|
|
return @($candidates)[0]
|
|
}
|
|
|
|
function Get-BackupPath {
|
|
<# Maps <ProjectRoot>\Library\PackageCache\<pkg>\...\X.dll to
|
|
<ProjectRoot>\radicade-backups\<pkg>\...\X.dll.radicade-orig, so the backup stays a
|
|
real project asset tree instead of polluting the immutable package folder. #>
|
|
param([string] $DllPath)
|
|
|
|
$marker = '\Library\PackageCache\'
|
|
$idx = $DllPath.IndexOf($marker, [System.StringComparison]::OrdinalIgnoreCase)
|
|
if ($idx -lt 0) {
|
|
# Not inside a package cache (a -DllPath into Assets, say): keep it as a sibling file.
|
|
return "$DllPath.radicade-orig"
|
|
}
|
|
|
|
$root = $DllPath.Substring(0, $idx)
|
|
$relative = $DllPath.Substring($idx + $marker.Length)
|
|
return (Join-Path $root (Join-Path 'radicade-backups' "$relative.radicade-orig"))
|
|
}
|
|
|
|
function Test-Writable {
|
|
param([string] $Directory)
|
|
|
|
$probe = Join-Path $Directory ('radicade-write-probe-' + [Guid]::NewGuid().ToString('N') + '.tmp')
|
|
try {
|
|
[System.IO.File]::WriteAllText($probe, 'probe')
|
|
Remove-Item -LiteralPath $probe -Force
|
|
return $true
|
|
} catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Resolve-Project {
|
|
param([string[]] $Candidates)
|
|
|
|
$notes = New-Object System.Collections.Generic.List[string]
|
|
|
|
foreach ($candidate in $Candidates) {
|
|
if (-not (Test-Path -LiteralPath $candidate)) {
|
|
$notes.Add("$candidate - does not exist")
|
|
continue
|
|
}
|
|
|
|
$dll = Find-EditorCoreDll -Root $candidate -Quiet
|
|
if (-not $dll) {
|
|
$notes.Add("$candidate - no com.recroom.studio.build package cache inside")
|
|
continue
|
|
}
|
|
|
|
if (-not (Test-Writable -Directory $candidate)) {
|
|
$notes.Add("$candidate - present, but the tree rejects writes")
|
|
continue
|
|
}
|
|
|
|
return $dll
|
|
}
|
|
|
|
$details = ($notes | ForEach-Object { " $_" }) -join [Environment]::NewLine
|
|
throw @"
|
|
No writable Rec Room Studio project found. Tried:
|
|
$details
|
|
|
|
A tree that rejects writes cannot hold this project: Unity has to create and overwrite
|
|
files in Library\ and Temp\ just to open it. Copy the project somewhere writable first
|
|
(always exclude the OneDrive copy from your Unity Hub list afterwards), for example:
|
|
|
|
robocopy "C:\RecRoom\OneDrive\Documents\EggFiles\ProjectTemplate" "C:\RecRoom\Documents\EggFiles\ProjectTemplate" /E /MT:16 /R:1 /W:1
|
|
|
|
then pass that root with -ProjectRoot, or just re-run this script.
|
|
"@
|
|
}
|
|
|
|
function Read-CompressedUInt {
|
|
param([byte[]] $Bytes, [int] $Offset)
|
|
$b0 = $Bytes[$Offset]
|
|
if (($b0 -band 0x80) -eq 0) { return @{ Value = [int]$b0; Size = 1 } }
|
|
if (($b0 -band 0xC0) -eq 0x80) {
|
|
return @{ Value = ((([int]$b0 -band 0x3F) -shl 8) -bor [int]$Bytes[$Offset + 1]); Size = 2 }
|
|
}
|
|
$v = (([int]$b0 -band 0x1F) -shl 24) -bor ([int]$Bytes[$Offset + 1] -shl 16) -bor `
|
|
([int]$Bytes[$Offset + 2] -shl 8) -bor [int]$Bytes[$Offset + 3]
|
|
return @{ Value = $v; Size = 4 }
|
|
}
|
|
|
|
function Write-CompressedUInt {
|
|
param([byte[]] $Bytes, [int] $Offset, [int] $Value)
|
|
if ($Value -lt 0x80) {
|
|
$Bytes[$Offset] = [byte]$Value
|
|
return 1
|
|
}
|
|
if ($Value -lt 0x4000) {
|
|
$Bytes[$Offset] = [byte](0x80 -bor ($Value -shr 8))
|
|
$Bytes[$Offset + 1] = [byte]($Value -band 0xFF)
|
|
return 2
|
|
}
|
|
$Bytes[$Offset] = [byte](0xC0 -bor ($Value -shr 24))
|
|
$Bytes[$Offset + 1] = [byte](($Value -shr 16) -band 0xFF)
|
|
$Bytes[$Offset + 2] = [byte](($Value -shr 8) -band 0xFF)
|
|
$Bytes[$Offset + 3] = [byte]($Value -band 0xFF)
|
|
return 4
|
|
}
|
|
|
|
function Get-NameserverEntry {
|
|
<# Locates the #US string heap entry whose payload is one of $Known or $Target.
|
|
Returns offset/prefix/payload geometry, or $null when the literal is absent. #>
|
|
param([string] $Path, [string[]] $Known, [string] $Target)
|
|
|
|
$bytes = [System.IO.File]::ReadAllBytes($Path)
|
|
$hay = $latin1.GetString($bytes)
|
|
|
|
foreach ($candidate in @($Known + @($Target))) {
|
|
$payload = [System.Text.Encoding]::Unicode.GetBytes($candidate)
|
|
$at = $hay.IndexOf($latin1.GetString($payload), [System.StringComparison]::Ordinal)
|
|
if ($at -lt 1) { continue }
|
|
|
|
for ($width = 1; $width -le 4; $width++) {
|
|
$prefixAt = $at - $width
|
|
if ($prefixAt -lt 0) { continue }
|
|
$prefix = Read-CompressedUInt -Bytes $bytes -Offset $prefixAt
|
|
if ($prefix.Size -ne $width) { continue }
|
|
if ($prefix.Value -ne (2 * $candidate.Length + 1)) { continue }
|
|
return [pscustomobject]@{
|
|
Bytes = $bytes
|
|
PayloadOffset = $at
|
|
PrefixOffset = $prefixAt
|
|
PrefixSize = $prefix.Size
|
|
PrefixValue = $prefix.Value
|
|
Original = $candidate
|
|
Footprint = $prefix.Size + 2 * $candidate.Length + 1
|
|
TerminalByte = $bytes[$at + 2 * $candidate.Length]
|
|
}
|
|
}
|
|
}
|
|
return $null
|
|
}
|
|
|
|
# ---- main -------------------------------------------------------------------------
|
|
|
|
Write-Host ''
|
|
|
|
if (-not $DllPath) {
|
|
$DllPath = Resolve-Project -Candidates $ProjectRoot
|
|
Write-Status 'INFO' 'project : writable, using the studio package cache below'
|
|
}
|
|
$backup = Get-BackupPath -DllPath $DllPath
|
|
|
|
Write-Status 'INFO' "studio editor core: $DllPath"
|
|
Write-Status 'INFO' "backup location : $backup"
|
|
|
|
if ($Restore) {
|
|
if (-not (Test-Path -LiteralPath $backup)) {
|
|
# Older runs kept the backup in the package folder; still honour one if it is all we have.
|
|
$legacy = "$DllPath.radicade-orig"
|
|
if (Test-Path -LiteralPath $legacy) {
|
|
Write-Status 'WARN' "using legacy in-package backup at $legacy"
|
|
$backup = $legacy
|
|
} else {
|
|
throw "No backup at '$backup' - nothing to restore."
|
|
}
|
|
}
|
|
try {
|
|
Copy-Item -LiteralPath $backup -Destination $DllPath -Force
|
|
} catch {
|
|
throw "Could not write to '$DllPath' - the project tree rejects writes. Copy the project somewhere writable and re-run with -ProjectRoot."
|
|
}
|
|
Write-Status 'PASS' 'restored the original DLL from backup'
|
|
exit 0
|
|
}
|
|
|
|
$entry = Get-NameserverEntry -Path $DllPath -Known $knownUrls -Target $Url
|
|
if (-not $entry) { throw "Nameserver literal not found in '$DllPath' (unexpected layout)." }
|
|
|
|
if ($entry.Original -eq $Url) {
|
|
Write-Status 'PASS' "already points at $Url"
|
|
exit 0
|
|
}
|
|
|
|
$newPayload = [System.Text.Encoding]::Unicode.GetBytes($Url)
|
|
$newValue = 2 * $Url.Length + 1
|
|
$free = $entry.Footprint - (1 + $newPayload.Length + 1)
|
|
if ($free -lt 0) {
|
|
throw "Replacement '$Url' ($($Url.Length) chars) does not fit in the original slot ($($entry.Original.Length) chars)."
|
|
}
|
|
|
|
try {
|
|
if (-not (Test-Path -LiteralPath $backup)) {
|
|
$backupDir = Split-Path -Parent $backup
|
|
if (-not (Test-Path -LiteralPath $backupDir)) {
|
|
New-Item -ItemType Directory -Path $backupDir -Force | Out-Null
|
|
}
|
|
Copy-Item -LiteralPath $DllPath -Destination $backup
|
|
Write-Status 'INFO' "backed up original to $backup"
|
|
}
|
|
} catch {
|
|
throw "Could not write to '$backup' - the project tree rejects writes. Copy the project somewhere writable and re-run with -ProjectRoot."
|
|
}
|
|
|
|
$bytes = [byte[]]::new($entry.Bytes.Length)
|
|
[Array]::Copy($entry.Bytes, $bytes, $entry.Bytes.Length)
|
|
|
|
# Rewrite prefix + payload + terminal byte, then zero the unused tail of the slot.
|
|
$cursor = $entry.PrefixOffset
|
|
$cursor += Write-CompressedUInt -Bytes $bytes -Offset $cursor -Value $newValue
|
|
[Array]::Copy($newPayload, 0, $bytes, $cursor, $newPayload.Length)
|
|
$cursor += $newPayload.Length
|
|
$bytes[$cursor] = $entry.TerminalByte
|
|
$cursor++
|
|
for ($i = $cursor; $i -lt ($entry.PrefixOffset + $entry.Footprint); $i++) { $bytes[$i] = 0 }
|
|
|
|
try {
|
|
[System.IO.File]::WriteAllBytes($DllPath, $bytes)
|
|
} catch {
|
|
throw "Could not write to '$DllPath' - the project tree rejects writes. Copy the project somewhere writable and re-run with -ProjectRoot."
|
|
}
|
|
|
|
# ---- verify -----------------------------------------------------------------------
|
|
$check = Get-NameserverEntry -Path $DllPath -Known $knownUrls -Target $Url
|
|
if (-not $check -or $check.Original -ne $Url) {
|
|
throw "Verification failed: heap entry does not decode to '$Url' after patching."
|
|
}
|
|
if ([System.IO.File]::ReadAllBytes($DllPath).Length -ne $entry.Bytes.Length) {
|
|
throw 'Verification failed: file length changed.'
|
|
}
|
|
|
|
# Every other heap entry must keep its offset, so compare everything outside the slot.
|
|
$after = [System.IO.File]::ReadAllBytes($DllPath)
|
|
$slotEnd = $entry.PrefixOffset + $entry.Footprint
|
|
$diffOutsideSlot = 0
|
|
for ($i = 0; $i -lt $after.Length; $i++) {
|
|
if ($i -ge $entry.PrefixOffset -and $i -lt $slotEnd) { continue }
|
|
if ($after[$i] -ne $entry.Bytes[$i]) { $diffOutsideSlot++ }
|
|
}
|
|
if ($diffOutsideSlot -ne 0) {
|
|
throw "Verification failed: $diffOutsideSlot bytes changed outside the string-heap slot."
|
|
}
|
|
|
|
Write-Status 'PASS' "nameserver: $($entry.Original) -> $Url"
|
|
Write-Status 'INFO' "slot $($entry.PrefixOffset), $($entry.Footprint) bytes, $free bytes of padding; file size unchanged"
|
|
Write-Status 'INFO' "open this copy from Unity Hub: $(Split-Path -Parent (Split-Path -Parent (Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $DllPath)))))"
|
|
Write-Status 'INFO' "undo with: pwsh -File tools/patch-rrs-nameserver.ps1 -Restore -DllPath `"$DllPath`""
|