mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
<#
|
||||
.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`""
|
||||
Reference in New Issue
Block a user