mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 06:31:27 -07:00
move rooms schema to package
This commit is contained in:
@@ -48,3 +48,5 @@ yarn-error.log*
|
|||||||
|
|
||||||
# Agents
|
# Agents
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
|
|
||||||
|
.idea
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function unauthorized(c: Context<App>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Username changes a fresh account starts with (until one has been consumed). */
|
/** Username changes a fresh account starts with (until one has been consumed). */
|
||||||
const DEFAULT_USERNAME_CHANGES = 1
|
const DEFAULT_USERNAME_CHANGES = 5
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Username-change result envelope: `{ success, error, value }`, always HTTP 200.
|
* Username-change result envelope: `{ success, error, value }`, always HTTP 200.
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"test": "run-vitest"
|
"test": "run-vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@repo/domain": "workspace:*",
|
||||||
"@repo/hono-helpers": "workspace:*",
|
"@repo/hono-helpers": "workspace:*",
|
||||||
"@repo/jwt": "workspace:*",
|
"@repo/jwt": "workspace:*",
|
||||||
"hono": "4.12.27",
|
"hono": "4.12.27",
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
/**
|
|
||||||
* Read helpers for the shared `recflare` D1 database. The schema, migrations,
|
|
||||||
* and seed are owned by the `rooms` worker (apps/rooms/src/rooms-db.ts +
|
|
||||||
* migrations); this worker binds the same database read-only to resolve room
|
|
||||||
* roles for the `/api/rooms/v1/verifyRole` endpoint. Keep these queries in sync
|
|
||||||
* with the rooms worker's.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** A stored room — the parsed JSON blob (full client-facing room response). */
|
|
||||||
export type Room = Record<string, unknown>
|
|
||||||
|
|
||||||
interface RoomRow {
|
|
||||||
data: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseOne = (row: RoomRow | null): Room | null => (row ? (JSON.parse(row.data) as Room) : null)
|
|
||||||
|
|
||||||
export async function getRoomById(db: D1Database, roomId: number): Promise<Room | null> {
|
|
||||||
return parseOne(
|
|
||||||
await db.prepare('SELECT data FROM room WHERE room_id = ?1').bind(roomId).first<RoomRow>()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
|
|
||||||
|
import { getRoomById } from '@repo/domain'
|
||||||
|
|
||||||
import { authedId } from '../http'
|
import { authedId } from '../http'
|
||||||
import { getRoomById } from '../rooms-db'
|
|
||||||
|
|
||||||
import type { App } from '../context'
|
import type { App } from '../context'
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
import { useWorkersLogger } from 'workers-tagged-logger'
|
import { useWorkersLogger } from 'workers-tagged-logger'
|
||||||
|
|
||||||
import { RoomInstanceType } from '@repo/domain'
|
import {
|
||||||
|
getOrCreateDormRoom,
|
||||||
|
getRoomById,
|
||||||
|
getRoomByName,
|
||||||
|
RoomInstanceType,
|
||||||
|
} from '@repo/domain'
|
||||||
import { withNotFound, withOnError } from '@repo/hono-helpers'
|
import { withNotFound, withOnError } from '@repo/hono-helpers'
|
||||||
import { validateAndGetAccountId } from '@repo/jwt'
|
import { validateAndGetAccountId } from '@repo/jwt'
|
||||||
|
|
||||||
@@ -11,11 +16,10 @@ import {
|
|||||||
getRoomInstancesByRoom,
|
getRoomInstancesByRoom,
|
||||||
setRoomInstanceInProgress,
|
setRoomInstanceInProgress,
|
||||||
} from './room-instance-db'
|
} from './room-instance-db'
|
||||||
import { getOrCreateDormRoom, getRoomById, getRoomByName } from './rooms-db'
|
|
||||||
|
|
||||||
|
import type { Room } from '@repo/domain'
|
||||||
import type { Context } from 'hono'
|
import type { Context } from 'hono'
|
||||||
import type { App } from './context'
|
import type { App } from './context'
|
||||||
import type { Room } from './rooms-db'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The matchmaking surface. Rooms and room instances are D1-backed (matchmaking
|
* The matchmaking surface. Rooms and room instances are D1-backed (matchmaking
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
/**
|
|
||||||
* Read helpers for the shared `recflare` D1 database. The schema, migrations,
|
|
||||||
* and seed are owned by the `rooms` worker (apps/rooms/src/rooms-db.ts +
|
|
||||||
* migrations); this worker binds the same database read-only to resolve a room's
|
|
||||||
* real scene/subroom when building a matchmake instance. Keep these queries in
|
|
||||||
* sync with the rooms worker's.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Accessibility, Role } from '@repo/domain'
|
|
||||||
|
|
||||||
/** A stored room — the parsed JSON blob (full client-facing room response). */
|
|
||||||
export type Room = Record<string, unknown>
|
|
||||||
|
|
||||||
interface RoomRow {
|
|
||||||
data: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseOne = (row: RoomRow | null): Room | null => (row ? (JSON.parse(row.data) as Room) : null)
|
|
||||||
|
|
||||||
export async function getRoomById(db: D1Database, roomId: number): Promise<Room | null> {
|
|
||||||
return parseOne(
|
|
||||||
await db.prepare('SELECT data FROM room WHERE room_id = ?1').bind(roomId).first<RoomRow>()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getRoomByName(db: D1Database, name: string): Promise<Room | null> {
|
|
||||||
return parseOne(
|
|
||||||
await db
|
|
||||||
.prepare('SELECT data FROM room WHERE name_lower = ?1')
|
|
||||||
.bind(name.toLowerCase())
|
|
||||||
.first<RoomRow>()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The seeded template dorm (RoomId 1) that personal dorms are cloned from. */
|
|
||||||
const DORM_TEMPLATE_ROOM_ID = 1
|
|
||||||
|
|
||||||
/** A player's username from the shared accounts table (for naming their dorm), or null. */
|
|
||||||
export async function getUsername(db: D1Database, accountId: number): Promise<string | null> {
|
|
||||||
const row = await db
|
|
||||||
.prepare('SELECT data FROM accounts WHERE account_id = ?1')
|
|
||||||
.bind(accountId)
|
|
||||||
.first<{ data: string }>()
|
|
||||||
if (!row) return null
|
|
||||||
const account = JSON.parse(row.data) as { username?: string }
|
|
||||||
return typeof account.username === 'string' ? account.username : null
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A player's personal dorm room (owned by them, IsDorm), or null if none yet. */
|
|
||||||
export async function getDormRoom(db: D1Database, accountId: number): Promise<Room | null> {
|
|
||||||
return parseOne(
|
|
||||||
await db
|
|
||||||
.prepare('SELECT data FROM room WHERE creator_account_id = ?1 AND is_dorm = 1 LIMIT 1')
|
|
||||||
.bind(accountId)
|
|
||||||
.first<RoomRow>()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The player's personal dorm room, created on first access. Cloned from the
|
|
||||||
* seeded template dorm (RoomId 1) but owned by the player and flagged IsDorm — so
|
|
||||||
* matchmaking routes them into their own dorm and they can save it via the
|
|
||||||
* owner-gated room-save. Idempotent: returns the existing dorm once created.
|
|
||||||
*
|
|
||||||
* NOTE: this is the one place the match worker writes to the rooms table (the
|
|
||||||
* `rooms` worker otherwise owns the schema).
|
|
||||||
*/
|
|
||||||
export async function getOrCreateDormRoom(db: D1Database, accountId: number): Promise<Room> {
|
|
||||||
const existing = await getDormRoom(db, accountId)
|
|
||||||
if (existing) return existing
|
|
||||||
|
|
||||||
const template = await getRoomById(db, DORM_TEMPLATE_ROOM_ID)
|
|
||||||
const idRow = await db
|
|
||||||
.prepare('SELECT COALESCE(MAX(room_id), 1) + 1 AS next FROM room')
|
|
||||||
.first<{ next: number }>()
|
|
||||||
const roomId = idRow?.next ?? 2
|
|
||||||
|
|
||||||
// Reuse the template's subroom (scene/capacity), owned by the player, starting
|
|
||||||
// from a clean save. Fall back to the base dorm scene if the template is absent.
|
|
||||||
const templateSub =
|
|
||||||
template && Array.isArray(template.SubRooms) && template.SubRooms.length > 0
|
|
||||||
? (template.SubRooms[0] as Record<string, unknown>)
|
|
||||||
: { SubRoomId: 1, UnitySceneId: '76d98498-60a1-430c-ab76-b54a29b7a163', MaxPlayers: 4 }
|
|
||||||
|
|
||||||
// Named after the owner: `@<username>'s Dorm` (falls back to the account id).
|
|
||||||
const username = (await getUsername(db, accountId)) ?? `Player${accountId}`
|
|
||||||
|
|
||||||
const room: Room = {
|
|
||||||
...(template ?? { Accessibility: Accessibility.Unlisted }),
|
|
||||||
RoomId: roomId,
|
|
||||||
Name: `@${username}'s Dorm`,
|
|
||||||
CreatorAccountId: accountId,
|
|
||||||
IsDorm: true,
|
|
||||||
Roles: [{ AccountId: accountId, Role: Role.Owner, LastChangedByAccountId: null, InvitedRole: 0 }],
|
|
||||||
SubRooms: [{ ...templateSub, CreatorAccountId: accountId }],
|
|
||||||
CreatedAt: new Date().toISOString(),
|
|
||||||
}
|
|
||||||
await db.prepare('INSERT INTO room (data) VALUES (?1)').bind(JSON.stringify(room)).run()
|
|
||||||
return room
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"test": "run-vitest"
|
"test": "run-vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@repo/domain": "workspace:*",
|
||||||
"@repo/hono-helpers": "workspace:*",
|
"@repo/hono-helpers": "workspace:*",
|
||||||
"@repo/jwt": "workspace:*",
|
"@repo/jwt": "workspace:*",
|
||||||
"hono": "4.12.27",
|
"hono": "4.12.27",
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
import { useWorkersLogger } from 'workers-tagged-logger'
|
import { useWorkersLogger } from 'workers-tagged-logger'
|
||||||
|
|
||||||
import { logger, withNotFound, withOnError } from '@repo/hono-helpers'
|
|
||||||
import { validateAndGetAccountId } from '@repo/jwt'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
cloneRoom,
|
cloneRoom,
|
||||||
findSubRoom,
|
findSubRoom,
|
||||||
@@ -30,7 +27,9 @@ import {
|
|||||||
toggleCheer,
|
toggleCheer,
|
||||||
toggleFavorite,
|
toggleFavorite,
|
||||||
toggleRoomTag,
|
toggleRoomTag,
|
||||||
} from './rooms-db'
|
} from '@repo/domain'
|
||||||
|
import { logger, withNotFound, withOnError } from '@repo/hono-helpers'
|
||||||
|
import { validateAndGetAccountId } from '@repo/jwt'
|
||||||
|
|
||||||
import type { Context } from 'hono'
|
import type { Context } from 'hono'
|
||||||
import type { App } from './context'
|
import type { App } from './context'
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import { beforeAll, describe, expect, it } from 'vitest'
|
|||||||
|
|
||||||
import '../../rooms.app'
|
import '../../rooms.app'
|
||||||
|
|
||||||
|
import { ROOM_SCHEMA_DDL } from '@repo/domain'
|
||||||
|
|
||||||
import importRooms from '../../../static/ImportRooms.json'
|
import importRooms from '../../../static/ImportRooms.json'
|
||||||
import {
|
import {
|
||||||
createRoomInstance,
|
createRoomInstance,
|
||||||
getRoomInstance,
|
getRoomInstance,
|
||||||
SCHEMA_DDL as ROOM_INSTANCE_SCHEMA_DDL,
|
SCHEMA_DDL as ROOM_INSTANCE_SCHEMA_DDL,
|
||||||
} from '../../room-instance-db'
|
} from '../../room-instance-db'
|
||||||
import { SCHEMA_DDL } from '../../rooms-db'
|
|
||||||
|
|
||||||
import type { Env } from '../../context'
|
import type { Env } from '../../context'
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ async function bearer(sub: string): Promise<Record<string, string>> {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
// Seed the shared JWT signing key into the local Secrets Store so .get() resolves.
|
// Seed the shared JWT signing key into the local Secrets Store so .get() resolves.
|
||||||
await adminSecretsStore(env.JWT_SECRET).create('test-signing-key')
|
await adminSecretsStore(env.JWT_SECRET).create('test-signing-key')
|
||||||
for (const stmt of SCHEMA_DDL) await env.DB.prepare(stmt).run()
|
for (const stmt of ROOM_SCHEMA_DDL) await env.DB.prepare(stmt).run()
|
||||||
for (const stmt of ROOM_INSTANCE_SCHEMA_DDL) await env.DB.prepare(stmt).run()
|
for (const stmt of ROOM_INSTANCE_SCHEMA_DDL) await env.DB.prepare(stmt).run()
|
||||||
const insert = env.DB.prepare('INSERT OR IGNORE INTO room (data) VALUES (?1)')
|
const insert = env.DB.prepare('INSERT OR IGNORE INTO room (data) VALUES (?1)')
|
||||||
await env.DB.batch(importRooms.map((r) => insert.bind(JSON.stringify(r))))
|
await env.DB.batch(importRooms.map((r) => insert.bind(JSON.stringify(r))))
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export { RoomInstanceType, Accessibility, Role } from './enums'
|
export { RoomInstanceType, Accessibility, Role } from './enums'
|
||||||
export * from './accounts-db'
|
export * from './accounts-db'
|
||||||
|
export * from './rooms-db'
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
/**
|
/**
|
||||||
* Room storage on D1. Each room is a single JSON blob in the `data` column;
|
* Room storage on the shared `recflare` D1 database. Each room is a single JSON
|
||||||
* queryable fields (RoomId, Name, CreatorAccountId, IsDorm) are SQLite
|
* blob in the `data` column; queryable fields (RoomId, Name, CreatorAccountId,
|
||||||
* generated (virtual) columns extracted from that JSON and indexed. This keeps
|
* IsDorm) are SQLite generated (virtual) columns extracted from that JSON and
|
||||||
* the room shape flexible while still allowing fast lookups by id/name/creator.
|
* indexed. This keeps the room shape flexible while still allowing fast lookups
|
||||||
|
* by id/name/creator — the same JSON-blob pattern `accounts-db` uses.
|
||||||
*
|
*
|
||||||
* `SCHEMA_DDL` mirrors the head schema after all migrations (`0001_init.sql`
|
* `ROOM_SCHEMA_DDL` mirrors the head schema after all migrations (`0001_init.sql`
|
||||||
* created the table as `rooms`; `0005_rename_room.sql` renamed it to `room`);
|
* created the table as `rooms`; `0005_rename_room.sql` renamed it to `room`); the
|
||||||
* the room data is seeded from `static/ImportRooms.json` by
|
* room data is seeded from `apps/rooms/static/ImportRooms.json` by
|
||||||
* `migrations/0002_import_rooms.sql`. Tests apply `SCHEMA_DDL` then seed the
|
* `migrations/0002_import_rooms.sql`. Tests apply `ROOM_SCHEMA_DDL` then seed the
|
||||||
* imported rooms directly.
|
* imported rooms directly.
|
||||||
|
*
|
||||||
|
* This module is the single source of truth for the helpers: the `rooms` worker
|
||||||
|
* (which owns the schema/migrations) uses the read/write set; the `match` worker
|
||||||
|
* uses the room lookups plus the dorm helpers; the `api` worker binds the same
|
||||||
|
* database read-only and uses `getRoomById`. Each imports the subset it needs.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Accessibility, Role } from './enums'
|
||||||
|
|
||||||
/** Schema DDL (mirror of the head migration schema, sans the seed INSERT). */
|
/** Schema DDL (mirror of the head migration schema, sans the seed INSERT). */
|
||||||
export const SCHEMA_DDL: string[] = [
|
export const ROOM_SCHEMA_DDL: string[] = [
|
||||||
`CREATE TABLE IF NOT EXISTS room (
|
`CREATE TABLE IF NOT EXISTS room (
|
||||||
data TEXT NOT NULL,
|
data TEXT NOT NULL,
|
||||||
room_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.RoomId')) VIRTUAL,
|
room_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.RoomId')) VIRTUAL,
|
||||||
@@ -47,9 +55,6 @@ interface RoomRole {
|
|||||||
InvitedRole: number
|
InvitedRole: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Owner role value (max byte) — the room creator's tier. */
|
|
||||||
const ROLE_OWNER = 255
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clone an existing room into a new one owned by `accountId`. Copies the source
|
* Clone an existing room into a new one owned by `accountId`. Copies the source
|
||||||
* room's content (scene/subrooms/settings), assigning a fresh RoomId, the given
|
* room's content (scene/subrooms/settings), assigning a fresh RoomId, the given
|
||||||
@@ -81,7 +86,7 @@ export async function cloneRoom(
|
|||||||
// any co-owners, e.g. the seeded base-room roles for accounts 1/2) must NOT
|
// any co-owners, e.g. the seeded base-room roles for accounts 1/2) must NOT
|
||||||
// carry over, or the clone would still list the template's owner as owner.
|
// carry over, or the clone would still list the template's owner as owner.
|
||||||
const roles: RoomRole[] = [
|
const roles: RoomRole[] = [
|
||||||
{ AccountId: accountId, Role: ROLE_OWNER, LastChangedByAccountId: null, InvitedRole: 0 },
|
{ AccountId: accountId, Role: Role.Owner, LastChangedByAccountId: null, InvitedRole: 0 },
|
||||||
]
|
]
|
||||||
|
|
||||||
const cloned: Room = {
|
const cloned: Room = {
|
||||||
@@ -127,17 +132,17 @@ export async function setRoomImage(db: D1Database, roomId: number, imageName: st
|
|||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a user tag (`Type: 0`) to a room's `Tags`, skipping it when already present
|
|
||||||
* (case-insensitive). The caller supplies the already-loaded room (owner-checked)
|
|
||||||
* to avoid a re-read; the whole room JSON is rewritten. Returns the updated room.
|
|
||||||
*/
|
|
||||||
/**
|
/**
|
||||||
* Mutually-exclusive "main" room tags. The UI presents these as radio buttons, so
|
* Mutually-exclusive "main" room tags. The UI presents these as radio buttons, so
|
||||||
* setting one clears any other main tag. Compared case-insensitively.
|
* setting one clears any other main tag. Compared case-insensitively.
|
||||||
*/
|
*/
|
||||||
const MAIN_TAGS = new Set(['pvp', 'quest', 'game', 'hangout', 'art'])
|
const MAIN_TAGS = new Set(['pvp', 'quest', 'game', 'hangout', 'art'])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a user tag (`Type: 0`) to a room's `Tags`, skipping it when already present
|
||||||
|
* (case-insensitive). The caller supplies the already-loaded room (owner-checked)
|
||||||
|
* to avoid a re-read; the whole room JSON is rewritten. Returns the updated room.
|
||||||
|
*/
|
||||||
export async function toggleRoomTag(
|
export async function toggleRoomTag(
|
||||||
db: D1Database,
|
db: D1Database,
|
||||||
roomId: number,
|
roomId: number,
|
||||||
@@ -668,3 +673,70 @@ export async function getBaseRooms(db: D1Database, skip: number, take: number):
|
|||||||
.sort((a, b) => roomIdOf(a) - roomIdOf(b))
|
.sort((a, b) => roomIdOf(a) - roomIdOf(b))
|
||||||
.slice(skip, skip + take)
|
.slice(skip, skip + take)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The seeded template dorm (RoomId 1) that personal dorms are cloned from. */
|
||||||
|
const DORM_TEMPLATE_ROOM_ID = 1
|
||||||
|
|
||||||
|
/** A player's username from the shared accounts table (for naming their dorm), or null. */
|
||||||
|
export async function getUsername(db: D1Database, accountId: number): Promise<string | null> {
|
||||||
|
const row = await db
|
||||||
|
.prepare('SELECT data FROM accounts WHERE account_id = ?1')
|
||||||
|
.bind(accountId)
|
||||||
|
.first<{ data: string }>()
|
||||||
|
if (!row) return null
|
||||||
|
const account = JSON.parse(row.data) as { username?: string }
|
||||||
|
return typeof account.username === 'string' ? account.username : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A player's personal dorm room (owned by them, IsDorm), or null if none yet. */
|
||||||
|
export async function getDormRoom(db: D1Database, accountId: number): Promise<Room | null> {
|
||||||
|
return parseOne(
|
||||||
|
await db
|
||||||
|
.prepare('SELECT data FROM room WHERE creator_account_id = ?1 AND is_dorm = 1 LIMIT 1')
|
||||||
|
.bind(accountId)
|
||||||
|
.first<RoomRow>()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The player's personal dorm room, created on first access. Cloned from the
|
||||||
|
* seeded template dorm (RoomId 1) but owned by the player and flagged IsDorm — so
|
||||||
|
* matchmaking routes them into their own dorm and they can save it via the
|
||||||
|
* owner-gated room-save. Idempotent: returns the existing dorm once created.
|
||||||
|
*
|
||||||
|
* NOTE: this is the one place the match worker writes to the rooms table (the
|
||||||
|
* `rooms` worker otherwise owns the schema).
|
||||||
|
*/
|
||||||
|
export async function getOrCreateDormRoom(db: D1Database, accountId: number): Promise<Room> {
|
||||||
|
const existing = await getDormRoom(db, accountId)
|
||||||
|
if (existing) return existing
|
||||||
|
|
||||||
|
const template = await getRoomById(db, DORM_TEMPLATE_ROOM_ID)
|
||||||
|
const idRow = await db
|
||||||
|
.prepare('SELECT COALESCE(MAX(room_id), 1) + 1 AS next FROM room')
|
||||||
|
.first<{ next: number }>()
|
||||||
|
const roomId = idRow?.next ?? 2
|
||||||
|
|
||||||
|
// Reuse the template's subroom (scene/capacity), owned by the player, starting
|
||||||
|
// from a clean save. Fall back to the base dorm scene if the template is absent.
|
||||||
|
const templateSub =
|
||||||
|
template && Array.isArray(template.SubRooms) && template.SubRooms.length > 0
|
||||||
|
? (template.SubRooms[0] as Record<string, unknown>)
|
||||||
|
: { SubRoomId: 1, UnitySceneId: '76d98498-60a1-430c-ab76-b54a29b7a163', MaxPlayers: 4 }
|
||||||
|
|
||||||
|
// Named after the owner: `@<username>'s Dorm` (falls back to the account id).
|
||||||
|
const username = (await getUsername(db, accountId)) ?? `Player${accountId}`
|
||||||
|
|
||||||
|
const room: Room = {
|
||||||
|
...(template ?? { Accessibility: Accessibility.Unlisted }),
|
||||||
|
RoomId: roomId,
|
||||||
|
Name: `@${username}'s Dorm`,
|
||||||
|
CreatorAccountId: accountId,
|
||||||
|
IsDorm: true,
|
||||||
|
Roles: [{ AccountId: accountId, Role: Role.Owner, LastChangedByAccountId: null, InvitedRole: 0 }],
|
||||||
|
SubRooms: [{ ...templateSub, CreatorAccountId: accountId }],
|
||||||
|
CreatedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
await db.prepare('INSERT INTO room (data) VALUES (?1)').bind(JSON.stringify(room)).run()
|
||||||
|
return room
|
||||||
|
}
|
||||||
Generated
+6
@@ -90,6 +90,9 @@ importers:
|
|||||||
|
|
||||||
apps/api:
|
apps/api:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@repo/domain':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/domain
|
||||||
'@repo/hono-helpers':
|
'@repo/hono-helpers':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/hono-helpers
|
version: link:../../packages/hono-helpers
|
||||||
@@ -492,6 +495,9 @@ importers:
|
|||||||
|
|
||||||
apps/rooms:
|
apps/rooms:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@repo/domain':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/domain
|
||||||
'@repo/hono-helpers':
|
'@repo/hono-helpers':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/hono-helpers
|
version: link:../../packages/hono-helpers
|
||||||
|
|||||||
Reference in New Issue
Block a user