mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 15:09:18 -07:00
[match] apply subroom capacity to instance cap
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
EMPTY_INSTANCE_GRACE_SECONDS,
|
||||
GAME_VERSION,
|
||||
getRoomInstance,
|
||||
modifySubRoom,
|
||||
PRESENCE_SCHEMA_DDL,
|
||||
ROOM_INSTANCE_SCHEMA_DDL,
|
||||
ROOM_INVITE_SCHEMA_DDL,
|
||||
@@ -2130,6 +2131,34 @@ describe('auth-gated endpoints', () => {
|
||||
expect((await getRoomInstance(env.DB, instanceId))?.isFull).toBe(false)
|
||||
})
|
||||
|
||||
test('raising a subroom’s MaxPlayers admits players into the running instance', async () => {
|
||||
// A room with a cap of 1 and its own id, so no other test’s instances of SoloRoom
|
||||
// are candidates here.
|
||||
await seedRoomWithSubRooms(env.DB, {
|
||||
RoomId: 5101,
|
||||
Name: 'GrowingRoom',
|
||||
IsDorm: false,
|
||||
Accessibility: 1,
|
||||
SubRooms: [{ SubRoomId: 5101, UnitySceneId: RECCENTER_SCENE, MaxPlayers: 1 }],
|
||||
} as unknown as Record<string, unknown>)
|
||||
const first = await matchmakeInto('5101', '830')
|
||||
expect((await getRoomInstance(env.DB, first))?.isFull).toBe(true)
|
||||
|
||||
// The owner raises the cap in the subroom’s settings while the instance is live
|
||||
// (what the `rooms` worker’s subroom settings route calls). The instance carries
|
||||
// its own copy of the cap, so the write has to reach the running instance.
|
||||
await modifySubRoom(env.DB, 5101, 5101, { maxPlayers: 2 })
|
||||
const second = await matchmakeInto('5101', '831')
|
||||
expect(second).toBe(first)
|
||||
|
||||
// And the served instance follows the subroom: 2 of 2 now, full again.
|
||||
const instance = await getRoomInstance(env.DB, first)
|
||||
expect(instance).toMatchObject({ maxCapacity: 2, isFull: true })
|
||||
expect(await countPlayersInInstance(env.DB, first)).toBe(2)
|
||||
const third = await matchmakeInto('5101', '832')
|
||||
expect(third).not.toBe(first)
|
||||
})
|
||||
|
||||
test('leaving a full instance clears its full flag', async () => {
|
||||
// Fill SoloRoom, then the same player matchmakes into RecCenter — the SoloRoom
|
||||
// instance they left should no longer be full.
|
||||
|
||||
@@ -370,6 +370,48 @@ export async function refreshInstanceFullness(
|
||||
return isFull
|
||||
}
|
||||
|
||||
/**
|
||||
* Restamp the `maxCapacity` of every live instance of a subroom and recompute each one's
|
||||
* `isFull` against it. Called by the subroom settings write (`modifySubRoom`), which is
|
||||
* the ONE place a subroom's `MaxPlayers` changes after creation.
|
||||
*
|
||||
* An instance's `maxCapacity` is a copy of the subroom's cap taken when the instance was
|
||||
* created, and it is what everything enforces — the fullness recompute, the join
|
||||
* searches' `is_full` filter, the served DTO. It is a copy rather than a live read
|
||||
* because the instance is read on every matchmake, heartbeat and presence fan-out, and
|
||||
* a subroom lookup on each of those is D1 cost paid for a setting that changes rarely.
|
||||
* So the copy is invalidated at the write instead: without this, an owner who raised
|
||||
* the cap mid-session found the running instance still refusing players at the old
|
||||
* number until it was torn down. Returns the number of instances rewritten.
|
||||
*/
|
||||
export async function resyncInstanceCapacity(
|
||||
db: D1Database,
|
||||
roomId: number,
|
||||
subRoomId: number,
|
||||
maxCapacity: number,
|
||||
now = Math.floor(Date.now() / 1000)
|
||||
): Promise<number> {
|
||||
const { results } = await db
|
||||
.prepare('SELECT data FROM room_instance WHERE room_id = ?1 AND sub_room_id = ?2')
|
||||
.bind(roomId, subRoomId)
|
||||
.all<{ data: string }>()
|
||||
let rewritten = 0
|
||||
for (const row of results) {
|
||||
const stored = parse(row.data)
|
||||
const count = await countPlayersInInstance(db, stored.roomInstanceId, now)
|
||||
const isFull = maxCapacity > 0 && count >= maxCapacity
|
||||
if (stored.maxCapacity === maxCapacity && stored.isFull === isFull) continue
|
||||
stored.maxCapacity = maxCapacity
|
||||
stored.isFull = isFull
|
||||
await db
|
||||
.prepare('UPDATE room_instance SET data = ?1 WHERE id = ?2')
|
||||
.bind(JSON.stringify(stored), stored.roomInstanceId)
|
||||
.run()
|
||||
rewritten++
|
||||
}
|
||||
return rewritten
|
||||
}
|
||||
|
||||
/**
|
||||
* How long (s) a room instance is left alone after it's created, even with nobody in
|
||||
* it. Every path that creates an instance writes the creator's presence in the same
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import { bindPlaceholders, chunkForBinds, MAX_BOUND_PARAMS } from './d1-binds'
|
||||
import { Accessibility, Role } from './enums'
|
||||
import { countPlayersByRoom } from './presence-db'
|
||||
import { resyncInstanceCapacity } from './room-instance-db'
|
||||
import {
|
||||
bakedStudioUnityAssets,
|
||||
isMissingStudioAssetTable,
|
||||
@@ -1876,6 +1877,10 @@ export interface ModifySubRoomInput {
|
||||
* (the fields the client's subroom `modify` form carries). Only the supplied fields
|
||||
* are changed; the subroom row is updated in the `subroom` table. Returns the updated
|
||||
* (hydrated) room, or null when the room or subroom doesn't exist.
|
||||
*
|
||||
* A changed `MaxPlayers` is pushed onto the subroom's LIVE instances too (see
|
||||
* {@link resyncInstanceCapacity}): each instance carries a copy of the cap it enforces,
|
||||
* and this is the write that makes that copy stale.
|
||||
*/
|
||||
export async function modifySubRoom(
|
||||
db: D1Database,
|
||||
@@ -1888,8 +1893,10 @@ export async function modifySubRoom(
|
||||
|
||||
if (input.name !== undefined) sub.Name = input.name
|
||||
if (input.accessibility !== undefined) sub.Accessibility = input.accessibility
|
||||
const capacityChanged = input.maxPlayers !== undefined && input.maxPlayers !== sub.MaxPlayers
|
||||
if (input.maxPlayers !== undefined) sub.MaxPlayers = input.maxPlayers
|
||||
await updateSubRoom(db, sub)
|
||||
if (capacityChanged) await resyncInstanceCapacity(db, roomId, subRoomId, input.maxPlayers!)
|
||||
|
||||
return getRoomById(db, roomId)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user