[match] tachyon support endpoint

This commit is contained in:
Devin Zuczek
2026-08-24 23:58:01 -04:00
parent bddd59ec1a
commit f9ffbcd374
3 changed files with 136 additions and 1 deletions
+58 -1
View File
@@ -61,6 +61,7 @@ import {
ExclusiveLoginResponse, ExclusiveLoginResponse,
form, form,
InProgressRequest, InProgressRequest,
InstanceIdResponse,
InviteRequest, InviteRequest,
InviteResponse, InviteResponse,
JoinModeRequest, JoinModeRequest,
@@ -612,7 +613,6 @@ function nextLiveMessageId(): number {
return Date.now() return Date.now()
} }
/** /**
* Deliver a game invite from `fromId` to `toId` for a room instance — a `MessageReceived` * Deliver a game invite from `fromId` to `toId` for a room instance — a `MessageReceived`
* frame carrying a game-invite `Message` the client renders the join prompt from. `data` * frame carrying a game-invite `Message` the client renders the join prompt from. `data`
@@ -662,6 +662,13 @@ async function sendGameInvite(
*/ */
const ORIENTATION_INSTANCE_ID = -2 const ORIENTATION_INSTANCE_ID = -2
/**
* What `/tachyon` answers for a player who is not in an instance. A stored
* `room_instance` id is never 0 — the table is AUTOINCREMENT from 1 — so the sentinel can't
* be mistaken for a real instance, and the body stays a number the way the caller reads it.
*/
const NO_INSTANCE = 0
/** /**
* Instance-relevant fields pulled from a stored room (scene, name, capacity, …). * Instance-relevant fields pulled from a stored room (scene, name, capacity, …).
* The `location` is the SubRoom's real `UnitySceneId` — an empty/unknown location * The `location` is the SubRoom's real `UnitySceneId` — an empty/unknown location
@@ -1389,6 +1396,56 @@ const app = new Hono<App>()
} }
) )
// Which room instance a player is in, as a bare number (`/tachyon?id=123`). The whole
// presence blob is what `/player` serves; this answers the one field out of it, for a
// caller that only needs to know where someone is.
//
// UNGATED, unlike the rest of this worker's presence surface: it takes the player id from
// the query rather than from a token, so anyone can ask about anyone. What it discloses
// is an instance id and nothing else — no room, no name, no status — and `/player` is
// already ungated on the same rows.
//
// 0 for "not in an instance": the body is a number, so absence has to be one too, and a
// real instance id is never 0 (the `room_instance` table is AUTOINCREMENT from 1). The
// synthetic negatives ARE real answers and pass through — -2 is the Orientation seed.
.get(
'/tachyon',
describeRoute({
tags: ['Presence'],
summary: 'The room instance a player is in',
description: [
'The `roomInstanceId` from a players live presence (`?id=123`), as a BARE NUMBER —',
'the whole body is the id, not an object around it. The single field out of what',
'`/player` serves whole.',
'',
'0 means not in an instance: no live presence, an expired row, or no usable `id`. A',
'real instance id is never 0, so the sentinel cant collide with one. Synthetic ids',
'pass through as they stand — -2 is the Orientation presence the `auth` worker seeds',
'a brand-new player with.',
'',
'Ungated: the player is named by the query rather than by a token, so anyone may ask',
'about anyone. It discloses an instance id and nothing else.',
].join(' '),
parameters: [
{
name: 'id',
in: 'query',
required: false,
description: 'The account to look up. Absent or unparseable answers 0',
schema: { type: 'integer' },
},
],
responses: { 200: json(InstanceIdResponse, 'The instance id, or 0') },
}),
async (c) => {
const playerId = Number.parseInt(c.req.query('id') ?? '', 10)
if (!Number.isInteger(playerId)) return c.json(NO_INSTANCE)
const presence = await getPresence<RoomInstance>(c.env.DB, playerId)
return c.json(presence?.roomInstance?.roomInstanceId ?? NO_INSTANCE)
}
)
.get( .get(
'/player', '/player',
describeRoute({ describeRoute({
+12
View File
@@ -401,3 +401,15 @@ export const InviteResponse = z.object({
.nullable() .nullable()
.describe('The room the invite points at; null when the room instance didnt resolve'), .describe('The room the invite points at; null when the room instance didnt resolve'),
}) })
/**
* `GET /tachyon?id=…` — the room instance a player is in, as a BARE NUMBER: the whole body
* is the id, with no object around it.
*
* 0 means "not in one" — no live presence for that account, an expired row, or no `id`
* given. Presence rows carry synthetic ids too, which are passed through as they stand:
* -2 is the Orientation seed the `auth` worker writes for a brand-new player.
*/
export const InstanceIdResponse = z
.int()
.describe('The players room instance id, or 0 when they are not in one')
@@ -332,6 +332,71 @@ describe('public endpoints', () => {
]) ])
}) })
test('GET /tachyon?id=N answers the bare instance id the player is in', async () => {
// Ungated — no bearer token anywhere in this test. The player is named by the query.
const tachyon = async (query: string) => {
const res = await exports.default.fetch(`${ORIGIN}/tachyon${query}`)
expect(res.status).toBe(200)
return res.json()
}
// Nobody has presence for 4242, so they are in nothing. 0 rather than null: the body
// is a number, and a real instance id is never 0.
expect(await tachyon('?id=4242')).toBe(0)
// Put someone in a room the ordinary way, and the id is the one the matchmake handed
// them — the same field `/player` serves inside the whole presence blob.
const matchmake = await exports.default.fetch(`${ORIGIN}/matchmake/room/2`, {
method: 'POST',
headers: { ...(await bearer('4243')), 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ JoinMode: '2' }).toString(),
})
const { roomInstance } = (await matchmake.json()) as {
roomInstance: { roomInstanceId: number } | null
}
expect(roomInstance).not.toBeNull()
expect(await tachyon('?id=4243')).toBe(roomInstance!.roomInstanceId)
// A synthetic instance id is a real answer and passes through — -2 is the Orientation
// presence `auth` seeds a new player with.
await env.DB.prepare('INSERT OR REPLACE INTO presence (data) VALUES (?1)')
.bind(
JSON.stringify({
accountId: 4244,
roomInstance: { roomInstanceId: -2 },
statusVisibility: 0,
deviceClass: 0,
vrMovementMode: 1,
platform: 0,
appVersion: GAME_VERSION,
expiresAt: Math.floor(Date.now() / 1000) + 900,
})
)
.run()
expect(await tachyon('?id=4244')).toBe(-2)
// An expired row is not presence, so its player is in nothing.
await env.DB.prepare('INSERT OR REPLACE INTO presence (data) VALUES (?1)')
.bind(
JSON.stringify({
accountId: 4245,
roomInstance: { roomInstanceId: 987 },
statusVisibility: 0,
deviceClass: 0,
vrMovementMode: 1,
platform: 0,
appVersion: GAME_VERSION,
expiresAt: Math.floor(Date.now() / 1000) - 60,
})
)
.run()
expect(await tachyon('?id=4245')).toBe(0)
// No id, and an unparseable one, answer the same 0 rather than erroring.
expect(await tachyon('')).toBe(0)
expect(await tachyon('?id=notanumber')).toBe(0)
})
test('GET /player?id=&id= returns one payload per id, in order', async () => { test('GET /player?id=&id= returns one payload per id, in order', async () => {
const res = await exports.default.fetch(`${ORIGIN}/player?id=1070&id=1380`) const res = await exports.default.fetch(`${ORIGIN}/player?id=1070&id=1380`)
const players = (await res.json()) as Array<{ playerId: number; isOnline: boolean }> const players = (await res.json()) as Array<{ playerId: number; isOnline: boolean }>
@@ -2654,6 +2719,7 @@ describe('auth-gated endpoints', () => {
'GET /room/{roomId}/instances', 'GET /room/{roomId}/instances',
'GET /rooms/requiring/developer', 'GET /rooms/requiring/developer',
'GET /rooms/requiring/rrplus', 'GET /rooms/requiring/rrplus',
'GET /tachyon',
'POST /invite', 'POST /invite',
'POST /matchmake/club/{clubId}', 'POST /matchmake/club/{clubId}',
'POST /matchmake/dorm', 'POST /matchmake/dorm',