mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[match] restrict matchmaking to event rooms only
This commit is contained in:
@@ -1070,7 +1070,8 @@ function roomInstanceFromRoom(
|
|||||||
isPrivate: boolean,
|
isPrivate: boolean,
|
||||||
instanceId: number,
|
instanceId: number,
|
||||||
photonRoomId: string,
|
photonRoomId: string,
|
||||||
subRoomId?: number
|
subRoomId?: number,
|
||||||
|
eventId = 0
|
||||||
) {
|
) {
|
||||||
const f = instanceFieldsFromRoom(room, subRoomId)
|
const f = instanceFieldsFromRoom(room, subRoomId)
|
||||||
// The same region the connection info names its Photon apps for — see photonApps.
|
// The same region the connection info names its Photon apps for — see photonApps.
|
||||||
@@ -1082,7 +1083,7 @@ function roomInstanceFromRoom(
|
|||||||
roomInstanceType: f.roomInstanceType,
|
roomInstanceType: f.roomInstanceType,
|
||||||
location: f.location,
|
location: f.location,
|
||||||
dataBlob: f.dataBlob,
|
dataBlob: f.dataBlob,
|
||||||
eventId: 0,
|
eventId,
|
||||||
clubId: 0,
|
clubId: 0,
|
||||||
roomCode: '',
|
roomCode: '',
|
||||||
photonRegion: region,
|
photonRegion: region,
|
||||||
@@ -1518,7 +1519,8 @@ async function resolveRoomInstance(
|
|||||||
isPrivate: boolean,
|
isPrivate: boolean,
|
||||||
ownerId: number,
|
ownerId: number,
|
||||||
requestedSubRoomId?: number,
|
requestedSubRoomId?: number,
|
||||||
gateOnAccessibility = false
|
gateOnAccessibility = false,
|
||||||
|
eventId = 0
|
||||||
): Promise<ResolvedInstance> {
|
): Promise<ResolvedInstance> {
|
||||||
const id = Number.parseInt(roomKey, 10)
|
const id = Number.parseInt(roomKey, 10)
|
||||||
const requested = Number.isNaN(id)
|
const requested = Number.isNaN(id)
|
||||||
@@ -1606,6 +1608,10 @@ async function resolveRoomInstance(
|
|||||||
// reuses a public instance; an unpublished one reuses its single private session.
|
// reuses a public instance; an unpublished one reuses its single private session.
|
||||||
// A private matchmake into a published room always gets a fresh instance. Create
|
// A private matchmake into a published room always gets a fresh instance. Create
|
||||||
// one when there's nothing to join.
|
// one when there's nothing to join.
|
||||||
|
//
|
||||||
|
// An event's sessions are its own: an event matchmake reuses only an instance stamped
|
||||||
|
// with that event (and stamps the one it creates), and a room matchmake — `eventId` 0
|
||||||
|
// — never reuses an event's.
|
||||||
let instance = unpublished
|
let instance = unpublished
|
||||||
? await getSharedPrivateInstance(
|
? await getSharedPrivateInstance(
|
||||||
c.env.DB,
|
c.env.DB,
|
||||||
@@ -1616,7 +1622,14 @@ async function resolveRoomInstance(
|
|||||||
)
|
)
|
||||||
: privateInstance
|
: privateInstance
|
||||||
? null
|
? null
|
||||||
: await getJoinableInstance(c.env.DB, f.roomId, gameVersion, f.subRoomId, currentInstanceId)
|
: await getJoinableInstance(
|
||||||
|
c.env.DB,
|
||||||
|
f.roomId,
|
||||||
|
gameVersion,
|
||||||
|
f.subRoomId,
|
||||||
|
currentInstanceId,
|
||||||
|
eventId
|
||||||
|
)
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
instance = await createRoomInstance(c.env.DB, {
|
instance = await createRoomInstance(c.env.DB, {
|
||||||
ownerAccountId: ownerId,
|
ownerAccountId: ownerId,
|
||||||
@@ -1630,6 +1643,7 @@ async function resolveRoomInstance(
|
|||||||
isPrivate: privateInstance || f.isDorm,
|
isPrivate: privateInstance || f.isDorm,
|
||||||
roomInstanceType: f.roomInstanceType,
|
roomInstanceType: f.roomInstanceType,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
|
eventId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -1639,7 +1653,8 @@ async function resolveRoomInstance(
|
|||||||
privateInstance,
|
privateInstance,
|
||||||
instance.roomInstanceId,
|
instance.roomInstanceId,
|
||||||
instance.photonRoomId,
|
instance.photonRoomId,
|
||||||
f.subRoomId
|
f.subRoomId,
|
||||||
|
instance.eventId
|
||||||
),
|
),
|
||||||
errorCode: MatchmakingErrorCode.Success,
|
errorCode: MatchmakingErrorCode.Success,
|
||||||
}
|
}
|
||||||
@@ -2428,12 +2443,16 @@ const app = new Hono<App>()
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { joinMode, additionalPlayerIds } = await readMatchmakeBody(c)
|
const { joinMode, additionalPlayerIds } = await readMatchmakeBody(c)
|
||||||
|
// The event id scopes the session: the caller joins an instance already stamped
|
||||||
|
// with this event, or opens one that is — never the room's ordinary instances.
|
||||||
const { instance, errorCode } = await resolveRoomInstance(
|
const { instance, errorCode } = await resolveRoomInstance(
|
||||||
c,
|
c,
|
||||||
String(event.RoomId),
|
String(event.RoomId),
|
||||||
joinMode === 2,
|
joinMode === 2,
|
||||||
id,
|
id,
|
||||||
event.SubRoomId ?? undefined
|
event.SubRoomId ?? undefined,
|
||||||
|
false,
|
||||||
|
eventId
|
||||||
)
|
)
|
||||||
if (!instance) return matchmakeResult(c, errorCode, null)
|
if (!instance) return matchmakeResult(c, errorCode, null)
|
||||||
await enterRoom(c, id, instance)
|
await enterRoom(c, id, instance)
|
||||||
@@ -2961,7 +2980,8 @@ const app = new Hono<App>()
|
|||||||
stored.isPrivate,
|
stored.isPrivate,
|
||||||
stored.roomInstanceId,
|
stored.roomInstanceId,
|
||||||
stored.photonRoomId,
|
stored.photonRoomId,
|
||||||
stored.subRoomId
|
stored.subRoomId,
|
||||||
|
stored.eventId
|
||||||
)
|
)
|
||||||
await enterRoom(c, id, instance)
|
await enterRoom(c, id, instance)
|
||||||
return matchmakeResult(c, 0, instance)
|
return matchmakeResult(c, 0, instance)
|
||||||
|
|||||||
@@ -1071,28 +1071,70 @@ describe('public endpoints', () => {
|
|||||||
})
|
})
|
||||||
type Body = {
|
type Body = {
|
||||||
errorCode: number
|
errorCode: number
|
||||||
roomInstance: { roomId: number; location: string; roomInstanceId: number } | null
|
roomInstance: {
|
||||||
|
roomId: number
|
||||||
|
location: string
|
||||||
|
roomInstanceId: number
|
||||||
|
eventId: number
|
||||||
|
} | null
|
||||||
}
|
}
|
||||||
const join = async (path: string, sub?: string) =>
|
const join = async (path: string, sub?: string) =>
|
||||||
(await (await matchmake(path, sub)).json()) as Body
|
(await (await matchmake(path, sub)).json()) as Body
|
||||||
|
|
||||||
// An invited player lands in an instance of the event's room (2)...
|
// An invited player lands in an instance of the event's room (2), which names the
|
||||||
|
// event it is for...
|
||||||
const invited = await join('/matchmake/event/8', '301')
|
const invited = await join('/matchmake/event/8', '301')
|
||||||
expect(invited.errorCode).toBe(0)
|
expect(invited.errorCode).toBe(0)
|
||||||
expect(invited.roomInstance).toMatchObject({ roomId: 2, location: RECCENTER_SCENE })
|
expect(invited.roomInstance).toMatchObject({
|
||||||
|
roomId: 2,
|
||||||
|
location: RECCENTER_SCENE,
|
||||||
|
eventId: 8,
|
||||||
|
})
|
||||||
|
|
||||||
// ...recorded as their presence, like any other matchmake.
|
// ...recorded as their presence, like any other matchmake.
|
||||||
const row = await env.DB.prepare('SELECT data FROM presence WHERE account_id = ?1')
|
const row = await env.DB.prepare('SELECT data FROM presence WHERE account_id = ?1')
|
||||||
.bind(301)
|
.bind(301)
|
||||||
.first<{ data: string }>()
|
.first<{ data: string }>()
|
||||||
const presence = JSON.parse(row!.data) as { roomInstance: { roomInstanceId: number } }
|
const presence = JSON.parse(row!.data) as {
|
||||||
|
roomInstance: { roomInstanceId: number; eventId: number }
|
||||||
|
}
|
||||||
expect(presence.roomInstance.roomInstanceId).toBe(invited.roomInstance!.roomInstanceId)
|
expect(presence.roomInstance.roomInstanceId).toBe(invited.roomInstance!.roomInstanceId)
|
||||||
|
expect(presence.roomInstance.eventId).toBe(8)
|
||||||
|
|
||||||
|
// The instance itself is stamped, which is what the next guest's search matches on.
|
||||||
|
const stored = await env.DB.prepare('SELECT event_id FROM room_instance WHERE id = ?1')
|
||||||
|
.bind(invited.roomInstance!.roomInstanceId)
|
||||||
|
.first<{ event_id: number }>()
|
||||||
|
expect(stored!.event_id).toBe(8)
|
||||||
|
|
||||||
// The creator gets in, and so does someone who was invited and DECLINED — the row
|
// The creator gets in, and so does someone who was invited and DECLINED — the row
|
||||||
// is the invite, whatever the answer.
|
// is the invite, whatever the answer. They join the SAME session: the event's.
|
||||||
expect((await join('/matchmake/event/8', '300')).errorCode).toBe(0)
|
const creator = await join('/matchmake/event/8', '300')
|
||||||
|
expect(creator.errorCode).toBe(0)
|
||||||
|
expect(creator.roomInstance).toMatchObject({
|
||||||
|
roomInstanceId: invited.roomInstance!.roomInstanceId,
|
||||||
|
eventId: 8,
|
||||||
|
})
|
||||||
expect((await join('/matchmake/event/8', '302')).errorCode).toBe(0)
|
expect((await join('/matchmake/event/8', '302')).errorCode).toBe(0)
|
||||||
|
|
||||||
|
// A plain matchmake into the same room stays out of the event's session, and
|
||||||
|
// another event in that room gets a session of its own.
|
||||||
|
const plain = await join('/matchmake/room/2', '399')
|
||||||
|
expect(plain.errorCode).toBe(0)
|
||||||
|
expect(plain.roomInstance!.eventId).toBe(0)
|
||||||
|
expect(plain.roomInstance!.roomInstanceId).not.toBe(invited.roomInstance!.roomInstanceId)
|
||||||
|
const other = await join('/matchmake/event/9', '303')
|
||||||
|
expect(other.roomInstance!.eventId).toBe(9)
|
||||||
|
expect(other.roomInstance!.roomInstanceId).not.toBe(invited.roomInstance!.roomInstanceId)
|
||||||
|
|
||||||
|
// Coming back to the event from elsewhere lands in the event's session again, not
|
||||||
|
// the room's ordinary one.
|
||||||
|
expect((await join('/matchmake/room/2', '300')).roomInstance!.eventId).toBe(0)
|
||||||
|
expect((await join('/matchmake/event/8', '300')).roomInstance).toMatchObject({
|
||||||
|
roomInstanceId: invited.roomInstance!.roomInstanceId,
|
||||||
|
eventId: 8,
|
||||||
|
})
|
||||||
|
|
||||||
// A stranger doesn't — and is told why (35 EventIsPrivate), not fobbed off with 20.
|
// A stranger doesn't — and is told why (35 EventIsPrivate), not fobbed off with 20.
|
||||||
expect(await join('/matchmake/event/8', '399')).toEqual(refused(35))
|
expect(await join('/matchmake/event/8', '399')).toEqual(refused(35))
|
||||||
|
|
||||||
|
|||||||
@@ -504,15 +504,22 @@ export async function deleteEmptyRoomInstances(
|
|||||||
*
|
*
|
||||||
* `gameVersion` is matched as a BUILD ({@link gameBuild}): `20250718.01` and `20250718.2`
|
* `gameVersion` is matched as a BUILD ({@link gameBuild}): `20250718.01` and `20250718.2`
|
||||||
* both find the `20250718` session, which is what the rows store.
|
* both find the `20250718` session, which is what the rows store.
|
||||||
|
*
|
||||||
|
* `eventId` scopes the search to one player event's sessions, and the default 0 to the
|
||||||
|
* room's ordinary ones — the two never mix. An event matchmake only reuses an instance
|
||||||
|
* stamped with that event, so its guests gather in the event's session rather than
|
||||||
|
* scattering into whatever public instance the room has open; and a plain room matchmake
|
||||||
|
* never lands in an event's session, which for a private event is its guest list's alone.
|
||||||
*/
|
*/
|
||||||
export async function getJoinableInstance(
|
export async function getJoinableInstance(
|
||||||
db: D1Database,
|
db: D1Database,
|
||||||
roomId: number,
|
roomId: number,
|
||||||
gameVersion: string,
|
gameVersion: string,
|
||||||
subRoomId?: number,
|
subRoomId?: number,
|
||||||
excludeInstanceId?: number
|
excludeInstanceId?: number,
|
||||||
|
eventId = 0
|
||||||
): Promise<RoomInstanceDto | null> {
|
): Promise<RoomInstanceDto | null> {
|
||||||
const binds: Array<number | string> = [roomId, gameBuild(gameVersion) ?? '']
|
const binds: Array<number | string> = [roomId, gameBuild(gameVersion) ?? '', eventId]
|
||||||
const filters: string[] = []
|
const filters: string[] = []
|
||||||
if (subRoomId !== undefined) {
|
if (subRoomId !== undefined) {
|
||||||
binds.push(subRoomId)
|
binds.push(subRoomId)
|
||||||
@@ -525,7 +532,7 @@ export async function getJoinableInstance(
|
|||||||
const row = await db
|
const row = await db
|
||||||
.prepare(
|
.prepare(
|
||||||
`SELECT data FROM room_instance
|
`SELECT data FROM room_instance
|
||||||
WHERE room_id = ?1 AND game_version = ?2
|
WHERE room_id = ?1 AND game_version = ?2 AND COALESCE(event_id, 0) = ?3
|
||||||
AND is_private = 0 AND is_full = 0 AND join_disabled = 0
|
AND is_private = 0 AND is_full = 0 AND join_disabled = 0
|
||||||
AND is_in_progress = 0
|
AND is_in_progress = 0
|
||||||
AND matchmaking_policy != ${MatchmakingPolicy.Ignore} ${filters.join(' ')}
|
AND matchmaking_policy != ${MatchmakingPolicy.Ignore} ${filters.join(' ')}
|
||||||
|
|||||||
Reference in New Issue
Block a user