mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
add rooms
This commit is contained in:
+105
-4
@@ -56,6 +56,64 @@ async function parseFormIds(c: Context<App>): Promise<number[]> {
|
||||
.filter((n) => !Number.isNaN(n))
|
||||
}
|
||||
|
||||
/** Unity scene id for the dorm (matches the match worker's instance location). */
|
||||
const DORM_SCENE_ID = '76d98498-60a1-430c-ab76-b54a29b7a163'
|
||||
|
||||
/**
|
||||
* Full room payload (PascalCase), mirroring the C#'s `BuildRoomResponse` /
|
||||
* `RoomserverRoomsBulk`. With no Rooms DB, room 1 is the dorm and other ids get
|
||||
* a generic published room. The SubRoom carries the UnitySceneId/DataBlob the
|
||||
* client needs to load the scene.
|
||||
*/
|
||||
function buildRoomResponse(roomId: number) {
|
||||
const isDorm = roomId === 1
|
||||
return {
|
||||
RoomId: roomId,
|
||||
Name: isDorm ? 'DormRoom' : `Room${roomId}`,
|
||||
Description: isDorm ? 'Your private room' : '',
|
||||
CreatorAccountId: 1,
|
||||
ImageName: 'DefaultRoomImage.jpg',
|
||||
State: 0,
|
||||
Accessibility: 0,
|
||||
SupportsLevelVoting: false,
|
||||
IsRRO: false,
|
||||
IsDorm: isDorm,
|
||||
CloningAllowed: false,
|
||||
SupportsVRLow: true,
|
||||
SupportsQuest2: true,
|
||||
SupportsMobile: true,
|
||||
SupportsScreens: true,
|
||||
SupportsWalkVR: true,
|
||||
SupportsTeleportVR: true,
|
||||
SupportsJuniors: true,
|
||||
MinLevel: 0,
|
||||
WarningMask: 0,
|
||||
CustomWarning: null,
|
||||
DisableMicAutoMute: false,
|
||||
DisableRoomComments: false,
|
||||
EncryptVoiceChat: false,
|
||||
CreatedAt: '2026-01-18T02:31:37.6171131',
|
||||
Stats: { CheerCount: 0, FavoriteCount: 0, VisitorCount: 1, VisitCount: 1 },
|
||||
SubRooms: [
|
||||
{
|
||||
SubRoomId: 1,
|
||||
Name: '',
|
||||
DataBlob: '',
|
||||
IsSandbox: false,
|
||||
MaxPlayers: 4,
|
||||
Accessibility: 0,
|
||||
UnitySceneId: isDorm ? DORM_SCENE_ID : '',
|
||||
DataSavedAt: '2026-01-18T02:31:37.6171131',
|
||||
},
|
||||
],
|
||||
Roles: [],
|
||||
LoadScreens: [],
|
||||
PromoImages: [],
|
||||
PromoExternalContent: [],
|
||||
Tags: [],
|
||||
}
|
||||
}
|
||||
|
||||
/** Default reputation for an account — the fallback the C# fills with no DB. */
|
||||
function defaultReputation(id: number) {
|
||||
return {
|
||||
@@ -95,6 +153,27 @@ const app = new Hono<App>({ strict: false })
|
||||
UseRudderStack: false,
|
||||
})
|
||||
)
|
||||
.get('/api/config/v1/azurespeech', (c) =>
|
||||
c.json({
|
||||
Key: 'dce8de5b297747d9b5bddcc7f19e8c5b',
|
||||
Region: 'eastus',
|
||||
Enabled: false,
|
||||
})
|
||||
)
|
||||
.get('/api/config/v1/backtrace', (c) =>
|
||||
c.json({
|
||||
ReportBudget: 125,
|
||||
FilterType: 0,
|
||||
SampleRate: 0.025,
|
||||
LogLineCount: 50,
|
||||
CaptureNativeCrashes: 1,
|
||||
AMRThresholdMS: 0,
|
||||
MessageCount: 1000,
|
||||
MessageRegex:
|
||||
"^Cannot set the parent of the GameObject .* while its new parent|^\\\\>\\\\x2010x\\\\:\\\\x20|\\\\'LabelTheme\\\\' contains missing PaletteTheme reference on",
|
||||
VersionRegex: '.*',
|
||||
})
|
||||
)
|
||||
.get('/api/config/v2', (c) => c.json(apiConfigV2))
|
||||
.get('/api/versioncheck/v4', (c) =>
|
||||
c.json({
|
||||
@@ -129,6 +208,11 @@ const app = new Hono<App>({ strict: false })
|
||||
await parseFormIds(c) // TODO: query PlayerProgressions for these ids
|
||||
return c.json([])
|
||||
})
|
||||
// C# v2 is identical to v1 — same ParseFormIds + PlayerProgressions query.
|
||||
.post('/api/players/v2/progression/bulk', async (c) => {
|
||||
await parseFormIds(c) // TODO: query PlayerProgressions for these ids
|
||||
return c.json([])
|
||||
})
|
||||
.post('/api/v1/progression/bulk', async (c) => {
|
||||
await parseFormIds(c) // TODO: query PlayerProgressions for these ids
|
||||
return c.json([])
|
||||
@@ -217,6 +301,17 @@ const app = new Hono<App>({ strict: false })
|
||||
return c.json({ success: false, error: 'Gift not found' }, 404)
|
||||
})
|
||||
|
||||
// Custom avatar item gates. None of these are in CannedNet — they're real Rec
|
||||
// Room client endpoints the C# never implemented. Each returns a bare JSON
|
||||
// boolean; we enable them. Flip to `false` to disable the corresponding flow.
|
||||
.get('/api/customAvatarItems/v1/isCreationAllowedForAccount', (c) => c.json(true))
|
||||
.get('/api/customAvatarItems/v1/isCreationEnabled', (c) => c.json(true))
|
||||
.get('/api/customAvatarItems/v1/isRenderingEnabled', (c) => c.json(true))
|
||||
|
||||
// Voice chat config. Not in CannedNet; the client fetches it to set up voice.
|
||||
// No reference shape, so return an empty object until the client needs fields.
|
||||
.get('/voice/config', (c) => c.json({}))
|
||||
|
||||
// ---- Player reporting -----------------------------------------------------
|
||||
.get('/api/PlayerReporting/v1/moderationBlockDetails', (c) =>
|
||||
c.json({
|
||||
@@ -330,19 +425,25 @@ const app = new Hono<App>({ strict: false })
|
||||
if (!idParam && !nameParam) {
|
||||
return c.text("Either 'id' or 'name' query parameter is required", 400)
|
||||
}
|
||||
return c.json([]) // TODO: query Rooms + related tables
|
||||
// Synthesize a room per requested id (the client needs SubRooms to load).
|
||||
// TODO: query Rooms + related tables once a DB binding exists.
|
||||
const ids = (idParam ?? '')
|
||||
.split(',')
|
||||
.map((s) => Number.parseInt(s.trim(), 10))
|
||||
.filter((n) => !Number.isNaN(n))
|
||||
return c.json(ids.map(buildRoomResponse))
|
||||
})
|
||||
.get('/roomserver/rooms/hot', (c) => c.json({ Results: [], TotalResults: 0 }))
|
||||
.get('/roomserver/roomsandplaylists/hot', (c) => c.json({ Results: [], TotalResults: 0 }))
|
||||
.get('/roomserver/rooms/createdby/me', (c) => c.json([])) // TODO: hydrate from JSON/ownedrooms.json
|
||||
.get('/roomserver/rooms/createdby/me', (c) => c.json([buildRoomResponse(1)]))
|
||||
.get('/roomserver/rooms/:id/interactionby/me', (c) =>
|
||||
c.json({ Cheered: false, Favorited: false })
|
||||
)
|
||||
.get('/roomserver/rooms/:id', (c) => {
|
||||
const roomId = Number.parseInt(c.req.param('id'), 10)
|
||||
if (Number.isNaN(roomId)) return c.notFound()
|
||||
// No Rooms binding → room can never be found.
|
||||
return c.notFound()
|
||||
// No Rooms binding → synthesize the room so the client can load it.
|
||||
return c.json(buildRoomResponse(roomId))
|
||||
})
|
||||
|
||||
export default app
|
||||
|
||||
@@ -46,6 +46,23 @@ describe('public endpoints', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('GET /api/config/v1/azurespeech', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/config/v1/azurespeech`)
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({
|
||||
Key: 'dce8de5b297747d9b5bddcc7f19e8c5b',
|
||||
Region: 'eastus',
|
||||
Enabled: false,
|
||||
})
|
||||
})
|
||||
|
||||
test('GET /api/config/v1/backtrace', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/config/v1/backtrace`)
|
||||
expect(res.status).toBe(200)
|
||||
const body = (await res.json()) as { ReportBudget: number; VersionRegex: string }
|
||||
expect(body).toMatchObject({ ReportBudget: 125, VersionRegex: '.*' })
|
||||
})
|
||||
|
||||
test('GET /api/versioncheck/v4', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/versioncheck/v4`)
|
||||
expect(await res.json()).toMatchObject({ VersionStatus: 0 })
|
||||
@@ -84,6 +101,42 @@ describe('public endpoints', () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/storefronts/v1/p2p/betaEnabled`)
|
||||
expect(await res.json()).toBe(false)
|
||||
})
|
||||
|
||||
test('POST /api/players/v2/progression/bulk returns an array', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/players/v2/progression/bulk`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ Ids: '1,2,3' }),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual([])
|
||||
})
|
||||
|
||||
test('GET /api/customAvatarItems/v1/isCreationAllowedForAccount returns true', async () => {
|
||||
const res = await exports.default.fetch(
|
||||
`${ORIGIN}/api/customAvatarItems/v1/isCreationAllowedForAccount`
|
||||
)
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toBe(true)
|
||||
})
|
||||
|
||||
test('GET /api/customAvatarItems/v1/isCreationEnabled returns true', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/customAvatarItems/v1/isCreationEnabled`)
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toBe(true)
|
||||
})
|
||||
|
||||
test('GET /api/customAvatarItems/v1/isRenderingEnabled returns true', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/customAvatarItems/v1/isRenderingEnabled`)
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toBe(true)
|
||||
})
|
||||
|
||||
test('GET /voice/config returns an object', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/voice/config`)
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('auth-gated endpoints', () => {
|
||||
@@ -129,10 +182,12 @@ describe('room server', () => {
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
test('GET /roomserver/rooms/bulk with id returns empty array', async () => {
|
||||
test('GET /roomserver/rooms/bulk with id returns rooms with SubRooms', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/roomserver/rooms/bulk?id=1,2`)
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual([])
|
||||
const rooms = (await res.json()) as Array<{ RoomId: number; SubRooms: unknown[] }>
|
||||
expect(rooms.map((r) => r.RoomId)).toEqual([1, 2])
|
||||
expect(rooms[0].SubRooms).toHaveLength(1)
|
||||
})
|
||||
|
||||
test('GET /roomserver/rooms/hot returns an empty result set', async () => {
|
||||
@@ -140,9 +195,16 @@ describe('room server', () => {
|
||||
expect(await res.json()).toEqual({ Results: [], TotalResults: 0 })
|
||||
})
|
||||
|
||||
test('GET /roomserver/rooms/:id 404s without data', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/roomserver/rooms/5`)
|
||||
expect(res.status).toBe(404)
|
||||
test('GET /roomserver/rooms/:id synthesizes a room with a SubRoom', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/roomserver/rooms/1`)
|
||||
expect(res.status).toBe(200)
|
||||
const room = (await res.json()) as {
|
||||
RoomId: number
|
||||
IsDorm: boolean
|
||||
SubRooms: Array<{ UnitySceneId: string }>
|
||||
}
|
||||
expect(room).toMatchObject({ RoomId: 1, IsDorm: true })
|
||||
expect(room.SubRooms[0].UnitySceneId).toBe('76d98498-60a1-430c-ab76-b54a29b7a163')
|
||||
})
|
||||
|
||||
test('GET /roomserver/rooms/:id/interactionby/me', async () => {
|
||||
|
||||
+2296
-168
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user