feat: per-sub-room max players, editable in game and from admin

Player caps were two settings pretending to be one. RoomEntity.MaxCapacity is
what the ROOM advertises (and what matchmaking hands to
RoomInstance.MaxCapacity); RoomSceneEntity.MaxPlayers is what ONE sub-room caps
itself at. The details payload was reading its room-level MaxPlayers off
sub-room 0, which collapsed the two — editing a sub-room would have moved the
room's advertised capacity with it. It now reports MaxCapacity, and each entry
in SubRooms[] reports its own figure.

The in-game path already existed (the "Max Player Count in This Subroom"
slider). Admin had no way to reach it at all — only zip import ever set it — so
a cap could not be corrected on a room the admin does not own:

  GET api/admin/v1/rooms/{id}/subrooms
  PUT api/admin/v1/rooms/{id}/subrooms/{subRoomId}/maxplayers  {"MaxPlayers":n}

subRoomId is the sub-room's ORDER INDEX — the id the client and the rest of the
wire use — not the row's primary key. Row ids are serialised as strings because
the admin SPA is JavaScript and mangles integers past 2^53. Both paths clamp to
1..80, matching rooms/{id}/props: 0 would make a sub-room unjoinable, and a
shared range stops the two settings disagreeing about what is legal.

The test drives the client's exact wire rather than a convenient one:
NLDBPDCNNCF.MNBPCGJNLNP builds a BLNOGFGHIIF whose single JSON key is
"maxPlayers" and PUTs it — this route does NOT form-encode like most of its
siblings, so a form-bodied test would have passed while the real client's body
went unread. It also asserts the reply carries the new value, since the client
deserialises it back as FGCPNAACHIK.
This commit is contained in:
Alexa
2026-07-28 11:06:49 +03:00
parent cc523181d2
commit f43ce3f368
4 changed files with 285 additions and 3 deletions
+40
View File
@@ -139,6 +139,46 @@ in S3 is modified. Toggle endpoint:
`{"Disabled": bool}`. Clamped responses are served with a 60s edge TTL so
flipping the toggle lands within a minute.
## Per-sub-room max players (`/rooms` → room detail)
Player caps are two separate settings, and conflating them is the easy
mistake here:
| Setting | Column | Scope |
| --- | --- | --- |
| `MaxCapacity` | `RoomEntity.MaxCapacity` | What the ROOM advertises; flows into `RoomInstance.MaxCapacity` for matchmaking and into the room-level `MaxPlayers` of the details payload. |
| `MaxPlayers` | `RoomSceneEntity.MaxPlayers` | What ONE sub-room caps itself at; appears per entry in the details `SubRooms[]`. |
The room-level figure must not be read off sub-room 0 — that collapses the
two into one and makes an admin edit to a sub-room silently move the
room's advertised capacity.
Sub-room caps have two write paths onto the same column:
- **In game** (room owner only) — the "Max Player Count in This Subroom"
slider issues `PUT rooms/{id}/subrooms/{sub}/maxplayers`. Unlike most of
its siblings this one does **not** form-encode: `NLDBPDCNNCF.MNBPCGJNLNP`
(`Int64, Int64, Int32`) builds a `BLNOGFGHIIF` whose single JSON key is
`maxPlayers`, with verb 3 = PUT. It reads the reply back as
`FGCPNAACHIK`, so the response is the full room-details shape and has to
carry the new value.
- **Admin** (any room, ownership not required):
- `GET api/admin/v1/rooms/{id}/subrooms` — every sub-room with its cap,
matchmaking flag, sandbox flag and current data blob. Row ids go out as
**strings**; the SPA is JavaScript and mangles integers past 2^53.
- `PUT api/admin/v1/rooms/{id}/subrooms/{subRoomId}/maxplayers` with
`{"MaxPlayers": int}`. `subRoomId` is the sub-room's **order index** —
the id the client and the rest of the wire use — not the row's primary
key.
Both paths clamp to 1..80, matching the room-level cap in
`rooms/{id}/props`: 0 would make a sub-room unjoinable, and a shared range
keeps the two settings from disagreeing about what is a legal value.
Enforcement caveat carried over from `RoomEntity.MaxCapacity`: this is the
advertised cap. The client never sets Photon `RoomOptions.MaxPlayers`, so
hard enforcement still needs a ClientMod Photon patch.
## Build And Deploy
The service Dockerfile builds the admin SPA for service images that need