refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages ## Package Structure Reorganization - Reorganized 55 packages into categorized subdirectories: - packages/framework/ - Generic framework (Laya/Cocos compatible) - packages/engine/ - ESEngine core modules - packages/rendering/ - Rendering modules (WASM dependent) - packages/physics/ - Physics modules - packages/streaming/ - World streaming - packages/network-ext/ - Network extensions - packages/editor/ - Editor framework and plugins - packages/rust/ - Rust WASM engine - packages/tools/ - Build tools and SDK ## Framework Package Decoupling - Decoupled behavior-tree and blueprint packages from ESEngine dependencies - Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent) - ESEngine-specific code moved to esengine/ subpath exports - Framework packages now usable with Cocos/Laya without ESEngine ## CI Configuration - Updated CI to only type-check and lint framework packages - Added type-check:framework and lint:framework scripts ## Breaking Changes - Package import paths changed due to directory reorganization - ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine') * fix: update es-engine file path after directory reorganization * docs: update README to focus on framework over engine * ci: only build framework packages, remove Rust/WASM dependencies * fix: remove esengine subpath from behavior-tree and blueprint builds ESEngine integration code will only be available in full engine builds. Framework packages are now purely engine-agnostic. * fix: move network-protocols to framework, build both in CI * fix: update workflow paths from packages/core to packages/framework/core * fix: exclude esengine folder from type-check in behavior-tree and blueprint * fix: update network tsconfig references to new paths * fix: add test:ci:framework to only test framework packages in CI * fix: only build core and math npm packages in CI * fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
40
packages/network-ext/network-server/package.json
Normal file
40
packages/network-ext/network-server/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@esengine/network-server",
|
||||
"version": "1.0.0",
|
||||
"description": "TSRPC-based network server for ESEngine",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsx src/main.ts",
|
||||
"start": "node dist/main.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@esengine/network-protocols": "workspace:*",
|
||||
"tsrpc": "^3.4.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^8.5.1",
|
||||
"tsx": "^4.19.0"
|
||||
},
|
||||
"keywords": [
|
||||
"esengine",
|
||||
"network",
|
||||
"server",
|
||||
"tsrpc",
|
||||
"websocket"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT"
|
||||
}
|
||||
31
packages/network-ext/network-server/src/index.ts
Normal file
31
packages/network-ext/network-server/src/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @esengine/network-server
|
||||
*
|
||||
* 基于 TSRPC 的网络服务器模块
|
||||
* TSRPC-based network server module
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Re-export from protocols | 从协议包重新导出
|
||||
// ============================================================================
|
||||
|
||||
export type {
|
||||
ServiceType,
|
||||
IEntityState,
|
||||
IPlayerInput,
|
||||
MsgSync,
|
||||
MsgInput,
|
||||
MsgSpawn,
|
||||
MsgDespawn,
|
||||
ReqJoin,
|
||||
ResJoin
|
||||
} from '@esengine/network-protocols';
|
||||
|
||||
export { serviceProto } from '@esengine/network-protocols';
|
||||
|
||||
// ============================================================================
|
||||
// Server | 服务器
|
||||
// ============================================================================
|
||||
|
||||
export { GameServer, type IServerConfig } from './services/GameServer';
|
||||
export { Room, type IPlayer, type IRoomConfig } from './services/Room';
|
||||
35
packages/network-ext/network-server/src/main.ts
Normal file
35
packages/network-ext/network-server/src/main.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 服务器入口
|
||||
* Server entry point
|
||||
*/
|
||||
import { GameServer } from './services/GameServer';
|
||||
|
||||
const PORT = parseInt(process.env['PORT'] ?? '3000', 10);
|
||||
|
||||
const server = new GameServer({
|
||||
port: PORT,
|
||||
roomConfig: {
|
||||
maxPlayers: 16,
|
||||
tickRate: 20
|
||||
}
|
||||
});
|
||||
|
||||
// 启动服务器
|
||||
// Start server
|
||||
server.start().catch((err) => {
|
||||
console.error('[Main] 服务器启动失败 | Server failed to start:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// 优雅关闭
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\n[Main] 正在关闭服务器... | Shutting down server...');
|
||||
await server.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await server.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
197
packages/network-ext/network-server/src/services/GameServer.ts
Normal file
197
packages/network-ext/network-server/src/services/GameServer.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import { WsServer, type BaseConnection } from 'tsrpc';
|
||||
import { serviceProto, type ServiceType, type MsgInput } from '@esengine/network-protocols';
|
||||
import { Room, type IRoomConfig } from './Room';
|
||||
|
||||
/**
|
||||
* 服务器配置
|
||||
* Server configuration
|
||||
*/
|
||||
export interface IServerConfig {
|
||||
port: number;
|
||||
roomConfig?: Partial<IRoomConfig>;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: IServerConfig = {
|
||||
port: 3000
|
||||
};
|
||||
|
||||
/**
|
||||
* 游戏服务器
|
||||
* Game server
|
||||
*
|
||||
* 管理 WebSocket 连接和房间。
|
||||
* Manages WebSocket connections and rooms.
|
||||
*/
|
||||
export class GameServer {
|
||||
private _server: WsServer<ServiceType>;
|
||||
private _config: IServerConfig;
|
||||
private _rooms: Map<string, Room> = new Map();
|
||||
private _connectionToRoom: Map<BaseConnection<ServiceType>, { roomId: string; clientId: number }> = new Map();
|
||||
|
||||
constructor(config: Partial<IServerConfig> = {}) {
|
||||
this._config = { ...DEFAULT_CONFIG, ...config };
|
||||
this._server = new WsServer(serviceProto, {
|
||||
port: this._config.port,
|
||||
json: true,
|
||||
logLevel: 'info'
|
||||
});
|
||||
|
||||
this._setupHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动服务器
|
||||
* Start server
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
await this._server.start();
|
||||
console.log(`[GameServer] 服务器已启动 | Server started on port ${this._config.port}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止服务器
|
||||
* Stop server
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
// 销毁所有房间
|
||||
// Destroy all rooms
|
||||
for (const room of this._rooms.values()) {
|
||||
room.destroy();
|
||||
}
|
||||
this._rooms.clear();
|
||||
this._connectionToRoom.clear();
|
||||
|
||||
await this._server.stop();
|
||||
console.log('[GameServer] 服务器已停止 | Server stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建房间
|
||||
* Get or create room
|
||||
*/
|
||||
getOrCreateRoom(roomId?: string): Room {
|
||||
// 如果没有指定房间 ID,寻找未满的房间或创建新房间
|
||||
// If no room ID specified, find a non-full room or create new one
|
||||
if (!roomId) {
|
||||
for (const room of this._rooms.values()) {
|
||||
if (!room.isFull) {
|
||||
return room;
|
||||
}
|
||||
}
|
||||
roomId = this._generateRoomId();
|
||||
}
|
||||
|
||||
let room = this._rooms.get(roomId);
|
||||
if (!room) {
|
||||
room = new Room(roomId, this._config.roomConfig);
|
||||
this._rooms.set(roomId, room);
|
||||
console.log(`[GameServer] 创建房间 | Room created: ${roomId}`);
|
||||
}
|
||||
|
||||
return room;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间
|
||||
* Get room
|
||||
*/
|
||||
getRoom(roomId: string): Room | undefined {
|
||||
return this._rooms.get(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接的房间信息
|
||||
* Get connection's room info
|
||||
*/
|
||||
getConnectionInfo(connection: BaseConnection<ServiceType>): { roomId: string; clientId: number } | undefined {
|
||||
return this._connectionToRoom.get(connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置连接的房间信息
|
||||
* Set connection's room info
|
||||
*/
|
||||
setConnectionInfo(connection: BaseConnection<ServiceType>, roomId: string, clientId: number): void {
|
||||
this._connectionToRoom.set(connection, { roomId, clientId });
|
||||
}
|
||||
|
||||
private _setupHandlers(): void {
|
||||
// 处理加入请求
|
||||
// Handle join request
|
||||
this._server.implementApi('Join', async (call) => {
|
||||
const { playerName, roomId } = call.req;
|
||||
|
||||
const room = this.getOrCreateRoom(roomId);
|
||||
if (room.isFull) {
|
||||
call.error('房间已满 | Room is full');
|
||||
return;
|
||||
}
|
||||
|
||||
const player = room.addPlayer(playerName, call.conn);
|
||||
if (!player) {
|
||||
call.error('加入房间失败 | Failed to join room');
|
||||
return;
|
||||
}
|
||||
|
||||
this.setConnectionInfo(call.conn, room.id, player.clientId);
|
||||
|
||||
// 向新玩家发送自己的生成消息
|
||||
// Send spawn message to new player for themselves
|
||||
call.conn.sendMsg('Spawn', {
|
||||
netId: player.netId,
|
||||
ownerId: player.clientId,
|
||||
prefab: 'player',
|
||||
pos: { x: 0, y: 0 },
|
||||
rot: 0
|
||||
});
|
||||
|
||||
call.succ({
|
||||
clientId: player.clientId,
|
||||
roomId: room.id,
|
||||
playerCount: room.playerCount
|
||||
});
|
||||
|
||||
console.log(`[GameServer] 玩家加入 | Player joined: ${playerName} (${player.clientId}) -> ${room.id}`);
|
||||
});
|
||||
|
||||
// 处理输入消息
|
||||
// Handle input message
|
||||
this._server.listenMsg('Input', (call) => {
|
||||
const info = this.getConnectionInfo(call.conn);
|
||||
if (!info) return;
|
||||
|
||||
const room = this.getRoom(info.roomId);
|
||||
if (!room) return;
|
||||
|
||||
const msg = call.msg as MsgInput;
|
||||
room.handleInput(info.clientId, msg.input);
|
||||
});
|
||||
|
||||
// 处理断开连接
|
||||
// Handle disconnect
|
||||
this._server.flows.postDisconnectFlow.push((v) => {
|
||||
const info = this._connectionToRoom.get(v.conn);
|
||||
if (info) {
|
||||
const room = this.getRoom(info.roomId);
|
||||
if (room) {
|
||||
room.removePlayer(info.clientId);
|
||||
console.log(`[GameServer] 玩家离开 | Player left: ${info.clientId} from ${info.roomId}`);
|
||||
|
||||
// 如果房间空了,删除房间
|
||||
// If room is empty, delete it
|
||||
if (room.playerCount === 0) {
|
||||
room.destroy();
|
||||
this._rooms.delete(info.roomId);
|
||||
console.log(`[GameServer] 删除空房间 | Empty room deleted: ${info.roomId}`);
|
||||
}
|
||||
}
|
||||
this._connectionToRoom.delete(v.conn);
|
||||
}
|
||||
return v;
|
||||
});
|
||||
}
|
||||
|
||||
private _generateRoomId(): string {
|
||||
return Math.random().toString(36).substring(2, 8).toUpperCase();
|
||||
}
|
||||
}
|
||||
234
packages/network-ext/network-server/src/services/Room.ts
Normal file
234
packages/network-ext/network-server/src/services/Room.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import type { BaseConnection } from 'tsrpc';
|
||||
import type { ServiceType, IEntityState } from '@esengine/network-protocols';
|
||||
|
||||
/**
|
||||
* 连接类型别名
|
||||
* Connection type alias
|
||||
*/
|
||||
type Connection = BaseConnection<ServiceType>;
|
||||
|
||||
/**
|
||||
* 玩家信息
|
||||
* Player information
|
||||
*/
|
||||
export interface IPlayer {
|
||||
clientId: number;
|
||||
name: string;
|
||||
connection: Connection;
|
||||
netId: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间配置
|
||||
* Room configuration
|
||||
*/
|
||||
export interface IRoomConfig {
|
||||
maxPlayers: number;
|
||||
tickRate: number;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: IRoomConfig = {
|
||||
maxPlayers: 16,
|
||||
tickRate: 20
|
||||
};
|
||||
|
||||
/**
|
||||
* 游戏房间
|
||||
* Game room
|
||||
*
|
||||
* 管理房间内的玩家和实体状态同步。
|
||||
* Manages players and entity state synchronization within a room.
|
||||
*/
|
||||
export class Room {
|
||||
private _id: string;
|
||||
private _config: IRoomConfig;
|
||||
private _players: Map<number, IPlayer> = new Map();
|
||||
private _entities: Map<number, IEntityState> = new Map();
|
||||
private _nextClientId: number = 1;
|
||||
private _nextNetId: number = 1;
|
||||
private _syncInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
constructor(id: string, config: Partial<IRoomConfig> = {}) {
|
||||
this._id = id;
|
||||
this._config = { ...DEFAULT_CONFIG, ...config };
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get playerCount(): number {
|
||||
return this._players.size;
|
||||
}
|
||||
|
||||
get isFull(): boolean {
|
||||
return this._players.size >= this._config.maxPlayers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加玩家
|
||||
* Add player
|
||||
*/
|
||||
addPlayer(name: string, connection: Connection): IPlayer | null {
|
||||
if (this.isFull) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const clientId = this._nextClientId++;
|
||||
const netId = this._nextNetId++;
|
||||
|
||||
const player: IPlayer = {
|
||||
clientId,
|
||||
name,
|
||||
connection,
|
||||
netId
|
||||
};
|
||||
|
||||
this._players.set(clientId, player);
|
||||
|
||||
// 创建玩家实体
|
||||
// Create player entity
|
||||
const entityState: IEntityState = {
|
||||
netId,
|
||||
pos: { x: 0, y: 0 },
|
||||
rot: 0
|
||||
};
|
||||
this._entities.set(netId, entityState);
|
||||
|
||||
// 通知其他玩家
|
||||
// Notify other players
|
||||
this._broadcastSpawn(player, entityState);
|
||||
|
||||
// 同步现有实体给新玩家
|
||||
// Sync existing entities to new player
|
||||
this._syncExistingEntities(player);
|
||||
|
||||
// 启动同步循环
|
||||
// Start sync loop
|
||||
if (this._syncInterval === null) {
|
||||
this._startSyncLoop();
|
||||
}
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除玩家
|
||||
* Remove player
|
||||
*/
|
||||
removePlayer(clientId: number): void {
|
||||
const player = this._players.get(clientId);
|
||||
if (!player) return;
|
||||
|
||||
this._players.delete(clientId);
|
||||
this._entities.delete(player.netId);
|
||||
|
||||
// 通知其他玩家
|
||||
// Notify other players
|
||||
this._broadcastDespawn(player.netId);
|
||||
|
||||
// 停止同步循环
|
||||
// Stop sync loop
|
||||
if (this._players.size === 0 && this._syncInterval !== null) {
|
||||
clearInterval(this._syncInterval);
|
||||
this._syncInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理玩家输入
|
||||
* Handle player input
|
||||
*/
|
||||
handleInput(
|
||||
clientId: number,
|
||||
input: { moveDir?: { x: number; y: number }; actions?: string[] }
|
||||
): void {
|
||||
const player = this._players.get(clientId);
|
||||
if (!player) return;
|
||||
|
||||
const entity = this._entities.get(player.netId);
|
||||
if (!entity || !entity.pos) return;
|
||||
|
||||
// 简单的移动处理
|
||||
// Simple movement handling
|
||||
if (input.moveDir) {
|
||||
const speed = 5;
|
||||
entity.pos.x += input.moveDir.x * speed;
|
||||
entity.pos.y += input.moveDir.y * speed;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家
|
||||
* Get player
|
||||
*/
|
||||
getPlayer(clientId: number): IPlayer | undefined {
|
||||
return this._players.get(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁房间
|
||||
* Destroy room
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this._syncInterval !== null) {
|
||||
clearInterval(this._syncInterval);
|
||||
this._syncInterval = null;
|
||||
}
|
||||
this._players.clear();
|
||||
this._entities.clear();
|
||||
}
|
||||
|
||||
private _startSyncLoop(): void {
|
||||
const interval = 1000 / this._config.tickRate;
|
||||
this._syncInterval = setInterval(() => {
|
||||
this._broadcastSync();
|
||||
}, interval);
|
||||
}
|
||||
|
||||
private _broadcastSync(): void {
|
||||
if (this._players.size === 0) return;
|
||||
|
||||
const entities = Array.from(this._entities.values());
|
||||
const time = Date.now();
|
||||
|
||||
for (const player of this._players.values()) {
|
||||
player.connection.sendMsg('Sync', { time, entities });
|
||||
}
|
||||
}
|
||||
|
||||
private _broadcastSpawn(newPlayer: IPlayer, state: IEntityState): void {
|
||||
for (const player of this._players.values()) {
|
||||
if (player.clientId === newPlayer.clientId) continue;
|
||||
|
||||
player.connection.sendMsg('Spawn', {
|
||||
netId: state.netId,
|
||||
ownerId: newPlayer.clientId,
|
||||
prefab: 'player',
|
||||
pos: state.pos ?? { x: 0, y: 0 },
|
||||
rot: state.rot ?? 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _broadcastDespawn(netId: number): void {
|
||||
for (const player of this._players.values()) {
|
||||
player.connection.sendMsg('Despawn', { netId });
|
||||
}
|
||||
}
|
||||
|
||||
private _syncExistingEntities(newPlayer: IPlayer): void {
|
||||
for (const [netId, state] of this._entities) {
|
||||
const owner = Array.from(this._players.values()).find((p) => p.netId === netId);
|
||||
if (!owner || owner.clientId === newPlayer.clientId) continue;
|
||||
|
||||
newPlayer.connection.sendMsg('Spawn', {
|
||||
netId,
|
||||
ownerId: owner.clientId,
|
||||
prefab: 'player',
|
||||
pos: state.pos ?? { x: 0, y: 0 },
|
||||
rot: state.rot ?? 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
13
packages/network-ext/network-server/tsconfig.build.json
Normal file
13
packages/network-ext/network-server/tsconfig.build.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
21
packages/network-ext/network-server/tsconfig.json
Normal file
21
packages/network-ext/network-server/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../network-ext/network-protocols"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
packages/network-ext/network-server/tsup.config.ts
Normal file
11
packages/network-ext/network-server/tsup.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts', 'src/main.ts'],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
external: ['tsrpc'],
|
||||
tsconfig: 'tsconfig.build.json'
|
||||
});
|
||||
Reference in New Issue
Block a user