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.
8.0 KiB
DorkNet ClientPatch BepInEx Plugin
Single client-side artefact that does everything DorkNet needs to patch into the 2020 watch:
- Rewrites every
*.rec.netURL the watch builds → user-configured replacement host (defaultlocalhost). Catches the bootstraphttps://ns.rec.net/?v=2; every other service URL flows in from the nameserver response which the DorkNet server controls. No more hosts file, no more wildcard mkcert (Cloudflare Tunnel terminates real TLS at the edge of the configured host). - Overrides Photon Cloud
AppId/VoiceAppIdright beforePhotonNetwork.ConnectUsingSettings. No more byte-edits toresources.assets, no more hardcoded offsets to chase across game versions. - Attaches
userid+LoginLocktoPhotonNetwork.AuthValuesright before any authenticate op leaves the wire, so DorkNet's/photon/customauthendpoint can identify the player and enforce single-session.
Configuration lives in BepInEx/config/sh.dork.clientpatch.cfg. Edit
values, restart the client, patches re-apply with the new settings.
Quickstart (recommended)
Use the wrapper script — handles BepInEx install, interop generation, plugin build, and config drop in one go:
.\tools\install-plugin.ps1 `
-RecRoomPath "C:\Program Files (x86)\Steam\steamapps\common\RecRoom\Recroom_Release_Data" `
-PhotonAppId <your-photon-realtime-app-id>
The script auto-downloads a pinned BepInEx 6 IL2CPP build from
builds.bepinex.dev on first run and caches it at
%LOCALAPPDATA%\DorkNet\BepInEx-Unity.IL2CPP-win-x64-6.0.0-be.696.zip
so re-runs on the same machine don't re-download. To override the
pinned build (newer/older), pass -BepInExZip <path>.
After a client update, just re-run with the same args. The script re-builds the plugin against the new game's interop assemblies.
To remove: .\tools\install-plugin.ps1 -RecRoomPath ... -Revert.
The legacy patch-client.ps1 (asset-byte-edit + hosts file + mkcert
wildcard) is kept for backwards compat — see the file's header for
when you'd still want it.
Manual install (if you don't want to use install-plugin.ps1)
Same steps, just done by hand. Useful for understanding the layout.
Why this exists
The vanilla 2020 client never sets PhotonNetwork.AuthValues — verified
by an exhaustive grep across both Assembly-CSharp.dll and the Photon
Assembly-CSharp-firstpass.dll (no callers of
PhotonNetwork.AuthValues =, new AuthenticationValues(...),
AddAuthParameter(...), or SetAuthPostData(...)). Photon Cloud
therefore assigns a random per-connection UserId and our
/photon/customauth callback receives no client identity, so we can't
reject duplicate sessions.
This plugin patches NetworkingPeer.OpAuthenticate /
OpAuthenticateOnce (the single chokepoint right before Photon's
authenticate op leaves the wire) and attaches:
AuthType = CustomAuthenticationType.CustomUserId = <RecNet account id>AuthGetParameters: userid=<id>, LoginLock=<token>
That makes the Photon Cloud {userid} / {LoginLock} placeholders
populate, our endpoint validates LoginLock against
PlayerPresenceService (Redis), and Photon's own UserId-collision
detection kicks duplicate connections automatically.
Prerequisites
- 2020 client build (Steam build
20200306, AppID471710). - BepInEx 6.x IL2CPP — not the regular Mono build. Download from
https://github.com/BepInEx/BepInEx/releases (look for the
BepInEx_unix_x86_64_6.0.0-be.696_*.zipline, Windows variant). - .NET 6 SDK on the build machine (this project uses
net6.0).
One-time Setup
-
Install BepInEx 6 IL2CPP into the client directory. Extract the release zip into the folder containing
Recroom_Release.exe. After extraction the layout looks like:<RecRoomPath>/ Recroom_Release.exe BepInEx/ core/ config/ plugins/ ← we drop our DLL here dotnet/ ← BepInEx-shipped runtime winhttp.dll ← BepInEx loader doorstop_config.ini -
Generate Il2Cpp interop assemblies. Run the game once. BepInEx inspects the IL2CPP metadata on first launch and writes interop assemblies to
BepInEx/interop/. This is a one-time ~30s step; re-do it any time the game updates.You should see lines like
[Info: BepInEx] Generating Il2Cpp interop assembliesinBepInEx/LogOutput.log. -
Quit the game. The interop assemblies are now sitting in
BepInEx/interop/Il2CppAssembly-CSharp.dlletc.
Building the plugin
The csproj references BepInEx core assemblies via BepInExCore and generated
interop assemblies via BepInExInterop. Build from the repo root:
$env:BepInExCore = "C:\Program Files (x86)\Steam\steamapps\common\RecRoom\BepInEx\core"
$env:BepInExInterop = "C:\Program Files (x86)\Steam\steamapps\common\RecRoom\BepInEx\interop"
dotnet build DorkNet.ClientPatch -c Release
Output lands at:
DorkNet.ClientPatch/bin/Release/DorkNet.ClientPatch.dll
Installing the plugin
Copy the built DLL into the game's BepInEx/plugins/ folder:
Copy-Item `
DorkNet.ClientPatch/bin/Release/DorkNet.ClientPatch.dll `
"$env:BepInExInterop\..\plugins\"
Or use the patcher (next section).
Integration with patch-client.ps1
The patcher tool grew an -InstallBepInEx switch. Pass the path to a
freshly-extracted BepInEx 6 IL2CPP zip and it'll:
- Extract BepInEx into the client folder if not already present.
- Build this plugin against the existing
BepInEx/interopdirectory. - Copy the DLL into
BepInEx/plugins/. - Append a
Photon CustomAuth integrationstep to the patch summary.
.\tools\patch-client.ps1 `
-RecRoomPath "C:\…\RecRoom\Recroom_Release_Data" `
-PhotonAppId cb0880d9-… `
-InstallBepInEx "C:\Downloads\BepInEx_unix_x86_64_6.0.0-be.696_…zip"
If the interop folder doesn't exist yet (game never launched
post-BepInEx-install), the patcher will tell you to launch the game
once and re-run with -InstallBepInEx to finish the build step.
Verifying the plugin loaded
Launch the client. Tail BepInEx/LogOutput.log and look for:
[Info : BepInEx] Loading [DorkNet ClientPatch 1.0.0]
[Info :DorkNet ClientPatch] DorkNet ClientPatch v1.0.0 loading…
[Info :DorkNet ClientPatch] Photon AuthValues injection patches applied.
After login + the first Photon connect, also expect:
[Info :DorkNet ClientPatch] [auth-injector] set Photon AuthValues — userid=1811750, LoginLock=<set>
On the server side, the [photon-auth] received query={...} log line
should now contain userid=1811750&LoginLock=<guid>.
Server-side: flip strict validation back on
Once the plugin is shipping LoginLock in production:
- Restore the strict-mode body of
PhotonCustomAuthController— accept the parameters, look up the canonical lock fromPlayerPresenceService.ValidateLock(playerId, loginLock), and returnResultCode 3on mismatch. - In the Photon dashboard, flip "Reject if Auth Failed" ON.
- Now a duplicate-account login on a different machine fails Photon auth → never reaches the dorm.
Troubleshooting
Could not load file or assembly 'Il2CppAssembly-CSharp'
The interop folder isn't where the csproj expects. Set the
BepInExInterop env var to your install's interop path before dotnet build.
No interop assemblies found, Il2CppType.Of<…>().GetField fails
Game never launched post-BepInEx-install. Launch once, quit, re-build.
Plugin loads but [auth-injector] skip — no LocalAccountId yet on every connect
The Photon connect happens before login completes. Inspect the log
order — if RecNet auth genuinely hasn't run yet, the plugin can't help
on that particular connect; it'll succeed on the next one (post-login
Photon reconnect when joining a room).
Game crashes on launch with BepInEx but works without
Either the plugin threw on Load (check LogOutput.log for stack
trace) or the BepInEx version mismatches — confirm you grabbed the
IL2CPP, not Mono, build for Windows x86_64.