import { Hono } from 'hono' import { describeRoute } from 'hono-openapi' import charadesWords from '../../static/charades.json' import communityBoard from '../../static/community-board.json' import { BareString, idParam, IsPureResponse, json, JsonArray, jsonBody, JsonObject, KeepsakeConfig, SanitizeRequest, stringParam, SubscriptionResponse, } from '../openapi' import type { App } from '../context' // Text sanitization, keepsakes, objectives/events/rewards, and the misc // analytics/subscription sinks the client hits during load. export const gameplayRoutes = new Hono({ strict: false }) // Text sanitization (display names, room names, chat). `v1` echoes the input // value back; `isPure` reports the text is clean. .post( '/api/sanitize/v1', describeRoute({ tags: ['Gameplay'], summary: 'Sanitize a string', description: 'Runs display names, room names and chat through the profanity filter. There is ' + 'no filter here — the input `Value` is echoed back verbatim as a bare JSON string ' + '(an empty string if the body has no `Value`).', requestBody: jsonBody(SanitizeRequest, 'The text to clean'), responses: { 200: json(BareString, 'The input text, unchanged (a bare JSON string)') }, }), async (c) => { const body = await c.req.json<{ Value?: unknown }>().catch(() => ({}) as { Value?: unknown }) return c.json(typeof body.Value === 'string' ? body.Value : '') } ) .post( '/api/sanitize/v1/isPure', describeRoute({ tags: ['Gameplay'], summary: 'Whether a string is clean', description: 'The yes/no form of the filter. Always `true` — nothing is filtered here.', requestBody: jsonBody(SanitizeRequest, 'The text to check'), responses: { 200: json(IsPureResponse, 'Always pure') }, }), (c) => c.json({ IsPure: true }) ) // ---- Activities ----------------------------------------------------------- // Word bank for the Charades activity. The client requests the list by // activity name (`.../words/Charades`); other activities have no data yet. .get( '/api/activities/charades/v1/words/:activity', describeRoute({ tags: ['Gameplay'], summary: 'An activity’s word bank', description: 'The words the Charades activity draws from. The client asks by activity name ' + '(`.../words/Charades`); the name is not matched on, so every activity gets the ' + 'charades list — no other activity has data yet.', parameters: [stringParam('activity', 'Activity name, e.g. `Charades`. Not matched on.')], responses: { 200: json(JsonArray, 'The word list') }, }), (c) => c.json(charadesWords) ) // Keepsakes (room mementos). Stubbed empty. .get( '/api/keepsakes/globalconfig', describeRoute({ tags: ['Gameplay'], summary: 'Keepsake feature switches', description: 'Whether keepsakes (room mementos) are on and how many a room may hold. The ' + 'feature reports as enabled, but nothing stores keepsakes yet.', responses: { 200: json(KeepsakeConfig, 'The keepsake config') }, }), (c) => c.json({ KeepsakeFeatureEnabled: true, KeepsakeRoomLimit: 10, SocialXpBoostEnabled: false }) ) .get( '/api/keepsakes/rooms/:roomId', describeRoute({ tags: ['Gameplay'], summary: 'A room’s keepsakes', description: 'No keepsake storage yet. Answers 204 with no body rather than an empty list — ' + 'that is what the reference does, and the client treats a body here as data.', parameters: [idParam('roomId', 'Room id')], responses: { 204: { description: 'No keepsakes (empty body)' } }, }), (c) => c.body(null, 204) ) .get( '/api/keepsakes/categories', describeRoute({ tags: ['Gameplay'], summary: 'Keepsake categories', description: 'No keepsake catalog yet, so this is an empty list.', responses: { 200: json(JsonArray, 'An empty list') }, }), (c) => c.json([]) ) // ---- Objectives / events / rewards --------------------------------------- // Objectives live on the `econ` host (`updateobjective` / `myprogress`), which is // where the client calls them — they are not served here. .get( '/api/communityboard/v2/current', describeRoute({ tags: ['Gameplay'], summary: 'The current community board', description: 'The rotating community board on the home screen — featured player, featured room ' + 'group, announcement and image strips. Served verbatim from a static blob.', responses: { 200: json(JsonObject, 'The community board') }, }), (c) => c.json(communityBoard) ) // Player events live in their own controller (routes/events.ts) — they're D1-backed // now, unlike the stubs around them here. .get( '/api/announcement/v1/get', describeRoute({ tags: ['Gameplay'], summary: 'Announcements', description: 'The announcement banners on the home screen. Not hydrated yet.', responses: { 200: json(JsonArray, 'An empty list') }, }), (c) => c.json([]) ) // TODO: hydrate from JSON/announcements.json // GameSight attribution/analytics event sink. Accept and ack without persisting. .post( '/api/gamesight/event', describeRoute({ tags: ['Gameplay'], summary: 'Analytics event sink', description: 'The client’s GameSight attribution/analytics events. Accepted and dropped — ' + 'nothing is persisted. Answers 200 with an empty body.', responses: { 200: { description: 'Accepted (empty body)' } }, }), (c) => c.body(null, 200) ) // ---- Subscription --------------------------------------------------------- .post( '/api/CampusCard/v1/UpdateAndGetSubscription', describeRoute({ tags: ['Gameplay'], summary: 'The caller’s subscription', description: 'Rec Room Plus subscription state. There are no subscriptions on this server, so ' + 'both fields are null. Also served by the `econ` worker on its own host.', responses: { 200: json(SubscriptionResponse, 'No subscription') }, }), (c) => c.json({ subscription: null, platformAccountSubscribedPlayerId: null }) )