support for 202507 endpoints (#37)

* [auth][api] accept the 20250424.01 client

* [2025] unstable

* 20250718.0

* correct one this time

* stubs

* more stubs

* more stubs

* [lists] add worker

* [ai] route stubs

* [api] player photo setting

* [econ] add roomEconConfig route

* [infra] update worker generators

* [worker] add cards/moderation/platformnotification workers

* [lists] updates to some endpoints

* [clubs] stub out announcement endpoint, for now

* [econ] stub out season endpoints for now

* [chat] apps/chat stub out party endpoint not sure the shape yet

* [api] stub out statsig and lockeditems

* [doc] new services

* [lists] stub the bulk endpoint

* [datacollection] add placeholder service until we can kill it

* [api] set gifting to lvl5

* update lock

* [cdn] enable cache

* [match] matchmake v2

* [lists] stub some lists

* [ai] stubs

* [rooms] new subroom save endpoint

* [econ] add bulk purchase endpoint

* [discovery] update featured creator to 1 for fun

* [api] add photo settings flag

* [chat] fixup chat permissions (sorta)

* [auth] restrictions endpoint

* [rooms] contributed endpoint

* [api] fix outfit endpoint

* [discovery] attempt to fix store

* [chat] privacy endpoints

* [api] cheered images

* [rooms] add xp endpoint (disbaled)

* [rooms] add xp endpoint (disabled)

* update images-db for cheers

* [rooms] add autocomplete endpoint

* [cdn/img] increase cache ttl for statics

* [api] bulk route for images

* [accounts] add banner image

* [api] add misc missing endpoints

* [discovery] remove AI tab

* [platformnotifications] stub some endpoints

* [lists] add some more lists

* [rooms] additional endpoints

* [chat] stub a few privacy endpoints

* [econ] stub some endpoints

* misc db fixes

* [api] tweak shape for images v6

* [rooms] dont show trending RROs
This commit is contained in:
devin
2026-08-18 23:07:24 -04:00
committed by Devin Zuczek
parent 66c09806f9
commit 178d3b5b0e
162 changed files with 114930 additions and 469 deletions
+160 -7
View File
@@ -140,14 +140,24 @@ export const PlayerDto = z.object({
})
/**
* The matchmake result envelope. `errorCode` 0 with a `roomInstance` is success;
* a non-zero code (e.g. 20 NoSuchRoom) comes with `roomInstance: null`.
* The matchmake result envelope. Code 0 with a `roomInstance` is success; a non-zero
* code (e.g. 20 NoSuchRoom) comes with `roomInstance: null`.
*
* `result` and `errorCode` are the same code under two names: `result` is what the
* client reads, `errorCode` is what this server has always sent (and what its own tests
* read), so both are served and they always agree. `correlationId` echoes the
* `CorrelationId` the request was tagged with — without it the client never matches the
* response to the attempt and fails with "Unable to connect to game session".
*/
export const MatchmakeResponse = z.object({
result: z.int().describe('The join-result code the client checks first; same as errorCode'),
errorCode: z
.int()
.describe('0 = success; 20 = NoSuchRoom; 55 = banned from the room (the one non-opaque code)'),
roomInstance: RoomInstanceDto.nullable(),
correlationId: z
.string()
.describe('Echoes the requests CorrelationId; all-zero GUID when it sent none'),
})
/**
@@ -170,6 +180,65 @@ export const AvoidJuniorsRequest = z.object({
/** `POST /player/exclusivelogin` — a bare error code. */
export const ExclusiveLoginResponse = z.object({ errorCode: z.int().describe('Always 0') })
/**
* The networking feature flags the client reads off its connection info — verbatim
* from the reference server. The client changes how it replicates based on these, so
* they are not free to tune. `shouldUseGameServerNetworking` is the load-bearing one:
* true points the client at a local game server (127.0.0.1:7777) instead of Photon.
*/
export const ConnectionExperiments = z.object({
networkTransformSyncInterval: z.number(),
shouldUseUnreliableOnChange: z.boolean(),
shouldAvoidDiscontinuityRPCs: z.boolean(),
shouldAvoidRedundantDiscontinuity: z.boolean(),
r2RuntimeStaticBaking: z.boolean(),
r2AutoEmbodiment: z.boolean(),
r2RuntimeStaticBakingMinShapeThreshold: z.int(),
r2UseCheapReplicas: z.boolean(),
shouldUseGameServerNetworking: z
.boolean()
.describe('true connects to a local game server instead of Photon'),
})
/**
* `GET /player/connection-info` — the realtime (Photon) credentials, in a
* `{ success, value, error }` envelope. The applications and region are fixed for
* recflare; what varies per caller is `photonAuthToken` (minted for them on the spot)
* and `photonRoomId`, the Photon room of the instance their presence says they're in
* — the same name every other player in that instance is handed. There's no separate
* voice server, so both voice fields are null. `photonRegion` matches the one stamped
* on every room instance, so the two can't disagree.
*/
export const ConnectionInfo = z.object({
photonAuthToken: z.string().describe('Short-lived HS256 token identifying the caller to Photon'),
photonRealtimeAppId: z.string().describe('Photon Realtime application id'),
photonVoiceAppId: z.string().describe('Photon Voice application id'),
photonChatAppId: z.string().describe('Photon Chat application id'),
photonRegion: z.string().describe('Region id, matching a room instances `photonRegion`'),
photonRoomId: z.string().describe('The callers current instance; empty when theyre in none'),
voiceConnectionInfo: z.literal('').describe('Empty — no separate voice server'),
voiceServerId: z.literal('').describe('Empty — no separate voice server'),
experiments: ConnectionExperiments,
})
/** `GET /player/connection-info` — the connection info in the client's standard envelope. */
export const ConnectionInfoResponse = z.object({
success: z.literal(true),
value: ConnectionInfo,
error: z.null(),
})
/**
* One QoS probe target (`GET /player/qos`) — a region the client pings to measure
* latency, then reports back through `PUT /player/photonregionpings`. A bare array,
* not the `{ success, value, error }` envelope. `id` is the region id the pings are
* keyed by; `address` is `host:port`, not a URL.
*/
export const QosRegion = z.object({
id: z.string().describe('Region id, e.g. `us-east1`'),
address: z.string().describe('`host:port` of the probe endpoint'),
})
/**
* The session `LoginLock` GUID form field. The client posts it on every presence
* lifecycle call — `POST /player/login`, `/player/exclusivelogin`, `/player/logout`,
@@ -199,11 +268,27 @@ export const NotifyDisconnectRequest = z.object({
RoomInstanceId: z.string().describe('The room instance they dropped'),
})
/**
* The `CorrelationId` every matchmake carries — a GUID the client generates per attempt
* and expects back on the response (see `MatchmakeResponse`). Posted in the form body;
* the field is matched case-insensitively and a query param is accepted too, for the
* matchmakes that post no body at all.
*
* This is the whole body of the target-less matchmakes (`/matchmake/dorm`,
* `/matchmake/none`, `/matchmake/player/:id`, `/matchmake/instance/:id`), which is why
* it's a schema of its own; the room matchmakes extend it. Other fields the client sends
* (`LoginLock`, `MaxPersistenceVersion`, `VoiceServerVersion`,
* `BypassMovementModeRestriction`) are accepted and ignored.
*/
export const CorrelationIdRequest = z.object({
CorrelationId: z.string().optional().describe('Per-attempt GUID; echoed on the response'),
})
/**
* The `JoinMode` form field the matchmake routes read (`2` = a private instance;
* anything else = public). Posted as a urlencoded/multipart body.
*/
export const JoinModeRequest = z.object({
export const JoinModeRequest = CorrelationIdRequest.extend({
JoinMode: z.string().optional().describe('"2" requests a private instance'),
})
@@ -211,11 +296,12 @@ export const JoinModeRequest = z.object({
* The room-matchmake form body (`/matchmake/room/:roomId[/:subRoomId]`). Beyond
* `JoinMode` the 2023 client posts `AdditionalPlayerIds` — the caller's party — so each
* of them is invited (a game invite) into the instance the leader lands in. It's a
* repeated field (one id each, not comma-separated). Other fields the client sends
* (`LoginLock`, `MaxPersistenceVersion`, `BypassMovementModeRestriction`) are accepted
* and ignored.
* repeated field (one id each, not comma-separated). `CorrelationId` rides along as it
* does on every matchmake. Other fields the client sends (`LoginLock`,
* `MaxPersistenceVersion`, `VoiceServerVersion`, `BypassMovementModeRestriction`) are
* accepted and ignored.
*/
export const MatchmakeRoomRequest = z.object({
export const MatchmakeRoomRequest = CorrelationIdRequest.extend({
JoinMode: z.string().optional().describe('"2" requests a private instance'),
AdditionalPlayerIds: z
.string()
@@ -223,6 +309,73 @@ export const MatchmakeRoomRequest = z.object({
.describe('Party members to invite into the room; repeated once per id'),
})
/**
* The v2 room-matchmake body (`/matchmake/v2/room/:roomId[/:subRoomId]`). Unlike the 2023
* client's urlencoded form, the newer client posts JSON with real types: `JoinMode` is a
* number, and `AdditionalPlayerIds` is an array — `null`, not `[]`, when the player is
* alone. Only `JoinMode`, `AdditionalPlayerIds` and `CorrelationId` are read; the rest are
* accepted and ignored, and are recorded here because they are what the client actually
* sends.
*/
export const MatchmakeRoomV2Request = z.object({
CorrelationId: z.string().optional().describe('Per-attempt GUID; echoed on the response'),
JoinMode: z.int().optional().describe('2 requests a private instance'),
AdditionalPlayerIds: z
.array(z.int())
.nullable()
.optional()
.describe('Party members to invite into the room; null when the player is alone'),
InviteMode: z.int().optional(),
ShouldKeepPlayerWithParty: z.boolean().optional(),
BypassMovementModeRestriction: z.boolean().optional(),
MaxPersistenceVersion: z.int().optional(),
Ugc1SubVersion: z.int().optional(),
Ugc2SubVersion: z.int().optional(),
VoiceServerVersion: z.string().optional(),
LoginLock: z.string().optional(),
ClientJoinData: z.string().nullable().optional(),
PlayerScores: z.unknown().optional(),
})
/**
* The v2 client's room instance: PascalCase, and a SUBSET of `RoomInstanceDto`. The
* reference server's v2 response carries no `DataBlob` and no Photon coordinates at all,
* and adds `MatchmakingPolicy` — this mirrors it field for field. The instance behind it
* is the same row a v1 matchmake answers with; only the projection differs.
*/
export const RoomInstanceV2Dto = z.object({
RoomInstanceId: z.int(),
RoomId: z.int(),
SubRoomId: z.int(),
Location: z.string().describe('SubRoom Unity scene id; empty is rejected by the client'),
EventId: z.int(),
ClubId: z.int(),
RoomCode: z.string(),
Name: z.string(),
MaxCapacity: z.int(),
IsFull: z.boolean(),
IsPrivate: z.boolean(),
IsInProgress: z.boolean(),
EncryptVoiceChat: z.boolean(),
RoomInstanceType: RoomInstanceType,
MatchmakingPolicy: z.int().describe('Always 0; this server has no policy to express'),
})
/**
* The v2 matchmake envelope. Same codes and the same correlation-id echo as
* `MatchmakeResponse`, but PascalCase and with no `result` twin — the v2 client reads
* `ErrorCode`. Refusals answer `RoomInstance: null` with a non-zero `ErrorCode`.
*/
export const MatchmakeV2Response = z.object({
ErrorCode: z
.int()
.describe('0 = success; 20 = NoSuchRoom; 55 = banned from the room (the one non-opaque code)'),
CorrelationId: z
.string()
.describe('Echoes the requests CorrelationId; all-zero GUID when it sent none'),
RoomInstance: RoomInstanceV2Dto.nullable(),
})
/** `POST /invite` form body — invite a player into the caller's room instance. */
export const InviteRequest = z.object({
playerId: z.string().describe('The account to invite; a non-zero integer (else 400)'),