mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 15:08:47 -07:00
4edf25b2d5
Targets Rec Room 2020.12.18 client.
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 the dorknet-clientpatch-{key}.zip
Release asset the launcher fetches.
- .github/workflows/release.yml — auto-publishes both Release assets
on v1-december-* tag push.
Scrubbed via maintainer-side scrub-for-public.ps1: same drop rules as
the prior import, plus build-patcher-zip.ps1 added to the tools/
allow-list so it survives the scrub.
250 lines
11 KiB
Markdown
250 lines
11 KiB
Markdown
# Architecture
|
|
|
|
A mental model of the DorkNet codebase for new contributors. Read this
|
|
before you touch code; it explains layout, request flow, the design
|
|
patterns that look weird in isolation, and where to start.
|
|
|
|
For deployment, see [`deploy.md`](deploy.md). For wire-protocol
|
|
reverse-engineering deliverables, see the `recroom-2020-client-*`
|
|
files in this folder.
|
|
|
|
---
|
|
|
|
## Projects in the solution
|
|
|
|
```
|
|
DorkNet.sln
|
|
├── DorkNet.Server ASP.NET Core 9 backend — the bulk of the code
|
|
├── DorkNet.Models Shared DTO + serialization types
|
|
├── DorkNet.ClientMod MelonLoader plugin (portable / experimental)
|
|
└── DorkNet.ClientPatch BepInEx 6 IL2CPP plugin (production)
|
|
```
|
|
|
|
| Project | Responsibility | Target |
|
|
|---|---|---|
|
|
| `DorkNet.Server` | REST API, SignalR hub, admin SPA host, static-site host, image CDN, matchmaking, persistence | .NET 9 |
|
|
| `DorkNet.Models` | DTOs the wire protocol speaks. No project dependencies. | .NET 9 |
|
|
| `DorkNet.ClientMod` | MelonLoader 0.6.x port — same patches as ClientPatch, JSON config | .NET 6 |
|
|
| `DorkNet.ClientPatch` | BepInEx 6 IL2CPP plugin — the standard client patcher, TOML config | .NET 6 |
|
|
|
|
`ClientMod` and `ClientPatch` are intentional twins: both apply the
|
|
same patches (URI rewrite `.rec.net` → configured apex, Photon AppId
|
|
override, TLS trust bypass, auth injection) against different mod
|
|
loaders. ClientPatch is the supported deployment; ClientMod exists for
|
|
contributors who already have MelonLoader installed and don't want to
|
|
swap loaders.
|
|
|
|
---
|
|
|
|
## Server folder layout
|
|
|
|
```
|
|
DorkNet.Server/
|
|
├── Auth/ JWT config, IP-ban + player-ban middleware, AdminOnly attribute
|
|
├── Compat/ Version detection middleware, client-version registry, HttpContext extensions
|
|
├── Controllers/ REST endpoints (see below)
|
|
├── Data/ DbContext, Entities/, EF migrations, LegacyUpgrades, seed JSON
|
|
├── Hubs/ SignalR NotifyHub (real-time presence on notify.{apex}/hub/v1)
|
|
├── Protos/ Protobuf schemas — trimmed `dorknet_room_data.proto` + full decompiled `recroom_2020.proto`
|
|
├── Services/ ~27 business-logic services (PlayerService, RoomService, etc.)
|
|
├── Versions/ Per-version plugins; Late2020VersionPlugin marks the 2020.12.18 build
|
|
├── admin-ui/ Vite + React SPA source — builds to wwwroot/admin/
|
|
├── site/ Public website Vite SPA source — builds to wwwroot/site/
|
|
├── wwwroot/ Static file root, with per-host subdirs (admin/, site/, feed/)
|
|
└── Program.cs Host + DI + middleware pipeline (split candidates noted at the bottom)
|
|
```
|
|
|
|
### Controllers/ groupings
|
|
|
|
20 top-level groups, each responsible for one product surface the
|
|
watch (game client) talks to. The grouping mirrors the watch's URL
|
|
layout, which is why some folders are large — splitting them would
|
|
obscure the mirror relationship.
|
|
|
|
| Folder | Surface |
|
|
|---|---|
|
|
| `API/` | Main REST API split by subsystem (Avatar, Store, Rooms, Messages, Inventions, Leaderboard, BugReporting, Moderation, …) |
|
|
| `Accounts/` | Account creation, login, profile updates |
|
|
| `Admin/` | Admin SPA backend, gated by `AdminOnlyAttribute` |
|
|
| `Auth/` | Photon custom-auth (`/photon/customauth`), version-check `/api/versioncheck/v4` |
|
|
| `Cdn/` | Image transforms, room blob serving |
|
|
| `Clubs/` | `clubs.{apex}/*` — clubs surface |
|
|
| `Commerce/` | `commerce.{apex}/*` — monetization catalog |
|
|
| `Discovery/` | `discovery.{apex}/*` — room discovery + search |
|
|
| `Econ/` | `econ.{apex}/*` — currency + inventory |
|
|
| `Geo/` | `geo.{apex}/*` — region lookup |
|
|
| `Health/` | `/healthz` probe |
|
|
| `Match/` | Matchmaking + `/goto/*` flow |
|
|
| `Notify/` | REST-side companion to the SignalR hub |
|
|
| `Ns/` | `ns.{apex}/*` — service-URL registry the watch queries on startup |
|
|
| `PlayerSettings/`, `Rooms/`, `Site/`, `Storage/`, `Strings/` | Smaller surfaces with self-explanatory scope |
|
|
|
|
---
|
|
|
|
## Request lifecycle
|
|
|
|
Middleware ordering matters in ASP.NET Core; the pipeline order in
|
|
`Program.cs` is below. Numbers are the order, not line numbers.
|
|
|
|
1. **Logging** — Serilog enriches every request with structured fields.
|
|
2. **HTTPS redirect** — Only when Kestrel has an HTTPS endpoint (direct
|
|
mode); a no-op behind nginx/cloudflared.
|
|
3. **WebSocket config** — 30-second keep-alive pings to keep
|
|
cloudflared from killing idle tunnels.
|
|
4. **Per-host static branches** (`MountStaticHost`) — `admin.{apex}`,
|
|
apex, `feed.{apex}` each serve their own SPA from `wwwroot/<dir>`.
|
|
Mounted with `UseWhen` (not `MapWhen`) so API calls under those
|
|
subdomains still reach the controllers.
|
|
5. **IP-ban check** — earliest filter; rejects banned IPs before any
|
|
work runs.
|
|
6. **Version detection** (`VersionDetectionMiddleware`) — reads
|
|
`X-DorkNet-Version`, validates against the version registry,
|
|
returns 426 on mismatch. Apex + admin/site/feed subdomains skip
|
|
the gate (humans on browsers don't carry a client version).
|
|
7. **JWT authentication** — `UseAuthentication`.
|
|
8. **Authorization** — role/policy checks.
|
|
9. **Player-ban check** (`BanCheckMiddleware`) — after auth so it can
|
|
see `ctx.User`.
|
|
10. **MapControllers** — route to controller actions.
|
|
11. **SignalR hub** — `NotifyHub` on `notify.{apex}/hub/v1`, WebSocket-only,
|
|
`RequireHost` filter.
|
|
|
|
---
|
|
|
|
## Multi-tenancy + the domain config
|
|
|
|
DorkNet is built to run many concurrent players. Every feature assumes
|
|
multi-user semantics, not single-player sandbox. There's no global
|
|
"current player"; everything threads `playerId` through the request.
|
|
|
|
The deployment apex (default `localhost`, overridable via
|
|
`Domain:Apex` config or `DORKNET_DOMAIN` env var) is read once at
|
|
startup into the `DomainConfig` singleton. Inject it anywhere code
|
|
needs to build a subdomain URL — never hardcode `localhost`.
|
|
|
|
```csharp
|
|
public sealed class Foo(DomainConfig domain) {
|
|
public string Url() => $"https://cdn.{domain.Apex}/...";
|
|
}
|
|
```
|
|
|
|
Per-controller `[Host("api.rec.net", "api.localhost")]` filters are
|
|
**gone**; allowed hosts are derived from `{apex}` + `*.{apex}` and
|
|
enforced by `HostFilteringMiddleware`. Subdomain-discriminating
|
|
handlers branch on `Request.Host.Host.Split('.')[0]`.
|
|
|
|
---
|
|
|
|
## Version plugins
|
|
|
|
`Versions/` holds an `IVersionPlugin` per client generation we support.
|
|
Today there's one (`Late2020VersionPlugin` for the 2020.12.18 build).
|
|
|
|
The pattern is a marker registry:
|
|
|
|
- `VersionKeys` — the wire-format identifiers a plugin claims.
|
|
- `Generation` — human-readable name controllers branch on via
|
|
`ctx.GetClientVersion().Generation`.
|
|
- `RegisterStrategies(IServiceCollection)` — for plugins that ship
|
|
generation-specific implementations (today empty; will grow as
|
|
wire shapes diverge across builds).
|
|
|
|
When a new client build is wire-compatible with an existing
|
|
generation, just add its key to `VersionKeys`. When it diverges, add
|
|
a new plugin and register strategy bindings keyed by generation.
|
|
|
|
---
|
|
|
|
## Reverse-engineering provenance
|
|
|
|
DorkNet is a clean-room reimplementation of the 2020.12.18 Rec Room
|
|
backend protocol. ~46 files carry `Cpp2IL` provenance comments tying
|
|
specific code to decompiled client internals. Example:
|
|
|
|
```csharp
|
|
// Wire shape per Cpp2IL_ISIL/.../PLILLKHMNDA.txt deserializer:
|
|
// gap-skipping enum, not 0..N
|
|
public const int Member = 10, Moderator = 20, CoOwner = 30, Creator = 100;
|
|
```
|
|
|
|
The decompiled dumps themselves are not in this repo (`dist/RecRoom-2020.12.18-isil/`
|
|
locally, gitignored). The comments are the bridge — if you're writing
|
|
or fixing wire-shape code, always check the decompiled source first
|
|
rather than guessing from symptoms.
|
|
|
|
The `docs/recroom-2020-client-*` files are an exhaustive catalog of
|
|
client-side endpoints, DTOs, and request expectations extracted from
|
|
the same decompile. They're the ground truth when a controller needs
|
|
a new endpoint.
|
|
|
|
---
|
|
|
|
## Database story
|
|
|
|
Two providers, switched by `Database:Provider` config:
|
|
|
|
- **SQLite** — default for local dev. File at `bin/<config>/data/dorknet.db`.
|
|
- **Postgres** — production. Connection string from `ConnectionStrings:Default`.
|
|
|
|
Schema lives in `Data/Entities/*` and `Data/DorkNetDbContext.cs`. EF
|
|
migrations under `Data/Migrations/` apply via `Database.Migrate()` on
|
|
boot. Data transforms that can't be expressed as schema migrations
|
|
(seed renames, computed backfills) go in `Data/LegacyUpgrades.cs` as
|
|
idempotent methods registered in `RunAsync()`. See
|
|
[`Data/MIGRATIONS.md`](../DorkNet.Server/Data/MIGRATIONS.md) for the
|
|
discipline: one release = one migration; everything else is
|
|
`LegacyUpgrades`.
|
|
|
|
---
|
|
|
|
## Where to start
|
|
|
|
If you've never touched this codebase, the most approachable path:
|
|
|
|
1. Read a small **Service** (`ConfigService`, `DomainConfig`,
|
|
`OnlinePresenceService`) to learn the DI pattern.
|
|
2. Read a small **Controller** (`Health/HealthController`,
|
|
`Strings/StringsController`) to see how services are injected and
|
|
responses are shaped.
|
|
3. Trace one **end-to-end request** through `Rooms/` or `Players/` —
|
|
middleware → controller → service → DbContext → response. The
|
|
middleware pipeline (above) is the lens.
|
|
4. Look at one **`Cpp2IL`-cited line** in a controller to see how
|
|
wire shapes are pinned to decompiled provenance.
|
|
|
|
When in doubt: open `DorkNet.Server/Program.cs` and the
|
|
`Versions/Late2020/Late2020VersionPlugin.cs` summary doc-comment.
|
|
Between them they explain 80% of the cross-cutting decisions.
|
|
|
|
---
|
|
|
|
## Acknowledged sharp edges
|
|
|
|
These look messy. They're messy on purpose — the alternative is
|
|
worse.
|
|
|
|
- **Some controllers are large.** `AdminController.cs`, `RoomsController.cs`,
|
|
`GoToController.cs` are big because they mirror watch surfaces that
|
|
are themselves sprawling. Splitting them by endpoint group would
|
|
obscure the surface-mirror relationship that makes the code
|
|
navigable when paired with the watch's decompiled call sites.
|
|
- **Cpp2IL comments reference paths outside the repo.** That's
|
|
intentional — the decompiled source is provenance only, not
|
|
shippable. Comments give a future contributor a breadcrumb to
|
|
re-derive the wire shape if the decompile changes.
|
|
- **`Program.cs` is long.** Most of it is one-time schema patching
|
|
(`RunPatchAsync` calls for the Postgres `EnsureCreated` path) that's
|
|
pending consolidation per [`Data/MIGRATIONS.md`](../DorkNet.Server/Data/MIGRATIONS.md).
|
|
Once that lands, the remaining ~500 lines of middleware + host
|
|
setup will be split into `Startup/*` extensions.
|
|
- **Two-provider EF.** SQLite and Postgres each have their own
|
|
EnsureCreated/Migrate dance because some Postgres-only column types
|
|
drift the model snapshot. The split is annoying but lets one
|
|
codebase serve both local dev (zero-config SQLite) and production
|
|
(managed Postgres).
|
|
- **Hardcoded `rec.net` URLs in `RoomService` + `HtrAssetMirrorService`.**
|
|
These pull canonical assets (room thumbnails, `.htr` asset data)
|
|
from the official Rec Room CDN at seed time. The alternative is
|
|
shipping the assets in the repo, which would put DorkNet on
|
|
legally weaker footing.
|