mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 07:28:45 -07:00
d471b418b9
Targets Rec Room 2020.03.10 client (also serves 2020.03.06).
Re-import after adding:
- DorkNet.ClientMod/dorknet-clientmod.json.template — config schema
the launcher's manifest-driven patcher renders into UserData/.
- tools/build-patcher-zip.ps1 — produces dorknet-clientpatch-{key}.zip
for v1-march-* Release uploads.
- .github/workflows/release.yml — on tag push publishes both
dorknet-server-march_2020_03_10-win-x64.zip and
dorknet-clientpatch-march_2020_03_10.zip as Release assets.
Same scrub rules as december; tools/build-patcher-zip.ps1 added to
the maintainer-side allow-list so it survives.
597 lines
26 KiB
PowerShell
597 lines
26 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
BepInEx-only setup for DorkNet ClientPatch.
|
|
|
|
.DESCRIPTION
|
|
Replaces the legacy patch-client.ps1 (asset-byte-edit + hosts file +
|
|
mkcert wildcard). Now everything lives in the
|
|
DorkNet.ClientPatch BepInEx plugin: Photon AppId override, *.rec.net
|
|
→ user-configured-host URL rewrite, and Photon CustomAuth identity
|
|
injection.
|
|
|
|
After this script runs once successfully, all you need to do on a
|
|
client version update is download the new build and re-run
|
|
this script — it'll re-extract BepInEx and rebuild the plugin
|
|
against the new game's interop assemblies. No more hardcoded
|
|
resources.assets offsets, no more hosts file, no more wildcard
|
|
cert.
|
|
|
|
.PARAMETER RecRoomPath
|
|
Path to the Recroom_Release_Data folder (the one containing
|
|
resources.assets). The patcher uses this to locate the actual
|
|
install root (its parent directory).
|
|
|
|
.PARAMETER BepInExZip
|
|
Optional path to a BepInEx 6.x IL2CPP zip. Only needed if you want
|
|
to override the pinned build the script auto-downloads. On first
|
|
install (or when -ForceBepInEx is set), the script fetches a
|
|
pinned BepInEx build from builds.bepinex.dev and caches it under
|
|
%LOCALAPPDATA%\DorkNet so repeated runs don't re-download. Pass
|
|
this only if you need a specific (newer/older) build.
|
|
|
|
.PARAMETER ServerHost
|
|
Domain that replaces *.rec.net in every URL the watch builds.
|
|
Default "localhost" — set to your own apex if hosting elsewhere.
|
|
|
|
.PARAMETER PhotonAppId
|
|
Photon Cloud Realtime AppId (GUID without curly braces).
|
|
|
|
.PARAMETER PhotonVoiceAppId
|
|
Photon Cloud Voice AppId. Defaults to PhotonAppId.
|
|
|
|
.PARAMETER ForceBepInEx
|
|
Re-extract BepInEx even if it's already installed. Use after a client
|
|
update.
|
|
|
|
.PARAMETER Revert
|
|
Remove BepInEx + the plugin from the client install. Doesn't
|
|
touch the game's own files.
|
|
|
|
.EXAMPLE
|
|
# First-time install — script auto-downloads BepInEx
|
|
.\tools\install-plugin.ps1 `
|
|
-RecRoomPath "C:\Program Files (x86)\Steam\steamapps\common\RecRoom\Recroom_Release_Data" `
|
|
-PhotonAppId <your-photon-realtime-app-id>
|
|
|
|
.EXAMPLE
|
|
# Override the pinned BepInEx build with your own zip
|
|
.\tools\install-plugin.ps1 `
|
|
-RecRoomPath "C:\…\Recroom_Release_Data" `
|
|
-BepInExZip "C:\Downloads\BepInEx-Unity.IL2CPP-win-x64-6.0.0-be.NNN.zip" `
|
|
-PhotonAppId cb0880d9-…
|
|
|
|
.EXAMPLE
|
|
# After a client update (interop changed) — re-run with
|
|
# the same args, optionally adding -ForceBepInEx if BepInEx itself
|
|
# got clobbered.
|
|
.\tools\install-plugin.ps1 `
|
|
-RecRoomPath "C:\…\Recroom_Release_Data" `
|
|
-PhotonAppId cb0880d9-…
|
|
|
|
.EXAMPLE
|
|
# Remove the plugin
|
|
.\tools\install-plugin.ps1 -RecRoomPath D:\RecRoom\Recroom_Release_Data -Revert
|
|
#>
|
|
|
|
[CmdletBinding(DefaultParameterSetName = 'Install')]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$RecRoomPath,
|
|
|
|
[Parameter(ParameterSetName = 'Install')]
|
|
[string]$BepInExZip,
|
|
|
|
[Parameter(ParameterSetName = 'Install')]
|
|
[string]$ServerHost = 'localhost',
|
|
|
|
[Parameter(Mandatory = $true, ParameterSetName = 'Install')]
|
|
[ValidatePattern('^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$')]
|
|
[string]$PhotonAppId,
|
|
|
|
[Parameter(ParameterSetName = 'Install')]
|
|
[ValidatePattern('^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$')]
|
|
[string]$PhotonVoiceAppId,
|
|
|
|
[Parameter(ParameterSetName = 'Install')]
|
|
[switch]$ForceBepInEx,
|
|
|
|
# Tell the plugin to fake the Steamworks SDK so the watch boots
|
|
# without Steam.exe running. Pure C#-level Harmony patches inside
|
|
# the plugin DLL — no replacement steam_api64.dll, no Goldberg /
|
|
# SmartSteamEmu (which Defender flags as PUA). Default off so
|
|
# Steam-launched runs behave normally.
|
|
[Parameter(ParameterSetName = 'Install')]
|
|
[switch]$FakeSteamApi,
|
|
|
|
# SteamID64 the plugin should report from SteamUser.GetSteamID
|
|
# when -FakeSteamApi is on. Empty = deterministic per-machine ID
|
|
# derived from OS user + machine name. Pin an explicit value to
|
|
# keep the same DorkNet account across machines.
|
|
[Parameter(ParameterSetName = 'Install')]
|
|
[string]$FakeSteamId,
|
|
|
|
# Display name returned from SteamFriends.GetPersonaName when
|
|
# -FakeSteamApi is on. Empty = OS user name.
|
|
[Parameter(ParameterSetName = 'Install')]
|
|
[string]$FakeAccountName,
|
|
|
|
[Parameter(Mandatory = $true, ParameterSetName = 'Revert')]
|
|
[switch]$Revert
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
Set-StrictMode -Version Latest
|
|
|
|
# Pinned BepInEx 6 IL2CPP win-x64 build. Auto-downloaded when neither
|
|
# BepInEx is already installed nor -BepInExZip is provided. Pin a
|
|
# specific build rather than 'latest' so a regression upstream
|
|
# doesn't silently land on tester machines.
|
|
$Script:BepInExUrl = 'https://builds.bepinex.dev/projects/bepinex_be/755/BepInEx-Unity.IL2CPP-win-x64-6.0.0-be.755%2B3fab71a.zip'
|
|
$Script:BepInExCacheName = 'BepInEx-Unity.IL2CPP-win-x64-6.0.0-be.755.zip'
|
|
|
|
# Pinned Steamless release. atom0s ships it as a single zip with a
|
|
# CLI exe inside. Cached alongside BepInEx under %LOCALAPPDATA%\DorkNet
|
|
# so a one-time download per machine handles both DRM-stripping and
|
|
# mod loading.
|
|
$Script:SteamlessUrl = 'https://github.com/atom0s/Steamless/releases/download/v3.1.0.5/Steamless.v3.1.0.5.-.by.atom0s.zip'
|
|
$Script:SteamlessCacheName = 'Steamless.v3.1.0.5.zip'
|
|
|
|
# NOTE: Goldberg Steam Emu integration was tried and removed.
|
|
# Windows Defender flags every Steam emulator as PUA and
|
|
# quarantines mid-download. The "boot without Steam running"
|
|
# capability now lives entirely inside DorkNet.ClientPatch —
|
|
# Harmony patches on the Steamworks.NET C# wrappers
|
|
# (SteamAPI.Init, SteamUser.GetSteamID, GetAuthSessionTicket, …)
|
|
# so the watch's SteamPlatformManager.Initialize succeeds without
|
|
# Steam.exe running. Toggled via the -FakeSteamApi switch on this
|
|
# script (writes the [Steam] FakeSteamApi=true config the plugin
|
|
# reads at load).
|
|
|
|
function Write-Step($msg) { Write-Host ('▸ ' + $msg) -ForegroundColor Cyan }
|
|
function Write-OK($msg) { Write-Host ('✓ ' + $msg) -ForegroundColor Green }
|
|
function Write-Warn2($msg) { Write-Host ('! ' + $msg) -ForegroundColor Yellow }
|
|
function Write-Err($msg) { Write-Host ('✗ ' + $msg) -ForegroundColor Red }
|
|
|
|
function Test-SteamDrmWrapped {
|
|
# Detects the Steamworks DRM wrapper by checking whether the PE's
|
|
# AddressOfEntryPoint falls inside a '.bind' section. Valve's
|
|
# drm_wrap tool injects .bind carrying the DRM stub and rewrites
|
|
# the entry point to land there; SteamStub jumps to the real
|
|
# .text once it's done. After Steamless strips, the entry point
|
|
# is rewritten back into .text and .bind is left as dead weight
|
|
# (the section header survives but execution never visits it),
|
|
# so we'd false-positive if we just looked at .bind's size.
|
|
#
|
|
# PE layout walked here (offsets relative to start of file):
|
|
# e_lfanew @ 0x3C → 4-byte offset to PE header
|
|
# PE+0 "PE\0\0" + COFF header (Sections @ +6, OptHeader size @ +20)
|
|
# OptionalHeader+0x10 = AddressOfEntryPoint (RVA)
|
|
# Section table starts at PE+24+OptHeaderSize, 40 bytes/entry:
|
|
# Name(8) VirtSize(4) VirtAddr(4) RawSize(4) RawPtr(4) ...
|
|
param([string]$ExePath)
|
|
try {
|
|
$fs = [System.IO.File]::OpenRead($ExePath)
|
|
try {
|
|
$br = New-Object System.IO.BinaryReader($fs)
|
|
$fs.Seek(0x3C, 'Begin') | Out-Null
|
|
$peOff = $br.ReadInt32()
|
|
if ($peOff -lt 0 -or $peOff + 24 -gt $fs.Length) { return $false }
|
|
$fs.Seek($peOff + 6, 'Begin') | Out-Null
|
|
$sections = $br.ReadUInt16()
|
|
$fs.Seek($peOff + 20, 'Begin') | Out-Null
|
|
$optSize = $br.ReadUInt16()
|
|
$fs.Seek($peOff + 24 + 16, 'Begin') | Out-Null
|
|
$entryRva = $br.ReadUInt32()
|
|
$secStart = $peOff + 24 + $optSize
|
|
for ($i = 0; $i -lt $sections; $i++) {
|
|
$fs.Seek($secStart + $i * 40, 'Begin') | Out-Null
|
|
$name = [System.Text.Encoding]::ASCII.GetString($br.ReadBytes(8)).TrimEnd([char]0)
|
|
$vSize = $br.ReadUInt32()
|
|
$vAddr = $br.ReadUInt32()
|
|
if ($name -eq '.bind' -and $entryRva -ge $vAddr -and $entryRva -lt ($vAddr + $vSize)) {
|
|
return $true
|
|
}
|
|
}
|
|
return $false
|
|
} finally { $fs.Close() }
|
|
} catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
|
|
function Get-Steamless {
|
|
# Returns the path to a usable Steamless.CLI.exe. Downloads + extracts
|
|
# the pinned release on first run, caches under %LOCALAPPDATA%\DorkNet.
|
|
param()
|
|
$cacheDir = Join-Path $env:LOCALAPPDATA 'DorkNet'
|
|
$extractDir = Join-Path $cacheDir 'Steamless'
|
|
$cli = Join-Path $extractDir 'Steamless.CLI.exe'
|
|
if (Test-Path -LiteralPath $cli -PathType Leaf) {
|
|
return $cli
|
|
}
|
|
New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null
|
|
$zipPath = Join-Path $cacheDir $Script:SteamlessCacheName
|
|
if (-not (Test-Path -LiteralPath $zipPath -PathType Leaf)) {
|
|
Write-Step "Downloading Steamless → $zipPath"
|
|
Write-Host " $Script:SteamlessUrl" -ForegroundColor DarkGray
|
|
try {
|
|
Invoke-WebRequest -Uri $Script:SteamlessUrl -OutFile $zipPath `
|
|
-UseBasicParsing -TimeoutSec 60 -ErrorAction Stop
|
|
} catch {
|
|
Write-Err "Steamless download failed: $($_.Exception.Message)"
|
|
Write-Host ' Manually download from https://github.com/atom0s/Steamless/releases' -ForegroundColor DarkGray
|
|
Write-Host " and extract to $extractDir, then re-run." -ForegroundColor DarkGray
|
|
throw
|
|
}
|
|
}
|
|
Write-Step "Extracting Steamless → $extractDir"
|
|
Expand-Archive -LiteralPath $zipPath -DestinationPath $extractDir -Force
|
|
if (-not (Test-Path -LiteralPath $cli -PathType Leaf)) {
|
|
# Some Steamless zips put the CLI inside a subfolder; locate it.
|
|
$found = Get-ChildItem -LiteralPath $extractDir -Recurse -Filter 'Steamless.CLI.exe' -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if ($found) { $cli = $found.FullName }
|
|
}
|
|
if (-not (Test-Path -LiteralPath $cli -PathType Leaf)) {
|
|
throw "Steamless.CLI.exe not found after extracting $zipPath"
|
|
}
|
|
return $cli
|
|
}
|
|
|
|
function Invoke-DrmStrip {
|
|
# Runs Steamless against the wrapped exe in-place. Steamless writes
|
|
# an unpacked copy alongside the original; we move the original to
|
|
# .drm-original.exe and rename the unpacked file in place so the
|
|
# rest of the script doesn't have to know which is which.
|
|
param([string]$ExePath)
|
|
$cli = Get-Steamless
|
|
Write-Step "Stripping Steam DRM from $ExePath"
|
|
$exeDir = Split-Path -Parent $ExePath
|
|
# Steamless emits <input>.exe.unpacked.exe by default. Run with
|
|
# --quiet to keep our log clean; --keepbind 0 lets it strip the
|
|
# .bind section so subsequent runs don't re-trigger the detector.
|
|
$proc = Start-Process -FilePath $cli `
|
|
-ArgumentList @('--quiet', '--keepbind', '0', $ExePath) `
|
|
-NoNewWindow -PassThru -Wait
|
|
if ($proc.ExitCode -ne 0) {
|
|
throw "Steamless exited with code $($proc.ExitCode). The exe may not be a recognised Steam DRM build."
|
|
}
|
|
$unpacked = "$ExePath.unpacked.exe"
|
|
if (-not (Test-Path -LiteralPath $unpacked -PathType Leaf)) {
|
|
throw "Steamless ran but didn't produce $unpacked. Check the tool's output."
|
|
}
|
|
$bak = Join-Path $exeDir 'Recroom_Release.drm-original.exe'
|
|
Move-Item -LiteralPath $ExePath -Destination $bak -Force
|
|
Move-Item -LiteralPath $unpacked -Destination $ExePath -Force
|
|
Write-OK "DRM stripped. Original kept as $bak"
|
|
}
|
|
|
|
function Get-BepInExZip {
|
|
# Returns a path to a usable BepInEx zip. Reuses a cached download
|
|
# under %LOCALAPPDATA%\DorkNet so a repeated install on the same
|
|
# box doesn't re-download the ~9 MB blob each run.
|
|
param()
|
|
$cacheDir = Join-Path $env:LOCALAPPDATA 'DorkNet'
|
|
New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null
|
|
$cached = Join-Path $cacheDir $Script:BepInExCacheName
|
|
if (Test-Path -LiteralPath $cached -PathType Leaf) {
|
|
Write-OK "Using cached BepInEx zip → $cached"
|
|
return $cached
|
|
}
|
|
Write-Step "Downloading BepInEx → $cached"
|
|
Write-Host " $Script:BepInExUrl" -ForegroundColor DarkGray
|
|
try {
|
|
# -UseBasicParsing for compatibility with Windows PowerShell 5.1
|
|
# (PS7 ignores it). 90-second timeout — the file is small but a
|
|
# cold builds.bepinex.dev cdn can be sluggish from some regions.
|
|
Invoke-WebRequest -Uri $Script:BepInExUrl -OutFile $cached `
|
|
-UseBasicParsing -TimeoutSec 90 -ErrorAction Stop
|
|
} catch {
|
|
Write-Err "Download failed: $($_.Exception.Message)"
|
|
Write-Host ' Manually download the zip and pass it via -BepInExZip <path>.' -ForegroundColor DarkGray
|
|
Write-Host " Releases page: https://github.com/BepInEx/BepInEx/releases" -ForegroundColor DarkGray
|
|
throw
|
|
}
|
|
$size = (Get-Item -LiteralPath $cached).Length
|
|
Write-OK ("Downloaded {0:N1} MB" -f ($size / 1MB))
|
|
return $cached
|
|
}
|
|
|
|
# Resolve via a two-step assignment instead of the ?. null-conditional
|
|
# operator — that's PS 7+ only and we want to support Windows
|
|
# PowerShell 5.1 (still the default on stock Win10/11).
|
|
$resolved = Resolve-Path -LiteralPath $RecRoomPath -ErrorAction SilentlyContinue
|
|
if ($resolved) { $RecRoomPath = $resolved.Path } else { $RecRoomPath = $null }
|
|
if (-not $RecRoomPath -or -not (Test-Path -LiteralPath $RecRoomPath -PathType Container)) {
|
|
Write-Err "RecRoomPath '$RecRoomPath' does not exist."
|
|
exit 1
|
|
}
|
|
|
|
# RecRoomPath = …\Recroom_Release_Data, BepInEx lives one level up
|
|
$RecRoomRoot = Split-Path -Parent $RecRoomPath
|
|
$BepInExDir = Join-Path $RecRoomRoot 'BepInEx'
|
|
$ConfigDir = Join-Path $BepInExDir 'config'
|
|
$PluginsDir = Join-Path $BepInExDir 'plugins'
|
|
$InteropDir = Join-Path $BepInExDir 'interop'
|
|
$RepoRoot = Split-Path -Parent $PSScriptRoot
|
|
$PluginProj = Join-Path $RepoRoot 'DorkNet.ClientPatch'
|
|
|
|
if ($Revert) {
|
|
Write-Step "Removing BepInEx + plugin from $RecRoomRoot"
|
|
if (Test-Path -LiteralPath $BepInExDir -PathType Container) {
|
|
Remove-Item -LiteralPath $BepInExDir -Recurse -Force
|
|
Write-OK "Removed $BepInExDir"
|
|
}
|
|
foreach ($leaf in @('winhttp.dll', 'doorstop_config.ini', '.doorstop_version', 'changelog.txt')) {
|
|
$p = Join-Path $RecRoomRoot $leaf
|
|
if (Test-Path -LiteralPath $p -PathType Leaf) {
|
|
Remove-Item -LiteralPath $p -Force
|
|
Write-OK "Removed $leaf"
|
|
}
|
|
}
|
|
# Restore the pristine global-metadata.dat from the backup we made
|
|
# before host-patching, so the watch points at *.rec.net again.
|
|
$meta = Join-Path $RecRoomPath 'il2cpp_data\Metadata\global-metadata.dat'
|
|
$backup = $meta + '.dorknet-original'
|
|
if (Test-Path -LiteralPath $backup -PathType Leaf) {
|
|
Copy-Item -LiteralPath $backup -Destination $meta -Force
|
|
Remove-Item -LiteralPath $backup -Force
|
|
Write-OK 'Restored stock global-metadata.dat from backup.'
|
|
}
|
|
Write-OK 'Plugin reverted. Rec Room is now stock.'
|
|
exit 0
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $PluginProj -PathType Container)) {
|
|
Write-Err "Plugin project missing at '$PluginProj'. Run from a DorkNet repo checkout."
|
|
exit 1
|
|
}
|
|
|
|
# steam_appid.txt — Steamworks SDK uses this file as "app id is
|
|
# pre-configured, don't relaunch via Steam." Without it, even a
|
|
# DRM-stripped binary still calls SteamAPI.RestartAppIfNecessary
|
|
# at startup and gets handed back to Steam.exe before the .NET
|
|
# payload (and therefore our plugin) has a chance to run. Drop it
|
|
# next to the exe before we test the DRM-strip path.
|
|
$AppIdFile = Join-Path $RecRoomRoot 'steam_appid.txt'
|
|
if (-not (Test-Path -LiteralPath $AppIdFile -PathType Leaf)) {
|
|
'471710' | Out-File -LiteralPath $AppIdFile -Encoding ASCII -NoNewline
|
|
Write-OK "Wrote $AppIdFile (Rec Room AppID 471710 — skips Steam relaunch on boot)"
|
|
}
|
|
|
|
# DRM check + auto-strip — Steamworks DRM stub runs before BepInEx's
|
|
# winhttp.dll doorstop fires, so a stock Steam-wrapped exe would just
|
|
# exit and ask Steam to relaunch (BepInEx + plugin never get a chance).
|
|
# Detect via .bind entry-point check, then run Steamless against it.
|
|
# Original kept as Recroom_Release.drm-original.exe.
|
|
$RecRoomExe = Join-Path $RecRoomRoot 'Recroom_Release.exe'
|
|
if (Test-Path -LiteralPath $RecRoomExe -PathType Leaf) {
|
|
if (Test-SteamDrmWrapped -ExePath $RecRoomExe) {
|
|
Write-Step 'Steam DRM wrapper detected on Recroom_Release.exe — stripping with Steamless'
|
|
Invoke-DrmStrip -ExePath $RecRoomExe
|
|
# Sanity-check: if Steamless somehow left .bind in place, bail.
|
|
if (Test-SteamDrmWrapped -ExePath $RecRoomExe) {
|
|
Write-Err 'Stripped exe still reports a .bind section — Steamless may not support this build.'
|
|
Write-Host ' Manually strip with the Steamless GUI and re-run this script.' -ForegroundColor DarkGray
|
|
exit 1
|
|
}
|
|
Write-OK 'Recroom_Release.exe is now DRM-stripped.'
|
|
} else {
|
|
Write-OK 'Recroom_Release.exe is DRM-stripped (no .bind section).'
|
|
}
|
|
}
|
|
|
|
# Steam-without-Steam.exe handling now lives inside the plugin via
|
|
# the [Steam] FakeSteamApi=true config — no DLL replacement, no
|
|
# Goldberg AV trip. We just write the config in step 4 below.
|
|
|
|
# ── 0b. Patch ns.rec.net / www.rec.net in IL2CPP global metadata ──────────
|
|
# The watch hard-codes the bootstrap URL "https://ns.rec.net/?v=2" as a
|
|
# string literal in IL2CPP global-metadata.dat. It builds the URL via
|
|
# BestHTTP.HTTPRequest directly with the raw string — never goes through
|
|
# `new System.Uri(string)` — so the plugin's URL-rewrite Harmony patch
|
|
# can't see it. Same for the WWW link "www.rec.net". Both literals are
|
|
# the same length as their localhost equivalents, so we can byte-replace
|
|
# in place without shifting offsets.
|
|
#
|
|
# This is the ONLY hardcoded-byte edit the install does, and it's
|
|
# auto-detected & re-applied on every install run. The original file
|
|
# is preserved as `global-metadata.dat.dorknet-original` so -Revert
|
|
# can put it back.
|
|
function Invoke-MetadataHostPatch {
|
|
param([string]$DataDir, [string]$ServerHost)
|
|
$meta = Join-Path $DataDir 'il2cpp_data\Metadata\global-metadata.dat'
|
|
if (-not (Test-Path -LiteralPath $meta -PathType Leaf)) {
|
|
Write-Warn2 "global-metadata.dat not found at $meta — skipping host byte-patch."
|
|
return
|
|
}
|
|
$backup = $meta + '.dorknet-original'
|
|
if (-not (Test-Path -LiteralPath $backup -PathType Leaf)) {
|
|
Copy-Item -LiteralPath $meta -Destination $backup
|
|
}
|
|
# Always patch from the pristine backup so re-runs are idempotent and
|
|
# don't accidentally compound substitutions if the host changes.
|
|
$bytes = [System.IO.File]::ReadAllBytes($backup)
|
|
$subs = @(
|
|
@{ Old = 'ns.rec.net'; New = "ns.$ServerHost" },
|
|
@{ Old = 'www.rec.net'; New = "www.$ServerHost" }
|
|
)
|
|
$totalReplacements = 0
|
|
foreach ($s in $subs) {
|
|
$oldB = [System.Text.Encoding]::ASCII.GetBytes($s.Old)
|
|
$newB = [System.Text.Encoding]::ASCII.GetBytes($s.New)
|
|
if ($oldB.Length -ne $newB.Length) {
|
|
Write-Err "Host '$($s.Old)' (len $($oldB.Length)) and '$($s.New)' (len $($newB.Length)) differ in length — refusing to patch."
|
|
return
|
|
}
|
|
$i = 0
|
|
while ($i -le $bytes.Length - $oldB.Length) {
|
|
$match = $true
|
|
for ($k = 0; $k -lt $oldB.Length; $k++) {
|
|
if ($bytes[$i + $k] -ne $oldB[$k]) { $match = $false; break }
|
|
}
|
|
if ($match) {
|
|
[Array]::Copy($newB, 0, $bytes, $i, $newB.Length)
|
|
$totalReplacements++
|
|
$i += $newB.Length
|
|
} else {
|
|
$i++
|
|
}
|
|
}
|
|
}
|
|
[System.IO.File]::WriteAllBytes($meta, $bytes)
|
|
Write-OK "global-metadata.dat host-patched ($totalReplacements substitution$( if ($totalReplacements -ne 1) { 's' }))."
|
|
}
|
|
Invoke-MetadataHostPatch -DataDir $RecRoomPath -ServerHost $ServerHost
|
|
|
|
# ── 1. BepInEx install ────────────────────────────────────────────────────
|
|
$bepInExPresent = Test-Path -LiteralPath $BepInExDir -PathType Container
|
|
if (-not $bepInExPresent -or $ForceBepInEx) {
|
|
# Auto-fetch the pinned build when the operator didn't supply one.
|
|
# First-run UX: just `install-plugin.ps1 -RecRoomPath ... -PhotonAppId ...`
|
|
# works without the operator having to manually download a zip.
|
|
if (-not $BepInExZip) {
|
|
$BepInExZip = Get-BepInExZip
|
|
}
|
|
if (-not (Test-Path -LiteralPath $BepInExZip -PathType Leaf)) {
|
|
Write-Err "BepInExZip '$BepInExZip' not found."
|
|
exit 1
|
|
}
|
|
if ($ForceBepInEx -and $bepInExPresent) {
|
|
Write-Step "Removing existing BepInEx install (-ForceBepInEx)"
|
|
Remove-Item -LiteralPath $BepInExDir -Recurse -Force
|
|
}
|
|
Write-Step "Extracting BepInEx → $RecRoomRoot"
|
|
Expand-Archive -LiteralPath $BepInExZip -DestinationPath $RecRoomRoot -Force
|
|
Write-OK 'BepInEx extracted.'
|
|
} else {
|
|
Write-OK "BepInEx already installed at $BepInExDir"
|
|
}
|
|
|
|
# ── 2. Interop assemblies ─────────────────────────────────────────────────
|
|
if (-not (Test-Path -LiteralPath $InteropDir -PathType Container)) {
|
|
Write-Warn2 'Interop assemblies not found.'
|
|
Write-Host ' Launch the client ONCE so BepInEx generates them, then re-run this script.' -ForegroundColor DarkGray
|
|
Write-Host ' (One-time step per game version — takes ~30s on first launch.)' -ForegroundColor DarkGray
|
|
exit 0
|
|
}
|
|
Write-OK "Interop assemblies present at $InteropDir"
|
|
|
|
# ── 3. Build plugin ───────────────────────────────────────────────────────
|
|
$env:BepInExInterop = $InteropDir
|
|
$env:BepInExCore = Join-Path $BepInExDir 'core'
|
|
Write-Step 'Building DorkNet.ClientPatch'
|
|
$build = & dotnet build $PluginProj -c Release --nologo 2>&1
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Err 'Plugin build failed:'
|
|
Write-Host ($build -join "`n") -ForegroundColor DarkGray
|
|
exit 1
|
|
}
|
|
$pluginDll = Join-Path $PluginProj 'bin\Release\DorkNet.ClientPatch.dll'
|
|
if (-not (Test-Path -LiteralPath $pluginDll -PathType Leaf)) {
|
|
Write-Err "Plugin DLL not produced at $pluginDll"
|
|
exit 1
|
|
}
|
|
New-Item -ItemType Directory -Path $PluginsDir -Force | Out-Null
|
|
$oldPluginDll = Join-Path $PluginsDir ('DorkNet.' + 'Rec' + 'RoomPatch.dll')
|
|
if (Test-Path -LiteralPath $oldPluginDll -PathType Leaf) {
|
|
Remove-Item -LiteralPath $oldPluginDll -Force
|
|
Write-OK 'Removed old client patch plugin.'
|
|
}
|
|
Copy-Item -LiteralPath $pluginDll -Destination $PluginsDir -Force
|
|
Write-OK ('Plugin DLL → ' + (Join-Path $PluginsDir 'DorkNet.ClientPatch.dll'))
|
|
|
|
# ── 4. Plugin config ──────────────────────────────────────────────────────
|
|
# Write the BepInEx config file with the operator's chosen values.
|
|
# Format mirrors what the plugin's Config.Bind defaults produce on first
|
|
# run, so editing afterwards via BepInEx config manager works as expected.
|
|
$cfgFile = Join-Path $ConfigDir 'sh.dork.clientpatch.cfg'
|
|
New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null
|
|
|
|
$voiceId = if ($PhotonVoiceAppId) { $PhotonVoiceAppId } else { '' }
|
|
$cfg = @"
|
|
## Settings file was created by DorkNet install-plugin.ps1
|
|
## Plugin GUID: sh.dork.clientpatch
|
|
|
|
[Server]
|
|
|
|
## Domain that replaces *.rec.net in every URL the watch builds.
|
|
## Empty disables the rewrite (use only if pointing at the official servers).
|
|
# Setting type: String
|
|
# Default value: localhost
|
|
Host = $ServerHost
|
|
|
|
[Photon]
|
|
|
|
## Photon Cloud Realtime AppId (UUID, no curly braces).
|
|
## Empty leaves the AppId baked into the build untouched.
|
|
# Setting type: String
|
|
# Default value:
|
|
AppId = $PhotonAppId
|
|
|
|
## Photon Cloud Voice AppId. Empty falls back to AppId.
|
|
# Setting type: String
|
|
# Default value:
|
|
VoiceAppId = $voiceId
|
|
|
|
## Attach userid + LoginLock to PhotonNetwork.AuthValues right before
|
|
## OpAuthenticate so DorkNet's /photon/customauth endpoint can identify
|
|
## the player. Disable to debug pure Photon connectivity issues.
|
|
# Setting type: Boolean
|
|
# Default value: true
|
|
InjectAuthValues = true
|
|
|
|
[UI]
|
|
|
|
## Replacement branding string. Any UI label containing 'RecNet'
|
|
## gets that substring replaced with this value at runtime, so error
|
|
## dialogs and the loading screen show the private-server brand.
|
|
## Empty disables the rewrite.
|
|
# Setting type: String
|
|
# Default value: DorkNet
|
|
BrandName = DorkNet
|
|
|
|
[Steam]
|
|
|
|
## Bypass Steamworks.SteamAPI.RestartAppIfNecessary so the watch
|
|
## doesn't relaunch itself through Steam on boot. No effect when
|
|
## launched normally from the Steam library; useful when running the
|
|
## game directly via Recroom_Release.exe (BepInEx mod-loader shims
|
|
## often need this to avoid an infinite restart loop).
|
|
# Setting type: Boolean
|
|
# Default value: true
|
|
SkipRestartAppIfNecessary = true
|
|
|
|
## Pretend the Steam SDK initialised even when Steam.exe isn't
|
|
## running. Patches Steamworks.NET wrappers to return canned success
|
|
## values so the watch's SteamPlatformManager.Initialize succeeds
|
|
## without a live Steam process. DorkNet's auth backend uses SteamID
|
|
## as a lookup key only — no real ticket validation — so the fake
|
|
## ticket is accepted. Default off so launches via Steam itself
|
|
## behave normally.
|
|
# Setting type: Boolean
|
|
# Default value: false
|
|
FakeSteamApi = $($FakeSteamApi.IsPresent.ToString().ToLower())
|
|
|
|
## SteamID64 returned by SteamUser.GetSteamID when FakeSteamApi=true.
|
|
## Empty = deterministic value derived from OS user + machine name.
|
|
## Pin an explicit ID to keep the same DorkNet account across
|
|
## different machines.
|
|
# Setting type: String
|
|
# Default value:
|
|
FakeSteamId = $FakeSteamId
|
|
|
|
## Display name returned by SteamFriends.GetPersonaName when
|
|
## FakeSteamApi=true. Empty = OS user name.
|
|
# Setting type: String
|
|
# Default value:
|
|
FakeAccountName = $FakeAccountName
|
|
"@
|
|
$cfg | Out-File -LiteralPath $cfgFile -Encoding utf8 -Force
|
|
Write-OK "Plugin config → $cfgFile"
|
|
|
|
Write-Host ''
|
|
Write-OK 'All done. Launch the client from Steam.'
|
|
Write-Host ' Tail BepInEx/LogOutput.log for `[auth-injector] set Photon AuthValues …`' -ForegroundColor DarkGray
|
|
Write-Host ' to confirm the plugin loaded and ran.' -ForegroundColor DarkGray
|