mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
add interactions (cheer/faves)
This commit is contained in:
+31
-11
@@ -31,7 +31,7 @@ const DEFAULT_GET_PLAYER = [
|
||||
vrMovementMode: 1,
|
||||
roomInstance: null,
|
||||
isOnline: true,
|
||||
appVersion: '20210129',
|
||||
appVersion: '20230302',
|
||||
platform: 0,
|
||||
},
|
||||
]
|
||||
@@ -88,6 +88,14 @@ interface Presence {
|
||||
/** Presence is kept this long (s) after the last matchmake/heartbeat refresh. */
|
||||
const PRESENCE_TTL = 900
|
||||
|
||||
/**
|
||||
* Game build version reported in presence. Like the reference servers (FemRec
|
||||
* `ServerConfig.GameVersion`, 2025 `HeartbeatDB`), this is a server-side
|
||||
* constant — the client doesn't supply it, and an empty value breaks the
|
||||
* client's presence/version handling. Matches our target 2023 client build.
|
||||
*/
|
||||
const GAME_VERSION = '20230302'
|
||||
|
||||
const presenceKey = (id: number) => `presence:${id}`
|
||||
|
||||
/** Persist the player's presence (room instance + status), refreshing the TTL. */
|
||||
@@ -111,7 +119,7 @@ async function enterRoom(c: Context<App>, id: number, roomInstance: RoomInstance
|
||||
deviceClass: prev?.deviceClass ?? 0,
|
||||
vrMovementMode: prev?.vrMovementMode ?? 1,
|
||||
platform: prev?.platform ?? 0,
|
||||
appVersion: prev?.appVersion ?? '',
|
||||
appVersion: prev?.appVersion || GAME_VERSION,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -146,7 +154,7 @@ function dormRoomInstance() {
|
||||
photonRegion: 'us',
|
||||
photonRegionId: 'us',
|
||||
photonRoomId: DORM_PHOTON_ROOM_ID,
|
||||
name: 'DormRoom',
|
||||
name: '^DormRoom',
|
||||
maxCapacity: 4,
|
||||
isFull: false,
|
||||
isPrivate: true,
|
||||
@@ -167,10 +175,21 @@ function roomInstanceFromRoom(room: Room, isPrivate: boolean): RoomInstance {
|
||||
| undefined
|
||||
const str = (v: unknown, fallback = '') => (typeof v === 'string' ? v : fallback)
|
||||
const num = (v: unknown, fallback: number) => (typeof v === 'number' ? v : fallback)
|
||||
const roomId = num(room.RoomId, 1)
|
||||
// All room instance names are prefixed with `^` (the username prefix `@` is a
|
||||
// separate thing, e.g. a dorm is `^@user's Dorm`). The client uses this prefix
|
||||
// to resolve the instance; without it the new scene won't load. Matches Stella.
|
||||
const rawName = str(room.Name, 'Room')
|
||||
const name = rawName.startsWith('^') ? rawName : `^${rawName}`
|
||||
// roomInstanceId must differ from the room the player is leaving — the client
|
||||
// keys the transition off it. The dorm is instance 1, so a room that also
|
||||
// returned 1 looked like "no change". Use the room id (per Stella), with a
|
||||
// unique suffix-free deterministic Photon room so public players share it.
|
||||
const photonRoomId = isPrivate ? `rec.${roomId}.${crypto.randomUUID()}` : `rec.${roomId}`
|
||||
return {
|
||||
roomInstanceId: 1,
|
||||
roomId: num(room.RoomId, 1),
|
||||
subRoomId: num(sub?.SubRoomId, 0),
|
||||
roomInstanceId: roomId,
|
||||
roomId,
|
||||
subRoomId: num(sub?.SubRoomId, 1),
|
||||
roomInstanceType: room.IsDorm === true ? 2 : 0,
|
||||
location: str(sub?.UnitySceneId),
|
||||
dataBlob: str(sub?.DataBlob),
|
||||
@@ -179,8 +198,8 @@ function roomInstanceFromRoom(room: Room, isPrivate: boolean): RoomInstance {
|
||||
roomCode: '',
|
||||
photonRegion: 'us',
|
||||
photonRegionId: 'us',
|
||||
photonRoomId: crypto.randomUUID(),
|
||||
name: str(room.Name),
|
||||
photonRoomId,
|
||||
name,
|
||||
maxCapacity: num(sub?.MaxPlayers, 4),
|
||||
isFull: false,
|
||||
isPrivate: isPrivate || room.IsDorm === true,
|
||||
@@ -261,7 +280,7 @@ const app = new Hono<App>()
|
||||
vrMovementMode: p?.vrMovementMode ?? 1,
|
||||
roomInstance: p?.roomInstance ?? null,
|
||||
isOnline: p?.roomInstance != null,
|
||||
appVersion: p?.appVersion ?? '',
|
||||
appVersion: p?.appVersion || GAME_VERSION,
|
||||
platform: p?.platform ?? 0,
|
||||
}
|
||||
})
|
||||
@@ -295,7 +314,8 @@ const app = new Hono<App>()
|
||||
if (hb.deviceClass !== undefined) presence.deviceClass = hb.deviceClass
|
||||
if (hb.vrMovementMode !== undefined) presence.vrMovementMode = hb.vrMovementMode
|
||||
if (hb.platform !== undefined) presence.platform = hb.platform
|
||||
if (hb.appVersion != null) presence.appVersion = hb.appVersion
|
||||
if (hb.appVersion) presence.appVersion = hb.appVersion
|
||||
if (!presence.appVersion) presence.appVersion = GAME_VERSION
|
||||
await setPresence(c, id, presence)
|
||||
}
|
||||
|
||||
@@ -306,7 +326,7 @@ const app = new Hono<App>()
|
||||
vrMovementMode: presence?.vrMovementMode ?? (hb.vrMovementMode ? hb.vrMovementMode : 1),
|
||||
roomInstance: presence?.roomInstance ?? null,
|
||||
isOnline: presence?.roomInstance != null,
|
||||
appVersion: presence?.appVersion ?? hb.appVersion ?? '',
|
||||
appVersion: presence?.appVersion || hb.appVersion || GAME_VERSION,
|
||||
platform: presence?.platform ?? hb.platform ?? 0,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -96,7 +96,7 @@ describe('public endpoints', () => {
|
||||
expect(players[0]).toMatchObject({
|
||||
playerId: 99,
|
||||
isOnline: false,
|
||||
appVersion: '',
|
||||
appVersion: '20230302',
|
||||
roomInstance: null,
|
||||
})
|
||||
})
|
||||
@@ -105,7 +105,7 @@ describe('public endpoints', () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/player`)
|
||||
expect(res.status).toBe(200)
|
||||
const players = (await res.json()) as Array<{ playerId: number; isOnline: boolean }>
|
||||
expect(players[0]).toMatchObject({ playerId: 1, isOnline: true, appVersion: '20210129' })
|
||||
expect(players[0]).toMatchObject({ playerId: 1, isOnline: true, appVersion: '20230302' })
|
||||
})
|
||||
|
||||
test('POST /goto/none returns the offline dorm', async () => {
|
||||
@@ -117,7 +117,7 @@ describe('public endpoints', () => {
|
||||
}
|
||||
expect(body.errorCode).toBe(0)
|
||||
expect(body.roomInstance).toMatchObject({
|
||||
name: 'DormRoom',
|
||||
name: '^DormRoom',
|
||||
location: '76d98498-60a1-430c-ab76-b54a29b7a163',
|
||||
isPrivate: true,
|
||||
})
|
||||
@@ -139,7 +139,7 @@ describe('public endpoints', () => {
|
||||
expect(body.errorCode).toBe(0)
|
||||
expect(body.roomInstance).toMatchObject({
|
||||
roomId: 2,
|
||||
name: 'RecCenter',
|
||||
name: '^RecCenter',
|
||||
location: RECCENTER_SCENE,
|
||||
isPrivate: true,
|
||||
})
|
||||
@@ -163,7 +163,7 @@ describe('public endpoints', () => {
|
||||
}
|
||||
expect(body.errorCode).toBe(0)
|
||||
expect(body.roomInstance).toMatchObject({
|
||||
name: 'DormRoom',
|
||||
name: '^DormRoom',
|
||||
location: '76d98498-60a1-430c-ab76-b54a29b7a163',
|
||||
isPrivate: true,
|
||||
})
|
||||
@@ -206,7 +206,7 @@ describe('auth-gated endpoints', () => {
|
||||
}
|
||||
expect(body.errorCode).toBe(0)
|
||||
expect(body.roomInstance).toMatchObject({
|
||||
name: 'DormRoom',
|
||||
name: '^DormRoom',
|
||||
location: '76d98498-60a1-430c-ab76-b54a29b7a163',
|
||||
isPrivate: true,
|
||||
roomId: 1,
|
||||
@@ -221,14 +221,26 @@ describe('auth-gated endpoints', () => {
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
const body = (await res.json()) as {
|
||||
roomInstance: { roomId: number; isPrivate: boolean; name: string; location: string }
|
||||
roomInstance: {
|
||||
roomId: number
|
||||
roomInstanceId: number
|
||||
isPrivate: boolean
|
||||
name: string
|
||||
location: string
|
||||
photonRoomId: string
|
||||
}
|
||||
}
|
||||
expect(body.roomInstance).toMatchObject({
|
||||
roomId: 2,
|
||||
name: 'RecCenter',
|
||||
// Must differ from the dorm's instance id (1) so the client treats this
|
||||
// as a new room and actually loads the scene.
|
||||
roomInstanceId: 2,
|
||||
name: '^RecCenter',
|
||||
location: RECCENTER_SCENE,
|
||||
isPrivate: true,
|
||||
})
|
||||
// Private instances get a unique Photon room id; public share `rec.<roomId>`.
|
||||
expect(body.roomInstance.photonRoomId.startsWith('rec.2')).toBe(true)
|
||||
})
|
||||
|
||||
test('POST /matchmake/:room 401s without a token', async () => {
|
||||
@@ -248,7 +260,7 @@ describe('auth-gated endpoints', () => {
|
||||
}
|
||||
expect(body.errorCode).toBe(0)
|
||||
expect(body.roomInstance).toMatchObject({
|
||||
name: 'DormRoom',
|
||||
name: '^DormRoom',
|
||||
location: '76d98498-60a1-430c-ab76-b54a29b7a163',
|
||||
isPrivate: true,
|
||||
roomId: 1,
|
||||
@@ -267,7 +279,7 @@ describe('auth-gated endpoints', () => {
|
||||
}
|
||||
expect(body.roomInstance).toMatchObject({
|
||||
roomId: 2,
|
||||
name: 'RecCenter',
|
||||
name: '^RecCenter',
|
||||
location: RECCENTER_SCENE,
|
||||
isPrivate: true,
|
||||
})
|
||||
@@ -351,7 +363,7 @@ describe('auth-gated endpoints', () => {
|
||||
await exports.default.fetch(`${ORIGIN}/player/heartbeat`, { method: 'POST', headers })
|
||||
).json()) as { roomInstance: { name: string } | null; isOnline: boolean }
|
||||
expect(hb.isOnline).toBe(true)
|
||||
expect(hb.roomInstance?.name).toBe('DormRoom')
|
||||
expect(hb.roomInstance?.name).toBe('^DormRoom')
|
||||
})
|
||||
|
||||
test('GET /player?id reports stored presence per id', async () => {
|
||||
|
||||
Reference in New Issue
Block a user