# match Matchmaking Worker served on the `match` subdomain (`match.recflare.net`) — a Hono app that handles player presence and places players into room instances. Rooms, room instances and presence all live in the shared `recflare` D1 database. ## Routes | Method | Path | Auth | Description | | ------ | ------------------------------------ | ---- | ------------------------------------------------ | | POST | `/player/login` | | Login ack (no-op; must not touch presence) | | POST | `/player/exclusivelogin` | | Exclusive-login ack (no-op) → `{ errorCode: 0 }` | | POST | `/player/logout` | ✓\* | Clear presence (except the Orientation seed) | | POST | `/player/notifydisconnect` | | Disconnect notification (no-op ack) | | GET | `/player?id=1&id=2,3` | | Batch player presence lookup | | POST | `/player/heartbeat` | ✓ | Presence heartbeat (JSON body) | | PUT | `/player/statusvisibility` | ✓\* | Set status visibility | | POST | `/goto/room/:room` | ✓ | Go to a room (`dormroom` → personal dorm) | | POST | `/matchmake/none` | | Preserve current instance, else dorm | | POST | `/matchmake/room/:roomId/:subRoomId` | ✓ | Matchmake into a specific subroom | | POST | `/matchmake/room/:roomId` | ✓ | Matchmake into a room (default subroom) | | POST | `/matchmake/:room` | ✓ | Matchmake by id or name (`dorm` → personal dorm) | | POST | `/goto/none` | | Go to the dorm | | PUT | `/player/photonregionpings` | | Region ping report (no-op ack) | | PUT | `/player/gameserverregionpings` | | Region ping report (no-op ack) | | POST | `/roominstance/:id/reportjoinresult` | | Report join result (no-op ack) | | PUT | `/roominstance/:id/inprogress` | ✓ | Set the instance's in-progress flag | | GET | `/room/:roomId/instances` | ✓ | A room's live instances (owner/co-owner only) | | GET | `/rooms/requiring/developer` | | Rooms requiring a developer → `[]` | | GET | `/rooms/requiring/rrplus` | | Rooms requiring RR+ → `[]` | | GET | `/openapi.json` | | Generated OpenAPI 3.1 spec (see below) | \* `logout` and `statusvisibility` read the token when present but never 401 — an unauthenticated call is a no-op ack. The other ✓ routes return an empty-body 401 when the Bearer JWT (issued by the `auth` worker) is missing or invalid. ## API documentation `GET /openapi.json` serves a spec generated from `describeRoute` blocks that sit alongside each handler, with the schemas in `src/openapi.ts`. **The spec is descriptive, not enforced** — same rationale as the `auth`/`accounts` workers: a reverse-engineered protocol, lenient handlers, no runtime validation. A test asserts every route appears in the spec, so adding one without documenting it fails. ## Presence Presence is a per-player row in the shared `presence` table recording the room instance that player is currently in, plus status fields (visibility, device class, VR movement mode, platform, app version). It's written by matchmake/goto and refreshed by the heartbeat, and read by the heartbeat and the batch `GET /player`. - **`isOnline` means "has a live presence row"**, not "is in a room". Rows expire on a TTL, so a player who stops heartbeating drops offline; a player can be online in the lobby with `roomInstance` null. - **The heartbeat is write-thrifty.** An unchanged heartbeat re-writes the row (to extend its TTL) only once the TTL is within `PRESENCE_REFRESH_THRESHOLD` seconds of lapsing — a still player is refreshed periodically rather than on every beat. - **A cron sweep** (`scheduled`) clears presence past its TTL and, crucially, recomputes the fullness of the instances those rows pointed at. Nothing else notices a player who crashed or hard-quit, so without the sweep their instance can stay flagged full — and unjoinable — with nobody in it. The heartbeat also accepts a non-JSON (LoginLock form) body, which it reads and ignores; only a JSON body carries status fields. ## Matchmaking and room instances A matchmake resolves the room (by numeric id or name) from D1, then finds a joinable public instance of the requested subroom or creates a new `room_instance`. The result is persisted as the player's presence so the heartbeat can replay it — keeping the client's local presence in sync. `errorCode` 0 with a `roomInstance` is success; an unknown room returns `errorCode` 20 (NoSuchRoom) with `roomInstance: null`. Several behaviours are load-bearing and reverse-engineered from the client: - **Instance names are `^`-prefixed** so the client resolves the new scene; personal dorms are the exception (`@owner's Dorm`, no `^`). An empty `location` (the SubRoom's Unity scene id) makes the client reject the session. - **Never re-place a player into their current instance.** The client keys the room transition off a _changing_ `roomInstanceId`; returning the same id hangs it mid-join, so the join search excludes the caller's current instance. - **Subrooms are separate places.** Joining one must never land you in an instance of another, so instance reuse is scoped to the exact `(roomId, subRoomId)`. - **The dorm is a single stable instance** with a constant Photon room id, returned identically by every dorm entry point and the heartbeat, so the client's whole-instance presence check never reads out-of-sync. - **Two dorm keywords:** `goto/room/dormroom` and `matchmake/dorm` — different spellings the 2023 client uses for the same destination. - **`matchmake/none` preserves existing presence** (it's how the client establishes the solo Orientation room) and only falls back to the dorm when the player has none. `goto/none` always goes to the dorm. ## Bindings | Binding | Type | Notes | | ------------ | ------------- | ------------------------------------------------------------ | | `DB` | D1 | Shared `recflare` database — rooms, room instances, presence | | `JWT_SECRET` | Secrets Store | Shared HS256 signing key (see the `auth` README) | The `presence` and `room_instance` tables are owned/migrated by the `rooms` worker; this worker has no migrations of its own. ## Known gaps - `/rooms/requiring/developer` and `/rooms/requiring/rrplus` always return `[]` — no such gating queue exists yet.