mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
make new account + orientation work
This commit is contained in:
@@ -9,40 +9,6 @@ export interface PlayerSetting {
|
||||
}
|
||||
|
||||
const DEFAULTS: ReadonlyArray<readonly [key: string, value: string]> = [
|
||||
['Recroom.OOBE', '77'],
|
||||
[
|
||||
'SplitTestAssignedSegments',
|
||||
'1|{"SplitTesting+PhotonMaxDatagrams_2021_01_11":"Off","SplitTesting+Curated_Rooms_2020_08_06":"Off","SplitTesting+RoomRecommendationsType_2020_08_14":"Aug14MinVisitors35000"}',
|
||||
],
|
||||
['PlayerSessionCount', '13'],
|
||||
['TUTORIAL_COMPLETE_MASK', '11'],
|
||||
['BACKPACK_FAVORITE_TOOL', '1'],
|
||||
['VoiceChat', '2'],
|
||||
['VRAUTOSPRINT', '1'],
|
||||
['VR_MOVEMENT_MODE', '0'],
|
||||
['COMFORT_SPRINT', '0'],
|
||||
['COMFORT_WALK', '0'],
|
||||
['COMFORT_VEHICLES', '0'],
|
||||
['COMFORT_FLY', '0'],
|
||||
['COMFORT_ROTATE', '0'],
|
||||
['COMFORT_FORCES', '0'],
|
||||
['COMFORT_FALL', '0'],
|
||||
['COMFORT_TELEPORT', '0'],
|
||||
['ROTATE_IN_PLACE_ENABLED', '1'],
|
||||
['ROTATION_INCREMENT', '2'],
|
||||
['CONTINUOUS_ROTATION_MODE', '1'],
|
||||
['DONT_LOCK_TOOLS_TO_HAND', '0'],
|
||||
['QualitySettings', '2'],
|
||||
['TeleportBuffer', '0'],
|
||||
['IgnoreBuffer', '1'],
|
||||
['FIRST_TIME_IN_FLAGS', '0'],
|
||||
['ShowRoomCenter', '1'],
|
||||
['USER_TRACKING', '1'],
|
||||
['STABILIZE_HANDS', '0'],
|
||||
['MakerPen_SnappingMode', '2'],
|
||||
['Recroom.ChallengeMap', '17'],
|
||||
['VoiceFilter2', '1'],
|
||||
['SFX_VOLUME_PERCENT_PREF', '1'],
|
||||
]
|
||||
|
||||
export function defaultSettings(playerId: number): PlayerSetting[] {
|
||||
|
||||
@@ -28,6 +28,12 @@ const PLATFORM_TYPES: Record<number, string> = {
|
||||
|
||||
/** New players start in the Orientation room (RoomId 13) — the new-user flow. */
|
||||
const ORIENTATION_ROOM_ID = 13
|
||||
/**
|
||||
* The client loads Orientation locally (no matchmake) and tags its instance with
|
||||
* the sentinel id -2. The heartbeat must echo that exact `roomInstanceId` or the
|
||||
* client treats presence as out-of-sync and bounces the player to the dorm.
|
||||
*/
|
||||
const ORIENTATION_INSTANCE_ID = -2
|
||||
/** Presence TTL (s) — matches the match worker; refreshed by each heartbeat. */
|
||||
const PRESENCE_TTL = 900
|
||||
|
||||
@@ -54,7 +60,7 @@ async function placeNewPlayerInOrientation(env: App['Bindings'], accountId: numb
|
||||
const num = (v: unknown, fallback: number) => (typeof v === 'number' ? v : fallback)
|
||||
|
||||
const roomInstance = {
|
||||
roomInstanceId: ORIENTATION_ROOM_ID,
|
||||
roomInstanceId: ORIENTATION_INSTANCE_ID,
|
||||
roomId: ORIENTATION_ROOM_ID,
|
||||
subRoomId: num(sub?.SubRoomId, 1),
|
||||
roomInstanceType: 0,
|
||||
|
||||
@@ -140,10 +140,11 @@ describe('auth worker routes', () => {
|
||||
const payload = await tokenFor('grant_type=create_account&platform_id=steam-456')
|
||||
const sub = payload.sub as string
|
||||
const presence = await env.MATCH_PRESENCE.get<{
|
||||
roomInstance: { roomId: number; location: string; name: string }
|
||||
roomInstance: { roomInstanceId: number; roomId: number; location: string; name: string }
|
||||
}>(`presence:${sub}`, 'json')
|
||||
expect(presence).not.toBeNull()
|
||||
expect(presence!.roomInstance).toMatchObject({
|
||||
roomInstanceId: -2,
|
||||
roomId: 13,
|
||||
location: ORIENTATION_SCENE,
|
||||
name: '^Orientation',
|
||||
|
||||
+19
-12
@@ -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 })
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
/**
|
||||
* Default player settings seeded on a player's first read, ported verbatim from
|
||||
* the C# `PlayerSettingsController.GetPlayerSettings`. Ordered; written to KV the
|
||||
* first time a player has no stored settings.
|
||||
* Default player settings seeded on a player's first read. Based on the C#
|
||||
* `PlayerSettingsController.GetPlayerSettings`, but the onboarding/progress flags
|
||||
* are reset to a *fresh* player — the C# values were copied from an already-
|
||||
* onboarded account, which made new accounts skip Orientation (the client saw
|
||||
* OOBE/tutorials as complete and warped them to the dorm within seconds).
|
||||
* Ordered; written to KV the first time a player has no stored settings.
|
||||
*/
|
||||
export const DEFAULT_SETTINGS: Array<{ Key: string; Value: string }> = [
|
||||
{ Key: 'Recroom.OOBE', Value: '77' },
|
||||
// 0 = no OOBE steps done, so the new-user Orientation flow runs.
|
||||
{ Key: 'Recroom.OOBE', Value: '0' },
|
||||
{
|
||||
Key: 'SplitTestAssignedSegments',
|
||||
Value:
|
||||
'1|{"SplitTesting+PhotonMaxDatagrams_2021_01_11":"Off","SplitTesting+Curated_Rooms_2020_08_06":"Off","SplitTesting+RoomRecommendationsType_2020_08_14":"Aug14MinVisitors35000"}',
|
||||
},
|
||||
{ Key: 'PlayerSessionCount', Value: '13' },
|
||||
{ Key: 'TUTORIAL_COMPLETE_MASK', Value: '11' },
|
||||
{ Key: 'PlayerSessionCount', Value: '0' },
|
||||
// 0 = no tutorials completed (fresh player).
|
||||
{ Key: 'TUTORIAL_COMPLETE_MASK', Value: '0' },
|
||||
{ Key: 'BACKPACK_FAVORITE_TOOL', Value: '1' },
|
||||
{ Key: 'VoiceChat', Value: '2' },
|
||||
{ Key: 'VRAUTOSPRINT', Value: '1' },
|
||||
|
||||
@@ -66,12 +66,13 @@ describe('playersettings endpoints', () => {
|
||||
const settings = (await res.json()) as Array<{ PlayerId: number; Key: string; Value: string }>
|
||||
expect(settings.length).toBeGreaterThan(0)
|
||||
expect(settings.every((s) => s.PlayerId === 100)).toBe(true)
|
||||
expect(settings.find((s) => s.Key === 'Recroom.OOBE')?.Value).toBe('77')
|
||||
expect(settings.find((s) => s.Key === 'PlayerSessionCount')?.Value).toBe('13')
|
||||
// Fresh-player onboarding defaults so the new-user Orientation flow runs.
|
||||
expect(settings.find((s) => s.Key === 'Recroom.OOBE')?.Value).toBe('0')
|
||||
expect(settings.find((s) => s.Key === 'TUTORIAL_COMPLETE_MASK')?.Value).toBe('0')
|
||||
|
||||
// Defaults were persisted to KV.
|
||||
const stored = await env.PLAYER_SETTINGS.get<Record<string, string>>('player:100', 'json')
|
||||
expect(stored?.['Recroom.OOBE']).toBe('77')
|
||||
expect(stored?.['Recroom.OOBE']).toBe('0')
|
||||
})
|
||||
|
||||
it('GET /playersettings reflects a value written by PUT', async () => {
|
||||
|
||||
Reference in New Issue
Block a user