adding more outfits, putting back matchmake/dorm

This commit is contained in:
Devin Zuczek
2026-08-01 17:35:38 -04:00
parent cf05026285
commit c5b602bbd2
8 changed files with 155 additions and 8 deletions
+26
View File
@@ -327,6 +327,32 @@ export const avatarRoutes = new Hono<App>({ strict: false })
}
)
// The caller's outfit wardrobe. An empty list for now — the outfits saved through
// `PUT /outfits/me` are in the shared `outfit` table already, but which of them
// belong in this list (and in what shape) has not been pinned down, so it answers []
// rather than guessing.
.get(
'/outfits/me/saved',
describeRoute({
tags: ['Avatar'],
summary: 'The callers saved outfits',
description:
'The wardrobe behind the newer outfit screen. Empty for now: the outfits saved ' +
'through `PUT /outfits/me` are in the shared `outfit` table, but which of them this ' +
'list should carry, and in what shape, is not pinned down yet.',
security: AUTHED,
responses: {
200: json(JsonArray, 'An empty list'),
401: UNAUTHORIZED_RESPONSE,
},
}),
async (c) => {
const id = await authedId(c)
if (id === null) return unauthorized(c)
return c.json([])
}
)
// A single invention by id (`?inventionId=…`). Returns the stored RRInvention,
// or 404 when there's no such invention.
.get(
+12
View File
@@ -460,6 +460,17 @@ describe('public endpoints', () => {
expect(((await worn.json()) as { Name: string | null }).Name).toBe(null)
})
test('GET /outfits/me/saved 401s without a token, returns [] with one', async () => {
const anon = await exports.default.fetch(`${ORIGIN}/outfits/me/saved`)
expect(anon.status).toBe(401)
// Empty even for account 42, which saved an outfit through PUT /outfits/me above.
const res = await exports.default.fetch(`${ORIGIN}/outfits/me/saved`, {
headers: await bearer(),
})
expect(res.status).toBe(200)
expect(await res.json()).toEqual([])
})
test('PUT /outfits/me 400s on an unparseable body', async () => {
const res = await exports.default.fetch(`${ORIGIN}/outfits/me`, {
method: 'PUT',
@@ -2092,6 +2103,7 @@ describe('openapi', () => {
'GET /api/rooms/v1/filters',
'GET /api/versioncheck/v4',
'GET /outfits/me',
'GET /outfits/me/saved',
'GET /voice/config',
'POST /api/CampusCard/v1/UpdateAndGetSubscription',
'POST /api/PlayerReporting/v1/deviceId',
+35
View File
@@ -1099,6 +1099,41 @@ const app = new Hono<App>()
return c.json({ errorCode: 0, roomInstance: instance })
}
)
// Matchmake with no target. The client posts this when it needs an instance but isn't
// going anywhere in particular — at startup, and while sitting in Orientation. It
// answers the instance the player is ALREADY in, so it never warps anyone out of the
// room they're standing in; only a player with no live presence falls back to their
// dorm. Either way presence is re-committed, which refreshes its TTL.
.post(
'/matchmake/none',
describeRoute({
tags: ['Navigation'],
summary: 'Matchmake with no target',
description: [
'Answers the instance the caller is already in, rather than sending them anywhere —',
'this is what the client posts at startup and while in Orientation, so forcing a',
'destination here would warp the player out of the room they are standing in. A',
'caller with no live presence (their TTL lapsed, or they have never entered a room)',
'falls back to their personal dorm. Re-commits presence either way, refreshing its',
'TTL.',
].join(' '),
security: AUTHED,
responses: {
200: json(MatchmakeResponse, 'The callers current instance, or their dorm'),
401: UNAUTHORIZED_RESPONSE,
},
}),
async (c) => {
const id = await authedId(c)
if (id === null) return unauthorized(c)
const presence = await getPresence<RoomInstance>(c.env.DB, id)
const current = presence?.roomInstance ?? (await playerDormInstance(c, id))
await enterRoom(c, id, current)
return c.json({ errorCode: 0, roomInstance: current })
}
)
.post(
'/matchmake/dorm',
describeRoute({
@@ -676,6 +676,45 @@ describe('auth-gated endpoints', () => {
})
})
test('POST /matchmake/none 401s without a token', async () => {
const res = await exports.default.fetch(`${ORIGIN}/matchmake/none`, { method: 'POST' })
expect(res.status).toBe(401)
})
test('POST /matchmake/none keeps the caller where they are, else falls back to the dorm', async () => {
const none = async (sub: string) =>
(await (
await exports.default.fetch(`${ORIGIN}/matchmake/none`, {
method: 'POST',
headers: await bearer(sub),
})
).json()) as { errorCode: number; roomInstance: { roomId: number; roomInstanceId: number } }
// Account 44 has never entered a room → their personal dorm, and a second call is
// idempotent now that presence holds it.
const fresh = await none('44')
expect(fresh.errorCode).toBe(0)
expect(fresh.roomInstance.roomId).toBeGreaterThan(2)
expect((await none('44')).roomInstance).toMatchObject({
roomId: fresh.roomInstance.roomId,
roomInstanceId: fresh.roomInstance.roomInstanceId,
})
// Once in a real room, `none` must NOT warp them out of it — that is the whole
// point of the endpoint, since the client posts it while sitting in Orientation.
const entered = (await (
await exports.default.fetch(`${ORIGIN}/matchmake/room/2`, {
method: 'POST',
headers: await bearer('44'),
})
).json()) as { roomInstance: { roomId: number; roomInstanceId: number } }
expect(entered.roomInstance.roomId).toBe(2)
expect((await none('44')).roomInstance).toMatchObject({
roomId: 2,
roomInstanceId: entered.roomInstance.roomInstanceId,
})
})
test('each players dorm gets a distinct global subroom id', async () => {
// Dorms used to copy the template subroom verbatim, so every dorm carried SubRoomId 1.
// With subrooms minted from the global sequence, each dorm gets its own unique id.
@@ -1453,6 +1492,7 @@ describe('auth-gated endpoints', () => {
'POST /invite',
'POST /matchmake/club/{clubId}',
'POST /matchmake/dorm',
'POST /matchmake/none',
'POST /matchmake/player/{playerId}',
'POST /matchmake/room/{roomId}',
'POST /matchmake/room/{roomId}/{subRoomId}',