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:
+25
-17
@@ -14,6 +14,7 @@ import {
|
||||
deletePresence,
|
||||
deleteRoomInvite,
|
||||
GAME_VERSION,
|
||||
gameBuild,
|
||||
getAccount,
|
||||
getClubSummary,
|
||||
getExpiredPresenceAccountIds,
|
||||
@@ -387,16 +388,18 @@ async function callerVersion(c: Context<App>): Promise<string | null> {
|
||||
}
|
||||
|
||||
/**
|
||||
* The build to matchmake the caller as — {@link callerVersion} with the server's own
|
||||
* `GAME_VERSION` standing in when the token doesn't say (the same fallback presence
|
||||
* uses, so a player's instance and their reported build agree).
|
||||
* The BUILD to matchmake the caller as — {@link callerVersion} reduced to its date
|
||||
* ({@link gameBuild}: `20250718.01` → `20250718`), with the server's own `GAME_VERSION`
|
||||
* standing in when the token doesn't say (the same fallback presence uses, so a player's
|
||||
* instance and their reported build agree). A point release is the same build: a
|
||||
* `20250718.2` client lands in the `20250718` session, beside the `.01` who opened it.
|
||||
*
|
||||
* Unlike presence, this can never be left unset: it is the key an instance is created
|
||||
* under and searched by, and an empty one would pool every unknown-build player into a
|
||||
* shared "" bucket — exactly the mixed session the stamp exists to prevent.
|
||||
*/
|
||||
async function callerGameVersion(c: Context<App>): Promise<string> {
|
||||
return (await callerVersion(c)) ?? GAME_VERSION
|
||||
return gameBuild(await callerVersion(c)) ?? GAME_VERSION
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -711,16 +714,20 @@ const BANNED_FROM_ROOM = MatchmakingErrorCode.BannedFromRoom
|
||||
* `UpdateRequired`, the one code that says "your client can't go there" and the honest
|
||||
* answer. There is no code for the other direction ("they must update"), so a caller on
|
||||
* the newer build gets the opaque NoSuchRoom every other unjoinable thing answers.
|
||||
* Builds are date-stamped (`20230414`, `20250718.01`), so they order as strings; an
|
||||
* instance carrying no version at all (written before the stamp existed) is nobody's
|
||||
* Both sides are compared as BUILDS ({@link gameBuild}) — either may arrive as a full
|
||||
* client version (a presence row's `appVersion` is verbatim) and a point release is the
|
||||
* same build. Builds are date-stamped (`20230414`, `20250718`), so they order as strings;
|
||||
* an instance carrying no version at all (written before the stamp existed) is nobody's
|
||||
* build and refuses both ways.
|
||||
*/
|
||||
function crossBuildRefusal(
|
||||
callerVersion: string,
|
||||
instanceVersion: string
|
||||
): MatchmakingErrorCode | null {
|
||||
if (callerVersion === instanceVersion) return null
|
||||
return instanceVersion > callerVersion ? MatchmakingErrorCode.UpdateRequired : NO_SUCH_ROOM
|
||||
const caller = gameBuild(callerVersion) ?? ''
|
||||
const instance = gameBuild(instanceVersion) ?? ''
|
||||
if (caller === instance) return null
|
||||
return instance > caller ? MatchmakingErrorCode.UpdateRequired : NO_SUCH_ROOM
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -854,12 +861,12 @@ function nextLiveMessageId(): number {
|
||||
const GAME_INVITE_V2_MIN_BUILD = 20230414
|
||||
|
||||
/**
|
||||
* The date portion of a client build (`20250718.01` → `20250718`) as a number, or null when
|
||||
* there's nothing parseable to compare. Builds are `<yyyymmdd>[.<n>]`, so the leading date
|
||||
* orders them; the point release after it never decides this gate.
|
||||
* A client build ({@link gameBuild}: `20250718.01` → `20250718`) as a number, or null when
|
||||
* there's nothing parseable to compare. The leading date orders builds; the point release
|
||||
* after it never decides a gate.
|
||||
*/
|
||||
function buildNumber(version: string | null): number | null {
|
||||
const date = Number.parseInt(version?.split('.')[0] ?? '', 10)
|
||||
const date = Number.parseInt(gameBuild(version) ?? '', 10)
|
||||
return Number.isNaN(date) ? null : date
|
||||
}
|
||||
|
||||
@@ -1563,10 +1570,11 @@ async function resolveRoomInstance(
|
||||
const currentInstanceId = reusesInstance
|
||||
? (await getPresence<RoomInstance>(c.env.DB, ownerId))?.roomInstance?.roomInstanceId
|
||||
: undefined
|
||||
// The same build, with GAME_VERSION standing in for a token that names none. It scopes
|
||||
// the search below and is stamped on the instance when one is created, which is what
|
||||
// keeps a session to a single client version.
|
||||
const gameVersion = tokenVersion ?? GAME_VERSION
|
||||
// The same BUILD (the date, with the client's point release dropped), with GAME_VERSION
|
||||
// standing in for a token that names none. It scopes the search below and is stamped on
|
||||
// the instance when one is created, which is what keeps a session to a single client
|
||||
// build — and lets every point release of that build share it.
|
||||
const gameVersion = gameBuild(tokenVersion) ?? GAME_VERSION
|
||||
// Reuse an existing joinable instance *of the same subroom and the same 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. A published room
|
||||
@@ -3114,7 +3122,7 @@ const app = new Hono<App>()
|
||||
'…). Both are empty when the pool is unset or the caller is in no instance. A',
|
||||
'sandbox account is picked out of the `TACHYON_HOST_PORT_SANDBOX` pool instead, the',
|
||||
'same way, with ids `dev-1`, `dev-2`, …; a caller whose token’s `rn.ver` is a build',
|
||||
'under staged rollout (`ROLLOUT_GAME_VERSIONS`, currently `20260323.14`) out of',
|
||||
'under staged rollout (`ROLLOUT_GAME_VERSIONS`, currently `20260323`) out of',
|
||||
'`TACHYON_HOST_PORT_ROLLOUT`, with ids `rollout-1`, `rollout-2`, …. Sandbox wins when',
|
||||
'both apply, and either special pool left unset falls back to the live one.',
|
||||
'`experiments` carries the client’s networking flags.',
|
||||
|
||||
@@ -1351,6 +1351,38 @@ describe('auth-gated endpoints', () => {
|
||||
expect(priv2.roomInstance.roomInstanceId).not.toBe(priv.roomInstance.roomInstanceId)
|
||||
})
|
||||
|
||||
test('POST /matchmake/room/:roomId pools every point release of a build into one instance', async () => {
|
||||
const matchmake = async (sub: string, version: string) =>
|
||||
(await (
|
||||
await exports.default.fetch(`${ORIGIN}/matchmake/room/2`, {
|
||||
method: 'POST',
|
||||
headers: await bearer(sub, version),
|
||||
})
|
||||
).json()) as { roomInstance: { photonRoomId: string; roomInstanceId: number } }
|
||||
|
||||
// The `.NN` after the date is a same-day rebuild of the same build, so a `.01` client,
|
||||
// a `.2` client and one reporting the bare date all belong in one session.
|
||||
const first = await matchmake('9170', '20250718.01')
|
||||
const point = await matchmake('9171', '20250718.2')
|
||||
const bare = await matchmake('9172', '20250718')
|
||||
expect(point.roomInstance.roomInstanceId).toBe(first.roomInstance.roomInstanceId)
|
||||
expect(bare.roomInstance.roomInstanceId).toBe(first.roomInstance.roomInstanceId)
|
||||
|
||||
// The instance is stamped with the BUILD, not the version that opened it — the key
|
||||
// the lookups match on. (The same shape migrations/0028 backfills older rows into.)
|
||||
const row = await env.DB.prepare('SELECT game_version FROM room_instance WHERE id = ?1')
|
||||
.bind(first.roomInstance.roomInstanceId)
|
||||
.first<{ game_version: string }>()
|
||||
expect(row?.game_version).toBe('20250718')
|
||||
|
||||
// The point release doesn't leak into presence: that still reports the version the
|
||||
// client posted, which is what describes the player to their friends.
|
||||
const presence = await env.DB.prepare(
|
||||
'SELECT data FROM presence WHERE account_id = 9171'
|
||||
).first<{ data: string }>()
|
||||
expect(JSON.parse(presence?.data ?? '{}').appVersion).toBe('20250718.2')
|
||||
})
|
||||
|
||||
test('POST /matchmake/room/:roomId only pools players on the same client build', async () => {
|
||||
const matchmake = async (sub: string, version: string) =>
|
||||
(await (
|
||||
@@ -2651,9 +2683,11 @@ describe('auth-gated endpoints', () => {
|
||||
const older = await spawn('915', '20250424.01')
|
||||
expect(await join(older, '20250718.01')).toEqual(refused(20))
|
||||
|
||||
// Same build → in they go.
|
||||
// Same build → in they go — and a point release IS the same build: the instance holds
|
||||
// the bare date, and the caller's `.2` reduces to it.
|
||||
const same = await spawn('916', '20250718.01')
|
||||
expect(await join(same, '20250718.01')).toMatchObject({ errorCode: 0 })
|
||||
expect(await join(same, '20250718.2')).toMatchObject({ errorCode: 0 })
|
||||
})
|
||||
|
||||
test('POST /matchmake/instance/:id joins that exact instance, owner-only', async () => {
|
||||
|
||||
Reference in New Issue
Block a user