mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
327dcb789c
Audits every route the March-2023 client can call against what this server
actually serves, and fixes the defects that were breaking gameplay. Adds
docs/recroom-2023-client-api-complete.md as the reference: ~500 route
literals across 13 subsystems, with verb, request/response shape, and a
per-subsystem diff. 315 defects catalogued; the ones fixed here are listed
in its "Already fixed" table.
Path coverage was never the problem — 465 of 506 routes already had
handlers. The failures were in shape, and they cluster into four causes:
- Wrong verb. The verb is a BestHTTP.HTTPMethods ordinal (GET=0 .. PATCH=5),
confirmed from that enum's static constructor. Routes registered POST-only
while the client PUTs returned 405, which made room consumables neither
buyable nor consumable and blocked every room-key edit.
- Bare scalar wrapped in an object. The client reads `1000`, not
`{"Limit":1000}`; the issuing method signature says which. Wrapping makes
the deserializer throw and the feature dies silently.
- [FromBody] vs form-urlencoded. The client sends nearly every write as form
data, which an [ApiController] action bound [FromBody] rejects with 415
before the handler runs. This alone broke all 13 club member-management
endpoints. Adds Binding/FormOrJsonModelBinder so this is fixed once.
- Notify dispatch keys. Both enum overloads of the client's RegisterHandler
lower the id via Int32.ToString() and register only that numeric key, and
lookup is a case-sensitive TryGetValue that returns silently on a miss. We
were sending the enum NAME for most ids, so friend-request updates, forced
logout, maintenance broadcasts, live balance and consumable updates, and
invention moderation pushes all evaporated with no error anywhere.
Notable behavioural bugs fixed:
- Editing a club announcement deleted it: PUT was a third binding on the
delete handler. Split apart, and the missing create route added.
- leaderboard/CheckAndSetStat did not exist. The 2023 client has no SetStat
literal at all, so every leaderboard score it wrote was discarded.
- Setting a club clubhouse cleared it: the handler read roomId from the query
while the client sends it in the body, then wrote the resulting null.
- Club join requests were inverted (the enum is Open=0, InviteOnly=1,
AskToJoin=2), so ask-to-join clubs rejected everyone and invite-only clubs
quietly admitted them.
- Room bans were pushed as id 24, for which the client registers no handler;
they now go out as ModerationKick with IsBan, so the ban takes effect
immediately instead of at next login.
- Room-key ownership queries answered for the wrong player, and "has anyone
bought this key" asked "did I buy it" — always false for the creator.
Also persists settings that were previously acknowledged and dropped
(playlist accessibility/restrictions/level-voting/warning, per-player status
visibility, VR movement mode and avoid-juniors, custom room codes), which is
what the new columns in the migration are for.
One test asserted the old nested PriceAndCurrency response shape; the
client's consumable formatter has no such key and reads Price and
PurchaseCurrencyId flat, so the test was corrected alongside the handler.
383 lines
17 KiB
Markdown
383 lines
17 KiB
Markdown
# DorkNet — `march-2023-03-21`
|
|
|
|
This is the **December 2020.12.18** branch of DorkNet — a self-hostable
|
|
reimplementation of the Rec Room backend tuned to the wire protocol of
|
|
the **Rec Room 2020.12.18** client build.
|
|
|
|
> ⚠️ **Not affiliated with Rec Room Inc. or Against Gravity.**
|
|
> See [DISCLAIMER](../../blob/main/DISCLAIMER.md). DorkNet ships no Rec
|
|
> Room game assets or modified binaries — you supply your own
|
|
> legally-acquired Rec Room 2020.12.18 install.
|
|
|
|
If you're not sure which DorkNet branch you want, start at the
|
|
[main branch's chart](../../blob/main/BRANCHES.md). The launcher on
|
|
main also auto-detects your install.
|
|
|
|
---
|
|
|
|
## What this branch targets
|
|
|
|
| | |
|
|
|---|---|
|
|
| **Client build** | Rec Room 2020.12.18 |
|
|
| **Version key** (sent as `X-DorkNet-Version` header) | `december_2020_12_18` |
|
|
| **Version plugin** | `DorkNet.Server/Versions/Late2020/Late2020VersionPlugin.cs` |
|
|
| **Schema** | One EF `Initial` migration; SQLite + Postgres both work |
|
|
| **Runtime topology** | Gateway-fronted microservices with dedicated service slices and a monolith fallback |
|
|
| **Diverges from `march-2020-03-10` by** | ~170 files |
|
|
|
|
Subsystems present on this branch that the March branch doesn't have:
|
|
|
|
- Chat threads + group DMs (`ChatThreads`, `ChatThreadMembers`)
|
|
- Clubs surface (`clubs.{apex}/*` — announcements, categories, subscriptions)
|
|
- Playlists (curated + user-created)
|
|
- Weekly challenges
|
|
- Room keys (purchasable room access)
|
|
- Room roles (per-room co-owner / moderator / host grants)
|
|
- Leaderboard channel metadata
|
|
- Loading-screen tips
|
|
|
|
---
|
|
|
|
## Quickstart
|
|
|
|
**1. Install .NET 9 SDK** and (optionally) `dotnet-ef`:
|
|
|
|
```pwsh
|
|
dotnet tool install --global dotnet-ef
|
|
```
|
|
|
|
**2. Clone this branch:**
|
|
|
|
```pwsh
|
|
git clone --branch december-2020-12-18 https://github.com/DorkSquadRR/DorkNet
|
|
cd DorkNet
|
|
```
|
|
|
|
**3. Configure secrets.** Drop in `DorkNet.Server/appsettings.Local.json`
|
|
(gitignored) or export environment variables:
|
|
|
|
| Config key | Env var | Required | Notes |
|
|
|---|---|---|---|
|
|
| `Jwt:Secret` | `DORKNET_JWT_SECRET` or `Jwt__Secret` | yes | >=64 random chars |
|
|
| `Photon:AppId` | `Photon__AppId` | yes | Photon Realtime AppId (free at dashboard.photonengine.com) |
|
|
| `Photon:VoiceAppId` | `Photon__VoiceAppId` | no | only if voice chat |
|
|
| `Photon:CloudRegion` | `Photon__CloudRegion` | no | defaults to `us` |
|
|
| `Database:Provider` | `Database__Provider` | no | `sqlite` (default) or `postgres` |
|
|
| `ConnectionStrings:Default` | `ConnectionStrings__Default` | postgres-only | full Npgsql connection string |
|
|
| `ConnectionStrings:Redis` | `ConnectionStrings__Redis` | no | enables SignalR backplane |
|
|
| `Domain:Apex` | `DORKNET_DOMAIN` | no | defaults to `localhost`; set for production |
|
|
| `S3:Endpoint` | `S3__Endpoint` | production | S3-compatible API endpoint for blobs/images |
|
|
| `S3:AccessKey` | `S3__AccessKey` | production | S3 access key |
|
|
| `S3:SecretKey` | `S3__SecretKey` | production | S3 secret key |
|
|
| `S3:Region` | `S3__Region` | no | defaults to `garage`; use `auto` for R2 |
|
|
|
|
**4. Boot a local standalone server:**
|
|
|
|
```pwsh
|
|
dotnet run --project DorkNet.Server
|
|
```
|
|
|
|
First boot creates `bin/Debug/net9.0/data/dorknet.db`, runs every
|
|
`LegacyUpgrades` pass, seeds canonical rooms / store catalog / club
|
|
categories / playlists, and starts listening. You should see
|
|
`Application started.` in the log; `curl http://localhost:8080/healthz`
|
|
returns 200.
|
|
|
|
For the production microservices stack, Docker, and Dokploy deploy
|
|
notes, see
|
|
[docs/deploy.md](docs/deploy.md).
|
|
|
|
---
|
|
|
|
## Patching your client
|
|
|
|
```pwsh
|
|
.\tools\install-melon.ps1 `
|
|
-RecRoomPath "C:\Path\To\Recroom_Release_Data" `
|
|
-PhotonAppId "<your-photon-realtime-app-id>" `
|
|
-PhotonVoiceAppId "<your-photon-voice-app-id>"
|
|
```
|
|
|
|
This installs the MelonLoader mod (`DorkNet.ClientMod`) into the
|
|
client's `Mods/` folder and writes its config to
|
|
`MelonLoader/UserData/dorknet-clientmod.json`. The mod rewrites all
|
|
`*.rec.net` URIs to your configured DorkNet apex, swaps the Photon
|
|
AppIds, and bypasses TLS verification on the client's HTTPS calls so
|
|
self-signed certs work. (First run prints "launch the game once"; do
|
|
that, then re-run with `-ResumeBuild` to finish — see the script's
|
|
`-?` help.)
|
|
|
|
### Standalone Quest build (experimental)
|
|
|
|
The December Quest APK is also IL2CPP and can build the same
|
|
`DorkNet.ClientMod` source against LemonLoader's Android runtime. The
|
|
current Quest path is still device-tested manually: build the mod as a
|
|
net8 LemonLoader DLL, install it into a LemonLoader-patched APK, then
|
|
sideload the resigned APK.
|
|
|
|
From this branch:
|
|
|
|
```pwsh
|
|
dotnet build .\DorkNet.ClientMod\DorkNet.ClientMod.csproj `
|
|
-p:TargetFrameworks=net8.0 `
|
|
-p:MelonLoaderDir="C:\path\to\LemonLoader\melon_data\MelonLoader"
|
|
```
|
|
|
|
Output lands at
|
|
`DorkNet.ClientMod\bin\Debug\net8.0\DorkNet.ClientMod.dll`. Desktop
|
|
builds still default to net6 and keep the historical flat output path.
|
|
|
|
Quest notes:
|
|
|
|
- Target package: `com.AgainstGravity.RecRoom`.
|
|
- Use a public HTTPS DorkNet host; keep `"EnableTlsTrustBypass": false`
|
|
for Quest builds.
|
|
- The Dec Photon hooks are discovered from the unique
|
|
`PUNNetworkManager` methods returning `AppSettings` and
|
|
`AuthenticationValues`, with the known Dec obfuscated names as fallback.
|
|
- Standalone APK patching/resigning is not yet wrapped by
|
|
`install-melon.ps1`; this is the porting path, not a public one-click
|
|
installer yet.
|
|
|
|
### Debugging (opt-in — `DorkNet.DebugMod`)
|
|
|
|
The shipping `DorkNet.ClientMod` carries only what's needed to connect and
|
|
survive anti-cheat. All diagnostics — HTTP/request tracing, join/quit/studio/
|
|
room-load traces, the process-exception sink, auth-token tracing, the
|
|
registration-dialog trace, and the in-game debug console — live in a separate
|
|
`DorkNet.DebugMod`. Drop `DorkNet.DebugMod.dll` into the `Mods` folder next to
|
|
`DorkNet.ClientMod.dll` to turn them on; remove it to ship a quiet, minimal
|
|
client. Debug knobs are read from the same `dorknet-clientmod.json`.
|
|
|
|
Retail builds keep the `RecRoom.Debugging.DebugConsole` UI and serialized
|
|
`DebugConsoleCommandConfig` list, but the public 2023 client strips the
|
|
native command submit path. With `DorkNet.DebugMod` installed, set
|
|
`"EnableDebugConsole": true` and relaunch: it force-toggles the console UI on
|
|
a hotkey (`"DebugConsoleToggleKey"`, default `BackQuote` = the `~` key),
|
|
intercepts the dead submit callback, and invokes commands from the runtime
|
|
command metadata directly. Type `help` in the console to list discovered
|
|
commands. `"DevCommands"` may contain optional `"Label=Command args"`
|
|
shortcuts. The feature stays off by default and also silences the retail
|
|
`CheatManager` callbacks used by movement/time commands.
|
|
|
|
### Desktop Screen Sharing FPS (opt-in)
|
|
|
|
The Maker Pen "Desktop Sharing Screen"
|
|
(`RecRoom.Tools.Productivity.DesktopScreenSharingDisplay`) broadcasts at a
|
|
baked refresh rate (~5 fps). Set `"DesktopScreenShareFps"` in
|
|
`dorknet-clientmod.json` (e.g. `30`) and relaunch to override it — the mod
|
|
rewrites the gadget's `screenShareImageRefreshFrequency` at runtime and,
|
|
when `"DesktopScreenShareRaisePhotonRate"` is true (default), lifts
|
|
`PhotonNetwork.SendRate`/`SerializationRate` to match so the frames
|
|
actually transmit (the image streams over `OnPhotonSerializeView`, capped
|
|
at the serialization rate otherwise). This is **global** — it raises every
|
|
object's network sync rate, so back it off on a busy room.
|
|
`"DesktopScreenShareResolution"`/`"DesktopScreenShareQuality"` (both `0` =
|
|
keep the prefab value) trade per-frame size for bandwidth if a high FPS
|
|
saturates the link. Only the player *sharing* needs the override. Default
|
|
`0` (off).
|
|
|
|
### RRO quest team size (opt-in)
|
|
|
|
RRO quests ship with a baked 4-player `GameConfigurationAsset` team cap.
|
|
Set `"QuestMaxTeamSize"` in `dorknet-clientmod.json` (for example `8`) to
|
|
raise that cap for the allowlisted quest configs in
|
|
`"QuestMaxTeamSizeRooms"`:
|
|
|
|
```json
|
|
{
|
|
"QuestMaxTeamSize": 8,
|
|
"QuestMaxTeamSizeRooms": [
|
|
"CrimsonCauldron",
|
|
"Crescendo",
|
|
"GoldenTrophy",
|
|
"IsleOfLostSkulls",
|
|
"TheRiseofJumbotron"
|
|
]
|
|
}
|
|
```
|
|
|
|
The quest host must run the mod: the host builds the game configuration
|
|
that gets networked to joiners. The patch also relaxes the quest spawn
|
|
filter for extra players, so player indexes above the four baked spawn
|
|
slots can reuse existing quest spawn points instead of being left out.
|
|
Default `0` (off).
|
|
|
|
---
|
|
|
|
## Code map
|
|
|
|
| Folder | Purpose |
|
|
|---|---|
|
|
| `DorkNet.Server/` | Shared ASP.NET Core controller/service stack, standalone fallback, monolith fallback |
|
|
| `DorkNet.Models/` | DTOs shared between server and client mod |
|
|
| `DorkNet.Contracts/` | Shared service names, route ownership, service-map options, and health/probe contracts |
|
|
| `DorkNet.ServiceDefaults/` | Shared service health, JSON, HTTP client, service identity, and route-guard setup |
|
|
| `DorkNet.Gateway/` | Edge reverse proxy with service-map, route-table, and service-health endpoints |
|
|
| `DorkNet.Services.*` | Gateway-fronted service hosts for identity, rooms, notify, content, social, commerce, platform, moderation, and web |
|
|
| `DorkNet.ClientMod/` | MelonLoader IL2CPP mod — the client patcher |
|
|
| `tools/` | Installers (`install-melon.ps1`, `install-legacy-client.ps1`, `remove-eac.ps1`), Cloudflare tunnel templates |
|
|
| `Dockerfile`, `Dockerfile.service`, `docker-compose.microservices*.yml` | Standalone fallback image, reusable service image, and local/Dokploy microservices entrypoints |
|
|
|
|
See [docs/architecture.md](docs/architecture.md) for the full mental
|
|
model (project layout, request lifecycle, watch-mirror controller
|
|
pattern, where to start contributing).
|
|
|
|
### Client API contracts
|
|
|
|
Rec Room is shut down, so the clients can only be studied statically.
|
|
[docs/recroom-2023-client-api-complete.md](docs/recroom-2023-client-api-complete.md)
|
|
is the complete contract for the March-2023 client: every route it can
|
|
call, the HTTP verb, the request and response shapes, and a per-subsystem
|
|
diff against what this server currently implements. Read it before adding
|
|
or changing any endpoint that build touches — it also documents how to
|
|
recover verbs and response shapes from the disassembly yourself.
|
|
|
|
The equivalent material for the December-2020 client is in the
|
|
`recroom-2020-client-*` files alongside it.
|
|
|
|
### Microservices
|
|
|
|
The production topology is the gateway-fronted compose stack.
|
|
`docker-compose.microservices.yml` starts the gateway, identity, rooms,
|
|
notify, content, social, commerce, platform, moderation, web, and
|
|
monolith fallback service hosts plus Compose-managed Postgres and Redis
|
|
for local testing. `docker-compose.microservices.dokploy.yml` is the
|
|
Dokploy version; it adds a `cloudflared` sidecar so Cloudflare Tunnel
|
|
can route the apex and wildcard hostnames to the gateway without Dokploy
|
|
domain rows.
|
|
|
|
The gateway routes owned host/path slices to the dedicated service hosts.
|
|
The `web` service owns the apex, `www`, `admin`, and `feed` static hosts.
|
|
The monolith fallback is still present for unknown route families, so
|
|
public client URLs remain stable while the split continues.
|
|
|
|
The compose file intentionally does not start object storage. Point it at
|
|
your separate S3-compatible instance:
|
|
|
|
```pwsh
|
|
$env:DORKNET_S3_ENDPOINT="https://your-s3-endpoint"
|
|
$env:DORKNET_S3_ACCESS_KEY="..."
|
|
$env:DORKNET_S3_SECRET_KEY="..."
|
|
$env:DORKNET_S3_REGION="garage" # or auto for R2
|
|
docker compose -f docker-compose.microservices.yml up --build
|
|
```
|
|
|
|
For deployment details, including Dokploy env vars, Cloudflare Tunnel
|
|
routing, external Postgres, and S3 setup, see
|
|
[docs/deploy.md](docs/deploy.md).
|
|
|
|
For the browser admin console, its routes, auth model, and production
|
|
troubleshooting, see [docs/admin.md](docs/admin.md).
|
|
|
|
---
|
|
|
|
## Database & migrations
|
|
|
|
One EF migration (`Migrations/<timestamp>_Initial.cs`) captures the
|
|
schema. SQLite dev DBs apply it via `Database.Migrate()`; Postgres uses
|
|
`EnsureCreated()` behind a transaction-scoped advisory lock. The
|
|
entity model is the single source of truth — to add a column, edit the
|
|
entity, boot, done (on SQLite — Postgres prod needs a migration).
|
|
|
|
Data transforms that can't be schema (seed renames, computed backfills)
|
|
go in [`DorkNet.Server/Data/LegacyUpgrades.cs`](DorkNet.Server/Data/LegacyUpgrades.cs).
|
|
See [`DorkNet.Server/Data/MIGRATIONS.md`](DorkNet.Server/Data/MIGRATIONS.md)
|
|
for the full discipline.
|
|
|
|
Tables added after the consolidated `Initial` migration (e.g.
|
|
`SignupCodes` / `PendingDevices`) are created by an idempotent
|
|
`CREATE TABLE IF NOT EXISTS` step in
|
|
[`Startup/DatabaseBootstrap.cs`](DorkNet.Server/Startup/DatabaseBootstrap.cs)
|
|
so both providers pick them up on existing DBs without a new migration
|
|
(the Postgres path is `EnsureCreated`-only and never replays migrations).
|
|
|
|
### Signup codes
|
|
|
|
When account creation is disabled (admin **Settings → Server**), the only
|
|
way in is an admin-issued single-use **signup code**: generate one in the
|
|
admin panel's **Settings → Signup codes** tab (with a descriptor + optional expiry),
|
|
hand it to the player, and they redeem it on the site's **`/join`** page —
|
|
which creates their account bound to the device their game client
|
|
reported, so the next launch logs straight in.
|
|
|
|
### Everyone-is-friends toggle
|
|
|
|
For small servers where searching + friend-requesting each other is
|
|
friction, admin **Settings → Server → "Everyone is friends"** makes every
|
|
account a friend of every other account. It writes **no** relationship
|
|
rows — the friend graph is synthesized at read time
|
|
(`RelationshipQueries.EffectiveFriendIdsAsync`, gated on
|
|
`ServerSettings.GlobalFriendsEnabled`), so flipping it off reverts
|
|
instantly. Blocks are still honored and the system/coach account is
|
|
excluded, as are auto-generated `Player_NNN` placeholder accounts that
|
|
never set a real username (they only appear if there's a genuine friend
|
|
row). Flipping it broadcasts `RelationshipsInvalid` to every connected
|
|
watch, which calls `Relationships.RefreshList` and re-pulls
|
|
`api/relationships/v2/get` — so players see everyone appear (or disappear)
|
|
**without relogging**. It covers the friends list, the friends-online HUD,
|
|
and room-move presence fan-out.
|
|
|
|
---
|
|
|
|
## Single-server defaults (review before going public)
|
|
|
|
Several values are hardcoded or defaulted for the convenience of running a
|
|
small, trusted, single-operator server. They work out of the box for a group
|
|
of friends but should be reviewed / adapted before exposing the server to the
|
|
public internet:
|
|
|
|
- **Client build version (`appVersion`).** Hardcoded to this branch's client
|
|
build (`20230317`) in the presence/match/notification responses
|
|
(`MatchPlayerController`, `PlayerPresenceController`, `NotificationService`,
|
|
`RoomsController`). It must match the build everyone runs, or players get a
|
|
"version mismatch" and can't join each other. If you serve a different
|
|
client build, change it (ideally hoist it to one shared constant).
|
|
- **No real authentication.** Accounts are keyed by a per-install `deviceId`
|
|
the client generates (`PlayerService.GetOrCreateByDeviceAsync`) — **not** a
|
|
verified identity (Steam ID is deliberately ignored so emulators work).
|
|
Anyone who can reach the server with a patched client gets/creates an
|
|
account. Use **Settings → Server → disable signups** + the
|
|
[signup-code](#signup-codes) flow to gate who can join; there is no password
|
|
wall by default.
|
|
- **First account becomes admin.** The first account created on a fresh DB is
|
|
granted `IsAdmin` so there's always a root admin. On a public server, create
|
|
your admin account first and verify no one else beat you to it.
|
|
- **Online status = Everyone.** Presence reports `StatusVisibility = 0`
|
|
(Everyone) so every player appears online to every other player, matching
|
|
the [everyone-is-friends](#everyone-is-friends-toggle) convenience. A public
|
|
server may instead want to persist and honor each player's own visibility
|
|
preference.
|
|
- **Room / matchmaking capacity.** New matchmade sessions and subrooms default
|
|
to **8** players and are capped at **40** (`GameSessionService`,
|
|
`RoomSceneEntity.MaxPlayers`, admin `MaxCapacity` clamp). The in-game "Max
|
|
Player Count in This Subroom" slider raises it per room (slider ceiling 40).
|
|
- **Avatar unlock clamp.** The wardrobe clamp defaults high
|
|
(`Avatar:MaxUnlockedItems` 4000 / per-slot 1000) for this desktop client —
|
|
tune in config if you change the ownership model.
|
|
- **Hosts / domain.** The apex and per-service subdomains come from
|
|
`DomainConfig` (`Domain:Apex`) / `appsettings`; set these to your own
|
|
domain. The `Everyone-is-friends`, signup-disable, and similar switches live
|
|
in **admin Settings**, not source.
|
|
|
|
None of these are security boundaries as shipped — treat the server as trusted
|
|
LAN/friends infrastructure unless you harden the items above.
|
|
|
|
---
|
|
|
|
## Contributing
|
|
|
|
See [main's CONTRIBUTING guide](../../blob/main/CONTRIBUTING.md) for
|
|
the project-wide rules.
|
|
|
|
Branch-specific: schema changes on this branch don't flow to
|
|
`march-2020-03-10` automatically. If a fix applies to both, cherry-pick
|
|
it across. Don't rebase across branches.
|
|
|
|
---
|
|
|
|
## License
|
|
|
|
[AGPL-3.0](LICENSE). Hosted forks must publish their source.
|