feat(server): 添加游戏服务器框架 | add game server framework (#366)
**@esengine/server** - 游戏服务器框架 | Game server framework - 文件路由系统 | File-based routing - Room 生命周期 (onCreate, onJoin, onLeave, onTick, onDispose) - @onMessage 装饰器 | Message handler decorator - 玩家管理与断线处理 | Player management with auto-disconnect - 内置 JoinRoom/LeaveRoom API | Built-in room APIs - defineApi/defineMsg 类型安全辅助函数 | Type-safe helpers **create-esengine-server** - CLI 脚手架工具 | CLI scaffolding - 生成 shared/server/client 项目结构 | Project structure - 类型安全的协议定义 | Type-safe protocol definitions - 包含 GameRoom 示例 | Example implementation **其他 | Other** - 删除旧的 network-server 包 | Remove old network-server - 更新服务器文档 | Update server documentation
This commit is contained in:
259
packages/framework/server/src/core/server.ts
Normal file
259
packages/framework/server/src/core/server.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* @zh 游戏服务器核心
|
||||
* @en Game server core
|
||||
*/
|
||||
|
||||
import * as path from 'node:path'
|
||||
import { serve, type RpcServer } from '@esengine/rpc/server'
|
||||
import { rpc } from '@esengine/rpc'
|
||||
import type {
|
||||
ServerConfig,
|
||||
ServerConnection,
|
||||
GameServer,
|
||||
ApiContext,
|
||||
MsgContext,
|
||||
LoadedApiHandler,
|
||||
LoadedMsgHandler,
|
||||
} from '../types/index.js'
|
||||
import { loadApiHandlers, loadMsgHandlers } from '../router/loader.js'
|
||||
import { RoomManager, type RoomClass, type Room } from '../room/index.js'
|
||||
|
||||
/**
|
||||
* @zh 默认配置
|
||||
* @en Default configuration
|
||||
*/
|
||||
const DEFAULT_CONFIG: Required<Omit<ServerConfig, 'onStart' | 'onConnect' | 'onDisconnect'>> = {
|
||||
port: 3000,
|
||||
apiDir: 'src/api',
|
||||
msgDir: 'src/msg',
|
||||
tickRate: 20,
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建游戏服务器
|
||||
* @en Create game server
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createServer, Room, onMessage } from '@esengine/server'
|
||||
*
|
||||
* class GameRoom extends Room {
|
||||
* onJoin(player) {
|
||||
* this.broadcast('Joined', { id: player.id })
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* const server = await createServer({ port: 3000 })
|
||||
* server.define('game', GameRoom)
|
||||
* await server.start()
|
||||
* ```
|
||||
*/
|
||||
export async function createServer(config: ServerConfig = {}): Promise<GameServer> {
|
||||
const opts = { ...DEFAULT_CONFIG, ...config }
|
||||
const cwd = process.cwd()
|
||||
|
||||
// 加载文件路由处理器
|
||||
const apiHandlers = await loadApiHandlers(path.resolve(cwd, opts.apiDir))
|
||||
const msgHandlers = await loadMsgHandlers(path.resolve(cwd, opts.msgDir))
|
||||
|
||||
if (apiHandlers.length > 0) {
|
||||
console.log(`[Server] Loaded ${apiHandlers.length} API handlers`)
|
||||
}
|
||||
if (msgHandlers.length > 0) {
|
||||
console.log(`[Server] Loaded ${msgHandlers.length} message handlers`)
|
||||
}
|
||||
|
||||
// 动态构建协议
|
||||
const apiDefs: Record<string, ReturnType<typeof rpc.api>> = {
|
||||
// 内置 API
|
||||
JoinRoom: rpc.api(),
|
||||
LeaveRoom: rpc.api(),
|
||||
}
|
||||
const msgDefs: Record<string, ReturnType<typeof rpc.msg>> = {
|
||||
// 内置消息(房间消息透传)
|
||||
RoomMessage: rpc.msg(),
|
||||
}
|
||||
|
||||
for (const handler of apiHandlers) {
|
||||
apiDefs[handler.name] = rpc.api()
|
||||
}
|
||||
for (const handler of msgHandlers) {
|
||||
msgDefs[handler.name] = rpc.msg()
|
||||
}
|
||||
|
||||
const protocol = rpc.define({
|
||||
api: apiDefs,
|
||||
msg: msgDefs,
|
||||
})
|
||||
|
||||
// 服务器状态
|
||||
let currentTick = 0
|
||||
let tickInterval: ReturnType<typeof setInterval> | null = null
|
||||
let rpcServer: RpcServer<typeof protocol, Record<string, unknown>> | null = null
|
||||
|
||||
// 房间管理器
|
||||
let roomManager: RoomManager | null = null
|
||||
|
||||
// 构建 API 处理器映射
|
||||
const apiMap: Record<string, LoadedApiHandler> = {}
|
||||
for (const handler of apiHandlers) {
|
||||
apiMap[handler.name] = handler
|
||||
}
|
||||
|
||||
// 构建消息处理器映射
|
||||
const msgMap: Record<string, LoadedMsgHandler> = {}
|
||||
for (const handler of msgHandlers) {
|
||||
msgMap[handler.name] = handler
|
||||
}
|
||||
|
||||
// 游戏服务器实例
|
||||
const gameServer: GameServer & {
|
||||
rooms: RoomManager | null
|
||||
} = {
|
||||
get connections() {
|
||||
return (rpcServer?.connections ?? []) as ReadonlyArray<ServerConnection>
|
||||
},
|
||||
|
||||
get tick() {
|
||||
return currentTick
|
||||
},
|
||||
|
||||
get rooms() {
|
||||
return roomManager
|
||||
},
|
||||
|
||||
/**
|
||||
* @zh 注册房间类型
|
||||
* @en Define room type
|
||||
*/
|
||||
define(name: string, roomClass: new () => unknown): void {
|
||||
if (!roomManager) {
|
||||
throw new Error('Server not started. Call define() after createServer().')
|
||||
}
|
||||
roomManager.define(name, roomClass as RoomClass)
|
||||
},
|
||||
|
||||
async start() {
|
||||
// 初始化房间管理器
|
||||
roomManager = new RoomManager((conn, type, data) => {
|
||||
rpcServer?.send(conn, 'RoomMessage' as any, { type, data } as any)
|
||||
})
|
||||
|
||||
// 构建 API handlers
|
||||
const apiHandlersObj: Record<string, (input: unknown, conn: any) => Promise<unknown>> = {}
|
||||
|
||||
// 内置 JoinRoom API
|
||||
apiHandlersObj['JoinRoom'] = async (input: any, conn) => {
|
||||
const { roomType, roomId, options } = input as {
|
||||
roomType?: string
|
||||
roomId?: string
|
||||
options?: Record<string, unknown>
|
||||
}
|
||||
|
||||
if (roomId) {
|
||||
const result = await roomManager!.joinById(roomId, conn.id, conn)
|
||||
if (!result) {
|
||||
throw new Error('Failed to join room')
|
||||
}
|
||||
return { roomId: result.room.id, playerId: result.player.id }
|
||||
}
|
||||
|
||||
if (roomType) {
|
||||
const result = await roomManager!.joinOrCreate(roomType, conn.id, conn, options)
|
||||
if (!result) {
|
||||
throw new Error('Failed to join or create room')
|
||||
}
|
||||
return { roomId: result.room.id, playerId: result.player.id }
|
||||
}
|
||||
|
||||
throw new Error('roomType or roomId required')
|
||||
}
|
||||
|
||||
// 内置 LeaveRoom API
|
||||
apiHandlersObj['LeaveRoom'] = async (_input, conn) => {
|
||||
await roomManager!.leave(conn.id)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
// 文件路由 API
|
||||
for (const [name, handler] of Object.entries(apiMap)) {
|
||||
apiHandlersObj[name] = async (input, conn) => {
|
||||
const ctx: ApiContext = {
|
||||
conn: conn as ServerConnection,
|
||||
server: gameServer,
|
||||
}
|
||||
return handler.definition.handler(input, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// 构建消息 handlers
|
||||
const msgHandlersObj: Record<string, (data: unknown, conn: any) => void | Promise<void>> = {}
|
||||
|
||||
// 内置 RoomMessage 处理
|
||||
msgHandlersObj['RoomMessage'] = async (data: any, conn) => {
|
||||
const { type, payload } = data as { type: string; payload: unknown }
|
||||
roomManager!.handleMessage(conn.id, type, payload)
|
||||
}
|
||||
|
||||
// 文件路由消息
|
||||
for (const [name, handler] of Object.entries(msgMap)) {
|
||||
msgHandlersObj[name] = async (data, conn) => {
|
||||
const ctx: MsgContext = {
|
||||
conn: conn as ServerConnection,
|
||||
server: gameServer,
|
||||
}
|
||||
await handler.definition.handler(data, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
rpcServer = serve(protocol, {
|
||||
port: opts.port,
|
||||
createConnData: () => ({}),
|
||||
onStart: (p) => {
|
||||
console.log(`[Server] Started on ws://localhost:${p}`)
|
||||
opts.onStart?.(p)
|
||||
},
|
||||
onConnect: async (conn) => {
|
||||
await config.onConnect?.(conn as ServerConnection)
|
||||
},
|
||||
onDisconnect: async (conn) => {
|
||||
// 玩家断线时自动离开房间
|
||||
await roomManager?.leave(conn.id, 'disconnected')
|
||||
await config.onDisconnect?.(conn as ServerConnection)
|
||||
},
|
||||
api: apiHandlersObj as any,
|
||||
msg: msgHandlersObj as any,
|
||||
})
|
||||
|
||||
await rpcServer.start()
|
||||
|
||||
// 启动 tick 循环
|
||||
if (opts.tickRate > 0) {
|
||||
tickInterval = setInterval(() => {
|
||||
currentTick++
|
||||
}, 1000 / opts.tickRate)
|
||||
}
|
||||
},
|
||||
|
||||
async stop() {
|
||||
if (tickInterval) {
|
||||
clearInterval(tickInterval)
|
||||
tickInterval = null
|
||||
}
|
||||
if (rpcServer) {
|
||||
await rpcServer.stop()
|
||||
rpcServer = null
|
||||
}
|
||||
},
|
||||
|
||||
broadcast(name, data) {
|
||||
rpcServer?.broadcast(name as any, data as any)
|
||||
},
|
||||
|
||||
send(conn, name, data) {
|
||||
rpcServer?.send(conn as any, name as any, data as any)
|
||||
},
|
||||
}
|
||||
|
||||
return gameServer as GameServer
|
||||
}
|
||||
51
packages/framework/server/src/helpers/define.ts
Normal file
51
packages/framework/server/src/helpers/define.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @zh API 和消息定义助手
|
||||
* @en API and message definition helpers
|
||||
*/
|
||||
|
||||
import type { ApiDefinition, MsgDefinition } from '../types/index.js'
|
||||
|
||||
/**
|
||||
* @zh 定义 API 处理器
|
||||
* @en Define API handler
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // src/api/join.ts
|
||||
* import { defineApi } from '@esengine/server'
|
||||
*
|
||||
* export default defineApi<ReqJoin, ResJoin>({
|
||||
* handler(req, ctx) {
|
||||
* ctx.conn.data.playerId = generateId()
|
||||
* return { playerId: ctx.conn.data.playerId }
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function defineApi<TReq, TRes, TData = Record<string, unknown>>(
|
||||
definition: ApiDefinition<TReq, TRes, TData>
|
||||
): ApiDefinition<TReq, TRes, TData> {
|
||||
return definition
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 定义消息处理器
|
||||
* @en Define message handler
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // src/msg/input.ts
|
||||
* import { defineMsg } from '@esengine/server'
|
||||
*
|
||||
* export default defineMsg<MsgInput>({
|
||||
* handler(msg, ctx) {
|
||||
* console.log('Input from', ctx.conn.id, msg)
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function defineMsg<TMsg, TData = Record<string, unknown>>(
|
||||
definition: MsgDefinition<TMsg, TData>
|
||||
): MsgDefinition<TMsg, TData> {
|
||||
return definition
|
||||
}
|
||||
52
packages/framework/server/src/index.ts
Normal file
52
packages/framework/server/src/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @zh ESEngine 游戏服务器框架
|
||||
* @en ESEngine Game Server Framework
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createServer, Room, onMessage } from '@esengine/server'
|
||||
*
|
||||
* class GameRoom extends Room {
|
||||
* maxPlayers = 4
|
||||
* tickRate = 20
|
||||
*
|
||||
* onJoin(player) {
|
||||
* this.broadcast('Joined', { id: player.id })
|
||||
* }
|
||||
*
|
||||
* @onMessage('Move')
|
||||
* handleMove(data, player) {
|
||||
* // handle move
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* const server = await createServer({ port: 3000 })
|
||||
* server.define('game', GameRoom)
|
||||
* await server.start()
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Core
|
||||
export { createServer } from './core/server.js'
|
||||
|
||||
// Helpers
|
||||
export { defineApi, defineMsg } from './helpers/define.js'
|
||||
|
||||
// Room System
|
||||
export { Room, type RoomOptions } from './room/Room.js'
|
||||
export { Player, type IPlayer } from './room/Player.js'
|
||||
export { onMessage } from './room/decorators.js'
|
||||
|
||||
// Types
|
||||
export type {
|
||||
ServerConfig,
|
||||
ServerConnection,
|
||||
GameServer,
|
||||
ApiContext,
|
||||
MsgContext,
|
||||
ApiDefinition,
|
||||
MsgDefinition,
|
||||
} from './types/index.js'
|
||||
|
||||
// Re-export useful types from @esengine/rpc
|
||||
export { RpcError, ErrorCode } from '@esengine/rpc'
|
||||
72
packages/framework/server/src/room/Player.ts
Normal file
72
packages/framework/server/src/room/Player.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* @zh 玩家类
|
||||
* @en Player class
|
||||
*/
|
||||
|
||||
import type { Connection } from '@esengine/rpc'
|
||||
|
||||
/**
|
||||
* @zh 玩家接口
|
||||
* @en Player interface
|
||||
*/
|
||||
export interface IPlayer<TData = Record<string, unknown>> {
|
||||
readonly id: string
|
||||
readonly roomId: string
|
||||
data: TData
|
||||
send<T>(type: string, data: T): void
|
||||
leave(reason?: string): void
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 玩家实现
|
||||
* @en Player implementation
|
||||
*/
|
||||
export class Player<TData = Record<string, unknown>> implements IPlayer<TData> {
|
||||
readonly id: string
|
||||
readonly roomId: string
|
||||
data: TData
|
||||
|
||||
private _conn: Connection<any>
|
||||
private _sendFn: (conn: Connection<any>, type: string, data: unknown) => void
|
||||
private _leaveFn: (player: Player<TData>, reason?: string) => void
|
||||
|
||||
constructor(options: {
|
||||
id: string
|
||||
roomId: string
|
||||
conn: Connection<any>
|
||||
sendFn: (conn: Connection<any>, type: string, data: unknown) => void
|
||||
leaveFn: (player: Player<TData>, reason?: string) => void
|
||||
initialData?: TData
|
||||
}) {
|
||||
this.id = options.id
|
||||
this.roomId = options.roomId
|
||||
this._conn = options.conn
|
||||
this._sendFn = options.sendFn
|
||||
this._leaveFn = options.leaveFn
|
||||
this.data = options.initialData ?? ({} as TData)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取底层连接
|
||||
* @en Get underlying connection
|
||||
*/
|
||||
get connection(): Connection<any> {
|
||||
return this._conn
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 发送消息给玩家
|
||||
* @en Send message to player
|
||||
*/
|
||||
send<T>(type: string, data: T): void {
|
||||
this._sendFn(this._conn, type, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 让玩家离开房间
|
||||
* @en Make player leave the room
|
||||
*/
|
||||
leave(reason?: string): void {
|
||||
this._leaveFn(this, reason)
|
||||
}
|
||||
}
|
||||
383
packages/framework/server/src/room/Room.ts
Normal file
383
packages/framework/server/src/room/Room.ts
Normal file
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* @zh 房间基类
|
||||
* @en Room base class
|
||||
*/
|
||||
|
||||
import { Player } from './Player.js'
|
||||
|
||||
/**
|
||||
* @zh 房间配置
|
||||
* @en Room options
|
||||
*/
|
||||
export interface RoomOptions {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 消息处理器元数据
|
||||
* @en Message handler metadata
|
||||
*/
|
||||
interface MessageHandlerMeta {
|
||||
type: string
|
||||
method: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 消息处理器存储 key
|
||||
* @en Message handler storage key
|
||||
*/
|
||||
const MESSAGE_HANDLERS = Symbol('messageHandlers')
|
||||
|
||||
/**
|
||||
* @zh 房间基类
|
||||
* @en Room base class
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* class GameRoom extends Room {
|
||||
* maxPlayers = 4
|
||||
* tickRate = 20
|
||||
*
|
||||
* onJoin(player: Player) {
|
||||
* this.broadcast('Joined', { id: player.id })
|
||||
* }
|
||||
*
|
||||
* @onMessage('Move')
|
||||
* handleMove(data: { x: number, y: number }, player: Player) {
|
||||
* // handle move
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export abstract class Room<TState = any, TPlayerData = Record<string, unknown>> {
|
||||
// ========================================================================
|
||||
// 配置 | Configuration
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* @zh 最大玩家数
|
||||
* @en Maximum players
|
||||
*/
|
||||
maxPlayers = 16
|
||||
|
||||
/**
|
||||
* @zh Tick 速率(每秒),0 = 不自动 tick
|
||||
* @en Tick rate (per second), 0 = no auto tick
|
||||
*/
|
||||
tickRate = 0
|
||||
|
||||
/**
|
||||
* @zh 空房间自动销毁
|
||||
* @en Auto dispose when empty
|
||||
*/
|
||||
autoDispose = true
|
||||
|
||||
// ========================================================================
|
||||
// 状态 | State
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* @zh 房间状态
|
||||
* @en Room state
|
||||
*/
|
||||
state: TState = {} as TState
|
||||
|
||||
// ========================================================================
|
||||
// 内部属性 | Internal properties
|
||||
// ========================================================================
|
||||
|
||||
private _id: string = ''
|
||||
private _players: Map<string, Player<TPlayerData>> = new Map()
|
||||
private _locked = false
|
||||
private _disposed = false
|
||||
private _tickInterval: ReturnType<typeof setInterval> | null = null
|
||||
private _lastTickTime = 0
|
||||
private _broadcastFn: ((type: string, data: unknown) => void) | null = null
|
||||
private _sendFn: ((conn: any, type: string, data: unknown) => void) | null = null
|
||||
private _disposeFn: (() => void) | null = null
|
||||
|
||||
// ========================================================================
|
||||
// 只读属性 | Readonly properties
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* @zh 房间 ID
|
||||
* @en Room ID
|
||||
*/
|
||||
get id(): string {
|
||||
return this._id
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 所有玩家
|
||||
* @en All players
|
||||
*/
|
||||
get players(): ReadonlyArray<Player<TPlayerData>> {
|
||||
return Array.from(this._players.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 玩家数量
|
||||
* @en Player count
|
||||
*/
|
||||
get playerCount(): number {
|
||||
return this._players.size
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 是否已满
|
||||
* @en Is full
|
||||
*/
|
||||
get isFull(): boolean {
|
||||
return this._players.size >= this.maxPlayers
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 是否已锁定
|
||||
* @en Is locked
|
||||
*/
|
||||
get isLocked(): boolean {
|
||||
return this._locked
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 是否已销毁
|
||||
* @en Is disposed
|
||||
*/
|
||||
get isDisposed(): boolean {
|
||||
return this._disposed
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 生命周期 | Lifecycle
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* @zh 房间创建时调用
|
||||
* @en Called when room is created
|
||||
*/
|
||||
onCreate(options?: RoomOptions): void | Promise<void> {}
|
||||
|
||||
/**
|
||||
* @zh 玩家加入时调用
|
||||
* @en Called when player joins
|
||||
*/
|
||||
onJoin(player: Player<TPlayerData>): void | Promise<void> {}
|
||||
|
||||
/**
|
||||
* @zh 玩家离开时调用
|
||||
* @en Called when player leaves
|
||||
*/
|
||||
onLeave(player: Player<TPlayerData>, reason?: string): void | Promise<void> {}
|
||||
|
||||
/**
|
||||
* @zh 游戏循环
|
||||
* @en Game tick
|
||||
*/
|
||||
onTick(dt: number): void {}
|
||||
|
||||
/**
|
||||
* @zh 房间销毁时调用
|
||||
* @en Called when room is disposed
|
||||
*/
|
||||
onDispose(): void | Promise<void> {}
|
||||
|
||||
// ========================================================================
|
||||
// 公共方法 | Public methods
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* @zh 广播消息给所有玩家
|
||||
* @en Broadcast message to all players
|
||||
*/
|
||||
broadcast<T>(type: string, data: T): void {
|
||||
for (const player of this._players.values()) {
|
||||
player.send(type, data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 广播消息给除指定玩家外的所有玩家
|
||||
* @en Broadcast message to all players except one
|
||||
*/
|
||||
broadcastExcept<T>(except: Player<TPlayerData>, type: string, data: T): void {
|
||||
for (const player of this._players.values()) {
|
||||
if (player.id !== except.id) {
|
||||
player.send(type, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取玩家
|
||||
* @en Get player by id
|
||||
*/
|
||||
getPlayer(id: string): Player<TPlayerData> | undefined {
|
||||
return this._players.get(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 踢出玩家
|
||||
* @en Kick player
|
||||
*/
|
||||
kick(player: Player<TPlayerData>, reason?: string): void {
|
||||
player.leave(reason ?? 'kicked')
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 锁定房间
|
||||
* @en Lock room
|
||||
*/
|
||||
lock(): void {
|
||||
this._locked = true
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 解锁房间
|
||||
* @en Unlock room
|
||||
*/
|
||||
unlock(): void {
|
||||
this._locked = false
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 手动销毁房间
|
||||
* @en Manually dispose room
|
||||
*/
|
||||
dispose(): void {
|
||||
if (this._disposed) return
|
||||
this._disposed = true
|
||||
|
||||
this._stopTick()
|
||||
|
||||
for (const player of this._players.values()) {
|
||||
player.leave('room_disposed')
|
||||
}
|
||||
this._players.clear()
|
||||
|
||||
this.onDispose()
|
||||
this._disposeFn?.()
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 内部方法 | Internal methods
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_init(options: {
|
||||
id: string
|
||||
sendFn: (conn: any, type: string, data: unknown) => void
|
||||
broadcastFn: (type: string, data: unknown) => void
|
||||
disposeFn: () => void
|
||||
}): void {
|
||||
this._id = options.id
|
||||
this._sendFn = options.sendFn
|
||||
this._broadcastFn = options.broadcastFn
|
||||
this._disposeFn = options.disposeFn
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async _create(options?: RoomOptions): Promise<void> {
|
||||
await this.onCreate(options)
|
||||
this._startTick()
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async _addPlayer(id: string, conn: any): Promise<Player<TPlayerData> | null> {
|
||||
if (this._locked || this.isFull || this._disposed) {
|
||||
return null
|
||||
}
|
||||
|
||||
const player = new Player<TPlayerData>({
|
||||
id,
|
||||
roomId: this._id,
|
||||
conn,
|
||||
sendFn: this._sendFn!,
|
||||
leaveFn: (p, reason) => this._removePlayer(p.id, reason),
|
||||
})
|
||||
|
||||
this._players.set(id, player)
|
||||
await this.onJoin(player)
|
||||
|
||||
return player
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async _removePlayer(id: string, reason?: string): Promise<void> {
|
||||
const player = this._players.get(id)
|
||||
if (!player) return
|
||||
|
||||
this._players.delete(id)
|
||||
await this.onLeave(player, reason)
|
||||
|
||||
if (this.autoDispose && this._players.size === 0) {
|
||||
this.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_handleMessage(type: string, data: unknown, playerId: string): void {
|
||||
const player = this._players.get(playerId)
|
||||
if (!player) return
|
||||
|
||||
const handlers = (this.constructor as any)[MESSAGE_HANDLERS] as MessageHandlerMeta[] | undefined
|
||||
if (handlers) {
|
||||
for (const handler of handlers) {
|
||||
if (handler.type === type) {
|
||||
const method = (this as any)[handler.method]
|
||||
if (typeof method === 'function') {
|
||||
method.call(this, data, player)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _startTick(): void {
|
||||
if (this.tickRate <= 0) return
|
||||
|
||||
this._lastTickTime = performance.now()
|
||||
this._tickInterval = setInterval(() => {
|
||||
const now = performance.now()
|
||||
const dt = (now - this._lastTickTime) / 1000
|
||||
this._lastTickTime = now
|
||||
this.onTick(dt)
|
||||
}, 1000 / this.tickRate)
|
||||
}
|
||||
|
||||
private _stopTick(): void {
|
||||
if (this._tickInterval) {
|
||||
clearInterval(this._tickInterval)
|
||||
this._tickInterval = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取消息处理器元数据
|
||||
* @en Get message handler metadata
|
||||
*/
|
||||
export function getMessageHandlers(target: any): MessageHandlerMeta[] {
|
||||
return target[MESSAGE_HANDLERS] || []
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 注册消息处理器元数据
|
||||
* @en Register message handler metadata
|
||||
*/
|
||||
export function registerMessageHandler(target: any, type: string, method: string): void {
|
||||
if (!target[MESSAGE_HANDLERS]) {
|
||||
target[MESSAGE_HANDLERS] = []
|
||||
}
|
||||
target[MESSAGE_HANDLERS].push({ type, method })
|
||||
}
|
||||
221
packages/framework/server/src/room/RoomManager.ts
Normal file
221
packages/framework/server/src/room/RoomManager.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* @zh 房间管理器
|
||||
* @en Room manager
|
||||
*/
|
||||
|
||||
import { Room, type RoomOptions } from './Room.js'
|
||||
import type { Player } from './Player.js'
|
||||
|
||||
/**
|
||||
* @zh 房间类型
|
||||
* @en Room class type
|
||||
*/
|
||||
export type RoomClass<T extends Room = Room> = new () => T
|
||||
|
||||
/**
|
||||
* @zh 房间定义
|
||||
* @en Room definition
|
||||
*/
|
||||
interface RoomDefinition {
|
||||
roomClass: RoomClass
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 房间管理器
|
||||
* @en Room manager
|
||||
*/
|
||||
export class RoomManager {
|
||||
private _definitions: Map<string, RoomDefinition> = new Map()
|
||||
private _rooms: Map<string, Room> = new Map()
|
||||
private _playerToRoom: Map<string, string> = new Map()
|
||||
private _nextRoomId = 1
|
||||
|
||||
private _sendFn: (conn: any, type: string, data: unknown) => void
|
||||
|
||||
constructor(sendFn: (conn: any, type: string, data: unknown) => void) {
|
||||
this._sendFn = sendFn
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 注册房间类型
|
||||
* @en Define room type
|
||||
*/
|
||||
define<T extends Room>(name: string, roomClass: RoomClass<T>): void {
|
||||
this._definitions.set(name, { roomClass })
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建房间
|
||||
* @en Create room
|
||||
*/
|
||||
async create(name: string, options?: RoomOptions): Promise<Room | null> {
|
||||
const def = this._definitions.get(name)
|
||||
if (!def) {
|
||||
console.warn(`[RoomManager] Room type not found: ${name}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const roomId = this._generateRoomId()
|
||||
const room = new def.roomClass()
|
||||
|
||||
room._init({
|
||||
id: roomId,
|
||||
sendFn: this._sendFn,
|
||||
broadcastFn: (type, data) => {
|
||||
for (const player of room.players) {
|
||||
player.send(type, data)
|
||||
}
|
||||
},
|
||||
disposeFn: () => {
|
||||
this._rooms.delete(roomId)
|
||||
},
|
||||
})
|
||||
|
||||
this._rooms.set(roomId, room)
|
||||
await room._create(options)
|
||||
|
||||
console.log(`[Room] Created: ${name} (${roomId})`)
|
||||
return room
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 加入或创建房间
|
||||
* @en Join or create room
|
||||
*/
|
||||
async joinOrCreate(
|
||||
name: string,
|
||||
playerId: string,
|
||||
conn: any,
|
||||
options?: RoomOptions
|
||||
): Promise<{ room: Room; player: Player } | null> {
|
||||
// 查找可加入的房间
|
||||
let room = this._findAvailableRoom(name)
|
||||
|
||||
// 没有则创建
|
||||
if (!room) {
|
||||
room = await this.create(name, options)
|
||||
if (!room) return null
|
||||
}
|
||||
|
||||
// 加入房间
|
||||
const player = await room._addPlayer(playerId, conn)
|
||||
if (!player) return null
|
||||
|
||||
this._playerToRoom.set(playerId, room.id)
|
||||
|
||||
console.log(`[Room] Player ${playerId} joined ${room.id}`)
|
||||
return { room, player }
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 加入指定房间
|
||||
* @en Join specific room
|
||||
*/
|
||||
async joinById(
|
||||
roomId: string,
|
||||
playerId: string,
|
||||
conn: any
|
||||
): Promise<{ room: Room; player: Player } | null> {
|
||||
const room = this._rooms.get(roomId)
|
||||
if (!room) return null
|
||||
|
||||
const player = await room._addPlayer(playerId, conn)
|
||||
if (!player) return null
|
||||
|
||||
this._playerToRoom.set(playerId, room.id)
|
||||
|
||||
console.log(`[Room] Player ${playerId} joined ${room.id}`)
|
||||
return { room, player }
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 玩家离开
|
||||
* @en Player leave
|
||||
*/
|
||||
async leave(playerId: string, reason?: string): Promise<void> {
|
||||
const roomId = this._playerToRoom.get(playerId)
|
||||
if (!roomId) return
|
||||
|
||||
const room = this._rooms.get(roomId)
|
||||
if (room) {
|
||||
await room._removePlayer(playerId, reason)
|
||||
}
|
||||
|
||||
this._playerToRoom.delete(playerId)
|
||||
console.log(`[Room] Player ${playerId} left ${roomId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 处理消息
|
||||
* @en Handle message
|
||||
*/
|
||||
handleMessage(playerId: string, type: string, data: unknown): void {
|
||||
const roomId = this._playerToRoom.get(playerId)
|
||||
if (!roomId) return
|
||||
|
||||
const room = this._rooms.get(roomId)
|
||||
if (room) {
|
||||
room._handleMessage(type, data, playerId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取房间
|
||||
* @en Get room
|
||||
*/
|
||||
getRoom(roomId: string): Room | undefined {
|
||||
return this._rooms.get(roomId)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取玩家所在房间
|
||||
* @en Get player's room
|
||||
*/
|
||||
getPlayerRoom(playerId: string): Room | undefined {
|
||||
const roomId = this._playerToRoom.get(playerId)
|
||||
return roomId ? this._rooms.get(roomId) : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取所有房间
|
||||
* @en Get all rooms
|
||||
*/
|
||||
getRooms(): ReadonlyArray<Room> {
|
||||
return Array.from(this._rooms.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取指定类型的所有房间
|
||||
* @en Get all rooms of a type
|
||||
*/
|
||||
getRoomsByType(name: string): Room[] {
|
||||
const def = this._definitions.get(name)
|
||||
if (!def) return []
|
||||
|
||||
return Array.from(this._rooms.values()).filter(
|
||||
room => room instanceof def.roomClass
|
||||
)
|
||||
}
|
||||
|
||||
private _findAvailableRoom(name: string): Room | undefined {
|
||||
const def = this._definitions.get(name)
|
||||
if (!def) return undefined
|
||||
|
||||
for (const room of this._rooms.values()) {
|
||||
if (
|
||||
room instanceof def.roomClass &&
|
||||
!room.isFull &&
|
||||
!room.isLocked &&
|
||||
!room.isDisposed
|
||||
) {
|
||||
return room
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
private _generateRoomId(): string {
|
||||
return `room_${this._nextRoomId++}`
|
||||
}
|
||||
}
|
||||
35
packages/framework/server/src/room/decorators.ts
Normal file
35
packages/framework/server/src/room/decorators.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @zh 房间装饰器
|
||||
* @en Room decorators
|
||||
*/
|
||||
|
||||
import { registerMessageHandler } from './Room.js'
|
||||
|
||||
/**
|
||||
* @zh 消息处理器装饰器
|
||||
* @en Message handler decorator
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* class GameRoom extends Room {
|
||||
* @onMessage('Move')
|
||||
* handleMove(data: { x: number, y: number }, player: Player) {
|
||||
* // handle move
|
||||
* }
|
||||
*
|
||||
* @onMessage('Chat')
|
||||
* handleChat(data: { text: string }, player: Player) {
|
||||
* this.broadcast('Chat', { from: player.id, text: data.text })
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function onMessage(type: string): MethodDecorator {
|
||||
return function (
|
||||
target: any,
|
||||
propertyKey: string | symbol,
|
||||
_descriptor: PropertyDescriptor
|
||||
) {
|
||||
registerMessageHandler(target.constructor, type, propertyKey as string)
|
||||
}
|
||||
}
|
||||
9
packages/framework/server/src/room/index.ts
Normal file
9
packages/framework/server/src/room/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @zh 房间系统
|
||||
* @en Room system
|
||||
*/
|
||||
|
||||
export { Room, type RoomOptions } from './Room.js'
|
||||
export { Player, type IPlayer } from './Player.js'
|
||||
export { RoomManager, type RoomClass } from './RoomManager.js'
|
||||
export { onMessage } from './decorators.js'
|
||||
112
packages/framework/server/src/router/loader.ts
Normal file
112
packages/framework/server/src/router/loader.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* @zh 文件路由加载器
|
||||
* @en File-based router loader
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import type { ApiDefinition, MsgDefinition, LoadedApiHandler, LoadedMsgHandler } from '../types/index.js'
|
||||
|
||||
/**
|
||||
* @zh 将文件名转换为 API/消息名称
|
||||
* @en Convert filename to API/message name
|
||||
*
|
||||
* @example
|
||||
* 'join.ts' -> 'Join'
|
||||
* 'spawn-agent.ts' -> 'SpawnAgent'
|
||||
* 'save_blueprint.ts' -> 'SaveBlueprint'
|
||||
*/
|
||||
function fileNameToHandlerName(fileName: string): string {
|
||||
const baseName = fileName.replace(/\.(ts|js|mts|mjs)$/, '')
|
||||
|
||||
return baseName
|
||||
.split(/[-_]/)
|
||||
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 扫描目录获取所有处理器文件
|
||||
* @en Scan directory for all handler files
|
||||
*/
|
||||
function scanDirectory(dir: string): string[] {
|
||||
if (!fs.existsSync(dir)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const files: string[] = []
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name)) {
|
||||
// 跳过 index 和下划线开头的文件
|
||||
if (entry.name.startsWith('_') || entry.name.startsWith('index.')) {
|
||||
continue
|
||||
}
|
||||
files.push(path.join(dir, entry.name))
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 加载 API 处理器
|
||||
* @en Load API handlers
|
||||
*/
|
||||
export async function loadApiHandlers(apiDir: string): Promise<LoadedApiHandler[]> {
|
||||
const files = scanDirectory(apiDir)
|
||||
const handlers: LoadedApiHandler[] = []
|
||||
|
||||
for (const filePath of files) {
|
||||
try {
|
||||
const fileUrl = pathToFileURL(filePath).href
|
||||
const module = await import(fileUrl)
|
||||
const definition = module.default as ApiDefinition<unknown, unknown, unknown>
|
||||
|
||||
if (definition && typeof definition.handler === 'function') {
|
||||
const name = fileNameToHandlerName(path.basename(filePath))
|
||||
handlers.push({
|
||||
name,
|
||||
path: filePath,
|
||||
definition,
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`[Server] Failed to load API handler: ${filePath}`, err)
|
||||
}
|
||||
}
|
||||
|
||||
return handlers
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 加载消息处理器
|
||||
* @en Load message handlers
|
||||
*/
|
||||
export async function loadMsgHandlers(msgDir: string): Promise<LoadedMsgHandler[]> {
|
||||
const files = scanDirectory(msgDir)
|
||||
const handlers: LoadedMsgHandler[] = []
|
||||
|
||||
for (const filePath of files) {
|
||||
try {
|
||||
const fileUrl = pathToFileURL(filePath).href
|
||||
const module = await import(fileUrl)
|
||||
const definition = module.default as MsgDefinition<unknown, unknown>
|
||||
|
||||
if (definition && typeof definition.handler === 'function') {
|
||||
const name = fileNameToHandlerName(path.basename(filePath))
|
||||
handlers.push({
|
||||
name,
|
||||
path: filePath,
|
||||
definition,
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`[Server] Failed to load msg handler: ${filePath}`, err)
|
||||
}
|
||||
}
|
||||
|
||||
return handlers
|
||||
}
|
||||
228
packages/framework/server/src/types/index.ts
Normal file
228
packages/framework/server/src/types/index.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* @zh ESEngine Server 类型定义
|
||||
* @en ESEngine Server type definitions
|
||||
*/
|
||||
|
||||
import type { Connection, ProtocolDef } from '@esengine/rpc'
|
||||
|
||||
// ============================================================================
|
||||
// Server Config
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 服务器配置
|
||||
* @en Server configuration
|
||||
*/
|
||||
export interface ServerConfig {
|
||||
/**
|
||||
* @zh 监听端口
|
||||
* @en Listen port
|
||||
* @default 3000
|
||||
*/
|
||||
port?: number
|
||||
|
||||
/**
|
||||
* @zh API 目录路径
|
||||
* @en API directory path
|
||||
* @default 'src/api'
|
||||
*/
|
||||
apiDir?: string
|
||||
|
||||
/**
|
||||
* @zh 消息处理器目录路径
|
||||
* @en Message handlers directory path
|
||||
* @default 'src/msg'
|
||||
*/
|
||||
msgDir?: string
|
||||
|
||||
/**
|
||||
* @zh 游戏 Tick 速率 (每秒)
|
||||
* @en Game tick rate (per second)
|
||||
* @default 20
|
||||
*/
|
||||
tickRate?: number
|
||||
|
||||
/**
|
||||
* @zh 服务器启动回调
|
||||
* @en Server start callback
|
||||
*/
|
||||
onStart?: (port: number) => void
|
||||
|
||||
/**
|
||||
* @zh 连接建立回调
|
||||
* @en Connection established callback
|
||||
*/
|
||||
onConnect?: (conn: ServerConnection) => void | Promise<void>
|
||||
|
||||
/**
|
||||
* @zh 连接断开回调
|
||||
* @en Connection closed callback
|
||||
*/
|
||||
onDisconnect?: (conn: ServerConnection) => void | Promise<void>
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Connection
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 服务器连接(扩展 RPC Connection)
|
||||
* @en Server connection (extends RPC Connection)
|
||||
*/
|
||||
export interface ServerConnection<TData = Record<string, unknown>> extends Connection<TData> {
|
||||
/**
|
||||
* @zh 用户自定义数据
|
||||
* @en User-defined data
|
||||
*/
|
||||
data: TData
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API Definition
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh API 处理器上下文
|
||||
* @en API handler context
|
||||
*/
|
||||
export interface ApiContext<TData = Record<string, unknown>> {
|
||||
/**
|
||||
* @zh 当前连接
|
||||
* @en Current connection
|
||||
*/
|
||||
conn: ServerConnection<TData>
|
||||
|
||||
/**
|
||||
* @zh 服务器实例
|
||||
* @en Server instance
|
||||
*/
|
||||
server: GameServer
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh API 定义选项
|
||||
* @en API definition options
|
||||
*/
|
||||
export interface ApiDefinition<TReq = unknown, TRes = unknown, TData = Record<string, unknown>> {
|
||||
/**
|
||||
* @zh API 处理函数
|
||||
* @en API handler function
|
||||
*/
|
||||
handler: (req: TReq, ctx: ApiContext<TData>) => TRes | Promise<TRes>
|
||||
|
||||
/**
|
||||
* @zh 请求验证函数(可选)
|
||||
* @en Request validation function (optional)
|
||||
*/
|
||||
validate?: (req: unknown) => req is TReq
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Message Definition
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 消息处理器上下文
|
||||
* @en Message handler context
|
||||
*/
|
||||
export interface MsgContext<TData = Record<string, unknown>> {
|
||||
/**
|
||||
* @zh 当前连接
|
||||
* @en Current connection
|
||||
*/
|
||||
conn: ServerConnection<TData>
|
||||
|
||||
/**
|
||||
* @zh 服务器实例
|
||||
* @en Server instance
|
||||
*/
|
||||
server: GameServer
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 消息定义选项
|
||||
* @en Message definition options
|
||||
*/
|
||||
export interface MsgDefinition<TMsg = unknown, TData = Record<string, unknown>> {
|
||||
/**
|
||||
* @zh 消息处理函数
|
||||
* @en Message handler function
|
||||
*/
|
||||
handler: (msg: TMsg, ctx: MsgContext<TData>) => void | Promise<void>
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Game Server Interface
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 游戏服务器接口
|
||||
* @en Game server interface
|
||||
*/
|
||||
export interface GameServer {
|
||||
/**
|
||||
* @zh 启动服务器
|
||||
* @en Start server
|
||||
*/
|
||||
start(): Promise<void>
|
||||
|
||||
/**
|
||||
* @zh 停止服务器
|
||||
* @en Stop server
|
||||
*/
|
||||
stop(): Promise<void>
|
||||
|
||||
/**
|
||||
* @zh 广播消息
|
||||
* @en Broadcast message
|
||||
*/
|
||||
broadcast<T>(name: string, data: T): void
|
||||
|
||||
/**
|
||||
* @zh 发送消息给指定连接
|
||||
* @en Send message to specific connection
|
||||
*/
|
||||
send<T>(conn: ServerConnection, name: string, data: T): void
|
||||
|
||||
/**
|
||||
* @zh 获取所有连接
|
||||
* @en Get all connections
|
||||
*/
|
||||
readonly connections: ReadonlyArray<ServerConnection>
|
||||
|
||||
/**
|
||||
* @zh 当前 Tick
|
||||
* @en Current tick
|
||||
*/
|
||||
readonly tick: number
|
||||
|
||||
/**
|
||||
* @zh 注册房间类型
|
||||
* @en Define room type
|
||||
*/
|
||||
define(name: string, roomClass: new () => unknown): void
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Loaded Handler Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 已加载的 API 处理器
|
||||
* @en Loaded API handler
|
||||
*/
|
||||
export interface LoadedApiHandler {
|
||||
name: string
|
||||
path: string
|
||||
definition: ApiDefinition<any, any, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 已加载的消息处理器
|
||||
* @en Loaded message handler
|
||||
*/
|
||||
export interface LoadedMsgHandler {
|
||||
name: string
|
||||
path: string
|
||||
definition: MsgDefinition<any, any>
|
||||
}
|
||||
Reference in New Issue
Block a user