mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[2026] relax versioncheck to major versions
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
-- A room instance's `gameVersion` is now a BUILD — the bare `YYYYMMDD` — rather than the
|
||||
-- client's full version. The `.NN` after the date is a same-day point release of the
|
||||
-- same build: it changes nothing a session keys on, so `20250718.01` and `20250718.2`
|
||||
-- belong in one Photon room, and `SUPPORTED_GAME_VERSIONS` lists dates alone.
|
||||
--
|
||||
-- Matchmaking now stamps the build (createRoomInstance runs the value through
|
||||
-- `gameBuild`) and every lookup normalizes its argument the same way, so a row still
|
||||
-- carrying `20250718.01` would match NOBODY from here on: a 20250718 player's search is
|
||||
-- `game_version = '20250718'`. Strip the point release off every live row so the sessions
|
||||
-- open at the moment of the deploy stay joinable. Rows with no `.` (already bare, or the
|
||||
-- NULL of a row older than the stamp) are left exactly as they are.
|
||||
--
|
||||
-- `game_version` is a virtual column over the `data` blob (0012), so the blob is what's
|
||||
-- rewritten; the column follows.
|
||||
|
||||
UPDATE room_instance
|
||||
SET data = json_set(data, '$.gameVersion', substr(game_version, 1, instr(game_version, '.') - 1))
|
||||
WHERE game_version LIKE '%.%';
|
||||
@@ -93,7 +93,7 @@ import {
|
||||
withNotFound,
|
||||
withOnError,
|
||||
} from '@repo/hono-helpers'
|
||||
import { validateAndGetAccountId, validateAndGetRoles, validateAndGetVersion } from '@repo/jwt'
|
||||
import { validateAndGetAccountId, validateAndGetBuild, validateAndGetRoles } from '@repo/jwt'
|
||||
|
||||
// The notification-type ids the hub carries (owned by the `notify` worker). Imported
|
||||
// as a value — the enum has no runtime dependencies.
|
||||
@@ -470,16 +470,17 @@ async function authedAccountId(c: Context<App>): Promise<number | null> {
|
||||
}
|
||||
|
||||
/**
|
||||
* The client build this request's token was minted for (`rn.ver`), or null when there's no
|
||||
* valid token. The claim is the build the CLIENT posted at login, not this server's
|
||||
* GAME_VERSION, so it identifies what the player is actually running.
|
||||
* The client BUILD this request's token was minted for (`rn.ver` as its bare `YYYYMMDD`,
|
||||
* so a `20250718.01` client reads `20250718`), or null when there's no valid token. The
|
||||
* claim is the build the CLIENT posted at login, not this server's GAME_VERSION, so it
|
||||
* identifies what the player is actually running.
|
||||
*
|
||||
* It is unverified — a client can claim any build — which is fine for the one thing it is
|
||||
* used for here: keeping a payload away from a build it breaks. Lying about your version to
|
||||
* opt IN to a broken room list only breaks your own client.
|
||||
*/
|
||||
async function authedGameVersion(c: Context<App>): Promise<string | null> {
|
||||
return validateAndGetVersion(c.req.raw, await c.env.JWT_SECRET.get())
|
||||
return validateAndGetBuild(c.req.raw, await c.env.JWT_SECRET.get())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -493,9 +494,11 @@ async function authedGameVersion(c: Context<App>): Promise<string | null> {
|
||||
*
|
||||
* An allow-list rather than a "this build or newer" comparison, deliberately: the thing
|
||||
* being asserted is that a build was CHECKED, and a version string that sorts high (a
|
||||
* debug build, say) must not opt itself in. Add a build here once it's been tried.
|
||||
* debug build, say) must not opt itself in. Add a build here once it's been tried — as
|
||||
* the bare date, which is what {@link authedGameVersion} yields; every point release of
|
||||
* it (`20250718.01`, `20250718.2`) is the same build.
|
||||
*/
|
||||
const FEATURED_ROOMS_VERSIONS: ReadonlySet<string> = new Set(['20250718.01'])
|
||||
const FEATURED_ROOMS_VERSIONS: ReadonlySet<string> = new Set(['20250718'])
|
||||
|
||||
/**
|
||||
* Operator-granted elevated roles — the ones the auth worker stamps from an account's
|
||||
@@ -1522,7 +1525,7 @@ const app = new Hono<App>()
|
||||
'',
|
||||
'Restricted by CLIENT BUILD. Serving this to the 2023 client breaks its other room',
|
||||
'listings (NREs, apparently from the featured-room load corrupting its room cache), so',
|
||||
'only the builds known to render it — `20250718.01` today — get the group; anything',
|
||||
'only the builds known to render it — `20250718` today — get the group; anything',
|
||||
'else gets a 404, the same answer it got while the route was parked. The build is read',
|
||||
'from the token’s `rn.ver` claim, which is why this needs a token at all: nothing in',
|
||||
'the group itself is per-player.',
|
||||
|
||||
@@ -1274,6 +1274,15 @@ describe('rooms endpoints', () => {
|
||||
expect(body.Rooms.every((r) => typeof r.RoomName === 'string')).toBe(true)
|
||||
// The dorm (RoomId 1) is non-public, so it's never featured.
|
||||
expect(body.Rooms.some((r) => r.RoomId === 1)).toBe(false)
|
||||
|
||||
// The allow-list holds the BUILD (the bare date), so every point release of it is
|
||||
// served: a `.2` rebuild of the 2025 client renders the group the same as `.01`.
|
||||
for (const version of ['20250718.2', '20250718']) {
|
||||
const point = await SELF.fetch(`${ORIGIN}/featuredrooms/current`, {
|
||||
headers: await bearer('1', undefined, version),
|
||||
})
|
||||
expect(point.status, version).toBe(200)
|
||||
}
|
||||
})
|
||||
|
||||
it('GET /featuredrooms/current serves at most 10 rooms', async () => {
|
||||
@@ -6336,6 +6345,53 @@ describe('studio room bundles', () => {
|
||||
UnitySubAssets: [],
|
||||
})
|
||||
})
|
||||
|
||||
// A FIRST-PARTY room's bundle — captured from the official game and loaded by hand
|
||||
// under its `unity/<roomId>/` layout — shares `unity_asset` with Studio builds but is
|
||||
// not UGC, and must not be folded onto the save: the 2025 client handed it inline
|
||||
// takes it instead of asking the bulk lookup, cannot name it, and loads nothing (every
|
||||
// imported room broke when the splice moved from the Studio-only table to `unity_asset`).
|
||||
// The bulk lookup is where that client resolves it, and still serves it.
|
||||
it('leaves a first-party bundle to the bulk lookup rather than the room read', async () => {
|
||||
await reset()
|
||||
const firstParty = 'unity/9901/8iaf2yemao2d1zurblo875cn2.assetbundle'
|
||||
await putUnityAsset(env.DB, {
|
||||
UnityAssetId: ASSET,
|
||||
Target: 0,
|
||||
Version: 3,
|
||||
Filename: firstParty,
|
||||
Hash: winHash,
|
||||
})
|
||||
|
||||
type Loaded = {
|
||||
SubRooms: Array<{ CurrentSave: { UnitySubAssets: unknown[]; SubRoomDataSaveId: number } }>
|
||||
}
|
||||
for (const query of ['', '?unityAssetTarget=0']) {
|
||||
const room = (await (await SELF.fetch(`${ORIGIN}/rooms/9901${query}`)).json()) as Loaded
|
||||
expect(room.SubRooms[0]!.CurrentSave.UnitySubAssets, query).toEqual([])
|
||||
}
|
||||
|
||||
const saveId = ((await (await SELF.fetch(`${ORIGIN}/rooms/9901`)).json()) as Loaded)
|
||||
.SubRooms[0]!.CurrentSave.SubRoomDataSaveId
|
||||
const detail = await SELF.fetch(`${ORIGIN}/rooms/9901/subrooms/99011/saves/${saveId}`, {
|
||||
headers: await bearer('1'),
|
||||
})
|
||||
expect(await detail.json()).toMatchObject({
|
||||
UnityAsset: null,
|
||||
UnityAssetHash: null,
|
||||
UnitySubAssets: [],
|
||||
})
|
||||
|
||||
const bulk = await SELF.fetch(`${ORIGIN}/unity_assets/baked/bulk`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ target: '0', version: '3', id: ASSET }).toString(),
|
||||
})
|
||||
expect(bulk.status).toBe(200)
|
||||
expect(await bulk.json()).toEqual([
|
||||
{ UnityAssetId: ASSET, Target: 0, Version: 3, Filename: firstParty, Hash: winHash },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
// Room and subroom names take letters, digits and underscores, at most 32 (see
|
||||
|
||||
Reference in New Issue
Block a user