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:
YHH
2025-12-28 12:23:55 +08:00
committed by GitHub
parent 41529f6fbb
commit b6f1235239
31 changed files with 2793 additions and 824 deletions

View 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
}

View 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
}

View 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'

View 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)
}
}

View 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 })
}

View 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++}`
}
}

View 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)
}
}

View 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'

View 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
}

View 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>
}