record deviceclass

This commit is contained in:
Devin Zuczek
2026-07-14 11:26:11 -04:00
parent 229b00ada8
commit 2e057e93b7
5 changed files with 143 additions and 7 deletions
+15 -3
View File
@@ -4,6 +4,7 @@ import { useWorkersLogger } from 'workers-tagged-logger'
import {
createRoomInstance,
deleteExpiredPresence,
getAccount,
getExpiredPresenceInstanceIds,
getJoinableInstance,
getOrCreateDormRoom,
@@ -131,16 +132,27 @@ const GAME_VERSION = '20230302'
*/
const DEFAULT_GET_PLAYER = [{ ...playerPayload(1), isOnline: true }]
/** Store the room instance the player just matchmade into, preserving status. */
/**
* Store the room instance the player just matchmade into, preserving status.
*
* With no live presence to carry forward (the player's first matchmake after login,
* or one after their presence lapsed) the device fields would otherwise default —
* writing a screen player into the instance as deviceClass 0 until their next
* heartbeat corrects it. Everyone already in the room sees that stale class in the
* meantime, so fall back to what the account reported at login (auth stores
* `deviceClass`/`platform` from the token request) instead of to 0. The account read
* only happens on that no-presence path; a normal matchmake carries `prev` forward.
*/
async function enterRoom(c: Context<App>, id: number, roomInstance: RoomInstance): Promise<void> {
const prev = await getPresence<RoomInstance>(c.env.DB, id)
const account = prev ? null : await getAccount(c.env.DB, id)
await setPresence(c.env.DB, {
accountId: id,
roomInstance,
statusVisibility: prev?.statusVisibility ?? 0,
deviceClass: prev?.deviceClass ?? 0,
deviceClass: prev?.deviceClass ?? account?.deviceClass ?? 0,
vrMovementMode: prev?.vrMovementMode ?? 1,
platform: prev?.platform ?? 0,
platform: prev?.platform ?? account?.platform ?? 0,
appVersion: prev?.appVersion || GAME_VERSION,
})
// Keep the destination instance's is_full flag in sync with live presence (the
@@ -229,6 +229,28 @@ describe('public endpoints', () => {
})
})
test('POST /matchmake/room/:roomId seeds presence with the account device class', async () => {
// A screen player (deviceClass 2, recorded by auth at login) matchmaking with no
// live presence: without the account fallback they'd enter the room as deviceClass
// 0 (VR) until their next heartbeat, and everyone in the room would see that.
await env.DB.prepare('INSERT OR IGNORE INTO account (data) VALUES (?1)')
.bind(JSON.stringify({ accountId: 55, username: 'Screenie', deviceClass: 2, platform: 0 }))
.run()
const headers = await bearer('55')
const res = await exports.default.fetch(`${ORIGIN}/matchmake/room/2`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ JoinMode: '2' }).toString(),
})
expect(res.status).toBe(200)
const row = await env.DB.prepare('SELECT data FROM presence WHERE account_id = ?1')
.bind(55)
.first<{ data: string }>()
const presence = JSON.parse(row!.data) as { deviceClass: number }
expect(presence.deviceClass).toBe(2)
})
test('POST /matchmake/room/:roomId/:subRoomId enters that subroom', async () => {
type Instance = {
roomId: number