[2026] split 2026 to tachy hosts

This commit is contained in:
Devin Zuczek
2026-10-20 21:21:24 -04:00
parent affdcb85e3
commit ed45f38137
6 changed files with 106 additions and 15 deletions
+10 -1
View File
@@ -159,6 +159,14 @@ RECFLARE_DOMAIN=rec.example.com
# out. A sandbox account on that build still gets the sandbox pool. Unset, rollout players # out. A sandbox account on that build still gets the sandbox pool. Unset, rollout players
# use the pool above. # use the pool above.
# RECFLARE_TACHYON_HOST_PORT_ROLLOUT=198.51.100.13:7777 # RECFLARE_TACHYON_HOST_PORT_ROLLOUT=198.51.100.13:7777
#
# WHICH builds are served that rollout pool, when it should not simply follow the sign-in
# gate: same format as RECFLARE_ROLLOUT_GAME_VERSIONS (builds, commas; `off` for none;
# `20260323` and `20260323.14` name the same build). Unset, the rollout pool goes to the
# builds under staged rollout, so ending the rollout (ROLLOUT_GAME_VERSIONS=off) also moves
# those players onto the live pool. Set this to keep the 2026 build on its own voice
# servers after it opens to everyone — the 2025 and 2023 builds stay on the pool above.
# RECFLARE_TACHYON_ROLLOUT_GAME_VERSIONS=20260323
# The Photon region every session is pinned to (`match`). Unlike the app ids above this # The Photon region every session is pinned to (`match`). Unlike the app ids above this
# does default, to `us` (us-east1) — an instance stamped with an empty region is one the # does default, to `us` (us-east1) — an instance stamped with an empty region is one the
@@ -245,7 +253,8 @@ RECFLARE_DOMAIN=rec.example.com
# made to upgrade. A build in ROLLOUT is served too, whether or not SUPPORTED names it, but # made to upgrade. A build in ROLLOUT is served too, whether or not SUPPORTED names it, but
# only to staff and the Discord roles below. The lifecycle these exist for: # only to staff and the Discord roles below. The lifecycle these exist for:
# 1. Rollout (both unset): the 2026 build is for staff and supporters only. # 1. Rollout (both unset): the 2026 build is for staff and supporters only.
# 2. General availability: ROLLOUT_GAME_VERSIONS=off — the 2026 build opens to everyone. # 2. General availability: ROLLOUT_GAME_VERSIONS=off — the 2026 build opens to everyone
# (and joins the live voice pool unless TACHYON_ROLLOUT_GAME_VERSIONS above keeps it).
# 3. Forced upgrade: SUPPORTED_GAME_VERSIONS=20230414,20260323 (with `off` above) — the # 3. Forced upgrade: SUPPORTED_GAME_VERSIONS=20230414,20260323 (with `off` above) — the
# 2025 build is refused and has to update. # 2025 build is refused and has to update.
# To pull a rollout back instead, list neither: SUPPORTED_GAME_VERSIONS=20230414,20250718 # To pull a rollout back instead, list neither: SUPPORTED_GAME_VERSIONS=20230414,20250718
+4 -1
View File
@@ -53,7 +53,10 @@ vars (`RECFLARE_*` in `.env`, reaching every worker) override them — read thro
that gates on "is this build served / under rollout" (`auth` tokens, `api` versioncheck, that gates on "is this build served / under rollout" (`auth` tokens, `api` versioncheck,
`match` rollout voice pool). A build outside the supported set is told to update by `api` `match` rollout voice pool). A build outside the supported set is told to update by `api`
AND refused a token by `auth`; a rollout build is always served, but gated to staff and AND refused a token by `auth`; a rollout build is always served, but gated to staff and
Discord roles until `ROLLOUT_GAME_VERSIONS=off`. Discord roles until `ROLLOUT_GAME_VERSIONS=off`. The rollout VOICE pool
(`TACHYON_HOST_PORT_ROLLOUT`) follows that list by default but `TACHYON_ROLLOUT_GAME_VERSIONS`
overrides it (`tachyonRolloutGameBuilds` in `match`), so a build can stay on its own Tachyon
servers after its rollout ends.
- `20230414` — default, official. Manifest `7859140924515540835` (2023). - `20230414` — default, official. Manifest `7859140924515540835` (2023).
- `20250718` — beta, official (the `20250718.01` client). Manifest `1151455856673601091`; - `20250718` — beta, official (the `20250718.01` client). Manifest `1151455856673601091`;
+2 -1
View File
@@ -61,7 +61,8 @@ The table is the default. An operator narrows it with `RECFLARE_SUPPORTED_GAME_V
in `.env` (a build left out is told to update and refused a sign-in, which is how players in `.env` (a build left out is told to update and refused a sign-in, which is how players
are moved to a newer build), and `RECFLARE_ROLLOUT_GAME_VERSIONS` names the builds under are moved to a newer build), and `RECFLARE_ROLLOUT_GAME_VERSIONS` names the builds under
staged rollout to Discord supporters — `20260323` by default, `off` to open it to staged rollout to Discord supporters — `20260323` by default, `off` to open it to
everyone. See `.env.example`. everyone (`RECFLARE_TACHYON_ROLLOUT_GAME_VERSIONS` keeps a build on the rollout voice
servers past that). See `.env.example`.
Generally speaking any client that effectively rewrites the nameserver with the Generally speaking any client that effectively rewrites the nameserver with the
right mods (see [RecNet Plugin](https://github.com/djdevin/recnet-plugin), [2025 patch](https://github.com/recflare/patch-2025)) can be used with this server. right mods (see [RecNet Plugin](https://github.com/djdevin/recnet-plugin), [2025 patch](https://github.com/recflare/patch-2025)) can be used with this server.
+15 -3
View File
@@ -87,11 +87,23 @@ export type Env = SharedHonoEnv & {
* The builds under staged rollout, overriding `ROLLOUT_GAME_VERSIONS` in @repo/domain: * The builds under staged rollout, overriding `ROLLOUT_GAME_VERSIONS` in @repo/domain:
* a comma-separated list of builds, or `off` for none. Set for every worker from the * a comma-separated list of builds, or `off` for none. Set for every worker from the
* root `.env` (`RECFLARE_ROLLOUT_GAME_VERSIONS`), so this agrees with the gate `auth` * root `.env` (`RECFLARE_ROLLOUT_GAME_VERSIONS`), so this agrees with the gate `auth`
* applies by construction. Read through `rolloutGameBuilds`; decides who is served the * applies by construction. Read through `rolloutGameBuilds`; the DEFAULT for which
* rollout Tachyon pool above. Ending the rollout (`off`) moves those callers to the * builds are served the rollout Tachyon pool above (see `TACHYON_ROLLOUT_GAME_VERSIONS`),
* live pool. * so ending the rollout (`off`) moves those callers to the live pool unless that var
* keeps them.
*/ */
ROLLOUT_GAME_VERSIONS?: string ROLLOUT_GAME_VERSIONS?: string
/**
* The builds served the rollout Tachyon pool, decoupled from the sign-in gate
* (`RECFLARE_TACHYON_ROLLOUT_GAME_VERSIONS` in `.env`): the same comma-separated list of
* builds, or `off` for none. Unset means `ROLLOUT_GAME_VERSIONS` decides, as above. Set,
* it is the only thing that does — so a build opened to everyone (`ROLLOUT_GAME_VERSIONS=off`)
* can stay on its own voice servers by naming it here. Matched as a BUILD: a listed
* `20260323` (or `20260323.14` — the point release is stripped) covers every point
* release of it, the way every other version gate compares. Read through
* `tachyonRolloutGameBuilds` in match.app.ts.
*/
TACHYON_ROLLOUT_GAME_VERSIONS?: string
/** /**
* The Photon region every session is pinned to — both the region named in the connection * The Photon region every session is pinned to — both the region named in the connection
* info and the one stamped on every room instance, which must agree. Optional; unlike the * info and the one stamped on every room instance, which must agree. Optional; unlike the
+28 -9
View File
@@ -43,6 +43,7 @@ import {
MatchmakingErrorCode, MatchmakingErrorCode,
MessageType, MessageType,
MOST_ACTIVE_CLUBHOUSE_LIMIT, MOST_ACTIVE_CLUBHOUSE_LIMIT,
parseGameBuilds,
putPlayerSettingsIfChanged, putPlayerSettingsIfChanged,
readPlayerSettings, readPlayerSettings,
recordRoomVisit, recordRoomVisit,
@@ -254,10 +255,24 @@ const ROLLOUT_TACHYON_SERVER_ID_PREFIX = 'rollout-'
interface TachyonCaller { interface TachyonCaller {
/** The account's `isSandbox` flag. */ /** The account's `isSandbox` flag. */
isSandbox: boolean isSandbox: boolean
/** Running a build under staged rollout (`rolloutGameBuilds`, off the token's `rn.ver`). */ /** Running a build served the rollout pool (`tachyonRolloutGameBuilds`, off the token's `rn.ver`). */
isRollout: boolean isRollout: boolean
} }
/**
* The builds served the rollout Tachyon pool: the operator's `TACHYON_ROLLOUT_GAME_VERSIONS`
* when set (`off` for none), otherwise the builds under staged rollout
* ({@link rolloutGameBuilds}, the same list `auth` gates sign-in on). The override exists
* because the two lists part ways at general availability: `ROLLOUT_GAME_VERSIONS=off`
* opens the new build to everyone, and without this it would also move every player on it
* onto the live voice servers. Entries are reduced to builds by {@link parseGameBuilds},
* so `20260323.14` lists the same build as `20260323`, and the caller's version is
* compared the same way by {@link isRolloutGameVersion}.
*/
function tachyonRolloutGameBuilds(env: Env): string[] {
return parseGameBuilds(env.TACHYON_ROLLOUT_GAME_VERSIONS) ?? rolloutGameBuilds(env)
}
/** /**
* The pool the caller's instance is picked out of — one of three, each the same * The pool the caller's instance is picked out of — one of three, each the same
* comma-separated list, with selection inside it the same instance-id round-robin * comma-separated list, with selection inside it the same instance-id round-robin
@@ -265,10 +280,12 @@ interface TachyonCaller {
* *
* - `TACHYON_HOST_PORT_SANDBOX` for a SANDBOX account, which runs against its own servers * - `TACHYON_HOST_PORT_SANDBOX` for a SANDBOX account, which runs against its own servers
* apart from the live pool; * apart from the live pool;
* - `TACHYON_HOST_PORT_ROLLOUT` for a caller on a build under staged rollout — the build * - `TACHYON_HOST_PORT_ROLLOUT` for a caller on a build in {@link tachyonRolloutGameBuilds}
* `auth` only lets Discord supporters sign in on, which gets its own voice servers while * — by default the build under staged rollout, which `auth` only lets Discord supporters
* it rolls out. Sessions are already split by build ({@link callerGameVersion}), so a * sign in on and which gets its own voice servers while it rolls out;
* rollout instance's players are all on that build and all land here together; * `TACHYON_ROLLOUT_GAME_VERSIONS` keeps a build there after the gate opens. Sessions are
* already split by build ({@link callerGameVersion}), so a rollout instance's players
* are all on that build and all land here together;
* - `TACHYON_HOST_PORT`, the live pool, for everyone else. * - `TACHYON_HOST_PORT`, the live pool, for everyone else.
* *
* Sandbox wins when both apply: a sandbox account on the rollout build is still a sandbox * Sandbox wins when both apply: a sandbox account on the rollout build is still a sandbox
@@ -3217,8 +3234,10 @@ const app = new Hono<App>()
'…). Both are empty when the pool is unset or the caller is in no instance. A', '…). 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', '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', 'same way, with ids `dev-1`, `dev-2`, …; a caller whose token’s `rn.ver` is a build',
'under staged rollout (`ROLLOUT_GAME_VERSIONS`, by default `20260323`) out of', 'under staged rollout (`ROLLOUT_GAME_VERSIONS`, by default `20260323`; or whatever',
'`TACHYON_HOST_PORT_ROLLOUT`, with ids `rollout-1`, `rollout-2`, …. Sandbox wins when', '`TACHYON_ROLLOUT_GAME_VERSIONS` names instead, so a build can keep its own voice',
'servers after its rollout ends) 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.', 'both apply, and either special pool left unset falls back to the live one.',
'`experiments` carries the client’s networking flags.', '`experiments` carries the client’s networking flags.',
].join(' '), ].join(' '),
@@ -3261,11 +3280,11 @@ const app = new Hono<App>()
const account = await getAccount(c.env.DB, id) const account = await getAccount(c.env.DB, id)
// The instance's server, the same one every other player in it is handed — except // The instance's server, the same one every other player in it is handed — except
// that a SANDBOX account is picked out of the sandbox pool, and a caller on a // that a SANDBOX account is picked out of the sandbox pool, and a caller on a
// rollout build out of the rollout pool, instead of the live one. // rollout-pool build out of the rollout pool, instead of the live one.
const tachyon = tachyonServerFor( const tachyon = tachyonServerFor(
tachyonPoolFor(c.env, { tachyonPoolFor(c.env, {
isSandbox: account?.isSandbox === true, isSandbox: account?.isSandbox === true,
isRollout: isRolloutGameVersion(await callerVersion(c), rolloutGameBuilds(c.env)), isRollout: isRolloutGameVersion(await callerVersion(c), tachyonRolloutGameBuilds(c.env)),
}), }),
roomInstanceId roomInstanceId
) )
@@ -1815,8 +1815,10 @@ describe('auth-gated endpoints', () => {
const originalSandbox = env.TACHYON_HOST_PORT_SANDBOX const originalSandbox = env.TACHYON_HOST_PORT_SANDBOX
const originalRollout = env.TACHYON_HOST_PORT_ROLLOUT const originalRollout = env.TACHYON_HOST_PORT_ROLLOUT
const originalRolloutVersions = env.ROLLOUT_GAME_VERSIONS const originalRolloutVersions = env.ROLLOUT_GAME_VERSIONS
const originalTachyonRolloutVersions = env.TACHYON_ROLLOUT_GAME_VERSIONS
try { try {
env.TACHYON_HOST_PORT = '198.51.100.12:7777' env.TACHYON_HOST_PORT = '198.51.100.12:7777'
env.TACHYON_ROLLOUT_GAME_VERSIONS = undefined
env.TACHYON_HOST_PORT_SANDBOX = '203.0.113.30:7777, 203.0.113.30:7778' env.TACHYON_HOST_PORT_SANDBOX = '203.0.113.30:7777, 203.0.113.30:7778'
const voiceFor = async (player: string, roomInstanceId?: number) => { const voiceFor = async (player: string, roomInstanceId?: number) => {
@@ -1928,6 +1930,50 @@ describe('auth-gated endpoints', () => {
voiceConnectionInfo: '198.51.100.12:7777', voiceConnectionInfo: '198.51.100.12:7777',
voiceServerId: 'tachyon-1', voiceServerId: 'tachyon-1',
}) })
// TACHYON_ROLLOUT_GAME_VERSIONS decouples the pool from the gate: set, it alone
// decides which builds get the rollout pool, so a build opened to everyone
// (ROLLOUT_GAME_VERSIONS=off) keeps its own voice servers while the 2025 build
// stays live. Matched as a build — a listed point release covers every point
// release of it, and a token carrying the bare build matches a listed one.
env.ROLLOUT_GAME_VERSIONS = 'off'
const keptRollout = await createRoomInstance(env.DB, {
ownerAccountId: 976,
roomId: 2,
photonRoomId: 'tachyon-rollout-kept',
maxCapacity: 12,
})
for (const listed of ['20260323', '20260323.14', ' 20260323.2 ,20231207']) {
env.TACHYON_ROLLOUT_GAME_VERSIONS = listed
for (const version of ['20260323', '20260323.14', '20260323.99']) {
expect(await rolloutVoiceFor('976', version, keptRollout.roomInstanceId)).toEqual(
rolloutPool[keptRollout.roomInstanceId % rolloutPool.length]
)
}
expect(await rolloutVoiceFor('976', '20250718.01', keptRollout.roomInstanceId)).toEqual({
voiceConnectionInfo: '198.51.100.12:7777',
voiceServerId: 'tachyon-1',
})
}
// Sandbox still wins over it.
env.TACHYON_ROLLOUT_GAME_VERSIONS = '20260323'
expect(await rolloutVoiceFor('974', '20260323.14', keptRollout.roomInstanceId)).toEqual(
sandboxPool[keptRollout.roomInstanceId % sandboxPool.length]
)
// Set, it overrides the gate's list in BOTH directions: `off` sends a build still
// under rollout to the live pool.
env.ROLLOUT_GAME_VERSIONS = '20260323'
env.TACHYON_ROLLOUT_GAME_VERSIONS = 'off'
expect(await rolloutVoiceFor('976', '20260323.14', keptRollout.roomInstanceId)).toEqual({
voiceConnectionInfo: '198.51.100.12:7777',
voiceServerId: 'tachyon-1',
})
// Blank is unset: the gate's list decides again.
env.TACHYON_ROLLOUT_GAME_VERSIONS = ''
expect(await rolloutVoiceFor('976', '20260323.14', keptRollout.roomInstanceId)).toEqual(
rolloutPool[keptRollout.roomInstanceId % rolloutPool.length]
)
env.TACHYON_ROLLOUT_GAME_VERSIONS = undefined
env.ROLLOUT_GAME_VERSIONS = originalRolloutVersions env.ROLLOUT_GAME_VERSIONS = originalRolloutVersions
// With no rollout pool configured, a rollout build falls back to the live one, not // With no rollout pool configured, a rollout build falls back to the live one, not
@@ -1961,6 +2007,7 @@ describe('auth-gated endpoints', () => {
env.TACHYON_HOST_PORT_SANDBOX = originalSandbox env.TACHYON_HOST_PORT_SANDBOX = originalSandbox
env.TACHYON_HOST_PORT_ROLLOUT = originalRollout env.TACHYON_HOST_PORT_ROLLOUT = originalRollout
env.ROLLOUT_GAME_VERSIONS = originalRolloutVersions env.ROLLOUT_GAME_VERSIONS = originalRolloutVersions
env.TACHYON_ROLLOUT_GAME_VERSIONS = originalTachyonRolloutVersions
} }
}) })