make new account + orientation work

This commit is contained in:
Devin Zuczek
2026-06-16 01:29:48 -04:00
parent 335b4d68ce
commit 1fa841f955
7 changed files with 72 additions and 73 deletions
+19 -12
View File
@@ -245,20 +245,15 @@ const app = new Hono<App>()
.notFound(withNotFound())
// ---- Player presence -----------------------------------------------------
// Login/exclusivelogin are no-op acks (matching every reference server). They
// MUST NOT touch presence: the client calls exclusivelogin when going online,
// and clearing here would wipe the room matchmake just stored → empty KV →
// the heartbeat reports no room. Only logout clears presence.
// login/exclusivelogin/logout are all no-op acks and MUST NOT touch presence.
// The client fires a spurious `player/logout` during the account-creation
// bootstrap (right after create_account seeds the new player into Orientation);
// deleting presence here wiped that seed and bounced the player to the dorm.
// Presence is overwritten by matchmake/goto and expires on its own TTL, so we
// don't need to clear it on these lifecycle calls.
.post('/player/login', (c) => c.body(null, 200))
.post('/player/exclusivelogin', (c) => c.json({ errorCode: 0 }))
// Logout: drop the player's presence (they're no longer in a room). Both
// reference servers expose this; returns 200.
.post('/player/logout', async (c) => {
const id = await authedId(c)
if (id !== null) await c.env.MATCH_PRESENCE.delete(presenceKey(id))
return c.body(null, 200)
})
.post('/player/logout', (c) => c.body(null, 200))
.get('/player', async (c) => {
// Returns each requested player's presence. The C# reads the `id` query
@@ -368,6 +363,18 @@ const app = new Hono<App>()
// isn't swallowed by the auth-gated matchmake handler.
.post('/matchmake/none', async (c) => {
const id = await authedId(c)
// FemRec (our 2023-client target) returns the player's *current* heartbeat
// here rather than forcing the dorm (the 2025 server's behavior we'd copied).
// Orientation is a solo room the client establishes via matchmake/none; if we
// force the dorm, the new player is warped out of Orientation within seconds.
// So: preserve existing presence; only fall back to the offline dorm when the
// player has none (e.g. the title screen before they've entered any room).
if (id !== null) {
const presence = await getPresence(c, id)
if (presence?.roomInstance) {
return c.json({ errorCode: 0, roomInstance: presence.roomInstance })
}
}
const instance = dormRoomInstance()
if (id !== null) await enterRoom(c, id, instance)
return c.json({ errorCode: 0, roomInstance: instance })
+29 -16
View File
@@ -154,7 +154,7 @@ describe('public endpoints', () => {
expect(await res.json()).toEqual({ errorCode: 20, roomInstance: null })
})
test('POST /matchmake/none returns the offline dorm', async () => {
test('POST /matchmake/none returns the offline dorm when the player has no presence', async () => {
const res = await exports.default.fetch(`${ORIGIN}/matchmake/none`, { method: 'POST' })
expect(res.status).toBe(200)
const body = (await res.json()) as {
@@ -170,6 +170,29 @@ describe('public endpoints', () => {
expect(body.roomInstance.photonRoomId).toMatch(/^[0-9a-f-]{36}$/)
})
test('POST /matchmake/none preserves an existing presence (does not warp to the dorm)', async () => {
const auth = await bearer('314')
// Put the player in a room first (RecCenter), establishing presence.
await exports.default.fetch(`${ORIGIN}/matchmake/2`, { method: 'POST', headers: auth })
// matchmake/none must return that same room, not force the dorm — this is
// what keeps a new player in the solo Orientation room.
const res = await exports.default.fetch(`${ORIGIN}/matchmake/none`, {
method: 'POST',
headers: auth,
})
expect(res.status).toBe(200)
const body = (await res.json()) as {
errorCode: number
roomInstance: { roomId: number; name: string; location: string }
}
expect(body.errorCode).toBe(0)
expect(body.roomInstance).toMatchObject({
roomId: 2,
name: '^RecCenter',
location: RECCENTER_SCENE,
})
})
test('PUT /player/statusvisibility returns 200', async () => {
const res = await exports.default.fetch(`${ORIGIN}/player/statusvisibility`, { method: 'PUT' })
expect(res.status).toBe(200)
@@ -340,23 +363,13 @@ describe('auth-gated endpoints', () => {
})
})
test('player/logout returns 200 and clears presence', async () => {
const headers = await bearer('77')
await exports.default.fetch(`${ORIGIN}/matchmake/dorm`, { method: 'POST', headers })
const out = await exports.default.fetch(`${ORIGIN}/player/logout`, { method: 'POST', headers })
expect(out.status).toBe(200)
const hb = (await (
await exports.default.fetch(`${ORIGIN}/player/heartbeat`, { method: 'POST', headers })
).json()) as { roomInstance: unknown; isOnline: boolean }
expect(hb.roomInstance).toBeNull()
expect(hb.isOnline).toBe(false)
})
test('login/exclusivelogin do NOT clear presence (only logout does)', async () => {
test('player/login, exclusivelogin and logout all preserve presence', async () => {
const headers = await bearer('9')
await exports.default.fetch(`${ORIGIN}/matchmake/dorm`, { method: 'POST', headers })
// The client calls exclusivelogin when going online — it must not wipe the
// room matchmake just stored.
// None of these lifecycle calls may wipe presence — the client fires a
// spurious logout during the account-creation bootstrap, and exclusivelogin
// when going online. Clearing here would bounce the player to the dorm.
await exports.default.fetch(`${ORIGIN}/player/logout`, { method: 'POST', headers })
await exports.default.fetch(`${ORIGIN}/player/exclusivelogin`, { method: 'POST', headers })
await exports.default.fetch(`${ORIGIN}/player/login`, { method: 'POST', headers })
const hb = (await (