mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 22:51:30 -07:00
[match] gate 2025 rooms
This commit is contained in:
@@ -597,6 +597,63 @@ function crossBuildRefusal(
|
|||||||
return instanceVersion > callerVersion ? MatchmakingErrorCode.UpdateRequired : NO_SUCH_ROOM
|
return instanceVersion > callerVersion ? MatchmakingErrorCode.UpdateRequired : NO_SUCH_ROOM
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The 2023 client build, as its token's `rn.ver` date stamps it (`20230414`, or a point
|
||||||
|
* release of it). See {@link persistenceVersionRefusal}.
|
||||||
|
*/
|
||||||
|
const BUILD_2023 = 20230414
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The first scene persistence version the 2023 client cannot load. A room whose published
|
||||||
|
* scene was saved at this version or later was built on a newer client; the old one fails
|
||||||
|
* to deserialize it.
|
||||||
|
*/
|
||||||
|
const MIN_UNLOADABLE_PERSISTENCE_VERSION_2023 = 227
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The persistence version of the scene a subroom LOADS — the published `CurrentSave`'s,
|
||||||
|
* the same save {@link subRoomDataBlob} serves, falling back to the flat legacy field.
|
||||||
|
* `null` when nothing recorded one (a fresh subroom, or one saved before the field
|
||||||
|
* existed): unknown is not "new".
|
||||||
|
*/
|
||||||
|
function subRoomPersistenceVersion(sub: Record<string, unknown> | undefined): number | null {
|
||||||
|
const save = sub?.CurrentSave
|
||||||
|
if (save && typeof save === 'object') {
|
||||||
|
const v = (save as Record<string, unknown>).PersistenceVersion
|
||||||
|
if (typeof v === 'number') return v
|
||||||
|
}
|
||||||
|
return typeof sub?.PersistenceVersion === 'number' ? sub.PersistenceVersion : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a player on `callerVersion` may enter `room` at all — `null` when they may,
|
||||||
|
* otherwise the code to refuse with.
|
||||||
|
*
|
||||||
|
* A caller on the 2023 build ({@link BUILD_2023}) is refused any room with a subroom whose
|
||||||
|
* published scene is at persistence version {@link MIN_UNLOADABLE_PERSISTENCE_VERSION_2023}
|
||||||
|
* or above: that scene was saved by a newer client and the 2023 one can't load it, so
|
||||||
|
* the honest answer is `UpdateRequired` — "your client can't go there" — rather than
|
||||||
|
* handing out an instance that never finishes loading. The WHOLE room is gated, not just
|
||||||
|
* the requested subroom, since the client walks between subrooms without re-matchmaking.
|
||||||
|
*
|
||||||
|
* Every other build, and a token that names none, passes: the gate is about one known
|
||||||
|
* client, not a general ordering.
|
||||||
|
*/
|
||||||
|
function persistenceVersionRefusal(
|
||||||
|
room: Room,
|
||||||
|
callerVersion: string | null
|
||||||
|
): MatchmakingErrorCode | null {
|
||||||
|
if (buildNumber(callerVersion) !== BUILD_2023) return null
|
||||||
|
const subRooms = (Array.isArray(room.SubRooms) ? room.SubRooms : []) as Array<
|
||||||
|
Record<string, unknown>
|
||||||
|
>
|
||||||
|
const tooNew = subRooms.some((sub) => {
|
||||||
|
const v = subRoomPersistenceVersion(sub)
|
||||||
|
return v !== null && v >= MIN_UNLOADABLE_PERSISTENCE_VERSION_2023
|
||||||
|
})
|
||||||
|
return tooNew ? MatchmakingErrorCode.UpdateRequired : null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "This event isn't open to you" — the refusal on a private event the caller wasn't
|
* "This event isn't open to you" — the refusal on a private event the caller wasn't
|
||||||
* invited to. Told plainly rather than hidden behind the opaque NoSuchRoom: a player
|
* invited to. Told plainly rather than hidden behind the opaque NoSuchRoom: a player
|
||||||
@@ -1205,6 +1262,20 @@ async function resolveRoomInstance(
|
|||||||
return { instance: null, errorCode: BANNED_FROM_ROOM }
|
return { instance: null, errorCode: BANNED_FROM_ROOM }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The build this player is on, from their token. A 2023 client can't load a scene
|
||||||
|
// saved at a newer persistence version, so it is refused the room outright (see
|
||||||
|
// persistenceVersionRefusal) before any instance is created or reused.
|
||||||
|
const tokenVersion = await callerVersion(c)
|
||||||
|
const tooNew = persistenceVersionRefusal(room, tokenVersion)
|
||||||
|
if (tooNew !== null) {
|
||||||
|
logger.info('matchmake refused: room persistence version too new for client build', {
|
||||||
|
roomId: f.roomId,
|
||||||
|
ownerId,
|
||||||
|
gameVersion: tokenVersion,
|
||||||
|
})
|
||||||
|
return { instance: null, errorCode: tooNew }
|
||||||
|
}
|
||||||
|
|
||||||
// Never place the player back into the instance they're already in: the client
|
// Never place the player back into the instance they're already in: the client
|
||||||
// keys the room transition off a changing `roomInstanceId`, so re-matchmaking into
|
// keys the room transition off a changing `roomInstanceId`, so re-matchmaking into
|
||||||
// your current instance (e.g. the only public instance of a room you're already in)
|
// your current instance (e.g. the only public instance of a room you're already in)
|
||||||
@@ -1215,10 +1286,10 @@ async function resolveRoomInstance(
|
|||||||
const currentInstanceId = isPrivate
|
const currentInstanceId = isPrivate
|
||||||
? undefined
|
? undefined
|
||||||
: (await getPresence<RoomInstance>(c.env.DB, ownerId))?.roomInstance?.roomInstanceId
|
: (await getPresence<RoomInstance>(c.env.DB, ownerId))?.roomInstance?.roomInstanceId
|
||||||
// The build this player is on, from their token. It scopes the search below and is
|
// The same build, with GAME_VERSION standing in for a token that names none. It scopes
|
||||||
// stamped on the instance when one is created, which is what keeps a session to a
|
// the search below and is stamped on the instance when one is created, which is what
|
||||||
// single client version.
|
// keeps a session to a single client version.
|
||||||
const gameVersion = await callerGameVersion(c)
|
const gameVersion = tokenVersion ?? GAME_VERSION
|
||||||
// Reuse an existing joinable public instance *of the same subroom and the same
|
// Reuse an existing joinable public instance *of the same subroom and the same
|
||||||
// build* — subrooms are separate places, so joining one must never land you in
|
// build* — subrooms are separate places, so joining one must never land you in
|
||||||
// another, and neither must a session running a different version of the room.
|
// another, and neither must a session running a different version of the room.
|
||||||
|
|||||||
@@ -3111,6 +3111,85 @@ describe('account bans', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// A 2023 client (`rn.ver` 20230414) can't load a scene saved at persistence version 227 or
|
||||||
|
// later, so any room with such a subroom refuses it with UpdateRequired. Every other build
|
||||||
|
// gets in as usual.
|
||||||
|
describe('persistence version gate for the 2023 client', () => {
|
||||||
|
const matchmake = async (roomId: number, sub: string, version?: string) => {
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/matchmake/room/${roomId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...(await bearer(sub, version)),
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({ JoinMode: '0' }).toString(),
|
||||||
|
})
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
return (await res.json()) as {
|
||||||
|
errorCode: number
|
||||||
|
result: number
|
||||||
|
roomInstance: { roomId: number } | null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Published scene at 227 — the first version the 2023 client can't load.
|
||||||
|
await seedRoomWithSubRooms(env.DB, {
|
||||||
|
RoomId: 7227,
|
||||||
|
Name: 'NewFormatRoom',
|
||||||
|
IsDorm: false,
|
||||||
|
Accessibility: 1,
|
||||||
|
CreatorAccountId: 7300,
|
||||||
|
SubRooms: [
|
||||||
|
{ SubRoomId: 7227, UnitySceneId: RECCENTER_SCENE, MaxPlayers: 10 },
|
||||||
|
{
|
||||||
|
SubRoomId: 7228,
|
||||||
|
UnitySceneId: SECOND_SUBROOM_SCENE,
|
||||||
|
MaxPlayers: 10,
|
||||||
|
CurrentSave: { DataBlob: 'new.room', PersistenceVersion: 227 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as Record<string, unknown>)
|
||||||
|
// Published at 226 — still loadable.
|
||||||
|
await seedRoomWithSubRooms(env.DB, {
|
||||||
|
RoomId: 7226,
|
||||||
|
Name: 'OldFormatRoom',
|
||||||
|
IsDorm: false,
|
||||||
|
Accessibility: 1,
|
||||||
|
CreatorAccountId: 7300,
|
||||||
|
SubRooms: [
|
||||||
|
{
|
||||||
|
SubRoomId: 7226,
|
||||||
|
UnitySceneId: RECCENTER_SCENE,
|
||||||
|
MaxPlayers: 10,
|
||||||
|
CurrentSave: { DataBlob: 'old.room', PersistenceVersion: 226 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as Record<string, unknown>)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('the 2023 build is refused a room with a subroom at 227+', async () => {
|
||||||
|
const res = await matchmake(7227, '7301', '20230414')
|
||||||
|
expect(res.errorCode).toBe(16) // UpdateRequired
|
||||||
|
expect(res.result).toBe(16)
|
||||||
|
expect(res.roomInstance).toBeNull()
|
||||||
|
|
||||||
|
// A point release of the same build is the same client.
|
||||||
|
expect((await matchmake(7227, '7302', '20230414.02')).errorCode).toBe(16)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('the 2023 build still enters a room saved below 227', async () => {
|
||||||
|
const res = await matchmake(7226, '7303', '20230414')
|
||||||
|
expect(res.errorCode).toBe(0)
|
||||||
|
expect(res.roomInstance?.roomId).toBe(7226)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('newer builds and unversioned tokens are not gated', async () => {
|
||||||
|
expect((await matchmake(7227, '7304', '20250718.01')).errorCode).toBe(0)
|
||||||
|
expect((await matchmake(7227, '7305')).errorCode).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// The ban follows the player past the account it was written on: a new account sharing a
|
// The ban follows the player past the account it was written on: a new account sharing a
|
||||||
// proven platform identity or an IP with a banned one is refused the same way. See
|
// proven platform identity or an IP with a banned one is refused the same way. See
|
||||||
// bans-db.ts in the api worker for the arms and the BAN_EVASION_MATCH knob.
|
// bans-db.ts in the api worker for the arms and the BAN_EVASION_MATCH knob.
|
||||||
|
|||||||
Reference in New Issue
Block a user