mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 07:28:45 -07:00
Import december-2020-12-18 from private repo (scrubbed 2026-05-27)
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.
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user