[match] restrict matchmaking to event rooms only

This commit is contained in:
Devin Zuczek
2026-10-13 15:31:03 -04:00
parent ba6c703050
commit 5ba57210b9
3 changed files with 85 additions and 16 deletions
+27 -7
View File
@@ -1070,7 +1070,8 @@ function roomInstanceFromRoom(
isPrivate: boolean,
instanceId: number,
photonRoomId: string,
subRoomId?: number
subRoomId?: number,
eventId = 0
) {
const f = instanceFieldsFromRoom(room, subRoomId)
// The same region the connection info names its Photon apps for — see photonApps.
@@ -1082,7 +1083,7 @@ function roomInstanceFromRoom(
roomInstanceType: f.roomInstanceType,
location: f.location,
dataBlob: f.dataBlob,
eventId: 0,
eventId,
clubId: 0,
roomCode: '',
photonRegion: region,
@@ -1518,7 +1519,8 @@ async function resolveRoomInstance(
isPrivate: boolean,
ownerId: number,
requestedSubRoomId?: number,
gateOnAccessibility = false
gateOnAccessibility = false,
eventId = 0
): Promise<ResolvedInstance> {
const id = Number.parseInt(roomKey, 10)
const requested = Number.isNaN(id)
@@ -1606,6 +1608,10 @@ async function resolveRoomInstance(
// 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
// 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
? await getSharedPrivateInstance(
c.env.DB,
@@ -1616,7 +1622,14 @@ async function resolveRoomInstance(
)
: privateInstance
? 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) {
instance = await createRoomInstance(c.env.DB, {
ownerAccountId: ownerId,
@@ -1630,6 +1643,7 @@ async function resolveRoomInstance(
isPrivate: privateInstance || f.isDorm,
roomInstanceType: f.roomInstanceType,
gameVersion,
eventId,
})
}
return {
@@ -1639,7 +1653,8 @@ async function resolveRoomInstance(
privateInstance,
instance.roomInstanceId,
instance.photonRoomId,
f.subRoomId
f.subRoomId,
instance.eventId
),
errorCode: MatchmakingErrorCode.Success,
}
@@ -2428,12 +2443,16 @@ const app = new Hono<App>()
}
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(
c,
String(event.RoomId),
joinMode === 2,
id,
event.SubRoomId ?? undefined
event.SubRoomId ?? undefined,
false,
eventId
)
if (!instance) return matchmakeResult(c, errorCode, null)
await enterRoom(c, id, instance)
@@ -2961,7 +2980,8 @@ const app = new Hono<App>()
stored.isPrivate,
stored.roomInstanceId,
stored.photonRoomId,
stored.subRoomId
stored.subRoomId,
stored.eventId
)
await enterRoom(c, id, instance)
return matchmakeResult(c, 0, instance)
+48 -6
View File
@@ -1071,28 +1071,70 @@ describe('public endpoints', () => {
})
type Body = {
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) =>
(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')
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.
const row = await env.DB.prepare('SELECT data FROM presence WHERE account_id = ?1')
.bind(301)
.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.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
// is the invite, whatever the answer.
expect((await join('/matchmake/event/8', '300')).errorCode).toBe(0)
// is the invite, whatever the answer. They join the SAME session: the event's.
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)
// 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.
expect(await join('/matchmake/event/8', '399')).toEqual(refused(35))