重构network库(mvp版本)搭建基础设施和核心接口
定义ITransport/ISerializer/INetworkMessage接口 NetworkIdentity组件 基础事件定义
This commit is contained in:
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -28,9 +28,6 @@
|
||||
[submodule "thirdparty/ecs-astar"]
|
||||
path = thirdparty/ecs-astar
|
||||
url = https://github.com/esengine/ecs-astar.git
|
||||
[submodule "examples/electric-world"]
|
||||
path = examples/electric-world
|
||||
url = https://github.com/esengine/electric-world.git
|
||||
[submodule "examples/lawn-mower-demo"]
|
||||
path = examples/lawn-mower-demo
|
||||
url = https://github.com/esengine/lawn-mower-demo.git
|
||||
Submodule examples/electric-world deleted from 2b36519bd9
6
package-lock.json
generated
6
package-lock.json
generated
@@ -11603,7 +11603,7 @@
|
||||
},
|
||||
"packages/network-client": {
|
||||
"name": "@esengine/network-client",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@esengine/ecs-framework": "file:../core",
|
||||
@@ -11622,7 +11622,7 @@
|
||||
},
|
||||
"packages/network-server": {
|
||||
"name": "@esengine/network-server",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@esengine/ecs-framework": "file:../core",
|
||||
@@ -11643,7 +11643,7 @@
|
||||
},
|
||||
"packages/network-shared": {
|
||||
"name": "@esengine/network-shared",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@esengine/ecs-framework": "file:../core",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"name": "@esengine/ecs-framework-math",
|
||||
"version": "1.0.5",
|
||||
"description": "ECS框架2D数学库 - 提供向量、矩阵、几何形状和碰撞检测功能",
|
||||
"type": "module",
|
||||
"main": "bin/index.js",
|
||||
"types": "bin/index.d.ts",
|
||||
"files": [
|
||||
@@ -22,7 +21,7 @@
|
||||
"typescript"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf bin dist",
|
||||
"clean": "rimraf bin dist tsconfig.tsbuildinfo",
|
||||
"build:ts": "tsc",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "npm run build:ts",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
# ECS Framework 网络库 - 客户端
|
||||
|
||||
该包提供了完整的网络客户端功能,包括连接管理、预测、插值等现代网络游戏必需的特性。
|
||||
|
||||
## 主要功能
|
||||
|
||||
- ✅ **传输层支持**: WebSocket 和 HTTP 两种传输方式
|
||||
- ✅ **网络客户端**: 完整的连接、认证、房间管理
|
||||
- ✅ **网络行为**: ClientNetworkBehaviour 基类和 NetworkIdentity 组件
|
||||
- ✅ **装饰器系统**: @SyncVar, @ClientRpc, @ServerRpc 装饰器
|
||||
- ✅ **客户端预测**: 减少网络延迟感知的预测系统
|
||||
- ✅ **插值系统**: 平滑的网络对象状态同步
|
||||
- ✅ **TypeScript**: 完整的类型支持
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework-network-client
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
```typescript
|
||||
import {
|
||||
NetworkClient,
|
||||
WebSocketClientTransport,
|
||||
ClientNetworkBehaviour,
|
||||
SyncVar,
|
||||
ServerRpc
|
||||
} from '@esengine/ecs-framework-network-client';
|
||||
|
||||
// 创建网络客户端
|
||||
const client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
secure: false
|
||||
}
|
||||
});
|
||||
|
||||
// 连接到服务器
|
||||
await client.connect();
|
||||
|
||||
// 认证
|
||||
const userInfo = await client.authenticate('username', 'password');
|
||||
|
||||
// 获取房间列表
|
||||
const rooms = await client.getRoomList();
|
||||
|
||||
// 加入房间
|
||||
const roomInfo = await client.joinRoom('room-id');
|
||||
```
|
||||
|
||||
## 网络行为示例
|
||||
|
||||
```typescript
|
||||
class PlayerController extends ClientNetworkBehaviour {
|
||||
@SyncVar({ clientCanModify: true })
|
||||
position: { x: number; y: number } = { x: 0, y: 0 };
|
||||
|
||||
@SyncVar()
|
||||
health: number = 100;
|
||||
|
||||
@ServerRpc({ requireLocalPlayer: true })
|
||||
async move(direction: string): Promise<void> {
|
||||
// 这个方法会被发送到服务器执行
|
||||
}
|
||||
|
||||
@ClientRpc()
|
||||
onDamaged(damage: number): void {
|
||||
// 这个方法会被服务器调用
|
||||
console.log(`Received damage: ${damage}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 预测和插值
|
||||
|
||||
```typescript
|
||||
import { PredictionSystem, InterpolationSystem } from '@esengine/ecs-framework-network-client';
|
||||
|
||||
// 启用预测系统
|
||||
const predictionSystem = new PredictionSystem(scene, 64, 500);
|
||||
scene.addSystem(predictionSystem);
|
||||
|
||||
// 启用插值系统
|
||||
const interpolationSystem = new InterpolationSystem(scene, {
|
||||
delay: 100,
|
||||
enableExtrapolation: false
|
||||
});
|
||||
scene.addSystem(interpolationSystem);
|
||||
```
|
||||
|
||||
## 编译状态
|
||||
|
||||
✅ **编译成功** - 所有 TypeScript 错误已修复,包生成完成
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -2,27 +2,32 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
console.log('🚀 使用 Rollup 构建 network-client 包...');
|
||||
console.log('🚀 使用 Rollup 构建 @esengine/network-client 包...');
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// 清理旧的dist目录
|
||||
if (fs.existsSync('./dist')) {
|
||||
console.log('🧹 清理旧的构建文件...');
|
||||
execSync('rimraf ./dist', { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
// 执行Rollup构建
|
||||
console.log('📦 执行 Rollup 构建...');
|
||||
execSync('rollup -c rollup.config.cjs', { stdio: 'inherit' });
|
||||
execSync('npx rollup -c rollup.config.cjs', { stdio: 'inherit' });
|
||||
|
||||
// 生成package.json
|
||||
console.log('📋 生成 package.json...');
|
||||
generatePackageJson();
|
||||
|
||||
// 复制其他文件
|
||||
console.log('📁 复制必要文件...');
|
||||
copyFiles();
|
||||
|
||||
// 输出构建结果
|
||||
showBuildResults();
|
||||
|
||||
console.log('✅ network-client 构建完成!');
|
||||
console.log('✅ @esengine/network-client 构建完成!');
|
||||
console.log('\n🚀 发布命令:');
|
||||
console.log('cd dist && npm publish');
|
||||
|
||||
@@ -63,18 +68,19 @@ function generatePackageJson() {
|
||||
],
|
||||
keywords: [
|
||||
'ecs',
|
||||
'networking',
|
||||
'network',
|
||||
'client',
|
||||
'prediction',
|
||||
'interpolation',
|
||||
'game-engine',
|
||||
'multiplayer',
|
||||
'game',
|
||||
'browser',
|
||||
'cocos',
|
||||
'typescript'
|
||||
],
|
||||
author: sourcePackage.author,
|
||||
license: sourcePackage.license,
|
||||
repository: sourcePackage.repository,
|
||||
dependencies: sourcePackage.dependencies,
|
||||
peerDependencies: sourcePackage.peerDependencies,
|
||||
publishConfig: sourcePackage.publishConfig,
|
||||
engines: {
|
||||
node: '>=16.0.0'
|
||||
},
|
||||
@@ -87,7 +93,7 @@ function generatePackageJson() {
|
||||
function copyFiles() {
|
||||
const filesToCopy = [
|
||||
{ src: './README.md', dest: './dist/README.md' },
|
||||
{ src: '../../LICENSE', dest: './dist/LICENSE' }
|
||||
{ src: './LICENSE', dest: './dist/LICENSE' }
|
||||
];
|
||||
|
||||
filesToCopy.forEach(({ src, dest }) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'jsdom', // 客户端库使用 jsdom 环境
|
||||
testEnvironment: 'jsdom', // 客户端使用jsdom环境
|
||||
roots: ['<rootDir>/tests'],
|
||||
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
|
||||
testPathIgnorePatterns: ['/node_modules/'],
|
||||
@@ -18,16 +18,10 @@ module.exports = {
|
||||
coverageReporters: ['text', 'lcov', 'html'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 60,
|
||||
branches: 70,
|
||||
functions: 70,
|
||||
lines: 70,
|
||||
statements: 70
|
||||
},
|
||||
'./src/core/': {
|
||||
branches: 70,
|
||||
functions: 80,
|
||||
lines: 80,
|
||||
statements: 80
|
||||
}
|
||||
},
|
||||
verbose: true,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"name": "@esengine/ecs-framework-network-client",
|
||||
"version": "1.0.17",
|
||||
"description": "ECS Framework 网络库 - 客户端实现",
|
||||
"type": "module",
|
||||
"name": "@esengine/network-client",
|
||||
"version": "1.0.1",
|
||||
"description": "ECS Framework网络层 - 客户端实现",
|
||||
"main": "bin/index.js",
|
||||
"types": "bin/index.d.ts",
|
||||
"exports": {
|
||||
@@ -22,15 +21,16 @@
|
||||
],
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"networking",
|
||||
"network",
|
||||
"client",
|
||||
"prediction",
|
||||
"interpolation",
|
||||
"game-engine",
|
||||
"multiplayer",
|
||||
"game",
|
||||
"browser",
|
||||
"cocos",
|
||||
"typescript"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf bin dist",
|
||||
"clean": "rimraf bin dist tsconfig.tsbuildinfo",
|
||||
"build:ts": "tsc",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "npm run build:ts",
|
||||
@@ -45,32 +45,21 @@
|
||||
"test": "jest --config jest.config.cjs",
|
||||
"test:watch": "jest --watch --config jest.config.cjs",
|
||||
"test:coverage": "jest --coverage --config jest.config.cjs",
|
||||
"test:ci": "jest --ci --coverage --config jest.config.cjs",
|
||||
"test:clear": "jest --clearCache"
|
||||
"test:ci": "jest --ci --coverage --config jest.config.cjs"
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@esengine/ecs-framework": ">=2.1.29",
|
||||
"@esengine/ecs-framework-network-shared": ">=1.0.0"
|
||||
"@esengine/ecs-framework": "file:../core",
|
||||
"@esengine/network-shared": "file:../network-shared",
|
||||
"reflect-metadata": "^0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "*",
|
||||
"@esengine/ecs-framework-network-shared": "*",
|
||||
"@rollup/plugin-commonjs": "^28.0.3",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.19.0",
|
||||
"@types/ws": "^8.5.13",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"rollup": "^4.42.0",
|
||||
"rollup-plugin-dts": "^6.2.1",
|
||||
"ts-jest": "^29.4.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
|
||||
@@ -7,17 +7,18 @@ const { readFileSync } = require('fs');
|
||||
const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||
|
||||
const banner = `/**
|
||||
* @esengine/ecs-framework-network-client v${pkg.version}
|
||||
* ECS Framework 网络库 - 客户端实现
|
||||
* @esengine/network-client v${pkg.version}
|
||||
* ECS网络层客户端实现
|
||||
*
|
||||
* @author ${pkg.author}
|
||||
* @license ${pkg.license}
|
||||
*/`;
|
||||
|
||||
// 外部依赖,不打包进bundle
|
||||
const external = [
|
||||
'ws',
|
||||
'@esengine/ecs-framework',
|
||||
'@esengine/ecs-framework-network-shared'
|
||||
'@esengine/network-shared',
|
||||
'reflect-metadata'
|
||||
];
|
||||
|
||||
const commonPlugins = [
|
||||
@@ -81,7 +82,7 @@ module.exports = [
|
||||
}
|
||||
},
|
||||
|
||||
// UMD构建
|
||||
// UMD构建 - 用于浏览器直接使用
|
||||
{
|
||||
input: 'bin/index.js',
|
||||
output: {
|
||||
@@ -92,10 +93,9 @@ module.exports = [
|
||||
sourcemap: true,
|
||||
exports: 'named',
|
||||
globals: {
|
||||
'ws': 'WebSocket',
|
||||
'uuid': 'uuid',
|
||||
'@esengine/ecs-framework': 'ECS',
|
||||
'@esengine/ecs-framework-network-shared': 'ECSNetworkShared'
|
||||
'@esengine/network-shared': 'ECSNetworkShared',
|
||||
'reflect-metadata': 'Reflect'
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
@@ -119,7 +119,7 @@ module.exports = [
|
||||
file: 'dist/index.d.ts',
|
||||
format: 'es',
|
||||
banner: `/**
|
||||
* @esengine/ecs-framework-network-client v${pkg.version}
|
||||
* @esengine/network-client v${pkg.version}
|
||||
* TypeScript definitions
|
||||
*/`
|
||||
},
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
/**
|
||||
* 客户端网络行为基类
|
||||
*
|
||||
* 类似Unity Mirror的NetworkBehaviour,提供网络功能
|
||||
*/
|
||||
|
||||
import { Component, Entity } from '@esengine/ecs-framework';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { NetworkClient } from './NetworkClient';
|
||||
import { NetworkIdentity } from './NetworkIdentity';
|
||||
|
||||
/**
|
||||
* 客户端网络行为基类
|
||||
*/
|
||||
export abstract class ClientNetworkBehaviour extends Component {
|
||||
/** 网络标识组件 */
|
||||
protected networkIdentity: NetworkIdentity | null = null;
|
||||
/** 网络客户端实例 */
|
||||
protected networkClient: NetworkClient | null = null;
|
||||
|
||||
/**
|
||||
* 组件初始化
|
||||
*/
|
||||
initialize(): void {
|
||||
|
||||
// 获取网络标识组件
|
||||
this.networkIdentity = this.entity.getComponent(NetworkIdentity);
|
||||
if (!this.networkIdentity) {
|
||||
throw new Error('NetworkBehaviour requires NetworkIdentity component');
|
||||
}
|
||||
|
||||
// 从全局获取网络客户端实例
|
||||
this.networkClient = this.getNetworkClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网络客户端实例
|
||||
*/
|
||||
protected getNetworkClient(): NetworkClient | null {
|
||||
// 这里需要实现从全局管理器获取客户端实例的逻辑
|
||||
// 暂时返回null,在实际使用时需要通过单例模式或依赖注入获取
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为本地玩家
|
||||
*/
|
||||
get isLocalPlayer(): boolean {
|
||||
return this.networkIdentity?.isLocalPlayer ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为服务器权威
|
||||
*/
|
||||
get hasAuthority(): boolean {
|
||||
return this.networkIdentity?.hasAuthority ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络ID
|
||||
*/
|
||||
get networkId(): string {
|
||||
return this.networkIdentity?.networkId ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已连接
|
||||
*/
|
||||
get isConnected(): boolean {
|
||||
return this.networkClient?.isInRoom() ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送RPC到服务器
|
||||
*/
|
||||
protected async sendServerRpc(methodName: string, ...args: NetworkValue[]): Promise<NetworkValue> {
|
||||
if (!this.networkClient || !this.networkIdentity) {
|
||||
throw new Error('Network client or identity not available');
|
||||
}
|
||||
|
||||
return this.networkClient.sendRpc(this.networkIdentity.networkId, methodName, args, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送不可靠RPC到服务器
|
||||
*/
|
||||
protected async sendServerRpcUnreliable(methodName: string, ...args: NetworkValue[]): Promise<void> {
|
||||
if (!this.networkClient || !this.networkIdentity) {
|
||||
throw new Error('Network client or identity not available');
|
||||
}
|
||||
|
||||
await this.networkClient.sendRpc(this.networkIdentity.networkId, methodName, args, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新SyncVar
|
||||
*/
|
||||
protected async updateSyncVar(fieldName: string, value: NetworkValue): Promise<void> {
|
||||
if (!this.networkClient || !this.networkIdentity) {
|
||||
throw new Error('Network client or identity not available');
|
||||
}
|
||||
|
||||
await this.networkClient.updateSyncVar(this.networkIdentity.networkId, fieldName, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当收到RPC调用时
|
||||
*/
|
||||
onRpcReceived(methodName: string, args: NetworkValue[]): void {
|
||||
// 尝试调用对应的方法
|
||||
const method = (this as any)[methodName];
|
||||
if (typeof method === 'function') {
|
||||
try {
|
||||
method.apply(this, args);
|
||||
} catch (error) {
|
||||
console.error(`Error calling RPC method ${methodName}:`, error);
|
||||
}
|
||||
} else {
|
||||
console.warn(`RPC method ${methodName} not found on ${this.constructor.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当SyncVar更新时
|
||||
*/
|
||||
onSyncVarChanged(fieldName: string, oldValue: NetworkValue, newValue: NetworkValue): void {
|
||||
// 子类可以重写此方法来处理SyncVar变化
|
||||
}
|
||||
|
||||
/**
|
||||
* 当获得权威时
|
||||
*/
|
||||
onStartAuthority(): void {
|
||||
// 子类可以重写此方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 当失去权威时
|
||||
*/
|
||||
onStopAuthority(): void {
|
||||
// 子类可以重写此方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 当成为本地玩家时
|
||||
*/
|
||||
onStartLocalPlayer(): void {
|
||||
// 子类可以重写此方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 当不再是本地玩家时
|
||||
*/
|
||||
onStopLocalPlayer(): void {
|
||||
// 子类可以重写此方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络启动时调用
|
||||
*/
|
||||
onNetworkStart(): void {
|
||||
// 子类可以重写此方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络停止时调用
|
||||
*/
|
||||
onNetworkStop(): void {
|
||||
// 子类可以重写此方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件销毁
|
||||
*/
|
||||
onDestroy(): void {
|
||||
this.networkIdentity = null;
|
||||
this.networkClient = null;
|
||||
}
|
||||
}
|
||||
@@ -1,638 +0,0 @@
|
||||
/**
|
||||
* 网络客户端主类
|
||||
*
|
||||
* 管理连接、认证、房间加入等功能
|
||||
*/
|
||||
|
||||
import { Scene, EntityManager, Emitter, ITimer, Core } from '@esengine/ecs-framework';
|
||||
import {
|
||||
NetworkIdentity as SharedNetworkIdentity,
|
||||
NetworkValue,
|
||||
RpcMessage,
|
||||
SyncVarMessage
|
||||
} from '@esengine/ecs-framework-network-shared';
|
||||
import {
|
||||
ClientTransport,
|
||||
WebSocketClientTransport,
|
||||
HttpClientTransport,
|
||||
ConnectionState,
|
||||
ClientMessage,
|
||||
ClientTransportConfig,
|
||||
WebSocketClientConfig,
|
||||
HttpClientConfig
|
||||
} from '../transport';
|
||||
|
||||
/**
|
||||
* 网络客户端配置
|
||||
*/
|
||||
export interface NetworkClientConfig {
|
||||
/** 传输类型 */
|
||||
transport: 'websocket' | 'http';
|
||||
/** 传输配置 */
|
||||
transportConfig: WebSocketClientConfig | HttpClientConfig;
|
||||
/** 是否启用预测 */
|
||||
enablePrediction?: boolean;
|
||||
/** 预测缓冲区大小 */
|
||||
predictionBuffer?: number;
|
||||
/** 是否启用插值 */
|
||||
enableInterpolation?: boolean;
|
||||
/** 插值延迟(毫秒) */
|
||||
interpolationDelay?: number;
|
||||
/** 网络对象同步间隔(毫秒) */
|
||||
syncInterval?: number;
|
||||
/** 是否启用调试 */
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
export interface UserInfo {
|
||||
/** 用户ID */
|
||||
userId: string;
|
||||
/** 用户名 */
|
||||
username: string;
|
||||
/** 用户数据 */
|
||||
data?: NetworkValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间信息
|
||||
*/
|
||||
export interface RoomInfo {
|
||||
/** 房间ID */
|
||||
roomId: string;
|
||||
/** 房间名称 */
|
||||
name: string;
|
||||
/** 当前人数 */
|
||||
playerCount: number;
|
||||
/** 最大人数 */
|
||||
maxPlayers: number;
|
||||
/** 房间元数据 */
|
||||
metadata?: NetworkValue;
|
||||
/** 是否私有房间 */
|
||||
isPrivate?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证消息
|
||||
*/
|
||||
export interface AuthMessage {
|
||||
action: string;
|
||||
username: string;
|
||||
password?: string;
|
||||
userData?: NetworkValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间消息
|
||||
*/
|
||||
export interface RoomMessage {
|
||||
action: string;
|
||||
roomId?: string;
|
||||
name?: string;
|
||||
maxPlayers?: number;
|
||||
metadata?: NetworkValue;
|
||||
isPrivate?: boolean;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络客户端事件
|
||||
*/
|
||||
export interface NetworkClientEvents {
|
||||
/** 连接建立 */
|
||||
'connected': () => void;
|
||||
/** 连接断开 */
|
||||
'disconnected': (reason: string) => void;
|
||||
/** 认证成功 */
|
||||
'authenticated': (userInfo: UserInfo) => void;
|
||||
/** 加入房间成功 */
|
||||
'joined-room': (roomInfo: RoomInfo) => void;
|
||||
/** 离开房间 */
|
||||
'left-room': (roomId: string) => void;
|
||||
/** 房间列表更新 */
|
||||
'room-list-updated': (rooms: RoomInfo[]) => void;
|
||||
/** 玩家加入房间 */
|
||||
'player-joined': (userId: string, userInfo: UserInfo) => void;
|
||||
/** 玩家离开房间 */
|
||||
'player-left': (userId: string) => void;
|
||||
/** 网络对象创建 */
|
||||
'network-object-created': (networkId: string, data: NetworkValue) => void;
|
||||
/** 网络对象销毁 */
|
||||
'network-object-destroyed': (networkId: string) => void;
|
||||
/** SyncVar 更新 */
|
||||
'syncvar-updated': (networkId: string, fieldName: string, value: NetworkValue) => void;
|
||||
/** RPC 调用 */
|
||||
'rpc-received': (networkId: string, methodName: string, args: NetworkValue[]) => void;
|
||||
/** 错误发生 */
|
||||
'error': (error: Error) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络客户端主类
|
||||
*/
|
||||
export class NetworkClient {
|
||||
private transport: ClientTransport;
|
||||
private config: NetworkClientConfig;
|
||||
private currentUser: UserInfo | null = null;
|
||||
private currentRoom: RoomInfo | null = null;
|
||||
private availableRooms: Map<string, RoomInfo> = new Map();
|
||||
private networkObjects: Map<string, SharedNetworkIdentity> = new Map();
|
||||
private pendingRpcs: Map<string, { resolve: Function; reject: Function; timeout: ITimer<any> }> = new Map();
|
||||
private scene: Scene | null = null;
|
||||
private eventEmitter: Emitter<keyof NetworkClientEvents, any>;
|
||||
|
||||
constructor(config: NetworkClientConfig) {
|
||||
this.eventEmitter = new Emitter();
|
||||
|
||||
this.config = {
|
||||
enablePrediction: true,
|
||||
predictionBuffer: 64,
|
||||
enableInterpolation: true,
|
||||
interpolationDelay: 100,
|
||||
syncInterval: 50,
|
||||
debug: false,
|
||||
...config
|
||||
};
|
||||
|
||||
this.transport = this.createTransport();
|
||||
this.setupTransportEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建传输层
|
||||
*/
|
||||
private createTransport(): ClientTransport {
|
||||
switch (this.config.transport) {
|
||||
case 'websocket':
|
||||
return new WebSocketClientTransport(this.config.transportConfig as WebSocketClientConfig);
|
||||
case 'http':
|
||||
return new HttpClientTransport(this.config.transportConfig as HttpClientConfig);
|
||||
default:
|
||||
throw new Error(`Unsupported transport type: ${this.config.transport}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置传输层事件监听
|
||||
*/
|
||||
private setupTransportEvents(): void {
|
||||
this.transport.on('connected', () => {
|
||||
this.eventEmitter.emit('connected');
|
||||
});
|
||||
|
||||
this.transport.on('disconnected', (reason) => {
|
||||
this.handleDisconnected(reason);
|
||||
});
|
||||
|
||||
this.transport.on('message', (message) => {
|
||||
this.handleMessage(message);
|
||||
});
|
||||
|
||||
this.transport.on('error', (error) => {
|
||||
this.eventEmitter.emit('error', error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到服务器
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
return this.transport.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
await this.transport.disconnect();
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户认证
|
||||
*/
|
||||
async authenticate(username: string, password?: string, userData?: NetworkValue): Promise<UserInfo> {
|
||||
if (!this.transport.isConnected()) {
|
||||
throw new Error('Not connected to server');
|
||||
}
|
||||
|
||||
const authMessage: AuthMessage = {
|
||||
action: 'login',
|
||||
username,
|
||||
password,
|
||||
userData
|
||||
};
|
||||
|
||||
const response = await this.sendRequestWithResponse('system', authMessage as any);
|
||||
|
||||
if (response.success && response.userInfo) {
|
||||
this.currentUser = response.userInfo as UserInfo;
|
||||
this.eventEmitter.emit('authenticated', this.currentUser);
|
||||
return this.currentUser;
|
||||
} else {
|
||||
throw new Error(response.error || 'Authentication failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间列表
|
||||
*/
|
||||
async getRoomList(): Promise<RoomInfo[]> {
|
||||
if (!this.isAuthenticated()) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const roomMessage: RoomMessage = {
|
||||
action: 'list-rooms'
|
||||
};
|
||||
|
||||
const response = await this.sendRequestWithResponse('system', roomMessage as any);
|
||||
|
||||
if (response.success && response.rooms) {
|
||||
this.availableRooms.clear();
|
||||
response.rooms.forEach((room: RoomInfo) => {
|
||||
this.availableRooms.set(room.roomId, room);
|
||||
});
|
||||
|
||||
this.eventEmitter.emit('room-list-updated', response.rooms);
|
||||
return response.rooms;
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to get room list');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建房间
|
||||
*/
|
||||
async createRoom(name: string, maxPlayers: number = 8, metadata?: NetworkValue, isPrivate = false): Promise<RoomInfo> {
|
||||
if (!this.isAuthenticated()) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const roomMessage: RoomMessage = {
|
||||
action: 'create-room',
|
||||
name,
|
||||
maxPlayers,
|
||||
metadata,
|
||||
isPrivate
|
||||
};
|
||||
|
||||
const response = await this.sendRequestWithResponse('system', roomMessage as any);
|
||||
|
||||
if (response.success && response.room) {
|
||||
this.currentRoom = response.room as RoomInfo;
|
||||
this.eventEmitter.emit('joined-room', this.currentRoom);
|
||||
return this.currentRoom;
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to create room');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入房间
|
||||
*/
|
||||
async joinRoom(roomId: string, password?: string): Promise<RoomInfo> {
|
||||
if (!this.isAuthenticated()) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const roomMessage: RoomMessage = {
|
||||
action: 'join-room',
|
||||
roomId,
|
||||
password
|
||||
};
|
||||
|
||||
const response = await this.sendRequestWithResponse('system', roomMessage as any);
|
||||
|
||||
if (response.success && response.room) {
|
||||
this.currentRoom = response.room as RoomInfo;
|
||||
this.eventEmitter.emit('joined-room', this.currentRoom);
|
||||
return this.currentRoom;
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to join room');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 离开房间
|
||||
*/
|
||||
async leaveRoom(): Promise<void> {
|
||||
if (!this.currentRoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
const roomMessage: RoomMessage = {
|
||||
action: 'leave-room',
|
||||
roomId: this.currentRoom.roomId
|
||||
};
|
||||
|
||||
try {
|
||||
await this.sendRequestWithResponse('system', roomMessage as any);
|
||||
} finally {
|
||||
const roomId = this.currentRoom.roomId;
|
||||
this.currentRoom = null;
|
||||
this.networkObjects.clear();
|
||||
this.eventEmitter.emit('left-room', roomId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送RPC调用
|
||||
*/
|
||||
async sendRpc(networkId: string, methodName: string, args: NetworkValue[] = [], reliable = true): Promise<NetworkValue> {
|
||||
if (!this.isInRoom()) {
|
||||
throw new Error('Not in a room');
|
||||
}
|
||||
|
||||
const rpcMessage: any = {
|
||||
networkId,
|
||||
methodName,
|
||||
args,
|
||||
isServer: false,
|
||||
messageId: this.generateMessageId()
|
||||
};
|
||||
|
||||
if (reliable) {
|
||||
return this.sendRequestWithResponse('rpc', rpcMessage);
|
||||
} else {
|
||||
await this.transport.sendMessage({
|
||||
type: 'rpc',
|
||||
data: rpcMessage as NetworkValue,
|
||||
reliable: false
|
||||
});
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新SyncVar
|
||||
*/
|
||||
async updateSyncVar(networkId: string, fieldName: string, value: NetworkValue): Promise<void> {
|
||||
if (!this.isInRoom()) {
|
||||
throw new Error('Not in a room');
|
||||
}
|
||||
|
||||
const syncMessage: any = {
|
||||
networkId,
|
||||
propertyName: fieldName,
|
||||
value,
|
||||
isServer: false
|
||||
};
|
||||
|
||||
await this.transport.sendMessage({
|
||||
type: 'syncvar',
|
||||
data: syncMessage as NetworkValue,
|
||||
reliable: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置ECS场景
|
||||
*/
|
||||
setScene(scene: Scene): void {
|
||||
this.scene = scene;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
getCurrentUser(): UserInfo | null {
|
||||
return this.currentUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前房间信息
|
||||
*/
|
||||
getCurrentRoom(): RoomInfo | null {
|
||||
return this.currentRoom;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接状态
|
||||
*/
|
||||
getConnectionState(): ConnectionState {
|
||||
return this.transport.getState();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已认证
|
||||
*/
|
||||
isAuthenticated(): boolean {
|
||||
return this.currentUser !== null && this.transport.isConnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否在房间中
|
||||
*/
|
||||
isInRoom(): boolean {
|
||||
return this.isAuthenticated() && this.currentRoom !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网络对象
|
||||
*/
|
||||
getNetworkObject(networkId: string): SharedNetworkIdentity | null {
|
||||
return this.networkObjects.get(networkId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有网络对象
|
||||
*/
|
||||
getAllNetworkObjects(): SharedNetworkIdentity[] {
|
||||
return Array.from(this.networkObjects.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理断开连接
|
||||
*/
|
||||
private handleDisconnected(reason: string): void {
|
||||
this.cleanup();
|
||||
this.eventEmitter.emit('disconnected', reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收到的消息
|
||||
*/
|
||||
private handleMessage(message: ClientMessage): void {
|
||||
try {
|
||||
switch (message.type) {
|
||||
case 'system':
|
||||
this.handleSystemMessage(message);
|
||||
break;
|
||||
case 'rpc':
|
||||
this.handleRpcMessage(message);
|
||||
break;
|
||||
case 'syncvar':
|
||||
this.handleSyncVarMessage(message);
|
||||
break;
|
||||
case 'custom':
|
||||
this.handleCustomMessage(message);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling message:', error);
|
||||
this.eventEmitter.emit('error', error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理系统消息
|
||||
*/
|
||||
private handleSystemMessage(message: ClientMessage): void {
|
||||
const data = message.data as any;
|
||||
|
||||
// 处理响应消息
|
||||
if (message.messageId && this.pendingRpcs.has(message.messageId)) {
|
||||
const pending = this.pendingRpcs.get(message.messageId)!;
|
||||
pending.timeout.stop();
|
||||
this.pendingRpcs.delete(message.messageId);
|
||||
|
||||
if (data.success) {
|
||||
pending.resolve(data);
|
||||
} else {
|
||||
pending.reject(new Error(data.error || 'Request failed'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理广播消息
|
||||
switch (data.action) {
|
||||
case 'player-joined':
|
||||
this.eventEmitter.emit('player-joined', data.userId, data.userInfo);
|
||||
break;
|
||||
case 'player-left':
|
||||
this.eventEmitter.emit('player-left', data.userId);
|
||||
break;
|
||||
case 'network-object-created':
|
||||
this.handleNetworkObjectCreated(data);
|
||||
break;
|
||||
case 'network-object-destroyed':
|
||||
this.handleNetworkObjectDestroyed(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理RPC消息
|
||||
*/
|
||||
private handleRpcMessage(message: ClientMessage): void {
|
||||
const rpcData = message.data as any;
|
||||
this.eventEmitter.emit('rpc-received', rpcData.networkId, rpcData.methodName, rpcData.args || []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理SyncVar消息
|
||||
*/
|
||||
private handleSyncVarMessage(message: ClientMessage): void {
|
||||
const syncData = message.data as any;
|
||||
this.eventEmitter.emit('syncvar-updated', syncData.networkId, syncData.propertyName, syncData.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理自定义消息
|
||||
*/
|
||||
private handleCustomMessage(message: ClientMessage): void {
|
||||
// 可扩展的自定义消息处理
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理网络对象创建
|
||||
*/
|
||||
private handleNetworkObjectCreated(data: any): void {
|
||||
const networkObject = new SharedNetworkIdentity();
|
||||
this.networkObjects.set(data.networkId, networkObject);
|
||||
this.eventEmitter.emit('network-object-created', data.networkId, data.data || {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理网络对象销毁
|
||||
*/
|
||||
private handleNetworkObjectDestroyed(data: any): void {
|
||||
this.networkObjects.delete(data.networkId);
|
||||
this.eventEmitter.emit('network-object-destroyed', data.networkId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送请求并等待响应
|
||||
*/
|
||||
private sendRequestWithResponse(type: ClientMessage['type'], data: NetworkValue, timeout = 30000): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const messageId = this.generateMessageId();
|
||||
|
||||
const timeoutTimer = Core.schedule(timeout / 1000, false, this, () => {
|
||||
this.pendingRpcs.delete(messageId);
|
||||
reject(new Error('Request timeout'));
|
||||
});
|
||||
|
||||
this.pendingRpcs.set(messageId, {
|
||||
resolve,
|
||||
reject,
|
||||
timeout: timeoutTimer
|
||||
});
|
||||
|
||||
this.transport.sendMessage({
|
||||
type,
|
||||
data,
|
||||
messageId,
|
||||
reliable: true
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成消息ID
|
||||
*/
|
||||
private generateMessageId(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
private cleanup(): void {
|
||||
this.currentUser = null;
|
||||
this.currentRoom = null;
|
||||
this.availableRooms.clear();
|
||||
this.networkObjects.clear();
|
||||
|
||||
// 取消所有待处理的RPC
|
||||
this.pendingRpcs.forEach(pending => {
|
||||
pending.timeout.stop();
|
||||
pending.reject(new Error('Connection closed'));
|
||||
});
|
||||
this.pendingRpcs.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁客户端
|
||||
*/
|
||||
destroy(): void {
|
||||
this.disconnect();
|
||||
this.transport.destroy();
|
||||
// 清理事件监听器,由于Emitter没有clear方法,我们重新创建一个
|
||||
this.eventEmitter = new Emitter();
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
on<K extends keyof NetworkClientEvents>(event: K, listener: NetworkClientEvents[K]): void {
|
||||
this.eventEmitter.addObserver(event, listener, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件监听
|
||||
*/
|
||||
off<K extends keyof NetworkClientEvents>(event: K, listener: NetworkClientEvents[K]): void {
|
||||
this.eventEmitter.removeObserver(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
emit<K extends keyof NetworkClientEvents>(event: K, ...args: Parameters<NetworkClientEvents[K]>): void {
|
||||
this.eventEmitter.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,378 +0,0 @@
|
||||
/**
|
||||
* 客户端网络标识组件
|
||||
*
|
||||
* 标识网络对象并管理其状态
|
||||
*/
|
||||
|
||||
import { Component, Entity } from '@esengine/ecs-framework';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { ClientNetworkBehaviour } from './ClientNetworkBehaviour';
|
||||
|
||||
/**
|
||||
* 网络权威类型
|
||||
*/
|
||||
export enum NetworkAuthority {
|
||||
/** 服务器权威 */
|
||||
SERVER = 'server',
|
||||
/** 客户端权威 */
|
||||
CLIENT = 'client',
|
||||
/** 所有者权威 */
|
||||
OWNER = 'owner'
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncVar信息
|
||||
*/
|
||||
export interface SyncVarInfo {
|
||||
/** 字段名 */
|
||||
fieldName: string;
|
||||
/** 当前值 */
|
||||
currentValue: NetworkValue;
|
||||
/** 上一个值 */
|
||||
previousValue: NetworkValue;
|
||||
/** 最后更新时间 */
|
||||
lastUpdateTime: number;
|
||||
/** 是否已变更 */
|
||||
isDirty: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络标识组件
|
||||
*/
|
||||
export class NetworkIdentity extends Component {
|
||||
/** 网络ID */
|
||||
private _networkId: string = '';
|
||||
/** 所有者用户ID */
|
||||
private _ownerId: string = '';
|
||||
/** 是否为本地玩家 */
|
||||
private _isLocalPlayer: boolean = false;
|
||||
/** 权威类型 */
|
||||
private _authority: NetworkAuthority = NetworkAuthority.SERVER;
|
||||
/** 是否有权威 */
|
||||
private _hasAuthority: boolean = false;
|
||||
/** 网络行为组件列表 */
|
||||
private networkBehaviours: ClientNetworkBehaviour[] = [];
|
||||
/** SyncVar信息映射 */
|
||||
private syncVars: Map<string, SyncVarInfo> = new Map();
|
||||
/** 预测状态 */
|
||||
private predictionEnabled: boolean = false;
|
||||
/** 插值状态 */
|
||||
private interpolationEnabled: boolean = true;
|
||||
|
||||
/**
|
||||
* 网络ID
|
||||
*/
|
||||
get networkId(): string {
|
||||
return this._networkId;
|
||||
}
|
||||
|
||||
set networkId(value: string) {
|
||||
this._networkId = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有者用户ID
|
||||
*/
|
||||
get ownerId(): string {
|
||||
return this._ownerId;
|
||||
}
|
||||
|
||||
set ownerId(value: string) {
|
||||
this._ownerId = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为本地玩家
|
||||
*/
|
||||
get isLocalPlayer(): boolean {
|
||||
return this._isLocalPlayer;
|
||||
}
|
||||
|
||||
set isLocalPlayer(value: boolean) {
|
||||
if (this._isLocalPlayer !== value) {
|
||||
this._isLocalPlayer = value;
|
||||
this.notifyLocalPlayerChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 权威类型
|
||||
*/
|
||||
get authority(): NetworkAuthority {
|
||||
return this._authority;
|
||||
}
|
||||
|
||||
set authority(value: NetworkAuthority) {
|
||||
if (this._authority !== value) {
|
||||
this._authority = value;
|
||||
this.updateAuthorityStatus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有权威
|
||||
*/
|
||||
get hasAuthority(): boolean {
|
||||
return this._hasAuthority;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否启用预测
|
||||
*/
|
||||
get isPredictionEnabled(): boolean {
|
||||
return this.predictionEnabled;
|
||||
}
|
||||
|
||||
set isPredictionEnabled(value: boolean) {
|
||||
this.predictionEnabled = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否启用插值
|
||||
*/
|
||||
get isInterpolationEnabled(): boolean {
|
||||
return this.interpolationEnabled;
|
||||
}
|
||||
|
||||
set isInterpolationEnabled(value: boolean) {
|
||||
this.interpolationEnabled = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件初始化
|
||||
*/
|
||||
initialize(): void {
|
||||
this.collectNetworkBehaviours();
|
||||
this.notifyNetworkStart();
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集网络行为组件
|
||||
*/
|
||||
private collectNetworkBehaviours(): void {
|
||||
// 暂时留空,等待实际集成时实现
|
||||
this.networkBehaviours = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新权威状态
|
||||
*/
|
||||
private updateAuthorityStatus(): void {
|
||||
const oldHasAuthority = this._hasAuthority;
|
||||
|
||||
// 根据权威类型计算是否有权威
|
||||
switch (this._authority) {
|
||||
case NetworkAuthority.SERVER:
|
||||
this._hasAuthority = false; // 客户端永远没有服务器权威
|
||||
break;
|
||||
case NetworkAuthority.CLIENT:
|
||||
this._hasAuthority = true; // 客户端权威
|
||||
break;
|
||||
case NetworkAuthority.OWNER:
|
||||
this._hasAuthority = this._isLocalPlayer; // 本地玩家才有权威
|
||||
break;
|
||||
}
|
||||
|
||||
// 通知权威变化
|
||||
if (oldHasAuthority !== this._hasAuthority) {
|
||||
this.notifyAuthorityChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知权威变化
|
||||
*/
|
||||
private notifyAuthorityChanged(): void {
|
||||
this.networkBehaviours.forEach(behaviour => {
|
||||
if (this._hasAuthority) {
|
||||
behaviour.onStartAuthority();
|
||||
} else {
|
||||
behaviour.onStopAuthority();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知本地玩家状态变化
|
||||
*/
|
||||
private notifyLocalPlayerChanged(): void {
|
||||
this.updateAuthorityStatus(); // 本地玩家状态影响权威
|
||||
|
||||
this.networkBehaviours.forEach(behaviour => {
|
||||
if (this._isLocalPlayer) {
|
||||
behaviour.onStartLocalPlayer();
|
||||
} else {
|
||||
behaviour.onStopLocalPlayer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知网络启动
|
||||
*/
|
||||
private notifyNetworkStart(): void {
|
||||
this.networkBehaviours.forEach(behaviour => {
|
||||
behaviour.onNetworkStart();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知网络停止
|
||||
*/
|
||||
private notifyNetworkStop(): void {
|
||||
this.networkBehaviours.forEach(behaviour => {
|
||||
behaviour.onNetworkStop();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理RPC调用
|
||||
*/
|
||||
handleRpcCall(methodName: string, args: NetworkValue[]): void {
|
||||
// 将RPC调用分发给所有网络行为组件
|
||||
this.networkBehaviours.forEach(behaviour => {
|
||||
behaviour.onRpcReceived(methodName, args);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册SyncVar
|
||||
*/
|
||||
registerSyncVar(fieldName: string, initialValue: NetworkValue): void {
|
||||
this.syncVars.set(fieldName, {
|
||||
fieldName,
|
||||
currentValue: initialValue,
|
||||
previousValue: initialValue,
|
||||
lastUpdateTime: Date.now(),
|
||||
isDirty: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新SyncVar
|
||||
*/
|
||||
updateSyncVar(fieldName: string, newValue: NetworkValue): void {
|
||||
const syncVar = this.syncVars.get(fieldName);
|
||||
if (!syncVar) {
|
||||
console.warn(`SyncVar ${fieldName} not registered on ${this._networkId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const oldValue = syncVar.currentValue;
|
||||
syncVar.previousValue = oldValue;
|
||||
syncVar.currentValue = newValue;
|
||||
syncVar.lastUpdateTime = Date.now();
|
||||
syncVar.isDirty = true;
|
||||
|
||||
// 通知所有网络行为组件
|
||||
this.networkBehaviours.forEach(behaviour => {
|
||||
behaviour.onSyncVarChanged(fieldName, oldValue, newValue);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取SyncVar值
|
||||
*/
|
||||
getSyncVar(fieldName: string): NetworkValue | undefined {
|
||||
return this.syncVars.get(fieldName)?.currentValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有SyncVar
|
||||
*/
|
||||
getAllSyncVars(): Map<string, SyncVarInfo> {
|
||||
return new Map(this.syncVars);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取脏SyncVar
|
||||
*/
|
||||
getDirtySyncVars(): SyncVarInfo[] {
|
||||
return Array.from(this.syncVars.values()).filter(syncVar => syncVar.isDirty);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除脏标记
|
||||
*/
|
||||
clearDirtyFlags(): void {
|
||||
this.syncVars.forEach(syncVar => {
|
||||
syncVar.isDirty = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化网络状态
|
||||
*/
|
||||
serializeState(): NetworkValue {
|
||||
const state: any = {
|
||||
networkId: this._networkId,
|
||||
ownerId: this._ownerId,
|
||||
isLocalPlayer: this._isLocalPlayer,
|
||||
authority: this._authority,
|
||||
syncVars: {}
|
||||
};
|
||||
|
||||
// 序列化SyncVar
|
||||
this.syncVars.forEach((syncVar, fieldName) => {
|
||||
state.syncVars[fieldName] = syncVar.currentValue;
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化网络状态
|
||||
*/
|
||||
deserializeState(state: any): void {
|
||||
if (state.networkId) this._networkId = state.networkId;
|
||||
if (state.ownerId) this._ownerId = state.ownerId;
|
||||
if (typeof state.isLocalPlayer === 'boolean') this.isLocalPlayer = state.isLocalPlayer;
|
||||
if (state.authority) this.authority = state.authority;
|
||||
|
||||
// 反序列化SyncVar
|
||||
if (state.syncVars) {
|
||||
Object.entries(state.syncVars).forEach(([fieldName, value]) => {
|
||||
if (this.syncVars.has(fieldName)) {
|
||||
this.updateSyncVar(fieldName, value as NetworkValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置预测状态
|
||||
*/
|
||||
setPredictionState(enabled: boolean): void {
|
||||
this.predictionEnabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置插值状态
|
||||
*/
|
||||
setInterpolationState(enabled: boolean): void {
|
||||
this.interpolationEnabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以发送RPC
|
||||
*/
|
||||
canSendRpc(): boolean {
|
||||
return this._hasAuthority || this._isLocalPlayer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以更新SyncVar
|
||||
*/
|
||||
canUpdateSyncVar(): boolean {
|
||||
return this._hasAuthority;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件销毁
|
||||
*/
|
||||
onDestroy(): void {
|
||||
this.notifyNetworkStop();
|
||||
this.networkBehaviours = [];
|
||||
this.syncVars.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* 核心模块导出
|
||||
*/
|
||||
|
||||
export * from './NetworkClient';
|
||||
export * from './ClientNetworkBehaviour';
|
||||
export * from './NetworkIdentity';
|
||||
@@ -1,108 +0,0 @@
|
||||
/**
|
||||
* ClientRpc装饰器 - 客户端版本
|
||||
*
|
||||
* 用于标记可以从服务器调用的客户端方法
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
|
||||
/**
|
||||
* ClientRpc配置选项
|
||||
*/
|
||||
export interface ClientRpcOptions {
|
||||
/** 是否可靠传输 */
|
||||
reliable?: boolean;
|
||||
/** 超时时间(毫秒) */
|
||||
timeout?: number;
|
||||
/** 是否仅发送给所有者 */
|
||||
ownerOnly?: boolean;
|
||||
/** 是否包含发送者 */
|
||||
includeSender?: boolean;
|
||||
/** 权限要求 */
|
||||
requireAuthority?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* ClientRpc元数据键
|
||||
*/
|
||||
export const CLIENT_RPC_METADATA_KEY = Symbol('client_rpc');
|
||||
|
||||
/**
|
||||
* ClientRpc元数据
|
||||
*/
|
||||
export interface ClientRpcMetadata {
|
||||
/** 方法名 */
|
||||
methodName: string;
|
||||
/** 配置选项 */
|
||||
options: ClientRpcOptions;
|
||||
/** 原始方法 */
|
||||
originalMethod: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* ClientRpc装饰器
|
||||
*/
|
||||
export function ClientRpc(options: ClientRpcOptions = {}): MethodDecorator {
|
||||
return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
|
||||
const methodName = propertyKey as string;
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
// 获取已有的ClientRpc元数据
|
||||
const existingMetadata: ClientRpcMetadata[] = Reflect.getMetadata(CLIENT_RPC_METADATA_KEY, target.constructor) || [];
|
||||
|
||||
// 添加新的ClientRpc元数据
|
||||
existingMetadata.push({
|
||||
methodName,
|
||||
options: {
|
||||
reliable: true,
|
||||
timeout: 30000,
|
||||
ownerOnly: false,
|
||||
includeSender: false,
|
||||
requireAuthority: false,
|
||||
...options
|
||||
},
|
||||
originalMethod
|
||||
});
|
||||
|
||||
// 设置元数据
|
||||
Reflect.defineMetadata(CLIENT_RPC_METADATA_KEY, existingMetadata, target.constructor);
|
||||
|
||||
// 包装原方法(客户端接收RPC调用时执行)
|
||||
descriptor.value = function (this: any, ...args: NetworkValue[]) {
|
||||
try {
|
||||
// 直接调用原方法,客户端接收RPC调用
|
||||
return originalMethod.apply(this, args);
|
||||
} catch (error) {
|
||||
console.error(`Error executing ClientRpc ${methodName}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类的所有ClientRpc元数据
|
||||
*/
|
||||
export function getClientRpcMetadata(target: any): ClientRpcMetadata[] {
|
||||
return Reflect.getMetadata(CLIENT_RPC_METADATA_KEY, target) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查方法是否为ClientRpc
|
||||
*/
|
||||
export function isClientRpc(target: any, methodName: string): boolean {
|
||||
const metadata = getClientRpcMetadata(target);
|
||||
return metadata.some(meta => meta.methodName === methodName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取特定方法的ClientRpc选项
|
||||
*/
|
||||
export function getClientRpcOptions(target: any, methodName: string): ClientRpcOptions | null {
|
||||
const metadata = getClientRpcMetadata(target);
|
||||
const rpc = metadata.find(meta => meta.methodName === methodName);
|
||||
return rpc ? rpc.options : null;
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/**
|
||||
* ServerRpc装饰器 - 客户端版本
|
||||
*
|
||||
* 用于标记可以向服务器发送的RPC方法
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { ClientNetworkBehaviour } from '../core/ClientNetworkBehaviour';
|
||||
|
||||
/**
|
||||
* ServerRpc配置选项
|
||||
*/
|
||||
export interface ServerRpcOptions {
|
||||
/** 是否可靠传输 */
|
||||
reliable?: boolean;
|
||||
/** 超时时间(毫秒) */
|
||||
timeout?: number;
|
||||
/** 是否需要权威 */
|
||||
requireAuthority?: boolean;
|
||||
/** 是否需要是本地玩家 */
|
||||
requireLocalPlayer?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServerRpc元数据键
|
||||
*/
|
||||
export const SERVER_RPC_METADATA_KEY = Symbol('server_rpc');
|
||||
|
||||
/**
|
||||
* ServerRpc元数据
|
||||
*/
|
||||
export interface ServerRpcMetadata {
|
||||
/** 方法名 */
|
||||
methodName: string;
|
||||
/** 配置选项 */
|
||||
options: ServerRpcOptions;
|
||||
/** 原始方法 */
|
||||
originalMethod: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServerRpc装饰器
|
||||
*/
|
||||
export function ServerRpc(options: ServerRpcOptions = {}): MethodDecorator {
|
||||
return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
|
||||
const methodName = propertyKey as string;
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
// 获取已有的ServerRpc元数据
|
||||
const existingMetadata: ServerRpcMetadata[] = Reflect.getMetadata(SERVER_RPC_METADATA_KEY, target.constructor) || [];
|
||||
|
||||
// 添加新的ServerRpc元数据
|
||||
existingMetadata.push({
|
||||
methodName,
|
||||
options: {
|
||||
reliable: true,
|
||||
timeout: 30000,
|
||||
requireAuthority: false,
|
||||
requireLocalPlayer: false,
|
||||
...options
|
||||
},
|
||||
originalMethod
|
||||
});
|
||||
|
||||
// 设置元数据
|
||||
Reflect.defineMetadata(SERVER_RPC_METADATA_KEY, existingMetadata, target.constructor);
|
||||
|
||||
// 替换方法实现为发送RPC调用
|
||||
descriptor.value = async function (this: ClientNetworkBehaviour, ...args: NetworkValue[]) {
|
||||
try {
|
||||
// 获取NetworkIdentity
|
||||
const networkIdentity = this.entity?.getComponent('NetworkIdentity' as any);
|
||||
if (!networkIdentity) {
|
||||
throw new Error('NetworkIdentity component not found');
|
||||
}
|
||||
|
||||
// 检查权限要求
|
||||
if (options.requireAuthority && !(networkIdentity as any).hasAuthority) {
|
||||
throw new Error(`ServerRpc ${methodName} requires authority`);
|
||||
}
|
||||
|
||||
if (options.requireLocalPlayer && !(networkIdentity as any).isLocalPlayer) {
|
||||
throw new Error(`ServerRpc ${methodName} requires local player`);
|
||||
}
|
||||
|
||||
// 发送RPC到服务器
|
||||
if (options.reliable) {
|
||||
const result = await this.sendServerRpc(methodName, ...args);
|
||||
return result;
|
||||
} else {
|
||||
await this.sendServerRpcUnreliable(methodName, ...args);
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error sending ServerRpc ${methodName}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 保存原方法到特殊属性,用于本地预测或调试
|
||||
(descriptor.value as any).__originalMethod = originalMethod;
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类的所有ServerRpc元数据
|
||||
*/
|
||||
export function getServerRpcMetadata(target: any): ServerRpcMetadata[] {
|
||||
return Reflect.getMetadata(SERVER_RPC_METADATA_KEY, target) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查方法是否为ServerRpc
|
||||
*/
|
||||
export function isServerRpc(target: any, methodName: string): boolean {
|
||||
const metadata = getServerRpcMetadata(target);
|
||||
return metadata.some(meta => meta.methodName === methodName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取特定方法的ServerRpc选项
|
||||
*/
|
||||
export function getServerRpcOptions(target: any, methodName: string): ServerRpcOptions | null {
|
||||
const metadata = getServerRpcMetadata(target);
|
||||
const rpc = metadata.find(meta => meta.methodName === methodName);
|
||||
return rpc ? rpc.options : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取方法的原始实现(未被装饰器修改的版本)
|
||||
*/
|
||||
export function getOriginalMethod(method: Function): Function | null {
|
||||
return (method as any).__originalMethod || null;
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
/**
|
||||
* SyncVar装饰器 - 客户端版本
|
||||
*
|
||||
* 用于标记需要同步的变量
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { ClientNetworkBehaviour } from '../core/ClientNetworkBehaviour';
|
||||
|
||||
/**
|
||||
* SyncVar配置选项
|
||||
*/
|
||||
export interface SyncVarOptions {
|
||||
/** 是否可从客户端修改 */
|
||||
clientCanModify?: boolean;
|
||||
/** 同步间隔(毫秒),0表示立即同步 */
|
||||
syncInterval?: number;
|
||||
/** 是否仅同步给所有者 */
|
||||
ownerOnly?: boolean;
|
||||
/** 自定义序列化器 */
|
||||
serializer?: (value: any) => NetworkValue;
|
||||
/** 自定义反序列化器 */
|
||||
deserializer?: (value: NetworkValue) => any;
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncVar元数据键
|
||||
*/
|
||||
export const SYNCVAR_METADATA_KEY = Symbol('syncvar');
|
||||
|
||||
/**
|
||||
* SyncVar元数据
|
||||
*/
|
||||
export interface SyncVarMetadata {
|
||||
/** 属性名 */
|
||||
propertyKey: string;
|
||||
/** 配置选项 */
|
||||
options: SyncVarOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncVar装饰器
|
||||
*/
|
||||
export function SyncVar(options: SyncVarOptions = {}): PropertyDecorator {
|
||||
return function (target: any, propertyKey: string | symbol) {
|
||||
const key = propertyKey as string;
|
||||
|
||||
// 获取已有的SyncVar元数据
|
||||
const existingMetadata: SyncVarMetadata[] = Reflect.getMetadata(SYNCVAR_METADATA_KEY, target.constructor) || [];
|
||||
|
||||
// 添加新的SyncVar元数据
|
||||
existingMetadata.push({
|
||||
propertyKey: key,
|
||||
options: {
|
||||
clientCanModify: false,
|
||||
syncInterval: 0,
|
||||
ownerOnly: false,
|
||||
...options
|
||||
}
|
||||
});
|
||||
|
||||
// 设置元数据
|
||||
Reflect.defineMetadata(SYNCVAR_METADATA_KEY, existingMetadata, target.constructor);
|
||||
|
||||
// 存储原始属性名(用于内部存储)
|
||||
const privateKey = `_syncvar_${key}`;
|
||||
|
||||
// 创建属性访问器
|
||||
Object.defineProperty(target, key, {
|
||||
get: function (this: ClientNetworkBehaviour) {
|
||||
// 从NetworkIdentity获取SyncVar值
|
||||
const networkIdentity = this.entity?.getComponent('NetworkIdentity' as any);
|
||||
if (networkIdentity) {
|
||||
const syncVarValue = (networkIdentity as any).getSyncVar(key);
|
||||
if (syncVarValue !== undefined) {
|
||||
return options.deserializer ? options.deserializer(syncVarValue) : syncVarValue;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果网络值不存在,返回本地存储的值
|
||||
return (this as any)[privateKey];
|
||||
},
|
||||
|
||||
set: function (this: ClientNetworkBehaviour, value: any) {
|
||||
const oldValue = (this as any)[privateKey];
|
||||
const newValue = options.serializer ? options.serializer(value) : value;
|
||||
|
||||
// 存储到本地
|
||||
(this as any)[privateKey] = value;
|
||||
|
||||
// 获取NetworkIdentity
|
||||
const networkIdentity = this.entity?.getComponent('NetworkIdentity' as any);
|
||||
if (!networkIdentity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否可以修改
|
||||
if (!options.clientCanModify && !(networkIdentity as any).hasAuthority) {
|
||||
console.warn(`Cannot modify SyncVar ${key} without authority`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 注册SyncVar(如果尚未注册)
|
||||
(networkIdentity as any).registerSyncVar(key, newValue);
|
||||
|
||||
// 更新NetworkIdentity中的值
|
||||
(networkIdentity as any).updateSyncVar(key, newValue);
|
||||
|
||||
// 如果有权威且值发生变化,发送到服务器
|
||||
if ((networkIdentity as any).hasAuthority && oldValue !== value) {
|
||||
this.updateSyncVar(key, newValue).catch(error => {
|
||||
console.error(`Failed to sync variable ${key}:`, error);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类的所有SyncVar元数据
|
||||
*/
|
||||
export function getSyncVarMetadata(target: any): SyncVarMetadata[] {
|
||||
return Reflect.getMetadata(SYNCVAR_METADATA_KEY, target) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查属性是否为SyncVar
|
||||
*/
|
||||
export function isSyncVar(target: any, propertyKey: string): boolean {
|
||||
const metadata = getSyncVarMetadata(target);
|
||||
return metadata.some(meta => meta.propertyKey === propertyKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取特定属性的SyncVar选项
|
||||
*/
|
||||
export function getSyncVarOptions(target: any, propertyKey: string): SyncVarOptions | null {
|
||||
const metadata = getSyncVarMetadata(target);
|
||||
const syncVar = metadata.find(meta => meta.propertyKey === propertyKey);
|
||||
return syncVar ? syncVar.options : null;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* 装饰器导出
|
||||
*/
|
||||
|
||||
export * from './SyncVar';
|
||||
export * from './ClientRpc';
|
||||
export * from './ServerRpc';
|
||||
@@ -1,23 +1,24 @@
|
||||
/**
|
||||
* ECS Framework 网络库 - 客户端
|
||||
*
|
||||
* 提供网络客户端功能,包括连接管理、预测、插值等
|
||||
* @esengine/network-client
|
||||
* ECS Framework网络层 - 客户端实现
|
||||
*/
|
||||
|
||||
// 核心模块
|
||||
export * from './core';
|
||||
// 核心客户端 (待实现)
|
||||
// export * from './core/NetworkClient';
|
||||
// export * from './core/ServerConnection';
|
||||
|
||||
// 传输层
|
||||
export * from './transport';
|
||||
// 传输层 (待实现)
|
||||
// export * from './transport/WebSocketClient';
|
||||
// export * from './transport/HttpClient';
|
||||
|
||||
// 装饰器
|
||||
export * from './decorators';
|
||||
// 系统层 (待实现)
|
||||
// export * from './systems/ClientSyncSystem';
|
||||
// export * from './systems/ClientRpcSystem';
|
||||
// export * from './systems/InterpolationSystem';
|
||||
|
||||
// 系统
|
||||
export * from './systems';
|
||||
// 平台适配器 (待实现)
|
||||
// export * from './adapters/BrowserAdapter';
|
||||
// export * from './adapters/CocosAdapter';
|
||||
|
||||
// 接口
|
||||
export * from './interfaces';
|
||||
|
||||
// 版本信息
|
||||
export const VERSION = '1.0.11';
|
||||
// 重新导出shared包的类型
|
||||
export * from '@esengine/network-shared';
|
||||
@@ -1,34 +0,0 @@
|
||||
/**
|
||||
* 网络系统相关接口
|
||||
*/
|
||||
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
|
||||
/**
|
||||
* 可预测组件接口
|
||||
*
|
||||
* 实现此接口的组件可以参与客户端预测系统
|
||||
*/
|
||||
export interface IPredictable {
|
||||
/**
|
||||
* 预测更新
|
||||
*
|
||||
* @param inputs 输入数据
|
||||
* @param timestamp 时间戳
|
||||
*/
|
||||
predictUpdate(inputs: NetworkValue, timestamp: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 可插值组件接口
|
||||
*
|
||||
* 实现此接口的组件可以参与插值系统
|
||||
*/
|
||||
export interface IInterpolatable {
|
||||
/**
|
||||
* 应用插值状态
|
||||
*
|
||||
* @param state 插值后的状态数据
|
||||
*/
|
||||
applyInterpolatedState(state: NetworkValue): void;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
/**
|
||||
* 接口导出
|
||||
*/
|
||||
|
||||
export * from './NetworkInterfaces';
|
||||
@@ -1,520 +0,0 @@
|
||||
/**
|
||||
* 客户端插值系统
|
||||
*
|
||||
* 实现网络对象的平滑插值
|
||||
*/
|
||||
|
||||
import { EntitySystem, Entity, Matcher, Time } from '@esengine/ecs-framework';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { NetworkIdentity } from '../core/NetworkIdentity';
|
||||
import { IInterpolatable } from '../interfaces/NetworkInterfaces';
|
||||
|
||||
/**
|
||||
* 插值状态快照
|
||||
*/
|
||||
export interface InterpolationSnapshot {
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
/** 网络ID */
|
||||
networkId: string;
|
||||
/** 状态数据 */
|
||||
state: NetworkValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插值目标
|
||||
*/
|
||||
export interface InterpolationTarget {
|
||||
/** 网络ID */
|
||||
networkId: string;
|
||||
/** 起始状态 */
|
||||
fromState: NetworkValue;
|
||||
/** 目标状态 */
|
||||
toState: NetworkValue;
|
||||
/** 起始时间 */
|
||||
fromTime: number;
|
||||
/** 结束时间 */
|
||||
toTime: number;
|
||||
/** 当前插值进度 (0-1) */
|
||||
progress: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插值配置
|
||||
*/
|
||||
export interface InterpolationConfig {
|
||||
/** 插值延迟(毫秒) */
|
||||
delay: number;
|
||||
/** 最大插值时间(毫秒) */
|
||||
maxTime: number;
|
||||
/** 插值缓冲区大小 */
|
||||
bufferSize: number;
|
||||
/** 外推是否启用 */
|
||||
enableExtrapolation: boolean;
|
||||
/** 最大外推时间(毫秒) */
|
||||
maxExtrapolationTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插值算法类型
|
||||
*/
|
||||
export enum InterpolationType {
|
||||
/** 线性插值 */
|
||||
LINEAR = 'linear',
|
||||
/** 平滑插值 */
|
||||
SMOOTHSTEP = 'smoothstep',
|
||||
/** 三次贝塞尔插值 */
|
||||
CUBIC = 'cubic'
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端插值系统
|
||||
*/
|
||||
export class InterpolationSystem extends EntitySystem {
|
||||
/** 插值状态缓冲区 */
|
||||
private stateBuffer: Map<string, InterpolationSnapshot[]> = new Map();
|
||||
/** 当前插值目标 */
|
||||
private interpolationTargets: Map<string, InterpolationTarget> = new Map();
|
||||
/** 插值配置 */
|
||||
private config: InterpolationConfig;
|
||||
/** 当前时间 */
|
||||
private currentTime: number = 0;
|
||||
|
||||
constructor(config?: Partial<InterpolationConfig>) {
|
||||
// 使用Matcher查询具有NetworkIdentity的实体
|
||||
super(Matcher.all(NetworkIdentity));
|
||||
|
||||
this.config = {
|
||||
delay: 100,
|
||||
maxTime: 500,
|
||||
bufferSize: 32,
|
||||
enableExtrapolation: false,
|
||||
maxExtrapolationTime: 50,
|
||||
...config
|
||||
};
|
||||
|
||||
this.currentTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统初始化
|
||||
*/
|
||||
override initialize(): void {
|
||||
super.initialize();
|
||||
this.currentTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统更新
|
||||
*/
|
||||
override update(): void {
|
||||
this.currentTime = Date.now();
|
||||
this.cleanupOldStates();
|
||||
|
||||
// 调用父类update,会自动调用process方法处理匹配的实体
|
||||
super.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理匹配的实体
|
||||
*/
|
||||
protected override process(entities: Entity[]): void {
|
||||
const interpolationTime = this.currentTime - this.config.delay;
|
||||
|
||||
for (const entity of entities) {
|
||||
const networkIdentity = entity.getComponent(NetworkIdentity);
|
||||
|
||||
if (networkIdentity && networkIdentity.isInterpolationEnabled) {
|
||||
const networkId = networkIdentity.networkId;
|
||||
const target = this.interpolationTargets.get(networkId);
|
||||
|
||||
if (target) {
|
||||
// 计算插值进度
|
||||
const duration = target.toTime - target.fromTime;
|
||||
if (duration > 0) {
|
||||
const elapsed = interpolationTime - target.fromTime;
|
||||
target.progress = Math.max(0, Math.min(1, elapsed / duration));
|
||||
|
||||
// 执行插值
|
||||
const interpolatedState = this.interpolateStates(
|
||||
target.fromState,
|
||||
target.toState,
|
||||
target.progress,
|
||||
InterpolationType.LINEAR
|
||||
);
|
||||
|
||||
// 应用插值状态
|
||||
this.applyInterpolatedState(entity, interpolatedState);
|
||||
|
||||
// 检查是否需要外推
|
||||
if (target.progress >= 1 && this.config.enableExtrapolation) {
|
||||
this.performExtrapolation(entity, target, interpolationTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加网络状态快照
|
||||
*/
|
||||
addStateSnapshot(networkId: string, state: NetworkValue, timestamp: number): void {
|
||||
// 获取或创建缓冲区
|
||||
if (!this.stateBuffer.has(networkId)) {
|
||||
this.stateBuffer.set(networkId, []);
|
||||
}
|
||||
|
||||
const buffer = this.stateBuffer.get(networkId)!;
|
||||
|
||||
const snapshot: InterpolationSnapshot = {
|
||||
timestamp,
|
||||
networkId,
|
||||
state
|
||||
};
|
||||
|
||||
// 插入到正确的位置(按时间戳排序)
|
||||
const insertIndex = this.findInsertIndex(buffer, timestamp);
|
||||
buffer.splice(insertIndex, 0, snapshot);
|
||||
|
||||
// 保持缓冲区大小
|
||||
if (buffer.length > this.config.bufferSize) {
|
||||
buffer.shift();
|
||||
}
|
||||
|
||||
// 更新插值目标
|
||||
this.updateInterpolationTarget(networkId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新插值目标
|
||||
*/
|
||||
private updateInterpolationTarget(networkId: string): void {
|
||||
const buffer = this.stateBuffer.get(networkId);
|
||||
if (!buffer || buffer.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interpolationTime = this.currentTime - this.config.delay;
|
||||
|
||||
// 查找插值区间
|
||||
const { from, to } = this.findInterpolationRange(buffer, interpolationTime);
|
||||
|
||||
if (!from || !to) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新或创建插值目标
|
||||
this.interpolationTargets.set(networkId, {
|
||||
networkId,
|
||||
fromState: from.state,
|
||||
toState: to.state,
|
||||
fromTime: from.timestamp,
|
||||
toTime: to.timestamp,
|
||||
progress: 0
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找插值区间
|
||||
*/
|
||||
private findInterpolationRange(buffer: InterpolationSnapshot[], time: number): {
|
||||
from: InterpolationSnapshot | null;
|
||||
to: InterpolationSnapshot | null;
|
||||
} {
|
||||
let from: InterpolationSnapshot | null = null;
|
||||
let to: InterpolationSnapshot | null = null;
|
||||
|
||||
for (let i = 0; i < buffer.length - 1; i++) {
|
||||
const current = buffer[i];
|
||||
const next = buffer[i + 1];
|
||||
|
||||
if (time >= current.timestamp && time <= next.timestamp) {
|
||||
from = current;
|
||||
to = next;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到区间,使用最近的两个状态
|
||||
if (!from && !to && buffer.length >= 2) {
|
||||
if (time < buffer[0].timestamp) {
|
||||
// 时间过早,使用前两个状态
|
||||
from = buffer[0];
|
||||
to = buffer[1];
|
||||
} else if (time > buffer[buffer.length - 1].timestamp) {
|
||||
// 时间过晚,使用后两个状态
|
||||
from = buffer[buffer.length - 2];
|
||||
to = buffer[buffer.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
return { from, to };
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态插值
|
||||
*/
|
||||
private interpolateStates(
|
||||
fromState: NetworkValue,
|
||||
toState: NetworkValue,
|
||||
progress: number,
|
||||
type: InterpolationType
|
||||
): NetworkValue {
|
||||
// 调整插值进度曲线
|
||||
const adjustedProgress = this.adjustProgress(progress, type);
|
||||
|
||||
try {
|
||||
return this.interpolateValue(fromState, toState, adjustedProgress);
|
||||
} catch (error) {
|
||||
console.error('Error interpolating states:', error);
|
||||
return toState; // 出错时返回目标状态
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归插值值
|
||||
*/
|
||||
private interpolateValue(from: NetworkValue, to: NetworkValue, progress: number): NetworkValue {
|
||||
// 如果类型不同,直接返回目标值
|
||||
if (typeof from !== typeof to) {
|
||||
return to;
|
||||
}
|
||||
|
||||
// 数字插值
|
||||
if (typeof from === 'number' && typeof to === 'number') {
|
||||
return from + (to - from) * progress;
|
||||
}
|
||||
|
||||
// 字符串插值(直接切换)
|
||||
if (typeof from === 'string' && typeof to === 'string') {
|
||||
return progress < 0.5 ? from : to;
|
||||
}
|
||||
|
||||
// 布尔插值(直接切换)
|
||||
if (typeof from === 'boolean' && typeof to === 'boolean') {
|
||||
return progress < 0.5 ? from : to;
|
||||
}
|
||||
|
||||
// 数组插值
|
||||
if (Array.isArray(from) && Array.isArray(to)) {
|
||||
const result: NetworkValue[] = [];
|
||||
const maxLength = Math.max(from.length, to.length);
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const fromValue = i < from.length ? from[i] : to[i];
|
||||
const toValue = i < to.length ? to[i] : from[i];
|
||||
result[i] = this.interpolateValue(fromValue, toValue, progress);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 对象插值
|
||||
if (from && to && typeof from === 'object' && typeof to === 'object') {
|
||||
const result: any = {};
|
||||
const allKeys = new Set([...Object.keys(from), ...Object.keys(to)]);
|
||||
|
||||
for (const key of allKeys) {
|
||||
const fromValue = (from as any)[key];
|
||||
const toValue = (to as any)[key];
|
||||
|
||||
if (fromValue !== undefined && toValue !== undefined) {
|
||||
result[key] = this.interpolateValue(fromValue, toValue, progress);
|
||||
} else {
|
||||
result[key] = toValue !== undefined ? toValue : fromValue;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 其他类型直接返回目标值
|
||||
return to;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整插值进度曲线
|
||||
*/
|
||||
private adjustProgress(progress: number, type: InterpolationType): number {
|
||||
switch (type) {
|
||||
case InterpolationType.LINEAR:
|
||||
return progress;
|
||||
|
||||
case InterpolationType.SMOOTHSTEP:
|
||||
return progress * progress * (3 - 2 * progress);
|
||||
|
||||
case InterpolationType.CUBIC:
|
||||
return progress < 0.5
|
||||
? 4 * progress * progress * progress
|
||||
: 1 - Math.pow(-2 * progress + 2, 3) / 2;
|
||||
|
||||
default:
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用插值状态到实体
|
||||
*/
|
||||
private applyInterpolatedState(entity: Entity, state: NetworkValue): void {
|
||||
// 获取所有可插值的组件
|
||||
const components: any[] = [];
|
||||
for (const component of components) {
|
||||
if (this.isInterpolatable(component)) {
|
||||
try {
|
||||
(component as IInterpolatable).applyInterpolatedState(state);
|
||||
} catch (error) {
|
||||
console.error('Error applying interpolated state:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新NetworkIdentity中的状态
|
||||
const networkIdentity = entity.getComponent(NetworkIdentity);
|
||||
if (networkIdentity && typeof networkIdentity.deserializeState === 'function') {
|
||||
try {
|
||||
networkIdentity.deserializeState(state);
|
||||
} catch (error) {
|
||||
console.error('Error deserializing interpolated state:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查组件是否实现了IInterpolatable接口
|
||||
*/
|
||||
private isInterpolatable(component: any): component is IInterpolatable {
|
||||
return component && typeof component.applyInterpolatedState === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行外推
|
||||
*/
|
||||
private performExtrapolation(entity: Entity, target: InterpolationTarget, currentTime: number): void {
|
||||
if (!this.config.enableExtrapolation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const extrapolationTime = currentTime - target.toTime;
|
||||
if (extrapolationTime > this.config.maxExtrapolationTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算外推状态
|
||||
const extrapolationProgress = extrapolationTime / (target.toTime - target.fromTime);
|
||||
const extrapolatedState = this.extrapolateState(
|
||||
target.fromState,
|
||||
target.toState,
|
||||
1 + extrapolationProgress
|
||||
);
|
||||
|
||||
// 应用外推状态
|
||||
this.applyInterpolatedState(entity, extrapolatedState);
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态外推
|
||||
*/
|
||||
private extrapolateState(fromState: NetworkValue, toState: NetworkValue, progress: number): NetworkValue {
|
||||
// 简单的线性外推
|
||||
return this.interpolateValue(fromState, toState, progress);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找插入位置
|
||||
*/
|
||||
private findInsertIndex(buffer: InterpolationSnapshot[], timestamp: number): number {
|
||||
let left = 0;
|
||||
let right = buffer.length;
|
||||
|
||||
while (left < right) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
if (buffer[mid].timestamp < timestamp) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
right = mid;
|
||||
}
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期状态
|
||||
*/
|
||||
private cleanupOldStates(): void {
|
||||
const cutoffTime = this.currentTime - this.config.maxTime;
|
||||
|
||||
this.stateBuffer.forEach((buffer, networkId) => {
|
||||
// 移除过期的状态
|
||||
const validStates = buffer.filter(snapshot => snapshot.timestamp > cutoffTime);
|
||||
|
||||
if (validStates.length !== buffer.length) {
|
||||
this.stateBuffer.set(networkId, validStates);
|
||||
}
|
||||
|
||||
// 如果缓冲区为空,移除它
|
||||
if (validStates.length === 0) {
|
||||
this.stateBuffer.delete(networkId);
|
||||
this.interpolationTargets.delete(networkId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据网络ID查找实体
|
||||
*/
|
||||
private findEntityByNetworkId(networkId: string): Entity | null {
|
||||
// 使用系统的entities属性来查找
|
||||
for (const entity of this.entities) {
|
||||
const networkIdentity = entity.getComponent(NetworkIdentity);
|
||||
if (networkIdentity && networkIdentity.networkId === networkId) {
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置插值配置
|
||||
*/
|
||||
setInterpolationConfig(config: Partial<InterpolationConfig>): void {
|
||||
this.config = { ...this.config, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插值统计信息
|
||||
*/
|
||||
getInterpolationStats(): { [networkId: string]: { bufferSize: number; progress: number } } {
|
||||
const stats: { [networkId: string]: { bufferSize: number; progress: number } } = {};
|
||||
|
||||
this.stateBuffer.forEach((buffer, networkId) => {
|
||||
const target = this.interpolationTargets.get(networkId);
|
||||
stats[networkId] = {
|
||||
bufferSize: buffer.length,
|
||||
progress: target ? target.progress : 0
|
||||
};
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有插值数据
|
||||
*/
|
||||
clearInterpolationData(): void {
|
||||
this.stateBuffer.clear();
|
||||
this.interpolationTargets.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统销毁
|
||||
*/
|
||||
onDestroy(): void {
|
||||
this.clearInterpolationData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,362 +0,0 @@
|
||||
/**
|
||||
* 客户端预测系统
|
||||
*
|
||||
* 实现客户端预测和服务器和解
|
||||
*/
|
||||
|
||||
import { EntitySystem, Entity, Matcher, Time } from '@esengine/ecs-framework';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { NetworkIdentity } from '../core/NetworkIdentity';
|
||||
import { IPredictable } from '../interfaces/NetworkInterfaces';
|
||||
|
||||
/**
|
||||
* 预测状态快照
|
||||
*/
|
||||
export interface PredictionSnapshot {
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
/** 网络ID */
|
||||
networkId: string;
|
||||
/** 状态数据 */
|
||||
state: NetworkValue;
|
||||
/** 输入数据 */
|
||||
inputs?: NetworkValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预测输入
|
||||
*/
|
||||
export interface PredictionInput {
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
/** 输入数据 */
|
||||
data: NetworkValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端预测系统
|
||||
*/
|
||||
export class PredictionSystem extends EntitySystem {
|
||||
/** 预测状态缓冲区 */
|
||||
private predictionBuffer: Map<string, PredictionSnapshot[]> = new Map();
|
||||
/** 输入缓冲区 */
|
||||
private inputBuffer: PredictionInput[] = [];
|
||||
/** 最大缓冲区大小 */
|
||||
private maxBufferSize: number = 64;
|
||||
/** 预测时间窗口(毫秒) */
|
||||
private predictionWindow: number = 500;
|
||||
/** 当前预测时间戳 */
|
||||
private currentPredictionTime: number = 0;
|
||||
|
||||
constructor(maxBufferSize = 64, predictionWindow = 500) {
|
||||
// 使用Matcher查询具有NetworkIdentity的实体
|
||||
super(Matcher.all(NetworkIdentity));
|
||||
|
||||
this.maxBufferSize = maxBufferSize;
|
||||
this.predictionWindow = predictionWindow;
|
||||
this.currentPredictionTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统初始化
|
||||
*/
|
||||
override initialize(): void {
|
||||
super.initialize();
|
||||
this.currentPredictionTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统更新
|
||||
*/
|
||||
override update(): void {
|
||||
this.currentPredictionTime = Date.now();
|
||||
this.cleanupOldSnapshots();
|
||||
|
||||
// 调用父类update,会自动调用process方法处理匹配的实体
|
||||
super.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理匹配的实体
|
||||
*/
|
||||
protected override process(entities: Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const networkIdentity = entity.getComponent(NetworkIdentity);
|
||||
|
||||
if (networkIdentity &&
|
||||
networkIdentity.isPredictionEnabled &&
|
||||
networkIdentity.isLocalPlayer) {
|
||||
|
||||
// 保存当前状态快照
|
||||
this.saveSnapshot(entity);
|
||||
|
||||
// 应用当前输入进行预测
|
||||
const currentInputs = this.getCurrentInputs();
|
||||
if (currentInputs) {
|
||||
this.applyInputs(entity, currentInputs, this.currentPredictionTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加预测输入
|
||||
*/
|
||||
addInput(input: PredictionInput): void {
|
||||
this.inputBuffer.push(input);
|
||||
|
||||
// 保持输入缓冲区大小
|
||||
if (this.inputBuffer.length > this.maxBufferSize) {
|
||||
this.inputBuffer.shift();
|
||||
}
|
||||
|
||||
// 按时间戳排序
|
||||
this.inputBuffer.sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存预测状态快照
|
||||
*/
|
||||
saveSnapshot(entity: Entity): void {
|
||||
const networkIdentity = entity.getComponent(NetworkIdentity);
|
||||
if (!networkIdentity || !networkIdentity.isPredictionEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const networkId = networkIdentity.networkId;
|
||||
const snapshot: PredictionSnapshot = {
|
||||
timestamp: this.currentPredictionTime,
|
||||
networkId,
|
||||
state: networkIdentity.serializeState(),
|
||||
inputs: this.getCurrentInputs() || undefined
|
||||
};
|
||||
|
||||
// 获取或创建缓冲区
|
||||
if (!this.predictionBuffer.has(networkId)) {
|
||||
this.predictionBuffer.set(networkId, []);
|
||||
}
|
||||
|
||||
const buffer = this.predictionBuffer.get(networkId)!;
|
||||
buffer.push(snapshot);
|
||||
|
||||
// 保持缓冲区大小
|
||||
if (buffer.length > this.maxBufferSize) {
|
||||
buffer.shift();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从服务器接收权威状态进行和解
|
||||
*/
|
||||
reconcileWithServer(networkId: string, serverState: NetworkValue, serverTimestamp: number): void {
|
||||
const buffer = this.predictionBuffer.get(networkId);
|
||||
if (!buffer || buffer.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找对应时间戳的预测状态
|
||||
const predictionSnapshot = this.findSnapshot(buffer, serverTimestamp);
|
||||
if (!predictionSnapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 比较预测状态和服务器状态
|
||||
if (this.statesMatch(predictionSnapshot.state, serverState)) {
|
||||
// 预测正确,移除已确认的快照
|
||||
this.removeSnapshotsBeforeTimestamp(buffer, serverTimestamp);
|
||||
return;
|
||||
}
|
||||
|
||||
// 预测错误,需要进行和解
|
||||
this.performReconciliation(networkId, serverState, serverTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行预测和解
|
||||
*/
|
||||
private performReconciliation(networkId: string, serverState: NetworkValue, serverTimestamp: number): void {
|
||||
const entity = this.findEntityByNetworkId(networkId);
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
const networkIdentity = entity.getComponent(NetworkIdentity);
|
||||
if (!networkIdentity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 回滚到服务器状态
|
||||
if (typeof networkIdentity.deserializeState === 'function') {
|
||||
networkIdentity.deserializeState(serverState);
|
||||
}
|
||||
|
||||
// 重新应用服务器时间戳之后的输入
|
||||
const buffer = this.predictionBuffer.get(networkId)!;
|
||||
const snapshotsToReplay = buffer.filter(snapshot => snapshot.timestamp > serverTimestamp);
|
||||
|
||||
for (const snapshot of snapshotsToReplay) {
|
||||
if (snapshot.inputs) {
|
||||
this.applyInputs(entity, snapshot.inputs, snapshot.timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理已和解的快照
|
||||
this.removeSnapshotsBeforeTimestamp(buffer, serverTimestamp);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 应用输入进行预测计算
|
||||
*/
|
||||
private applyInputs(entity: Entity, inputs: NetworkValue, timestamp: number): void {
|
||||
const networkIdentity = entity.getComponent(NetworkIdentity);
|
||||
if (!networkIdentity) return;
|
||||
|
||||
// 获取实体的所有组件并检查是否实现了IPredictable接口
|
||||
const components: any[] = [];
|
||||
for (const component of components) {
|
||||
if (this.isPredictable(component)) {
|
||||
try {
|
||||
(component as IPredictable).predictUpdate(inputs, timestamp);
|
||||
} catch (error) {
|
||||
console.error('Error applying prediction:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查组件是否实现了IPredictable接口
|
||||
*/
|
||||
private isPredictable(component: any): component is IPredictable {
|
||||
return component && typeof component.predictUpdate === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前输入
|
||||
*/
|
||||
private getCurrentInputs(): NetworkValue | null {
|
||||
if (this.inputBuffer.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取最新的输入
|
||||
return this.inputBuffer[this.inputBuffer.length - 1].data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找指定时间戳的快照
|
||||
*/
|
||||
private findSnapshot(buffer: PredictionSnapshot[], timestamp: number): PredictionSnapshot | null {
|
||||
// 查找最接近的快照
|
||||
let closest: PredictionSnapshot | null = null;
|
||||
let minDiff = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
for (const snapshot of buffer) {
|
||||
const diff = Math.abs(snapshot.timestamp - timestamp);
|
||||
if (diff < minDiff) {
|
||||
minDiff = diff;
|
||||
closest = snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个状态是否匹配
|
||||
*/
|
||||
private statesMatch(predictedState: NetworkValue, serverState: NetworkValue): boolean {
|
||||
try {
|
||||
// 简单的JSON比较,实际应用中可能需要更精确的比较
|
||||
return JSON.stringify(predictedState) === JSON.stringify(serverState);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除指定时间戳之前的快照
|
||||
*/
|
||||
private removeSnapshotsBeforeTimestamp(buffer: PredictionSnapshot[], timestamp: number): void {
|
||||
for (let i = buffer.length - 1; i >= 0; i--) {
|
||||
if (buffer[i].timestamp < timestamp) {
|
||||
buffer.splice(0, i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的快照
|
||||
*/
|
||||
private cleanupOldSnapshots(): void {
|
||||
const cutoffTime = this.currentPredictionTime - this.predictionWindow;
|
||||
|
||||
this.predictionBuffer.forEach((buffer, networkId) => {
|
||||
this.removeSnapshotsBeforeTimestamp(buffer, cutoffTime);
|
||||
|
||||
// 如果缓冲区为空,移除它
|
||||
if (buffer.length === 0) {
|
||||
this.predictionBuffer.delete(networkId);
|
||||
}
|
||||
});
|
||||
|
||||
// 清理过期的输入
|
||||
this.inputBuffer = this.inputBuffer.filter(input =>
|
||||
input.timestamp > cutoffTime
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据网络ID查找实体
|
||||
*/
|
||||
private findEntityByNetworkId(networkId: string): Entity | null {
|
||||
// 使用系统的entities属性来查找
|
||||
for (const entity of this.entities) {
|
||||
const networkIdentity = entity.getComponent(NetworkIdentity);
|
||||
if (networkIdentity && networkIdentity.networkId === networkId) {
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置预测配置
|
||||
*/
|
||||
setPredictionConfig(maxBufferSize: number, predictionWindow: number): void {
|
||||
this.maxBufferSize = maxBufferSize;
|
||||
this.predictionWindow = predictionWindow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预测统计信息
|
||||
*/
|
||||
getPredictionStats(): { [networkId: string]: number } {
|
||||
const stats: { [networkId: string]: number } = {};
|
||||
|
||||
this.predictionBuffer.forEach((buffer, networkId) => {
|
||||
stats[networkId] = buffer.length;
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有预测数据
|
||||
*/
|
||||
clearPredictionData(): void {
|
||||
this.predictionBuffer.clear();
|
||||
this.inputBuffer = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统销毁
|
||||
*/
|
||||
onDestroy(): void {
|
||||
this.clearPredictionData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* 系统导出
|
||||
*/
|
||||
|
||||
export * from './PredictionSystem';
|
||||
export * from './InterpolationSystem';
|
||||
@@ -1,445 +0,0 @@
|
||||
/**
|
||||
* 客户端传输层抽象接口
|
||||
*/
|
||||
|
||||
import { Emitter, ITimer, Core } from '@esengine/ecs-framework';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
|
||||
/**
|
||||
* 客户端传输配置
|
||||
*/
|
||||
export interface ClientTransportConfig {
|
||||
/** 服务器地址 */
|
||||
host: string;
|
||||
/** 服务器端口 */
|
||||
port: number;
|
||||
/** 是否使用安全连接 */
|
||||
secure?: boolean;
|
||||
/** 连接超时时间(毫秒) */
|
||||
connectionTimeout?: number;
|
||||
/** 重连间隔(毫秒) */
|
||||
reconnectInterval?: number;
|
||||
/** 最大重连次数 */
|
||||
maxReconnectAttempts?: number;
|
||||
/** 心跳间隔(毫秒) */
|
||||
heartbeatInterval?: number;
|
||||
/** 消息队列最大大小 */
|
||||
maxQueueSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接状态
|
||||
*/
|
||||
export enum ConnectionState {
|
||||
/** 断开连接 */
|
||||
DISCONNECTED = 'disconnected',
|
||||
/** 连接中 */
|
||||
CONNECTING = 'connecting',
|
||||
/** 已连接 */
|
||||
CONNECTED = 'connected',
|
||||
/** 认证中 */
|
||||
AUTHENTICATING = 'authenticating',
|
||||
/** 已认证 */
|
||||
AUTHENTICATED = 'authenticated',
|
||||
/** 重连中 */
|
||||
RECONNECTING = 'reconnecting',
|
||||
/** 连接错误 */
|
||||
ERROR = 'error'
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端消息
|
||||
*/
|
||||
export interface ClientMessage {
|
||||
/** 消息类型 */
|
||||
type: 'rpc' | 'syncvar' | 'system' | 'custom';
|
||||
/** 消息数据 */
|
||||
data: NetworkValue;
|
||||
/** 消息ID(用于响应匹配) */
|
||||
messageId?: string;
|
||||
/** 是否可靠传输 */
|
||||
reliable?: boolean;
|
||||
/** 时间戳 */
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接统计信息
|
||||
*/
|
||||
export interface ConnectionStats {
|
||||
/** 连接时间 */
|
||||
connectedAt: Date | null;
|
||||
/** 连接持续时间(毫秒) */
|
||||
connectionDuration: number;
|
||||
/** 发送消息数 */
|
||||
messagesSent: number;
|
||||
/** 接收消息数 */
|
||||
messagesReceived: number;
|
||||
/** 发送字节数 */
|
||||
bytesSent: number;
|
||||
/** 接收字节数 */
|
||||
bytesReceived: number;
|
||||
/** 重连次数 */
|
||||
reconnectCount: number;
|
||||
/** 丢失消息数 */
|
||||
messagesLost: number;
|
||||
/** 平均延迟(毫秒) */
|
||||
averageLatency: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端传输事件
|
||||
*/
|
||||
export interface ClientTransportEvents {
|
||||
/** 连接建立 */
|
||||
'connected': () => void;
|
||||
/** 连接断开 */
|
||||
'disconnected': (reason: string) => void;
|
||||
/** 连接状态变化 */
|
||||
'state-changed': (oldState: ConnectionState, newState: ConnectionState) => void;
|
||||
/** 收到消息 */
|
||||
'message': (message: ClientMessage) => void;
|
||||
/** 连接错误 */
|
||||
'error': (error: Error) => void;
|
||||
/** 重连开始 */
|
||||
'reconnecting': (attempt: number, maxAttempts: number) => void;
|
||||
/** 重连成功 */
|
||||
'reconnected': () => void;
|
||||
/** 重连失败 */
|
||||
'reconnect-failed': () => void;
|
||||
/** 延迟更新 */
|
||||
'latency-updated': (latency: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端传输层抽象类
|
||||
*/
|
||||
export abstract class ClientTransport {
|
||||
protected config: ClientTransportConfig;
|
||||
protected state: ConnectionState = ConnectionState.DISCONNECTED;
|
||||
protected stats: ConnectionStats;
|
||||
protected messageQueue: ClientMessage[] = [];
|
||||
protected reconnectAttempts = 0;
|
||||
protected reconnectTimer: ITimer<any> | null = null;
|
||||
protected heartbeatTimer: ITimer<any> | null = null;
|
||||
private latencyMeasurements: number[] = [];
|
||||
private eventEmitter: Emitter<keyof ClientTransportEvents, any>;
|
||||
|
||||
constructor(config: ClientTransportConfig) {
|
||||
this.eventEmitter = new Emitter<keyof ClientTransportEvents, any>();
|
||||
|
||||
this.config = {
|
||||
secure: false,
|
||||
connectionTimeout: 10000, // 10秒
|
||||
reconnectInterval: 3000, // 3秒
|
||||
maxReconnectAttempts: 10,
|
||||
heartbeatInterval: 30000, // 30秒
|
||||
maxQueueSize: 1000,
|
||||
...config
|
||||
};
|
||||
|
||||
this.stats = {
|
||||
connectedAt: null,
|
||||
connectionDuration: 0,
|
||||
messagesSent: 0,
|
||||
messagesReceived: 0,
|
||||
bytesSent: 0,
|
||||
bytesReceived: 0,
|
||||
reconnectCount: 0,
|
||||
messagesLost: 0,
|
||||
averageLatency: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到服务器
|
||||
*/
|
||||
abstract connect(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
abstract disconnect(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
abstract sendMessage(message: ClientMessage): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 获取当前连接状态
|
||||
*/
|
||||
getState(): ConnectionState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已连接
|
||||
*/
|
||||
isConnected(): boolean {
|
||||
return this.state === ConnectionState.CONNECTED ||
|
||||
this.state === ConnectionState.AUTHENTICATED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接统计信息
|
||||
*/
|
||||
getStats(): ConnectionStats {
|
||||
if (this.stats.connectedAt) {
|
||||
this.stats.connectionDuration = Date.now() - this.stats.connectedAt.getTime();
|
||||
}
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置
|
||||
*/
|
||||
getConfig(): Readonly<ClientTransportConfig> {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置状态
|
||||
*/
|
||||
protected setState(newState: ConnectionState): void {
|
||||
if (this.state !== newState) {
|
||||
const oldState = this.state;
|
||||
this.state = newState;
|
||||
this.eventEmitter.emit('state-changed', oldState, newState);
|
||||
|
||||
// 特殊状态处理
|
||||
if (newState === ConnectionState.CONNECTED) {
|
||||
this.stats.connectedAt = new Date();
|
||||
this.reconnectAttempts = 0;
|
||||
this.startHeartbeat();
|
||||
this.processMessageQueue();
|
||||
this.eventEmitter.emit('connected');
|
||||
|
||||
if (oldState === ConnectionState.RECONNECTING) {
|
||||
this.eventEmitter.emit('reconnected');
|
||||
}
|
||||
} else if (newState === ConnectionState.DISCONNECTED) {
|
||||
this.stats.connectedAt = null;
|
||||
this.stopHeartbeat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收到的消息
|
||||
*/
|
||||
protected handleMessage(message: ClientMessage): void {
|
||||
this.stats.messagesReceived++;
|
||||
|
||||
if (message.data) {
|
||||
try {
|
||||
const messageSize = JSON.stringify(message.data).length;
|
||||
this.stats.bytesReceived += messageSize;
|
||||
} catch (error) {
|
||||
// 忽略序列化错误
|
||||
}
|
||||
}
|
||||
|
||||
// 处理系统消息
|
||||
if (message.type === 'system') {
|
||||
this.handleSystemMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
this.eventEmitter.emit('message', message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理系统消息
|
||||
*/
|
||||
protected handleSystemMessage(message: ClientMessage): void {
|
||||
const data = message.data as any;
|
||||
|
||||
switch (data.action) {
|
||||
case 'ping':
|
||||
// 响应ping
|
||||
this.sendMessage({
|
||||
type: 'system',
|
||||
data: { action: 'pong', timestamp: data.timestamp }
|
||||
});
|
||||
break;
|
||||
|
||||
case 'pong':
|
||||
// 计算延迟
|
||||
if (data.timestamp) {
|
||||
const latency = Date.now() - data.timestamp;
|
||||
this.updateLatency(latency);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理连接错误
|
||||
*/
|
||||
protected handleError(error: Error): void {
|
||||
console.error('Transport error:', error.message);
|
||||
this.eventEmitter.emit('error', error);
|
||||
|
||||
if (this.isConnected()) {
|
||||
this.setState(ConnectionState.ERROR);
|
||||
this.startReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始重连
|
||||
*/
|
||||
protected startReconnect(): void {
|
||||
if (this.reconnectAttempts >= this.config.maxReconnectAttempts!) {
|
||||
this.eventEmitter.emit('reconnect-failed');
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(ConnectionState.RECONNECTING);
|
||||
this.reconnectAttempts++;
|
||||
this.stats.reconnectCount++;
|
||||
|
||||
this.eventEmitter.emit('reconnecting', this.reconnectAttempts, this.config.maxReconnectAttempts!);
|
||||
|
||||
this.reconnectTimer = Core.schedule(this.config.reconnectInterval! / 1000, false, this, async () => {
|
||||
try {
|
||||
await this.connect();
|
||||
} catch (error) {
|
||||
this.startReconnect(); // 继续重连
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止重连
|
||||
*/
|
||||
protected stopReconnect(): void {
|
||||
if (this.reconnectTimer) {
|
||||
this.reconnectTimer.stop();
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将消息加入队列
|
||||
*/
|
||||
protected queueMessage(message: ClientMessage): boolean {
|
||||
if (this.messageQueue.length >= this.config.maxQueueSize!) {
|
||||
this.stats.messagesLost++;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.messageQueue.push(message);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消息队列
|
||||
*/
|
||||
protected async processMessageQueue(): Promise<void> {
|
||||
while (this.messageQueue.length > 0 && this.isConnected()) {
|
||||
const message = this.messageQueue.shift()!;
|
||||
await this.sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始心跳
|
||||
*/
|
||||
protected startHeartbeat(): void {
|
||||
if (this.config.heartbeatInterval && this.config.heartbeatInterval > 0) {
|
||||
this.heartbeatTimer = Core.schedule(this.config.heartbeatInterval / 1000, true, this, () => {
|
||||
this.sendHeartbeat();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止心跳
|
||||
*/
|
||||
protected stopHeartbeat(): void {
|
||||
if (this.heartbeatTimer) {
|
||||
this.heartbeatTimer.stop();
|
||||
this.heartbeatTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送心跳
|
||||
*/
|
||||
protected sendHeartbeat(): void {
|
||||
this.sendMessage({
|
||||
type: 'system',
|
||||
data: { action: 'ping', timestamp: Date.now() }
|
||||
}).catch(() => {
|
||||
// 心跳发送失败,可能连接有问题
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新延迟统计
|
||||
*/
|
||||
protected updateLatency(latency: number): void {
|
||||
this.latencyMeasurements.push(latency);
|
||||
|
||||
// 只保留最近的10个测量值
|
||||
if (this.latencyMeasurements.length > 10) {
|
||||
this.latencyMeasurements.shift();
|
||||
}
|
||||
|
||||
// 计算平均延迟
|
||||
const sum = this.latencyMeasurements.reduce((a, b) => a + b, 0);
|
||||
this.stats.averageLatency = sum / this.latencyMeasurements.length;
|
||||
|
||||
this.eventEmitter.emit('latency-updated', latency);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新发送统计
|
||||
*/
|
||||
protected updateSendStats(message: ClientMessage): void {
|
||||
this.stats.messagesSent++;
|
||||
|
||||
if (message.data) {
|
||||
try {
|
||||
const messageSize = JSON.stringify(message.data).length;
|
||||
this.stats.bytesSent += messageSize;
|
||||
} catch (error) {
|
||||
// 忽略序列化错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁传输层
|
||||
*/
|
||||
destroy(): void {
|
||||
this.stopReconnect();
|
||||
this.stopHeartbeat();
|
||||
this.messageQueue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
on<K extends keyof ClientTransportEvents>(event: K, listener: ClientTransportEvents[K]): this {
|
||||
this.eventEmitter.addObserver(event, listener, this);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件监听
|
||||
*/
|
||||
off<K extends keyof ClientTransportEvents>(event: K, listener: ClientTransportEvents[K]): this {
|
||||
this.eventEmitter.removeObserver(event, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
emit<K extends keyof ClientTransportEvents>(event: K, ...args: Parameters<ClientTransportEvents[K]>): void {
|
||||
this.eventEmitter.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,427 +0,0 @@
|
||||
/**
|
||||
* HTTP 客户端传输实现
|
||||
*
|
||||
* 支持 REST API 和长轮询
|
||||
*/
|
||||
|
||||
import { Core, ITimer } from '@esengine/ecs-framework';
|
||||
import {
|
||||
ClientTransport,
|
||||
ClientTransportConfig,
|
||||
ConnectionState,
|
||||
ClientMessage
|
||||
} from './ClientTransport';
|
||||
|
||||
/**
|
||||
* HTTP 客户端配置
|
||||
*/
|
||||
export interface HttpClientConfig extends ClientTransportConfig {
|
||||
/** API 路径前缀 */
|
||||
apiPrefix?: string;
|
||||
/** 请求超时时间(毫秒) */
|
||||
requestTimeout?: number;
|
||||
/** 长轮询超时时间(毫秒) */
|
||||
longPollTimeout?: number;
|
||||
/** 是否启用长轮询 */
|
||||
enableLongPolling?: boolean;
|
||||
/** 额外的请求头 */
|
||||
headers?: Record<string, string>;
|
||||
/** 认证令牌 */
|
||||
authToken?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 响应接口
|
||||
*/
|
||||
interface HttpResponse {
|
||||
success: boolean;
|
||||
data?: any;
|
||||
error?: string;
|
||||
messages?: ClientMessage[];
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 客户端传输
|
||||
*/
|
||||
export class HttpClientTransport extends ClientTransport {
|
||||
private connectionId: string | null = null;
|
||||
private longPollController: AbortController | null = null;
|
||||
private longPollRunning = false;
|
||||
private connectPromise: Promise<void> | null = null;
|
||||
private requestTimers: Set<ITimer<any>> = new Set();
|
||||
|
||||
protected override config: HttpClientConfig;
|
||||
|
||||
constructor(config: HttpClientConfig) {
|
||||
super(config);
|
||||
|
||||
this.config = {
|
||||
apiPrefix: '/api',
|
||||
requestTimeout: 30000, // 30秒
|
||||
longPollTimeout: 25000, // 25秒
|
||||
enableLongPolling: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到服务器
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (this.state === ConnectionState.CONNECTING ||
|
||||
this.state === ConnectionState.CONNECTED) {
|
||||
return this.connectPromise || Promise.resolve();
|
||||
}
|
||||
|
||||
this.setState(ConnectionState.CONNECTING);
|
||||
this.stopReconnect();
|
||||
|
||||
this.connectPromise = this.performConnect();
|
||||
return this.connectPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行连接
|
||||
*/
|
||||
private async performConnect(): Promise<void> {
|
||||
try {
|
||||
// 发送连接请求
|
||||
const response = await this.makeRequest('/connect', 'POST', {});
|
||||
|
||||
if (response.success && response.data.connectionId) {
|
||||
this.connectionId = response.data.connectionId;
|
||||
this.setState(ConnectionState.CONNECTED);
|
||||
|
||||
// 启动长轮询
|
||||
if (this.config.enableLongPolling) {
|
||||
this.startLongPolling();
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.error || 'Connection failed');
|
||||
}
|
||||
} catch (error) {
|
||||
this.setState(ConnectionState.ERROR);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
this.stopReconnect();
|
||||
this.stopLongPolling();
|
||||
|
||||
if (this.connectionId) {
|
||||
try {
|
||||
await this.makeRequest('/disconnect', 'POST', {
|
||||
connectionId: this.connectionId
|
||||
});
|
||||
} catch (error) {
|
||||
// 忽略断开连接时的错误
|
||||
}
|
||||
|
||||
this.connectionId = null;
|
||||
}
|
||||
|
||||
this.setState(ConnectionState.DISCONNECTED);
|
||||
this.connectPromise = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
async sendMessage(message: ClientMessage): Promise<boolean> {
|
||||
if (!this.connectionId) {
|
||||
// 如果未连接,将消息加入队列
|
||||
if (this.state === ConnectionState.CONNECTING ||
|
||||
this.state === ConnectionState.RECONNECTING) {
|
||||
return this.queueMessage(message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.makeRequest('/send', 'POST', {
|
||||
connectionId: this.connectionId,
|
||||
message: {
|
||||
...message,
|
||||
timestamp: message.timestamp || Date.now()
|
||||
}
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
this.updateSendStats(message);
|
||||
return true;
|
||||
} else {
|
||||
console.error('Send message failed:', response.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.handleError(error as Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动长轮询
|
||||
*/
|
||||
private startLongPolling(): void {
|
||||
if (this.longPollRunning || !this.connectionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.longPollRunning = true;
|
||||
this.performLongPoll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止长轮询
|
||||
*/
|
||||
private stopLongPolling(): void {
|
||||
this.longPollRunning = false;
|
||||
|
||||
if (this.longPollController) {
|
||||
this.longPollController.abort();
|
||||
this.longPollController = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行长轮询
|
||||
*/
|
||||
private async performLongPoll(): Promise<void> {
|
||||
while (this.longPollRunning && this.connectionId) {
|
||||
try {
|
||||
this.longPollController = new AbortController();
|
||||
|
||||
const response = await this.makeRequest('/poll', 'GET', {
|
||||
connectionId: this.connectionId
|
||||
}, {
|
||||
signal: this.longPollController.signal,
|
||||
timeout: this.config.longPollTimeout
|
||||
});
|
||||
|
||||
if (response.success && response.messages && response.messages.length > 0) {
|
||||
// 处理接收到的消息
|
||||
for (const message of response.messages) {
|
||||
this.handleMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果服务器指示断开连接
|
||||
if (response.data && response.data.disconnected) {
|
||||
this.handleServerDisconnect();
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if ((error as any).name === 'AbortError') {
|
||||
// 被主动取消,正常情况
|
||||
break;
|
||||
}
|
||||
|
||||
console.warn('Long polling error:', (error as Error).message);
|
||||
|
||||
// 如果是网络错误,尝试重连
|
||||
if (this.isNetworkError(error as Error)) {
|
||||
this.handleError(error as Error);
|
||||
break;
|
||||
}
|
||||
|
||||
// 短暂等待后重试
|
||||
await this.delay(1000);
|
||||
}
|
||||
|
||||
this.longPollController = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理服务器主动断开连接
|
||||
*/
|
||||
private handleServerDisconnect(): void {
|
||||
this.connectionId = null;
|
||||
this.stopLongPolling();
|
||||
this.emit('disconnected', 'Server disconnect');
|
||||
|
||||
if (this.reconnectAttempts < this.config.maxReconnectAttempts!) {
|
||||
this.startReconnect();
|
||||
} else {
|
||||
this.setState(ConnectionState.DISCONNECTED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 HTTP 请求
|
||||
*/
|
||||
private async makeRequest(
|
||||
path: string,
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
||||
data?: any,
|
||||
options: {
|
||||
signal?: AbortSignal;
|
||||
timeout?: number;
|
||||
} = {}
|
||||
): Promise<HttpResponse> {
|
||||
const url = this.buildUrl(path);
|
||||
const headers = this.buildHeaders();
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
method,
|
||||
headers,
|
||||
signal: options.signal
|
||||
};
|
||||
|
||||
// 添加请求体
|
||||
if (method !== 'GET' && data) {
|
||||
requestOptions.body = JSON.stringify(data);
|
||||
} else if (method === 'GET' && data) {
|
||||
// GET 请求将数据作为查询参数
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
params.append(key, String(value));
|
||||
});
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
return this.fetchWithTimeout(`${url}${separator}${params}`, requestOptions, options.timeout);
|
||||
}
|
||||
|
||||
return this.fetchWithTimeout(url, requestOptions, options.timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带超时的 fetch 请求
|
||||
*/
|
||||
private async fetchWithTimeout(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
timeout?: number
|
||||
): Promise<HttpResponse> {
|
||||
const actualTimeout = timeout || this.config.requestTimeout!;
|
||||
|
||||
const controller = new AbortController();
|
||||
let timeoutTimer: ITimer<any> | null = null;
|
||||
|
||||
// 创建超时定时器
|
||||
timeoutTimer = Core.schedule(actualTimeout / 1000, false, this, () => {
|
||||
controller.abort();
|
||||
if (timeoutTimer) {
|
||||
this.requestTimers.delete(timeoutTimer);
|
||||
}
|
||||
});
|
||||
|
||||
this.requestTimers.add(timeoutTimer);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
signal: options.signal || controller.signal
|
||||
});
|
||||
|
||||
// 清理定时器
|
||||
if (timeoutTimer) {
|
||||
timeoutTimer.stop();
|
||||
this.requestTimers.delete(timeoutTimer);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result as HttpResponse;
|
||||
|
||||
} catch (error) {
|
||||
// 清理定时器
|
||||
if (timeoutTimer) {
|
||||
timeoutTimer.stop();
|
||||
this.requestTimers.delete(timeoutTimer);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建请求URL
|
||||
*/
|
||||
private buildUrl(path: string): string {
|
||||
const protocol = this.config.secure ? 'https' : 'http';
|
||||
const basePath = this.config.apiPrefix || '';
|
||||
const cleanPath = path.startsWith('/') ? path : `/${path}`;
|
||||
|
||||
return `${protocol}://${this.config.host}:${this.config.port}${basePath}${cleanPath}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建请求头
|
||||
*/
|
||||
private buildHeaders(): Record<string, string> {
|
||||
const headers = { ...this.config.headers };
|
||||
|
||||
if (this.config.authToken) {
|
||||
headers['Authorization'] = `Bearer ${this.config.authToken}`;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为网络错误
|
||||
*/
|
||||
private isNetworkError(error: Error): boolean {
|
||||
return error.message.includes('fetch') ||
|
||||
error.message.includes('network') ||
|
||||
error.message.includes('timeout') ||
|
||||
error.name === 'TypeError';
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟函数
|
||||
*/
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
const timer = Core.schedule(ms / 1000, false, this, () => {
|
||||
this.requestTimers.delete(timer);
|
||||
resolve();
|
||||
});
|
||||
this.requestTimers.add(timer);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置认证令牌
|
||||
*/
|
||||
setAuthToken(token: string): void {
|
||||
this.config.authToken = token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接ID
|
||||
*/
|
||||
getConnectionId(): string | null {
|
||||
return this.connectionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持 Fetch API
|
||||
*/
|
||||
static isSupported(): boolean {
|
||||
return typeof fetch !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁传输层
|
||||
*/
|
||||
override destroy(): void {
|
||||
// 清理所有请求定时器
|
||||
this.requestTimers.forEach(timer => timer.stop());
|
||||
this.requestTimers.clear();
|
||||
|
||||
this.disconnect();
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
/**
|
||||
* WebSocket 客户端传输实现
|
||||
*/
|
||||
|
||||
import { Core, ITimer } from '@esengine/ecs-framework';
|
||||
import {
|
||||
ClientTransport,
|
||||
ClientTransportConfig,
|
||||
ConnectionState,
|
||||
ClientMessage
|
||||
} from './ClientTransport';
|
||||
|
||||
/**
|
||||
* WebSocket 客户端配置
|
||||
*/
|
||||
export interface WebSocketClientConfig extends ClientTransportConfig {
|
||||
/** WebSocket 路径 */
|
||||
path?: string;
|
||||
/** 协议列表 */
|
||||
protocols?: string | string[];
|
||||
/** 额外的请求头 */
|
||||
headers?: Record<string, string>;
|
||||
/** 是否启用二进制消息 */
|
||||
binaryType?: 'blob' | 'arraybuffer';
|
||||
/** WebSocket 扩展 */
|
||||
extensions?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 客户端传输
|
||||
*/
|
||||
export class WebSocketClientTransport extends ClientTransport {
|
||||
private websocket: WebSocket | null = null;
|
||||
private connectionPromise: Promise<void> | null = null;
|
||||
private connectionTimeoutTimer: ITimer<any> | null = null;
|
||||
|
||||
protected override config: WebSocketClientConfig;
|
||||
|
||||
constructor(config: WebSocketClientConfig) {
|
||||
super(config);
|
||||
|
||||
this.config = {
|
||||
path: '/ws',
|
||||
protocols: [],
|
||||
headers: {},
|
||||
binaryType: 'arraybuffer',
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到服务器
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (this.state === ConnectionState.CONNECTING ||
|
||||
this.state === ConnectionState.CONNECTED) {
|
||||
return this.connectionPromise || Promise.resolve();
|
||||
}
|
||||
|
||||
this.setState(ConnectionState.CONNECTING);
|
||||
this.stopReconnect(); // 停止任何正在进行的重连
|
||||
|
||||
this.connectionPromise = new Promise((resolve, reject) => {
|
||||
try {
|
||||
// 构建WebSocket URL
|
||||
const protocol = this.config.secure ? 'wss' : 'ws';
|
||||
const url = `${protocol}://${this.config.host}:${this.config.port}${this.config.path}`;
|
||||
|
||||
// 创建WebSocket连接
|
||||
this.websocket = new WebSocket(url, this.config.protocols);
|
||||
|
||||
if (this.config.binaryType) {
|
||||
this.websocket.binaryType = this.config.binaryType;
|
||||
}
|
||||
|
||||
// 设置连接超时
|
||||
this.connectionTimeoutTimer = Core.schedule(this.config.connectionTimeout! / 1000, false, this, () => {
|
||||
if (this.websocket && this.websocket.readyState === WebSocket.CONNECTING) {
|
||||
this.websocket.close();
|
||||
reject(new Error('Connection timeout'));
|
||||
}
|
||||
});
|
||||
|
||||
// WebSocket 事件处理
|
||||
this.websocket.onopen = () => {
|
||||
if (this.connectionTimeoutTimer) {
|
||||
this.connectionTimeoutTimer.stop();
|
||||
this.connectionTimeoutTimer = null;
|
||||
}
|
||||
this.setState(ConnectionState.CONNECTED);
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.websocket.onclose = (event) => {
|
||||
if (this.connectionTimeoutTimer) {
|
||||
this.connectionTimeoutTimer.stop();
|
||||
this.connectionTimeoutTimer = null;
|
||||
}
|
||||
this.handleClose(event.code, event.reason);
|
||||
|
||||
if (this.state === ConnectionState.CONNECTING) {
|
||||
reject(new Error(`Connection failed: ${event.reason || 'Unknown error'}`));
|
||||
}
|
||||
};
|
||||
|
||||
this.websocket.onerror = (event) => {
|
||||
if (this.connectionTimeoutTimer) {
|
||||
this.connectionTimeoutTimer.stop();
|
||||
this.connectionTimeoutTimer = null;
|
||||
}
|
||||
const error = new Error('WebSocket error');
|
||||
this.handleError(error);
|
||||
|
||||
if (this.state === ConnectionState.CONNECTING) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
this.websocket.onmessage = (event) => {
|
||||
this.handleWebSocketMessage(event);
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.setState(ConnectionState.ERROR);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
this.stopReconnect();
|
||||
|
||||
if (this.websocket) {
|
||||
// 设置状态为断开连接,避免触发重连
|
||||
this.setState(ConnectionState.DISCONNECTED);
|
||||
|
||||
if (this.websocket.readyState === WebSocket.OPEN ||
|
||||
this.websocket.readyState === WebSocket.CONNECTING) {
|
||||
this.websocket.close(1000, 'Client disconnect');
|
||||
}
|
||||
|
||||
this.websocket = null;
|
||||
}
|
||||
|
||||
this.connectionPromise = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
async sendMessage(message: ClientMessage): Promise<boolean> {
|
||||
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
|
||||
// 如果未连接,将消息加入队列
|
||||
if (this.state === ConnectionState.CONNECTING ||
|
||||
this.state === ConnectionState.RECONNECTING) {
|
||||
return this.queueMessage(message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 序列化消息
|
||||
const serialized = JSON.stringify({
|
||||
...message,
|
||||
timestamp: message.timestamp || Date.now()
|
||||
});
|
||||
|
||||
// 发送消息
|
||||
this.websocket.send(serialized);
|
||||
this.updateSendStats(message);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
this.handleError(error as Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 WebSocket 消息
|
||||
*/
|
||||
private handleWebSocketMessage(event: MessageEvent): void {
|
||||
try {
|
||||
let data: string;
|
||||
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
// 处理二进制数据
|
||||
data = new TextDecoder().decode(event.data);
|
||||
} else if (event.data instanceof Blob) {
|
||||
// Blob 需要异步处理
|
||||
event.data.text().then(text => {
|
||||
this.processMessage(text);
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
// 字符串数据
|
||||
data = event.data;
|
||||
}
|
||||
|
||||
this.processMessage(data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing WebSocket message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消息内容
|
||||
*/
|
||||
private processMessage(data: string): void {
|
||||
try {
|
||||
const message: ClientMessage = JSON.parse(data);
|
||||
this.handleMessage(message);
|
||||
} catch (error) {
|
||||
console.error('Error parsing message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理连接关闭
|
||||
*/
|
||||
private handleClose(code: number, reason: string): void {
|
||||
this.websocket = null;
|
||||
this.connectionPromise = null;
|
||||
|
||||
const wasConnected = this.isConnected();
|
||||
|
||||
// 根据关闭代码决定是否重连
|
||||
if (code === 1000) {
|
||||
// 正常关闭,不重连
|
||||
this.setState(ConnectionState.DISCONNECTED);
|
||||
this.emit('disconnected', reason || 'Normal closure');
|
||||
} else if (wasConnected && this.reconnectAttempts < this.config.maxReconnectAttempts!) {
|
||||
// 异常关闭,尝试重连
|
||||
this.emit('disconnected', reason || `Abnormal closure (${code})`);
|
||||
this.startReconnect();
|
||||
} else {
|
||||
// 达到最大重连次数或其他情况
|
||||
this.setState(ConnectionState.DISCONNECTED);
|
||||
this.emit('disconnected', reason || `Connection lost (${code})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebSocket 就绪状态
|
||||
*/
|
||||
getReadyState(): number {
|
||||
return this.websocket?.readyState ?? WebSocket.CLOSED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebSocket 实例
|
||||
*/
|
||||
getWebSocket(): WebSocket | null {
|
||||
return this.websocket;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持 WebSocket
|
||||
*/
|
||||
static isSupported(): boolean {
|
||||
return typeof WebSocket !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁传输层
|
||||
*/
|
||||
override destroy(): void {
|
||||
if (this.connectionTimeoutTimer) {
|
||||
this.connectionTimeoutTimer.stop();
|
||||
this.connectionTimeoutTimer = null;
|
||||
}
|
||||
this.disconnect();
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* 传输层导出
|
||||
*/
|
||||
|
||||
export * from './ClientTransport';
|
||||
export * from './WebSocketClientTransport';
|
||||
export * from './HttpClientTransport';
|
||||
@@ -1,384 +0,0 @@
|
||||
/**
|
||||
* NetworkClient 集成测试
|
||||
* 测试网络客户端的完整功能,包括依赖注入和错误处理
|
||||
*/
|
||||
|
||||
import { NetworkClient } from '../src/core/NetworkClient';
|
||||
|
||||
// Mock 所有外部依赖
|
||||
jest.mock('@esengine/ecs-framework', () => ({
|
||||
Core: {
|
||||
scene: null,
|
||||
schedule: {
|
||||
scheduleRepeating: jest.fn((callback: Function, interval: number) => ({
|
||||
stop: jest.fn()
|
||||
}))
|
||||
}
|
||||
},
|
||||
Emitter: jest.fn().mockImplementation(() => ({
|
||||
emit: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
removeAllListeners: jest.fn()
|
||||
}))
|
||||
}));
|
||||
|
||||
jest.mock('@esengine/ecs-framework-network-shared', () => ({
|
||||
NetworkValue: {},
|
||||
generateMessageId: jest.fn(() => 'test-message-id-123'),
|
||||
generateNetworkId: jest.fn(() => 12345),
|
||||
NetworkUtils: {
|
||||
generateMessageId: jest.fn(() => 'test-message-id-456'),
|
||||
calculateDistance: jest.fn(() => 100),
|
||||
isNodeEnvironment: jest.fn(() => false),
|
||||
isBrowserEnvironment: jest.fn(() => true)
|
||||
}
|
||||
}));
|
||||
|
||||
// Mock WebSocket
|
||||
class MockWebSocket {
|
||||
public readyState: number = WebSocket.CONNECTING;
|
||||
public onopen: ((event: Event) => void) | null = null;
|
||||
public onclose: ((event: CloseEvent) => void) | null = null;
|
||||
public onmessage: ((event: MessageEvent) => void) | null = null;
|
||||
public onerror: ((event: Event) => void) | null = null;
|
||||
|
||||
constructor(public url: string, public protocols?: string | string[]) {}
|
||||
|
||||
send(data: string | ArrayBuffer | Blob): void {}
|
||||
close(code?: number, reason?: string): void {
|
||||
this.readyState = WebSocket.CLOSED;
|
||||
if (this.onclose) {
|
||||
this.onclose(new CloseEvent('close', { code: code || 1000, reason: reason || '' }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(global as any).WebSocket = MockWebSocket;
|
||||
(global as any).WebSocket.CONNECTING = 0;
|
||||
(global as any).WebSocket.OPEN = 1;
|
||||
(global as any).WebSocket.CLOSING = 2;
|
||||
(global as any).WebSocket.CLOSED = 3;
|
||||
|
||||
describe('NetworkClient 集成测试', () => {
|
||||
let client: NetworkClient;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (client) {
|
||||
client.disconnect().catch(() => {});
|
||||
client = null as any;
|
||||
}
|
||||
});
|
||||
|
||||
describe('依赖注入测试', () => {
|
||||
it('应该正确处理所有依赖模块', () => {
|
||||
expect(() => {
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
}).not.toThrow();
|
||||
|
||||
expect(client).toBeInstanceOf(NetworkClient);
|
||||
});
|
||||
|
||||
it('应该正确使用network-shared中的工具函数', () => {
|
||||
const { generateMessageId, NetworkUtils } = require('@esengine/ecs-framework-network-shared');
|
||||
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
|
||||
// 验证network-shared模块被正确导入
|
||||
expect(generateMessageId).toBeDefined();
|
||||
expect(NetworkUtils).toBeDefined();
|
||||
});
|
||||
|
||||
it('应该正确使用ecs-framework中的Core模块', () => {
|
||||
const { Core } = require('@esengine/ecs-framework');
|
||||
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
|
||||
expect(Core).toBeDefined();
|
||||
expect(Core.schedule).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('构造函数错误处理', () => {
|
||||
it('应该处理network-shared模块导入失败', () => {
|
||||
// 重置模块并模拟导入失败
|
||||
jest.resetModules();
|
||||
jest.doMock('@esengine/ecs-framework-network-shared', () => {
|
||||
throw new Error('network-shared模块导入失败');
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
const { NetworkClient } = require('../src/core/NetworkClient');
|
||||
new NetworkClient({
|
||||
transportType: 'websocket',
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('应该处理ecs-framework模块导入失败', () => {
|
||||
// 重置模块并模拟导入失败
|
||||
jest.resetModules();
|
||||
jest.doMock('@esengine/ecs-framework', () => {
|
||||
throw new Error('ecs-framework模块导入失败');
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
const { NetworkClient } = require('../src/core/NetworkClient');
|
||||
new NetworkClient({
|
||||
transportType: 'websocket',
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('应该处理传输层构造失败', () => {
|
||||
// Mock传输层构造函数抛出异常
|
||||
const originalWebSocket = (global as any).WebSocket;
|
||||
(global as any).WebSocket = jest.fn(() => {
|
||||
throw new Error('WebSocket不可用');
|
||||
});
|
||||
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
|
||||
expect(client.connect()).rejects.toThrow();
|
||||
|
||||
// 恢复原始WebSocket
|
||||
(global as any).WebSocket = originalWebSocket;
|
||||
});
|
||||
});
|
||||
|
||||
describe('功能测试', () => {
|
||||
beforeEach(() => {
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('应该能够成功连接', async () => {
|
||||
const connectPromise = client.connect();
|
||||
|
||||
// 模拟连接成功
|
||||
setTimeout(() => {
|
||||
const transport = (client as any).transport;
|
||||
if (transport && transport.websocket && transport.websocket.onopen) {
|
||||
transport.websocket.readyState = WebSocket.OPEN;
|
||||
transport.websocket.onopen(new Event('open'));
|
||||
}
|
||||
}, 0);
|
||||
|
||||
await expect(connectPromise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该能够发送消息', async () => {
|
||||
// 先连接
|
||||
const connectPromise = client.connect();
|
||||
setTimeout(() => {
|
||||
const transport = (client as any).transport;
|
||||
if (transport && transport.websocket && transport.websocket.onopen) {
|
||||
transport.websocket.readyState = WebSocket.OPEN;
|
||||
transport.websocket.onopen(new Event('open'));
|
||||
}
|
||||
}, 0);
|
||||
await connectPromise;
|
||||
|
||||
// 发送消息
|
||||
const message = {
|
||||
type: 'custom' as const,
|
||||
data: { test: 'message' },
|
||||
reliable: true
|
||||
};
|
||||
|
||||
// NetworkClient没有直接的sendMessage方法,它通过RPC调用
|
||||
});
|
||||
|
||||
it('应该能够正确断开连接', async () => {
|
||||
await expect(client.disconnect()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该返回正确的认证状态', () => {
|
||||
expect(client.isAuthenticated()).toBe(false);
|
||||
});
|
||||
|
||||
it('应该能够获取网络对象列表', () => {
|
||||
const networkObjects = client.getAllNetworkObjects();
|
||||
expect(Array.isArray(networkObjects)).toBe(true);
|
||||
expect(networkObjects.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('消息ID生成测试', () => {
|
||||
beforeEach(() => {
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('应该能够生成唯一的消息ID', () => {
|
||||
const messageId1 = (client as any).generateMessageId();
|
||||
const messageId2 = (client as any).generateMessageId();
|
||||
|
||||
expect(typeof messageId1).toBe('string');
|
||||
expect(typeof messageId2).toBe('string');
|
||||
expect(messageId1).not.toBe(messageId2);
|
||||
});
|
||||
|
||||
it('生成的消息ID应该符合预期格式', () => {
|
||||
const messageId = (client as any).generateMessageId();
|
||||
|
||||
// 检查消息ID格式(时间戳 + 随机字符串)
|
||||
expect(messageId).toMatch(/^[a-z0-9]+$/);
|
||||
expect(messageId.length).toBeGreaterThan(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误恢复测试', () => {
|
||||
beforeEach(() => {
|
||||
client = new NetworkClient({
|
||||
transportType: 'websocket',
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
maxReconnectAttempts: 2,
|
||||
reconnectInterval: 100
|
||||
});
|
||||
});
|
||||
|
||||
it('连接失败后应该尝试重连', async () => {
|
||||
let connectAttempts = 0;
|
||||
const originalWebSocket = (global as any).WebSocket;
|
||||
|
||||
(global as any).WebSocket = jest.fn().mockImplementation(() => {
|
||||
connectAttempts++;
|
||||
const ws = new originalWebSocket('ws://localhost:8080');
|
||||
// 模拟连接失败
|
||||
setTimeout(() => {
|
||||
if (ws.onerror) {
|
||||
ws.onerror(new Event('error'));
|
||||
}
|
||||
}, 0);
|
||||
return ws;
|
||||
});
|
||||
|
||||
await expect(client.connect()).rejects.toThrow();
|
||||
|
||||
// 等待重连尝试
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
expect(connectAttempts).toBeGreaterThan(1);
|
||||
|
||||
// 恢复原始WebSocket
|
||||
(global as any).WebSocket = originalWebSocket;
|
||||
});
|
||||
|
||||
it('达到最大重连次数后应该停止重连', async () => {
|
||||
const maxAttempts = 2;
|
||||
client = new NetworkClient({
|
||||
transportType: 'websocket',
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
maxReconnectAttempts: maxAttempts,
|
||||
reconnectInterval: 50
|
||||
});
|
||||
|
||||
let connectAttempts = 0;
|
||||
const originalWebSocket = (global as any).WebSocket;
|
||||
|
||||
(global as any).WebSocket = jest.fn().mockImplementation(() => {
|
||||
connectAttempts++;
|
||||
const ws = new originalWebSocket('ws://localhost:8080');
|
||||
setTimeout(() => {
|
||||
if (ws.onerror) {
|
||||
ws.onerror(new Event('error'));
|
||||
}
|
||||
}, 0);
|
||||
return ws;
|
||||
});
|
||||
|
||||
await expect(client.connect()).rejects.toThrow();
|
||||
|
||||
// 等待所有重连尝试完成
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
expect(connectAttempts).toBeLessThanOrEqual(maxAttempts + 1);
|
||||
|
||||
// 恢复原始WebSocket
|
||||
(global as any).WebSocket = originalWebSocket;
|
||||
});
|
||||
});
|
||||
|
||||
describe('内存泄漏防护测试', () => {
|
||||
it('断开连接时应该清理所有资源', async () => {
|
||||
client = new NetworkClient({
|
||||
transport: 'websocket',
|
||||
transportConfig: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
const emitterInstance = Emitter.mock.results[Emitter.mock.results.length - 1].value;
|
||||
|
||||
await client.disconnect();
|
||||
|
||||
expect(emitterInstance.removeAllListeners).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('多次创建和销毁客户端不应该造成内存泄漏', () => {
|
||||
const initialEmitterCallCount = require('@esengine/ecs-framework').Emitter.mock.calls.length;
|
||||
|
||||
// 创建和销毁多个客户端实例
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const tempClient = new NetworkClient({
|
||||
transportType: 'websocket',
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
});
|
||||
tempClient.disconnect().catch(() => {});
|
||||
}
|
||||
|
||||
const finalEmitterCallCount = require('@esengine/ecs-framework').Emitter.mock.calls.length;
|
||||
|
||||
// 验证Emitter实例数量符合预期
|
||||
expect(finalEmitterCallCount - initialEmitterCallCount).toBe(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,27 +1,65 @@
|
||||
/**
|
||||
* Jest测试环境设置 - 客户端
|
||||
*/
|
||||
|
||||
// 导入reflect-metadata以支持装饰器
|
||||
import 'reflect-metadata';
|
||||
|
||||
// Mock WebSocket for testing
|
||||
(global as any).WebSocket = class MockWebSocket {
|
||||
// 模拟浏览器环境的WebSocket
|
||||
Object.defineProperty(global, 'WebSocket', {
|
||||
value: class MockWebSocket {
|
||||
static CONNECTING = 0;
|
||||
static OPEN = 1;
|
||||
static CLOSING = 2;
|
||||
static CLOSED = 3;
|
||||
|
||||
readyState = MockWebSocket.CONNECTING;
|
||||
url: string;
|
||||
onopen: ((event: Event) => void) | null = null;
|
||||
onmessage: ((event: MessageEvent) => void) | null = null;
|
||||
onclose: ((event: CloseEvent) => void) | null = null;
|
||||
onmessage: ((event: MessageEvent) => void) | null = null;
|
||||
onerror: ((event: Event) => void) | null = null;
|
||||
|
||||
constructor(public url: string) {}
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
// 模拟异步连接
|
||||
setTimeout(() => {
|
||||
this.readyState = MockWebSocket.OPEN;
|
||||
if (this.onopen) {
|
||||
this.onopen(new Event('open'));
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
send(data: string | ArrayBuffer | Blob) {
|
||||
// Mock implementation
|
||||
send(data: string | ArrayBuffer) {
|
||||
// 模拟发送
|
||||
}
|
||||
|
||||
close() {
|
||||
// Mock implementation
|
||||
this.readyState = MockWebSocket.CLOSED;
|
||||
if (this.onclose) {
|
||||
this.onclose(new CloseEvent('close'));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
global.beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
global.afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
// 全局测试配置
|
||||
beforeAll(() => {
|
||||
// 设置测试环境
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.NETWORK_ENV = 'client';
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// 清理测试环境
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// 每个测试前的准备工作
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 每个测试后的清理工作
|
||||
// 清理可能的网络连接、定时器等
|
||||
});
|
||||
@@ -1,374 +0,0 @@
|
||||
/**
|
||||
* ClientTransport 基类测试
|
||||
* 测试客户端传输层基类的构造函数和依赖问题
|
||||
*/
|
||||
|
||||
import { ClientTransport, ClientTransportConfig, ConnectionState } from '../../src/transport/ClientTransport';
|
||||
|
||||
// Mock Emitter 和 Core
|
||||
jest.mock('@esengine/ecs-framework', () => ({
|
||||
Emitter: jest.fn().mockImplementation(() => ({
|
||||
emit: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
removeAllListeners: jest.fn()
|
||||
})),
|
||||
Core: {
|
||||
schedule: {
|
||||
scheduleRepeating: jest.fn((callback: Function, interval: number) => ({
|
||||
stop: jest.fn()
|
||||
}))
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Mock network-shared
|
||||
jest.mock('@esengine/ecs-framework-network-shared', () => ({
|
||||
NetworkValue: {}
|
||||
}));
|
||||
|
||||
// 创建测试用的具体实现类
|
||||
class TestClientTransport extends ClientTransport {
|
||||
async connect(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async sendMessage(message: any): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
describe('ClientTransport', () => {
|
||||
let transport: TestClientTransport;
|
||||
const defaultConfig: ClientTransportConfig = {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (transport) {
|
||||
transport = null as any;
|
||||
}
|
||||
});
|
||||
|
||||
describe('构造函数测试', () => {
|
||||
it('应该能够成功创建ClientTransport实例', () => {
|
||||
expect(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(transport).toBeInstanceOf(ClientTransport);
|
||||
});
|
||||
|
||||
it('应该正确设置默认配置', () => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
|
||||
const config = (transport as any).config;
|
||||
expect(config.host).toBe('localhost');
|
||||
expect(config.port).toBe(8080);
|
||||
expect(config.secure).toBe(false);
|
||||
expect(config.connectionTimeout).toBe(10000);
|
||||
expect(config.reconnectInterval).toBe(3000);
|
||||
expect(config.maxReconnectAttempts).toBe(10);
|
||||
expect(config.heartbeatInterval).toBe(30000);
|
||||
expect(config.maxQueueSize).toBe(1000);
|
||||
});
|
||||
|
||||
it('应该允许自定义配置覆盖默认值', () => {
|
||||
const customConfig: ClientTransportConfig = {
|
||||
host: 'example.com',
|
||||
port: 9090,
|
||||
secure: true,
|
||||
connectionTimeout: 15000,
|
||||
reconnectInterval: 5000,
|
||||
maxReconnectAttempts: 5,
|
||||
heartbeatInterval: 60000,
|
||||
maxQueueSize: 500
|
||||
};
|
||||
|
||||
transport = new TestClientTransport(customConfig);
|
||||
|
||||
const config = (transport as any).config;
|
||||
expect(config.host).toBe('example.com');
|
||||
expect(config.port).toBe(9090);
|
||||
expect(config.secure).toBe(true);
|
||||
expect(config.connectionTimeout).toBe(15000);
|
||||
expect(config.reconnectInterval).toBe(5000);
|
||||
expect(config.maxReconnectAttempts).toBe(5);
|
||||
expect(config.heartbeatInterval).toBe(60000);
|
||||
expect(config.maxQueueSize).toBe(500);
|
||||
});
|
||||
|
||||
it('应该正确初始化内部状态', () => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
|
||||
expect((transport as any).state).toBe(ConnectionState.DISCONNECTED);
|
||||
expect((transport as any).messageQueue).toEqual([]);
|
||||
expect((transport as any).reconnectAttempts).toBe(0);
|
||||
expect((transport as any).reconnectTimer).toBeNull();
|
||||
expect((transport as any).heartbeatTimer).toBeNull();
|
||||
expect((transport as any).latencyMeasurements).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该正确初始化统计信息', () => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
|
||||
const stats = transport.getStats();
|
||||
expect(stats.connectedAt).toBeNull();
|
||||
expect(stats.connectionDuration).toBe(0);
|
||||
expect(stats.messagesSent).toBe(0);
|
||||
expect(stats.messagesReceived).toBe(0);
|
||||
expect(stats.bytesSent).toBe(0);
|
||||
expect(stats.bytesReceived).toBe(0);
|
||||
expect(stats.averageLatency).toBe(0);
|
||||
expect(stats.averageLatency).toBe(0);
|
||||
expect(stats.reconnectCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('依赖注入测试', () => {
|
||||
it('应该正确处理@esengine/ecs-framework中的Emitter', () => {
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
|
||||
expect(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(Emitter).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('构造函数中Emitter初始化失败应该抛出异常', () => {
|
||||
// Mock Emitter构造函数抛出异常
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
Emitter.mockImplementation(() => {
|
||||
throw new Error('Emitter初始化失败');
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
}).toThrow('Emitter初始化失败');
|
||||
});
|
||||
|
||||
it('应该正确处理@esengine/ecs-framework-network-shared依赖', () => {
|
||||
const networkShared = require('@esengine/ecs-framework-network-shared');
|
||||
|
||||
expect(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(networkShared).toBeDefined();
|
||||
expect(networkShared.NetworkValue).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('事件系统测试', () => {
|
||||
beforeEach(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
});
|
||||
|
||||
it('应该能够注册事件监听器', () => {
|
||||
const mockCallback = jest.fn();
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
const emitterInstance = Emitter.mock.results[0].value;
|
||||
|
||||
transport.on('connected', mockCallback);
|
||||
|
||||
expect(emitterInstance.on).toHaveBeenCalledWith('connected', mockCallback);
|
||||
});
|
||||
|
||||
it('应该能够移除事件监听器', () => {
|
||||
const mockCallback = jest.fn();
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
const emitterInstance = Emitter.mock.results[0].value;
|
||||
|
||||
transport.off('connected', mockCallback);
|
||||
|
||||
expect(emitterInstance.off).toHaveBeenCalledWith('connected', mockCallback);
|
||||
});
|
||||
|
||||
it('应该能够发出事件', () => {
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
const emitterInstance = Emitter.mock.results[0].value;
|
||||
|
||||
(transport as any).emit('connected');
|
||||
|
||||
expect(emitterInstance.emit).toHaveBeenCalledWith('connected');
|
||||
});
|
||||
});
|
||||
|
||||
describe('消息队列测试', () => {
|
||||
beforeEach(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
});
|
||||
|
||||
it('应该能够将消息加入队列', async () => {
|
||||
const message = {
|
||||
type: 'custom' as const,
|
||||
data: { test: 'data' },
|
||||
reliable: true,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
await transport.sendMessage(message);
|
||||
|
||||
const messageQueue = (transport as any).messageQueue;
|
||||
expect(messageQueue).toHaveLength(1);
|
||||
expect(messageQueue[0]).toEqual(message);
|
||||
});
|
||||
|
||||
it('消息队列达到最大大小时应该移除旧消息', async () => {
|
||||
// 设置较小的队列大小
|
||||
const smallQueueConfig = { ...defaultConfig, maxQueueSize: 2 };
|
||||
transport = new TestClientTransport(smallQueueConfig);
|
||||
|
||||
const message1 = { type: 'custom' as const, data: { id: 1 }, reliable: true, timestamp: Date.now() };
|
||||
const message2 = { type: 'custom' as const, data: { id: 2 }, reliable: true, timestamp: Date.now() };
|
||||
const message3 = { type: 'custom' as const, data: { id: 3 }, reliable: true, timestamp: Date.now() };
|
||||
|
||||
await transport.sendMessage(message1);
|
||||
await transport.sendMessage(message2);
|
||||
await transport.sendMessage(message3);
|
||||
|
||||
const messageQueue = (transport as any).messageQueue;
|
||||
expect(messageQueue).toHaveLength(2);
|
||||
expect(messageQueue[0]).toEqual(message2);
|
||||
expect(messageQueue[1]).toEqual(message3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('连接状态测试', () => {
|
||||
beforeEach(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
});
|
||||
|
||||
it('应该正确获取连接状态', () => {
|
||||
expect(transport.getState()).toBe(ConnectionState.DISCONNECTED);
|
||||
});
|
||||
|
||||
it('应该正确检查连接状态', () => {
|
||||
expect(transport.isConnected()).toBe(false);
|
||||
|
||||
(transport as any).state = ConnectionState.CONNECTED;
|
||||
expect(transport.isConnected()).toBe(true);
|
||||
|
||||
(transport as any).state = ConnectionState.AUTHENTICATED;
|
||||
expect(transport.isConnected()).toBe(true);
|
||||
});
|
||||
|
||||
it('状态变化时应该发出事件', () => {
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
const emitterInstance = Emitter.mock.results[0].value;
|
||||
|
||||
(transport as any).setState(ConnectionState.CONNECTING);
|
||||
|
||||
expect(emitterInstance.emit).toHaveBeenCalledWith(
|
||||
'state-changed',
|
||||
ConnectionState.DISCONNECTED,
|
||||
ConnectionState.CONNECTING
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('延迟测量测试', () => {
|
||||
beforeEach(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
});
|
||||
|
||||
it('应该能够更新延迟测量', () => {
|
||||
(transport as any).updateLatency(100);
|
||||
(transport as any).updateLatency(200);
|
||||
(transport as any).updateLatency(150);
|
||||
|
||||
const stats = transport.getStats();
|
||||
expect(stats.averageLatency).toBe(150);
|
||||
});
|
||||
|
||||
it('应该限制延迟测量样本数量', () => {
|
||||
// 添加超过最大样本数的测量
|
||||
for (let i = 0; i < 150; i++) {
|
||||
(transport as any).updateLatency(i * 10);
|
||||
}
|
||||
|
||||
const latencyMeasurements = (transport as any).latencyMeasurements;
|
||||
expect(latencyMeasurements.length).toBeLessThanOrEqual(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('配置验证测试', () => {
|
||||
it('应该拒绝无效的主机名', () => {
|
||||
expect(() => {
|
||||
transport = new TestClientTransport({ host: '', port: 8080 });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('应该拒绝无效的端口号', () => {
|
||||
expect(() => {
|
||||
transport = new TestClientTransport({ host: 'localhost', port: 0 });
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
transport = new TestClientTransport({ host: 'localhost', port: 65536 });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('应该拒绝负数的超时配置', () => {
|
||||
expect(() => {
|
||||
transport = new TestClientTransport({
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
connectionTimeout: -1000
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('资源清理测试', () => {
|
||||
beforeEach(() => {
|
||||
transport = new TestClientTransport(defaultConfig);
|
||||
});
|
||||
|
||||
it('应该能够清理所有定时器', () => {
|
||||
const { Core } = require('@esengine/ecs-framework');
|
||||
const mockTimer = { stop: jest.fn() };
|
||||
Core.schedule.scheduleRepeating.mockReturnValue(mockTimer);
|
||||
|
||||
// 设置一些定时器
|
||||
(transport as any).reconnectTimer = mockTimer;
|
||||
(transport as any).heartbeatTimer = mockTimer;
|
||||
|
||||
// 调用清理方法
|
||||
(transport as any).cleanup();
|
||||
|
||||
expect(mockTimer.stop).toHaveBeenCalledTimes(2);
|
||||
expect((transport as any).reconnectTimer).toBeNull();
|
||||
expect((transport as any).heartbeatTimer).toBeNull();
|
||||
});
|
||||
|
||||
it('应该能够清理消息队列', () => {
|
||||
(transport as any).messageQueue = [
|
||||
{ type: 'custom', data: {}, reliable: true, timestamp: Date.now() }
|
||||
];
|
||||
|
||||
(transport as any).cleanup();
|
||||
|
||||
expect((transport as any).messageQueue).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('应该能够移除所有事件监听器', () => {
|
||||
const { Emitter } = require('@esengine/ecs-framework');
|
||||
const emitterInstance = Emitter.mock.results[0].value;
|
||||
|
||||
(transport as any).cleanup();
|
||||
|
||||
expect(emitterInstance.removeAllListeners).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,348 +0,0 @@
|
||||
/**
|
||||
* WebSocketClientTransport 测试
|
||||
* 测试WebSocket客户端传输层的构造函数和依赖问题
|
||||
*/
|
||||
|
||||
import { WebSocketClientTransport, WebSocketClientConfig } from '../../src/transport/WebSocketClientTransport';
|
||||
import { ConnectionState } from '../../src/transport/ClientTransport';
|
||||
|
||||
// Mock WebSocket
|
||||
class MockWebSocket {
|
||||
public readyState: number = WebSocket.CONNECTING;
|
||||
public onopen: ((event: Event) => void) | null = null;
|
||||
public onclose: ((event: CloseEvent) => void) | null = null;
|
||||
public onmessage: ((event: MessageEvent) => void) | null = null;
|
||||
public onerror: ((event: Event) => void) | null = null;
|
||||
|
||||
constructor(public url: string, public protocols?: string | string[]) {}
|
||||
|
||||
send(data: string | ArrayBuffer | Blob): void {}
|
||||
close(code?: number, reason?: string): void {
|
||||
this.readyState = WebSocket.CLOSED;
|
||||
if (this.onclose) {
|
||||
this.onclose(new CloseEvent('close', { code: code || 1000, reason: reason || '' }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock依赖 - 直接创建mock对象而不依赖外部模块
|
||||
const mockCore = {
|
||||
schedule: {
|
||||
scheduleRepeating: jest.fn((callback: Function, interval: number) => ({
|
||||
stop: jest.fn()
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
const mockEmitter = {
|
||||
emit: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
removeAllListeners: jest.fn()
|
||||
};
|
||||
|
||||
const mockNetworkShared = {
|
||||
NetworkValue: {},
|
||||
generateMessageId: jest.fn(() => 'mock-message-id-123')
|
||||
};
|
||||
|
||||
// 设置模块mock
|
||||
jest.doMock('@esengine/ecs-framework', () => ({
|
||||
Core: mockCore,
|
||||
Emitter: jest.fn(() => mockEmitter)
|
||||
}));
|
||||
|
||||
jest.doMock('@esengine/ecs-framework-network-shared', () => mockNetworkShared);
|
||||
|
||||
// 设置全局WebSocket mock
|
||||
(global as any).WebSocket = MockWebSocket;
|
||||
(global as any).WebSocket.CONNECTING = 0;
|
||||
(global as any).WebSocket.OPEN = 1;
|
||||
(global as any).WebSocket.CLOSING = 2;
|
||||
(global as any).WebSocket.CLOSED = 3;
|
||||
|
||||
describe('WebSocketClientTransport', () => {
|
||||
let transport: WebSocketClientTransport;
|
||||
const defaultConfig: WebSocketClientConfig = {
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
reconnectInterval: 1000,
|
||||
maxReconnectAttempts: 3,
|
||||
heartbeatInterval: 30000
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (transport) {
|
||||
transport.disconnect().catch(() => {});
|
||||
transport = null as any;
|
||||
}
|
||||
});
|
||||
|
||||
describe('构造函数测试', () => {
|
||||
it('应该能够成功创建WebSocketClientTransport实例', () => {
|
||||
expect(() => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(transport).toBeInstanceOf(WebSocketClientTransport);
|
||||
});
|
||||
|
||||
it('应该正确合并默认配置', () => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
|
||||
const config = (transport as any).config;
|
||||
expect(config.path).toBe('/ws');
|
||||
expect(config.protocols).toEqual([]);
|
||||
expect(config.headers).toEqual({});
|
||||
expect(config.binaryType).toBe('arraybuffer');
|
||||
expect(config.host).toBe('localhost');
|
||||
expect(config.port).toBe(8080);
|
||||
});
|
||||
|
||||
it('应该允许自定义配置覆盖默认值', () => {
|
||||
const customConfig: WebSocketClientConfig = {
|
||||
...defaultConfig,
|
||||
path: '/custom-ws',
|
||||
protocols: ['custom-protocol'],
|
||||
headers: { 'X-Custom': 'value' },
|
||||
binaryType: 'blob'
|
||||
};
|
||||
|
||||
transport = new WebSocketClientTransport(customConfig);
|
||||
|
||||
const config = (transport as any).config;
|
||||
expect(config.path).toBe('/custom-ws');
|
||||
expect(config.protocols).toEqual(['custom-protocol']);
|
||||
expect(config.headers).toEqual({ 'X-Custom': 'value' });
|
||||
expect(config.binaryType).toBe('blob');
|
||||
});
|
||||
|
||||
it('应该正确初始化内部状态', () => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
|
||||
expect((transport as any).websocket).toBeNull();
|
||||
expect((transport as any).connectionPromise).toBeNull();
|
||||
expect((transport as any).connectionTimeoutTimer).toBeNull();
|
||||
expect((transport as any).state).toBe(ConnectionState.DISCONNECTED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('依赖注入测试', () => {
|
||||
it('应该正确处理@esengine/ecs-framework依赖', () => {
|
||||
const { Core } = require('@esengine/ecs-framework');
|
||||
|
||||
expect(() => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(Core).toBeDefined();
|
||||
});
|
||||
|
||||
it('应该正确处理@esengine/ecs-framework-network-shared依赖', () => {
|
||||
const { generateMessageId } = require('@esengine/ecs-framework-network-shared');
|
||||
|
||||
expect(() => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(generateMessageId).toBeDefined();
|
||||
expect(typeof generateMessageId).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('连接功能测试', () => {
|
||||
beforeEach(() => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
});
|
||||
|
||||
it('应该能够发起连接', async () => {
|
||||
const connectPromise = transport.connect();
|
||||
|
||||
expect((transport as any).websocket).toBeInstanceOf(MockWebSocket);
|
||||
expect((transport as any).state).toBe(ConnectionState.CONNECTING);
|
||||
|
||||
// 模拟连接成功
|
||||
const ws = (transport as any).websocket as MockWebSocket;
|
||||
ws.readyState = WebSocket.OPEN;
|
||||
if (ws.onopen) {
|
||||
ws.onopen(new Event('open'));
|
||||
}
|
||||
|
||||
await expect(connectPromise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该构造正确的WebSocket URL', async () => {
|
||||
transport.connect();
|
||||
|
||||
const ws = (transport as any).websocket as MockWebSocket;
|
||||
expect(ws.url).toBe('ws://localhost:8080/ws');
|
||||
});
|
||||
|
||||
it('使用安全连接时应该构造HTTPS URL', async () => {
|
||||
const secureConfig = { ...defaultConfig, secure: true };
|
||||
transport = new WebSocketClientTransport(secureConfig);
|
||||
|
||||
transport.connect();
|
||||
|
||||
const ws = (transport as any).websocket as MockWebSocket;
|
||||
expect(ws.url).toBe('wss://localhost:8080/ws');
|
||||
});
|
||||
|
||||
it('应该设置WebSocket事件处理器', async () => {
|
||||
transport.connect();
|
||||
|
||||
const ws = (transport as any).websocket as MockWebSocket;
|
||||
expect(ws.onopen).toBeDefined();
|
||||
expect(ws.onclose).toBeDefined();
|
||||
expect(ws.onmessage).toBeDefined();
|
||||
expect(ws.onerror).toBeDefined();
|
||||
});
|
||||
|
||||
it('连接超时应该被正确处理', async () => {
|
||||
const shortTimeoutConfig = { ...defaultConfig, connectionTimeout: 100 };
|
||||
transport = new WebSocketClientTransport(shortTimeoutConfig);
|
||||
|
||||
const connectPromise = transport.connect();
|
||||
|
||||
// 不触发onopen事件,让连接超时
|
||||
await expect(connectPromise).rejects.toThrow('连接超时');
|
||||
});
|
||||
|
||||
it('应该能够正确断开连接', async () => {
|
||||
transport.connect();
|
||||
|
||||
// 模拟连接成功
|
||||
const ws = (transport as any).websocket as MockWebSocket;
|
||||
ws.readyState = WebSocket.OPEN;
|
||||
if (ws.onopen) {
|
||||
ws.onopen(new Event('open'));
|
||||
}
|
||||
|
||||
await transport.disconnect();
|
||||
expect((transport as any).state).toBe(ConnectionState.DISCONNECTED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('消息发送测试', () => {
|
||||
beforeEach(async () => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
});
|
||||
|
||||
it('未连接时发送消息应该加入队列', async () => {
|
||||
const message = {
|
||||
type: 'custom' as const,
|
||||
data: { test: 'data' },
|
||||
reliable: true,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
await transport.sendMessage(message);
|
||||
|
||||
const messageQueue = (transport as any).messageQueue;
|
||||
expect(messageQueue).toHaveLength(1);
|
||||
expect(messageQueue[0]).toEqual(message);
|
||||
});
|
||||
|
||||
it('连接后应该发送队列中的消息', async () => {
|
||||
const message = {
|
||||
type: 'custom' as const,
|
||||
data: { test: 'data' },
|
||||
reliable: true,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// 先发送消息到队列
|
||||
await transport.sendMessage(message);
|
||||
|
||||
// 然后连接
|
||||
transport.connect();
|
||||
const ws = (transport as any).websocket as MockWebSocket;
|
||||
const sendSpy = jest.spyOn(ws, 'send');
|
||||
|
||||
// 模拟连接成功
|
||||
ws.readyState = WebSocket.OPEN;
|
||||
if (ws.onopen) {
|
||||
ws.onopen(new Event('open'));
|
||||
}
|
||||
|
||||
expect(sendSpy).toHaveBeenCalled();
|
||||
expect((transport as any).messageQueue).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理测试', () => {
|
||||
it('应该处理WebSocket构造函数异常', () => {
|
||||
// Mock WebSocket构造函数抛出异常
|
||||
const originalWebSocket = (global as any).WebSocket;
|
||||
(global as any).WebSocket = jest.fn(() => {
|
||||
throw new Error('WebSocket构造失败');
|
||||
});
|
||||
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
|
||||
expect(transport.connect()).rejects.toThrow('WebSocket构造失败');
|
||||
|
||||
// 恢复原始WebSocket
|
||||
(global as any).WebSocket = originalWebSocket;
|
||||
});
|
||||
|
||||
it('应该处理网络连接错误', async () => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
|
||||
const connectPromise = transport.connect();
|
||||
|
||||
// 模拟连接错误
|
||||
const ws = (transport as any).websocket as MockWebSocket;
|
||||
if (ws.onerror) {
|
||||
ws.onerror(new Event('error'));
|
||||
}
|
||||
|
||||
await expect(connectPromise).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('应该处理意外的连接关闭', () => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
transport.connect();
|
||||
|
||||
const ws = (transport as any).websocket as MockWebSocket;
|
||||
|
||||
// 模拟连接意外关闭
|
||||
if (ws.onclose) {
|
||||
ws.onclose(new CloseEvent('close', { code: 1006, reason: '意外关闭' }));
|
||||
}
|
||||
|
||||
expect((transport as any).state).toBe(ConnectionState.DISCONNECTED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('统计信息测试', () => {
|
||||
it('应该正确计算连接统计信息', async () => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
|
||||
const initialStats = transport.getStats();
|
||||
expect(initialStats.connectedAt).toBeNull();
|
||||
expect(initialStats.messagesSent).toBe(0);
|
||||
expect(initialStats.messagesReceived).toBe(0);
|
||||
});
|
||||
|
||||
it('连接后应该更新统计信息', async () => {
|
||||
transport = new WebSocketClientTransport(defaultConfig);
|
||||
|
||||
transport.connect();
|
||||
const ws = (transport as any).websocket as MockWebSocket;
|
||||
ws.readyState = WebSocket.OPEN;
|
||||
if (ws.onopen) {
|
||||
ws.onopen(new Event('open'));
|
||||
}
|
||||
|
||||
const stats = transport.getStats();
|
||||
expect(stats.connectedAt).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,10 +4,11 @@
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"allowImportingTsExtensions": false,
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"lib": ["ES2020", "DOM", "WebWorker"],
|
||||
"outDir": "./bin",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
@@ -41,5 +42,13 @@
|
||||
"bin",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
},
|
||||
{
|
||||
"path": "../network-shared"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
# @esengine/ecs-framework-network-server
|
||||
|
||||
ECS Framework 网络库 - 服务端实现
|
||||
|
||||
## 概述
|
||||
|
||||
这是 ECS Framework 网络库的服务端包,提供了:
|
||||
|
||||
- 权威服务端实现
|
||||
- 客户端会话管理
|
||||
- 房间和匹配系统
|
||||
- 反作弊验证
|
||||
- 网络同步权威控制
|
||||
|
||||
## 特性
|
||||
|
||||
- **权威服务端**: 所有网络状态由服务端权威控制
|
||||
- **客户端验证**: 验证客户端输入和操作的合法性
|
||||
- **房间系统**: 支持多房间和实例管理
|
||||
- **反作弊**: 内置反作弊验证机制
|
||||
- **高性能**: 针对大量客户端连接进行优化
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework-network-server
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
```typescript
|
||||
import { NetworkServerManager } from '@esengine/ecs-framework-network-server';
|
||||
import { NetworkComponent, SyncVar, ServerRpc } from '@esengine/ecs-framework-network-shared';
|
||||
|
||||
// 启动服务端
|
||||
const server = new NetworkServerManager();
|
||||
await server.startServer({
|
||||
port: 7777,
|
||||
maxConnections: 100
|
||||
});
|
||||
|
||||
// 创建权威网络组件
|
||||
@NetworkComponent()
|
||||
class ServerPlayerController extends NetworkBehaviour {
|
||||
@SyncVar()
|
||||
public position: Vector3 = { x: 0, y: 0, z: 0 };
|
||||
|
||||
@SyncVar()
|
||||
public health: number = 100;
|
||||
|
||||
@ServerRpc({ requiresOwnership: true, rateLimit: 10 })
|
||||
public movePlayer(direction: Vector3): void {
|
||||
// 服务端权威的移动处理
|
||||
if (this.validateMovement(direction)) {
|
||||
this.position.add(direction);
|
||||
}
|
||||
}
|
||||
|
||||
@ServerRpc({ requiresAuth: true })
|
||||
public takeDamage(damage: number, attackerId: number): void {
|
||||
// 服务端权威的伤害处理
|
||||
if (this.validateDamage(damage, attackerId)) {
|
||||
this.health -= damage;
|
||||
|
||||
if (this.health <= 0) {
|
||||
this.handlePlayerDeath();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 房间系统
|
||||
|
||||
```typescript
|
||||
import { RoomManager, Room } from '@esengine/ecs-framework-network-server';
|
||||
|
||||
// 创建房间管理器
|
||||
const roomManager = new RoomManager();
|
||||
|
||||
// 创建房间
|
||||
const gameRoom = roomManager.createRoom({
|
||||
name: 'Game Room 1',
|
||||
maxPlayers: 4,
|
||||
isPrivate: false
|
||||
});
|
||||
|
||||
// 玩家加入房间
|
||||
gameRoom.addPlayer(clientId, playerData);
|
||||
|
||||
// 房间事件处理
|
||||
gameRoom.onPlayerJoined((player) => {
|
||||
console.log(`Player ${player.name} joined room ${gameRoom.name}`);
|
||||
});
|
||||
|
||||
gameRoom.onPlayerLeft((player) => {
|
||||
console.log(`Player ${player.name} left room ${gameRoom.name}`);
|
||||
});
|
||||
```
|
||||
|
||||
## 权限验证
|
||||
|
||||
```typescript
|
||||
import { AuthSystem } from '@esengine/ecs-framework-network-server';
|
||||
|
||||
// 配置认证系统
|
||||
const authSystem = new AuthSystem({
|
||||
tokenSecret: 'your-secret-key',
|
||||
sessionTimeout: 30 * 60 * 1000, // 30分钟
|
||||
maxLoginAttempts: 5
|
||||
});
|
||||
|
||||
// 客户端认证
|
||||
authSystem.onClientAuth(async (clientId, credentials) => {
|
||||
const user = await validateCredentials(credentials);
|
||||
if (user) {
|
||||
return { userId: user.id, permissions: user.permissions };
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// RPC 权限检查
|
||||
@ServerRpc({ requiresAuth: true, requiresOwnership: true })
|
||||
public adminCommand(command: string): void {
|
||||
// 只有已认证且拥有权限的客户端可以调用
|
||||
this.executeAdminCommand(command);
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -2,27 +2,32 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
console.log('🚀 使用 Rollup 构建 network-server 包...');
|
||||
console.log('🚀 使用 Rollup 构建 @esengine/network-server 包...');
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// 清理旧的dist目录
|
||||
if (fs.existsSync('./dist')) {
|
||||
console.log('🧹 清理旧的构建文件...');
|
||||
execSync('rimraf ./dist', { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
// 执行Rollup构建
|
||||
console.log('📦 执行 Rollup 构建...');
|
||||
execSync('rollup -c rollup.config.cjs', { stdio: 'inherit' });
|
||||
execSync('npx rollup -c rollup.config.cjs', { stdio: 'inherit' });
|
||||
|
||||
// 生成package.json
|
||||
console.log('📋 生成 package.json...');
|
||||
generatePackageJson();
|
||||
|
||||
// 复制其他文件
|
||||
console.log('📁 复制必要文件...');
|
||||
copyFiles();
|
||||
|
||||
// 输出构建结果
|
||||
showBuildResults();
|
||||
|
||||
console.log('✅ network-server 构建完成!');
|
||||
console.log('✅ @esengine/network-server 构建完成!');
|
||||
console.log('\n🚀 发布命令:');
|
||||
console.log('cd dist && npm publish');
|
||||
|
||||
@@ -60,19 +65,18 @@ function generatePackageJson() {
|
||||
],
|
||||
keywords: [
|
||||
'ecs',
|
||||
'networking',
|
||||
'network',
|
||||
'server',
|
||||
'authority',
|
||||
'validation',
|
||||
'rooms',
|
||||
'game-server',
|
||||
'multiplayer',
|
||||
'game',
|
||||
'nodejs',
|
||||
'typescript'
|
||||
],
|
||||
author: sourcePackage.author,
|
||||
license: sourcePackage.license,
|
||||
repository: sourcePackage.repository,
|
||||
dependencies: sourcePackage.dependencies,
|
||||
peerDependencies: sourcePackage.peerDependencies,
|
||||
publishConfig: sourcePackage.publishConfig,
|
||||
engines: {
|
||||
node: '>=16.0.0'
|
||||
},
|
||||
@@ -85,7 +89,7 @@ function generatePackageJson() {
|
||||
function copyFiles() {
|
||||
const filesToCopy = [
|
||||
{ src: './README.md', dest: './dist/README.md' },
|
||||
{ src: '../../LICENSE', dest: './dist/LICENSE' }
|
||||
{ src: './LICENSE', dest: './dist/LICENSE' }
|
||||
];
|
||||
|
||||
filesToCopy.forEach(({ src, dest }) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node', // 服务端库使用 node 环境
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/tests'],
|
||||
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
|
||||
testPathIgnorePatterns: ['/node_modules/'],
|
||||
@@ -18,16 +18,10 @@ module.exports = {
|
||||
coverageReporters: ['text', 'lcov', 'html'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 60,
|
||||
branches: 70,
|
||||
functions: 70,
|
||||
lines: 70,
|
||||
statements: 70
|
||||
},
|
||||
'./src/core/': {
|
||||
branches: 70,
|
||||
functions: 80,
|
||||
lines: 80,
|
||||
statements: 80
|
||||
}
|
||||
},
|
||||
verbose: true,
|
||||
@@ -42,7 +36,7 @@ module.exports = {
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
|
||||
testTimeout: 10000,
|
||||
testTimeout: 15000, // 服务端测试可能需要更长时间
|
||||
clearMocks: true,
|
||||
restoreMocks: true,
|
||||
modulePathIgnorePatterns: [
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"name": "@esengine/ecs-framework-network-server",
|
||||
"version": "1.0.5",
|
||||
"description": "ECS Framework 网络库 - 服务端实现",
|
||||
"type": "module",
|
||||
"name": "@esengine/network-server",
|
||||
"version": "1.0.1",
|
||||
"description": "ECS Framework网络层 - 服务端实现",
|
||||
"main": "bin/index.js",
|
||||
"types": "bin/index.d.ts",
|
||||
"exports": {
|
||||
@@ -22,16 +21,15 @@
|
||||
],
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"networking",
|
||||
"network",
|
||||
"server",
|
||||
"authority",
|
||||
"validation",
|
||||
"rooms",
|
||||
"game-server",
|
||||
"multiplayer",
|
||||
"game",
|
||||
"nodejs",
|
||||
"typescript"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf bin dist",
|
||||
"clean": "rimraf bin dist tsconfig.tsbuildinfo",
|
||||
"build:ts": "tsc",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "npm run build:ts",
|
||||
@@ -43,38 +41,29 @@
|
||||
"publish:minor": "npm version minor && npm run build:npm && cd dist && npm publish",
|
||||
"publish:major": "npm version major && npm run build:npm && cd dist && npm publish",
|
||||
"preversion": "npm run rebuild",
|
||||
"dev": "ts-node src/dev-server.ts",
|
||||
"start": "node bin/index.js",
|
||||
"test": "jest --config jest.config.cjs",
|
||||
"test:watch": "jest --watch --config jest.config.cjs",
|
||||
"test:coverage": "jest --coverage --config jest.config.cjs",
|
||||
"test:ci": "jest --ci --coverage --config jest.config.cjs",
|
||||
"test:clear": "jest --clearCache"
|
||||
"test:ci": "jest --ci --coverage --config jest.config.cjs"
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ws": "^8.18.0",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@esengine/ecs-framework": ">=2.1.29",
|
||||
"@esengine/ecs-framework-network-shared": ">=1.0.0"
|
||||
"@esengine/ecs-framework": "file:../core",
|
||||
"@esengine/network-shared": "file:../network-shared",
|
||||
"ws": "^8.18.2",
|
||||
"reflect-metadata": "^0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "*",
|
||||
"@esengine/ecs-framework-network-shared": "*",
|
||||
"@rollup/plugin-commonjs": "^28.0.3",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.19.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/ws": "^8.5.13",
|
||||
"@types/ws": "^8.18.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-node": "^29.7.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"rollup": "^4.42.0",
|
||||
"rollup-plugin-dts": "^6.2.1",
|
||||
"ts-jest": "^29.4.0",
|
||||
"ts-node": "^10.9.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -7,23 +7,33 @@ const { readFileSync } = require('fs');
|
||||
const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||
|
||||
const banner = `/**
|
||||
* @esengine/ecs-framework-network-server v${pkg.version}
|
||||
* ECS Framework 网络库 - 服务端实现
|
||||
* @esengine/network-server v${pkg.version}
|
||||
* ECS网络层服务端实现
|
||||
*
|
||||
* @author ${pkg.author}
|
||||
* @license ${pkg.license}
|
||||
*/`;
|
||||
|
||||
// 外部依赖,不打包进bundle (Node.js环境,保持依赖外部化)
|
||||
const external = [
|
||||
'ws',
|
||||
'uuid',
|
||||
'@esengine/ecs-framework',
|
||||
'@esengine/ecs-framework-network-shared'
|
||||
'@esengine/network-shared',
|
||||
'ws',
|
||||
'reflect-metadata',
|
||||
'http',
|
||||
'https',
|
||||
'crypto',
|
||||
'events',
|
||||
'stream',
|
||||
'util',
|
||||
'fs',
|
||||
'path'
|
||||
];
|
||||
|
||||
const commonPlugins = [
|
||||
resolve({
|
||||
preferBuiltins: true
|
||||
preferBuiltins: true,
|
||||
browser: false
|
||||
}),
|
||||
commonjs({
|
||||
include: /node_modules/
|
||||
@@ -57,7 +67,7 @@ module.exports = [
|
||||
}
|
||||
},
|
||||
|
||||
// CommonJS构建
|
||||
// CommonJS构建 (Node.js主要格式)
|
||||
{
|
||||
input: 'bin/index.js',
|
||||
output: {
|
||||
@@ -88,7 +98,7 @@ module.exports = [
|
||||
file: 'dist/index.d.ts',
|
||||
format: 'es',
|
||||
banner: `/**
|
||||
* @esengine/ecs-framework-network-server v${pkg.version}
|
||||
* @esengine/network-server v${pkg.version}
|
||||
* TypeScript definitions
|
||||
*/`
|
||||
},
|
||||
|
||||
@@ -1,622 +0,0 @@
|
||||
/**
|
||||
* 身份验证管理器
|
||||
*
|
||||
* 处理客户端身份验证、令牌验证等功能
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { createHash, randomBytes } from 'crypto';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { ClientConnection } from '../core/ClientConnection';
|
||||
|
||||
/**
|
||||
* 认证配置
|
||||
*/
|
||||
export interface AuthConfig {
|
||||
/** 令牌过期时间(毫秒) */
|
||||
tokenExpirationTime?: number;
|
||||
/** 最大登录尝试次数 */
|
||||
maxLoginAttempts?: number;
|
||||
/** 登录尝试重置时间(毫秒) */
|
||||
loginAttemptResetTime?: number;
|
||||
/** 是否启用令牌刷新 */
|
||||
enableTokenRefresh?: boolean;
|
||||
/** 令牌刷新阈值(毫秒) */
|
||||
tokenRefreshThreshold?: number;
|
||||
/** 是否启用IP限制 */
|
||||
enableIpRestriction?: boolean;
|
||||
/** 密码哈希算法 */
|
||||
passwordHashAlgorithm?: 'sha256' | 'sha512';
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
export interface UserInfo {
|
||||
/** 用户ID */
|
||||
id: string;
|
||||
/** 用户名 */
|
||||
username: string;
|
||||
/** 密码哈希 */
|
||||
passwordHash: string;
|
||||
/** 用户角色 */
|
||||
roles: string[];
|
||||
/** 用户元数据 */
|
||||
metadata: Record<string, NetworkValue>;
|
||||
/** 创建时间 */
|
||||
createdAt: Date;
|
||||
/** 最后登录时间 */
|
||||
lastLoginAt?: Date;
|
||||
/** 是否激活 */
|
||||
isActive: boolean;
|
||||
/** 允许的IP地址列表 */
|
||||
allowedIps?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证令牌
|
||||
*/
|
||||
export interface AuthToken {
|
||||
/** 令牌ID */
|
||||
id: string;
|
||||
/** 用户ID */
|
||||
userId: string;
|
||||
/** 令牌值 */
|
||||
token: string;
|
||||
/** 创建时间 */
|
||||
createdAt: Date;
|
||||
/** 过期时间 */
|
||||
expiresAt: Date;
|
||||
/** 是否已撤销 */
|
||||
isRevoked: boolean;
|
||||
/** 令牌元数据 */
|
||||
metadata: Record<string, NetworkValue>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录尝试记录
|
||||
*/
|
||||
interface LoginAttempt {
|
||||
/** IP地址 */
|
||||
ip: string;
|
||||
/** 用户名 */
|
||||
username: string;
|
||||
/** 尝试次数 */
|
||||
attempts: number;
|
||||
/** 最后尝试时间 */
|
||||
lastAttempt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证结果
|
||||
*/
|
||||
export interface AuthResult {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
/** 用户信息 */
|
||||
user?: UserInfo;
|
||||
/** 认证令牌 */
|
||||
token?: AuthToken;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
/** 错误代码 */
|
||||
errorCode?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证管理器事件
|
||||
*/
|
||||
export interface AuthManagerEvents {
|
||||
/** 用户登录成功 */
|
||||
'login-success': (user: UserInfo, token: AuthToken, clientId: string) => void;
|
||||
/** 用户登录失败 */
|
||||
'login-failed': (username: string, reason: string, clientId: string) => void;
|
||||
/** 用户注销 */
|
||||
'logout': (userId: string, clientId: string) => void;
|
||||
/** 令牌过期 */
|
||||
'token-expired': (userId: string, tokenId: string) => void;
|
||||
/** 令牌刷新 */
|
||||
'token-refreshed': (userId: string, oldTokenId: string, newTokenId: string) => void;
|
||||
/** 认证错误 */
|
||||
'auth-error': (error: Error, clientId?: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 身份验证管理器
|
||||
*/
|
||||
export class AuthenticationManager extends EventEmitter {
|
||||
private config: AuthConfig;
|
||||
private users = new Map<string, UserInfo>();
|
||||
private tokens = new Map<string, AuthToken>();
|
||||
private loginAttempts = new Map<string, LoginAttempt>();
|
||||
private cleanupTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(config: AuthConfig = {}) {
|
||||
super();
|
||||
|
||||
this.config = {
|
||||
tokenExpirationTime: 24 * 60 * 60 * 1000, // 24小时
|
||||
maxLoginAttempts: 5,
|
||||
loginAttemptResetTime: 15 * 60 * 1000, // 15分钟
|
||||
enableTokenRefresh: true,
|
||||
tokenRefreshThreshold: 60 * 60 * 1000, // 1小时
|
||||
enableIpRestriction: false,
|
||||
passwordHashAlgorithm: 'sha256',
|
||||
...config
|
||||
};
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册用户
|
||||
*/
|
||||
async registerUser(userData: {
|
||||
username: string;
|
||||
password: string;
|
||||
roles?: string[];
|
||||
metadata?: Record<string, NetworkValue>;
|
||||
allowedIps?: string[];
|
||||
}): Promise<UserInfo> {
|
||||
const { username, password, roles = ['user'], metadata = {}, allowedIps } = userData;
|
||||
|
||||
// 检查用户名是否已存在
|
||||
if (this.findUserByUsername(username)) {
|
||||
throw new Error('Username already exists');
|
||||
}
|
||||
|
||||
const userId = this.generateId();
|
||||
const passwordHash = this.hashPassword(password);
|
||||
|
||||
const user: UserInfo = {
|
||||
id: userId,
|
||||
username,
|
||||
passwordHash,
|
||||
roles,
|
||||
metadata,
|
||||
createdAt: new Date(),
|
||||
isActive: true,
|
||||
allowedIps
|
||||
};
|
||||
|
||||
this.users.set(userId, user);
|
||||
|
||||
console.log(`User registered: ${username} (${userId})`);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
async login(
|
||||
username: string,
|
||||
password: string,
|
||||
client: ClientConnection
|
||||
): Promise<AuthResult> {
|
||||
try {
|
||||
const clientIp = client.remoteAddress;
|
||||
const attemptKey = `${clientIp}-${username}`;
|
||||
|
||||
// 检查登录尝试次数
|
||||
if (this.isLoginBlocked(attemptKey)) {
|
||||
const result: AuthResult = {
|
||||
success: false,
|
||||
error: 'Too many login attempts. Please try again later.',
|
||||
errorCode: 'LOGIN_BLOCKED'
|
||||
};
|
||||
this.emit('login-failed', username, result.error!, client.id);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
const user = this.findUserByUsername(username);
|
||||
if (!user || !user.isActive) {
|
||||
this.recordLoginAttempt(attemptKey);
|
||||
const result: AuthResult = {
|
||||
success: false,
|
||||
error: 'Invalid username or password',
|
||||
errorCode: 'INVALID_CREDENTIALS'
|
||||
};
|
||||
this.emit('login-failed', username, result.error!, client.id);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const passwordHash = this.hashPassword(password);
|
||||
if (user.passwordHash !== passwordHash) {
|
||||
this.recordLoginAttempt(attemptKey);
|
||||
const result: AuthResult = {
|
||||
success: false,
|
||||
error: 'Invalid username or password',
|
||||
errorCode: 'INVALID_CREDENTIALS'
|
||||
};
|
||||
this.emit('login-failed', username, result.error!, client.id);
|
||||
return result;
|
||||
}
|
||||
|
||||
// IP限制检查
|
||||
if (this.config.enableIpRestriction && user.allowedIps && user.allowedIps.length > 0) {
|
||||
if (!user.allowedIps.includes(clientIp)) {
|
||||
const result: AuthResult = {
|
||||
success: false,
|
||||
error: 'Access denied from this IP address',
|
||||
errorCode: 'IP_RESTRICTED'
|
||||
};
|
||||
this.emit('login-failed', username, result.error!, client.id);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建认证令牌
|
||||
const token = this.createToken(user.id);
|
||||
|
||||
// 更新用户最后登录时间
|
||||
user.lastLoginAt = new Date();
|
||||
|
||||
// 清除登录尝试记录
|
||||
this.loginAttempts.delete(attemptKey);
|
||||
|
||||
const result: AuthResult = {
|
||||
success: true,
|
||||
user,
|
||||
token
|
||||
};
|
||||
|
||||
console.log(`User logged in: ${username} (${user.id}) from ${clientIp}`);
|
||||
this.emit('login-success', user, token, client.id);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
const result: AuthResult = {
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
errorCode: 'INTERNAL_ERROR'
|
||||
};
|
||||
this.emit('auth-error', error as Error, client.id);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注销
|
||||
*/
|
||||
async logout(tokenValue: string, client: ClientConnection): Promise<boolean> {
|
||||
try {
|
||||
const token = this.findTokenByValue(tokenValue);
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 撤销令牌
|
||||
token.isRevoked = true;
|
||||
|
||||
console.log(`User logged out: ${token.userId} from ${client.remoteAddress}`);
|
||||
this.emit('logout', token.userId, client.id);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
this.emit('auth-error', error as Error, client.id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证令牌
|
||||
*/
|
||||
async validateToken(tokenValue: string): Promise<AuthResult> {
|
||||
try {
|
||||
const token = this.findTokenByValue(tokenValue);
|
||||
|
||||
if (!token || token.isRevoked) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid token',
|
||||
errorCode: 'INVALID_TOKEN'
|
||||
};
|
||||
}
|
||||
|
||||
if (token.expiresAt < new Date()) {
|
||||
token.isRevoked = true;
|
||||
this.emit('token-expired', token.userId, token.id);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Token expired',
|
||||
errorCode: 'TOKEN_EXPIRED'
|
||||
};
|
||||
}
|
||||
|
||||
const user = this.users.get(token.userId);
|
||||
if (!user || !user.isActive) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'User not found or inactive',
|
||||
errorCode: 'USER_NOT_FOUND'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user,
|
||||
token
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.emit('auth-error', error as Error);
|
||||
return {
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
errorCode: 'INTERNAL_ERROR'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*/
|
||||
async refreshToken(tokenValue: string): Promise<AuthResult> {
|
||||
try {
|
||||
const validationResult = await this.validateToken(tokenValue);
|
||||
if (!validationResult.success || !validationResult.user || !validationResult.token) {
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
const token = validationResult.token;
|
||||
const timeUntilExpiration = token.expiresAt.getTime() - Date.now();
|
||||
|
||||
// 检查是否需要刷新
|
||||
if (timeUntilExpiration > this.config.tokenRefreshThreshold!) {
|
||||
return validationResult; // 不需要刷新
|
||||
}
|
||||
|
||||
// 创建新令牌
|
||||
const newToken = this.createToken(token.userId, token.metadata);
|
||||
|
||||
// 撤销旧令牌
|
||||
token.isRevoked = true;
|
||||
|
||||
console.log(`Token refreshed for user: ${token.userId}`);
|
||||
this.emit('token-refreshed', token.userId, token.id, newToken.id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user: validationResult.user,
|
||||
token: newToken
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.emit('auth-error', error as Error);
|
||||
return {
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
errorCode: 'INTERNAL_ERROR'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
getUserById(userId: string): UserInfo | undefined {
|
||||
return this.users.get(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息(通过用户名)
|
||||
*/
|
||||
getUserByUsername(username: string): UserInfo | undefined {
|
||||
return this.findUserByUsername(username);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
async updateUser(userId: string, updates: Partial<UserInfo>): Promise<boolean> {
|
||||
const user = this.users.get(userId);
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 不允许更新某些字段
|
||||
const { id, createdAt, ...allowedUpdates } = updates as any;
|
||||
Object.assign(user, allowedUpdates);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销所有用户令牌
|
||||
*/
|
||||
async revokeAllUserTokens(userId: string): Promise<number> {
|
||||
let revokedCount = 0;
|
||||
|
||||
for (const token of this.tokens.values()) {
|
||||
if (token.userId === userId && !token.isRevoked) {
|
||||
token.isRevoked = true;
|
||||
revokedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return revokedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取活跃令牌数量
|
||||
*/
|
||||
getActiveTokenCount(): number {
|
||||
return Array.from(this.tokens.values())
|
||||
.filter(token => !token.isRevoked && token.expiresAt > new Date()).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期令牌和登录尝试记录
|
||||
*/
|
||||
cleanup(): void {
|
||||
const now = new Date();
|
||||
let cleanedTokens = 0;
|
||||
let cleanedAttempts = 0;
|
||||
|
||||
// 清理过期令牌
|
||||
for (const [tokenId, token] of this.tokens.entries()) {
|
||||
if (token.expiresAt < now || token.isRevoked) {
|
||||
this.tokens.delete(tokenId);
|
||||
cleanedTokens++;
|
||||
}
|
||||
}
|
||||
|
||||
// 清理过期的登录尝试记录
|
||||
const resetTime = this.config.loginAttemptResetTime!;
|
||||
for (const [attemptKey, attempt] of this.loginAttempts.entries()) {
|
||||
if (now.getTime() - attempt.lastAttempt.getTime() > resetTime) {
|
||||
this.loginAttempts.delete(attemptKey);
|
||||
cleanedAttempts++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedTokens > 0 || cleanedAttempts > 0) {
|
||||
console.log(`Auth cleanup: ${cleanedTokens} tokens, ${cleanedAttempts} login attempts`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁认证管理器
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.cleanupTimer) {
|
||||
clearInterval(this.cleanupTimer);
|
||||
this.cleanupTimer = null;
|
||||
}
|
||||
|
||||
this.users.clear();
|
||||
this.tokens.clear();
|
||||
this.loginAttempts.clear();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
private initialize(): void {
|
||||
// 启动清理定时器(每小时清理一次)
|
||||
this.cleanupTimer = setInterval(() => {
|
||||
this.cleanup();
|
||||
}, 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找用户(通过用户名)
|
||||
*/
|
||||
private findUserByUsername(username: string): UserInfo | undefined {
|
||||
return Array.from(this.users.values())
|
||||
.find(user => user.username === username);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找令牌(通过令牌值)
|
||||
*/
|
||||
private findTokenByValue(tokenValue: string): AuthToken | undefined {
|
||||
return Array.from(this.tokens.values())
|
||||
.find(token => token.token === tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成ID
|
||||
*/
|
||||
private generateId(): string {
|
||||
return randomBytes(16).toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 哈希密码
|
||||
*/
|
||||
private hashPassword(password: string): string {
|
||||
return createHash(this.config.passwordHashAlgorithm!)
|
||||
.update(password)
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建认证令牌
|
||||
*/
|
||||
private createToken(userId: string, metadata: Record<string, NetworkValue> = {}): AuthToken {
|
||||
const tokenId = this.generateId();
|
||||
const tokenValue = randomBytes(32).toString('hex');
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(now.getTime() + this.config.tokenExpirationTime!);
|
||||
|
||||
const token: AuthToken = {
|
||||
id: tokenId,
|
||||
userId,
|
||||
token: tokenValue,
|
||||
createdAt: now,
|
||||
expiresAt,
|
||||
isRevoked: false,
|
||||
metadata
|
||||
};
|
||||
|
||||
this.tokens.set(tokenId, token);
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录是否被阻止
|
||||
*/
|
||||
private isLoginBlocked(attemptKey: string): boolean {
|
||||
const attempt = this.loginAttempts.get(attemptKey);
|
||||
if (!attempt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const resetTime = this.config.loginAttemptResetTime!;
|
||||
|
||||
// 检查重置时间
|
||||
if (now.getTime() - attempt.lastAttempt.getTime() > resetTime) {
|
||||
this.loginAttempts.delete(attemptKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
return attempt.attempts >= this.config.maxLoginAttempts!;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录尝试
|
||||
*/
|
||||
private recordLoginAttempt(attemptKey: string): void {
|
||||
const now = new Date();
|
||||
const [ip, username] = attemptKey.split('-', 2);
|
||||
|
||||
const existingAttempt = this.loginAttempts.get(attemptKey);
|
||||
if (existingAttempt) {
|
||||
// 检查是否需要重置
|
||||
if (now.getTime() - existingAttempt.lastAttempt.getTime() > this.config.loginAttemptResetTime!) {
|
||||
existingAttempt.attempts = 1;
|
||||
} else {
|
||||
existingAttempt.attempts++;
|
||||
}
|
||||
existingAttempt.lastAttempt = now;
|
||||
} else {
|
||||
this.loginAttempts.set(attemptKey, {
|
||||
ip,
|
||||
username,
|
||||
attempts: 1,
|
||||
lastAttempt: now
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
override on<K extends keyof AuthManagerEvents>(event: K, listener: AuthManagerEvents[K]): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
override emit<K extends keyof AuthManagerEvents>(event: K, ...args: Parameters<AuthManagerEvents[K]>): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,684 +0,0 @@
|
||||
/**
|
||||
* 权限管理器
|
||||
*
|
||||
* 处理用户权限、角色管理、访问控制等功能
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { UserInfo } from './AuthenticationManager';
|
||||
|
||||
/**
|
||||
* 权限类型
|
||||
*/
|
||||
export type Permission = string;
|
||||
|
||||
/**
|
||||
* 角色定义
|
||||
*/
|
||||
export interface Role {
|
||||
/** 角色ID */
|
||||
id: string;
|
||||
/** 角色名称 */
|
||||
name: string;
|
||||
/** 角色描述 */
|
||||
description?: string;
|
||||
/** 权限列表 */
|
||||
permissions: Permission[];
|
||||
/** 父角色ID */
|
||||
parentRoleId?: string;
|
||||
/** 是否系统角色 */
|
||||
isSystemRole: boolean;
|
||||
/** 角色元数据 */
|
||||
metadata: Record<string, NetworkValue>;
|
||||
/** 创建时间 */
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限检查上下文
|
||||
*/
|
||||
export interface PermissionContext {
|
||||
/** 用户ID */
|
||||
userId: string;
|
||||
/** 用户角色 */
|
||||
userRoles: string[];
|
||||
/** 请求的权限 */
|
||||
permission: Permission;
|
||||
/** 资源ID(可选) */
|
||||
resourceId?: string;
|
||||
/** 附加上下文数据 */
|
||||
context?: Record<string, NetworkValue>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限检查结果
|
||||
*/
|
||||
export interface PermissionResult {
|
||||
/** 是否允许 */
|
||||
granted: boolean;
|
||||
/** 原因 */
|
||||
reason?: string;
|
||||
/** 匹配的角色 */
|
||||
matchingRole?: string;
|
||||
/** 使用的权限 */
|
||||
usedPermission?: Permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限管理器配置
|
||||
*/
|
||||
export interface AuthorizationConfig {
|
||||
/** 是否启用权限继承 */
|
||||
enableInheritance?: boolean;
|
||||
/** 是否启用权限缓存 */
|
||||
enableCache?: boolean;
|
||||
/** 缓存过期时间(毫秒) */
|
||||
cacheExpirationTime?: number;
|
||||
/** 默认权限策略 */
|
||||
defaultPolicy?: 'deny' | 'allow';
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限管理器事件
|
||||
*/
|
||||
export interface AuthorizationEvents {
|
||||
/** 权限被授予 */
|
||||
'permission-granted': (context: PermissionContext, result: PermissionResult) => void;
|
||||
/** 权限被拒绝 */
|
||||
'permission-denied': (context: PermissionContext, result: PermissionResult) => void;
|
||||
/** 角色创建 */
|
||||
'role-created': (role: Role) => void;
|
||||
/** 角色更新 */
|
||||
'role-updated': (roleId: string, updates: Partial<Role>) => void;
|
||||
/** 角色删除 */
|
||||
'role-deleted': (roleId: string) => void;
|
||||
/** 权限错误 */
|
||||
'authorization-error': (error: Error, context?: PermissionContext) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限缓存项
|
||||
*/
|
||||
interface CacheItem {
|
||||
result: PermissionResult;
|
||||
expiresAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预定义权限
|
||||
*/
|
||||
export const Permissions = {
|
||||
// 系统权限
|
||||
SYSTEM_ADMIN: 'system:admin',
|
||||
SYSTEM_CONFIG: 'system:config',
|
||||
|
||||
// 用户管理权限
|
||||
USER_CREATE: 'user:create',
|
||||
USER_READ: 'user:read',
|
||||
USER_UPDATE: 'user:update',
|
||||
USER_DELETE: 'user:delete',
|
||||
USER_MANAGE_ROLES: 'user:manage-roles',
|
||||
|
||||
// 房间权限
|
||||
ROOM_CREATE: 'room:create',
|
||||
ROOM_JOIN: 'room:join',
|
||||
ROOM_LEAVE: 'room:leave',
|
||||
ROOM_MANAGE: 'room:manage',
|
||||
ROOM_KICK_PLAYERS: 'room:kick-players',
|
||||
|
||||
// 网络权限
|
||||
NETWORK_SEND_RPC: 'network:send-rpc',
|
||||
NETWORK_SYNC_VARS: 'network:sync-vars',
|
||||
NETWORK_BROADCAST: 'network:broadcast',
|
||||
|
||||
// 聊天权限
|
||||
CHAT_SEND: 'chat:send',
|
||||
CHAT_MODERATE: 'chat:moderate',
|
||||
CHAT_PRIVATE: 'chat:private',
|
||||
|
||||
// 文件权限
|
||||
FILE_UPLOAD: 'file:upload',
|
||||
FILE_DOWNLOAD: 'file:download',
|
||||
FILE_DELETE: 'file:delete'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 预定义角色
|
||||
*/
|
||||
export const SystemRoles = {
|
||||
ADMIN: 'admin',
|
||||
MODERATOR: 'moderator',
|
||||
USER: 'user',
|
||||
GUEST: 'guest'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 权限管理器
|
||||
*/
|
||||
export class AuthorizationManager extends EventEmitter {
|
||||
private config: AuthorizationConfig;
|
||||
private roles = new Map<string, Role>();
|
||||
private permissionCache = new Map<string, CacheItem>();
|
||||
private cleanupTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(config: AuthorizationConfig = {}) {
|
||||
super();
|
||||
|
||||
this.config = {
|
||||
enableInheritance: true,
|
||||
enableCache: true,
|
||||
cacheExpirationTime: 5 * 60 * 1000, // 5分钟
|
||||
defaultPolicy: 'deny',
|
||||
...config
|
||||
};
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建角色
|
||||
*/
|
||||
async createRole(roleData: {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
permissions: Permission[];
|
||||
parentRoleId?: string;
|
||||
metadata?: Record<string, NetworkValue>;
|
||||
}): Promise<Role> {
|
||||
const { id, name, description, permissions, parentRoleId, metadata = {} } = roleData;
|
||||
|
||||
if (this.roles.has(id)) {
|
||||
throw new Error(`Role with id "${id}" already exists`);
|
||||
}
|
||||
|
||||
// 验证父角色是否存在
|
||||
if (parentRoleId && !this.roles.has(parentRoleId)) {
|
||||
throw new Error(`Parent role "${parentRoleId}" not found`);
|
||||
}
|
||||
|
||||
const role: Role = {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
permissions: [...permissions],
|
||||
parentRoleId,
|
||||
isSystemRole: false,
|
||||
metadata,
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
this.roles.set(id, role);
|
||||
this.clearPermissionCache(); // 清除缓存
|
||||
|
||||
console.log(`Role created: ${name} (${id})`);
|
||||
this.emit('role-created', role);
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色
|
||||
*/
|
||||
getRole(roleId: string): Role | undefined {
|
||||
return this.roles.get(roleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有角色
|
||||
*/
|
||||
getAllRoles(): Role[] {
|
||||
return Array.from(this.roles.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新角色
|
||||
*/
|
||||
async updateRole(roleId: string, updates: Partial<Role>): Promise<boolean> {
|
||||
const role = this.roles.get(roleId);
|
||||
if (!role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 系统角色不允许修改某些字段
|
||||
if (role.isSystemRole) {
|
||||
const { permissions, parentRoleId, ...allowedUpdates } = updates;
|
||||
Object.assign(role, allowedUpdates);
|
||||
} else {
|
||||
// 不允许更新某些字段
|
||||
const { id, createdAt, isSystemRole, ...allowedUpdates } = updates as any;
|
||||
Object.assign(role, allowedUpdates);
|
||||
}
|
||||
|
||||
this.clearPermissionCache(); // 清除缓存
|
||||
|
||||
console.log(`Role updated: ${role.name} (${roleId})`);
|
||||
this.emit('role-updated', roleId, updates);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*/
|
||||
async deleteRole(roleId: string): Promise<boolean> {
|
||||
const role = this.roles.get(roleId);
|
||||
if (!role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (role.isSystemRole) {
|
||||
throw new Error('Cannot delete system role');
|
||||
}
|
||||
|
||||
// 检查是否有子角色依赖此角色
|
||||
const childRoles = Array.from(this.roles.values())
|
||||
.filter(r => r.parentRoleId === roleId);
|
||||
|
||||
if (childRoles.length > 0) {
|
||||
throw new Error(`Cannot delete role "${roleId}": ${childRoles.length} child roles depend on it`);
|
||||
}
|
||||
|
||||
this.roles.delete(roleId);
|
||||
this.clearPermissionCache(); // 清除缓存
|
||||
|
||||
console.log(`Role deleted: ${role.name} (${roleId})`);
|
||||
this.emit('role-deleted', roleId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查权限
|
||||
*/
|
||||
async checkPermission(context: PermissionContext): Promise<PermissionResult> {
|
||||
try {
|
||||
// 检查缓存
|
||||
const cacheKey = this.getCacheKey(context);
|
||||
if (this.config.enableCache) {
|
||||
const cached = this.permissionCache.get(cacheKey);
|
||||
if (cached && cached.expiresAt > new Date()) {
|
||||
return cached.result;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.performPermissionCheck(context);
|
||||
|
||||
// 缓存结果
|
||||
if (this.config.enableCache) {
|
||||
const expiresAt = new Date(Date.now() + this.config.cacheExpirationTime!);
|
||||
this.permissionCache.set(cacheKey, { result, expiresAt });
|
||||
}
|
||||
|
||||
// 触发事件
|
||||
if (result.granted) {
|
||||
this.emit('permission-granted', context, result);
|
||||
} else {
|
||||
this.emit('permission-denied', context, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
this.emit('authorization-error', error as Error, context);
|
||||
|
||||
return {
|
||||
granted: this.config.defaultPolicy === 'allow',
|
||||
reason: `Authorization error: ${(error as Error).message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有权限
|
||||
*/
|
||||
async hasPermission(user: UserInfo, permission: Permission, resourceId?: string): Promise<boolean> {
|
||||
const context: PermissionContext = {
|
||||
userId: user.id,
|
||||
userRoles: user.roles,
|
||||
permission,
|
||||
resourceId
|
||||
};
|
||||
|
||||
const result = await this.checkPermission(context);
|
||||
return result.granted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的所有权限
|
||||
*/
|
||||
async getUserPermissions(user: UserInfo): Promise<Permission[]> {
|
||||
const permissions = new Set<Permission>();
|
||||
|
||||
for (const roleId of user.roles) {
|
||||
const rolePermissions = await this.getRolePermissions(roleId);
|
||||
rolePermissions.forEach(p => permissions.add(p));
|
||||
}
|
||||
|
||||
return Array.from(permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色的所有权限(包括继承的权限)
|
||||
*/
|
||||
async getRolePermissions(roleId: string): Promise<Permission[]> {
|
||||
const permissions = new Set<Permission>();
|
||||
const visited = new Set<string>();
|
||||
|
||||
const collectPermissions = (currentRoleId: string) => {
|
||||
if (visited.has(currentRoleId)) {
|
||||
return; // 防止循环引用
|
||||
}
|
||||
visited.add(currentRoleId);
|
||||
|
||||
const role = this.roles.get(currentRoleId);
|
||||
if (!role) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加当前角色的权限
|
||||
role.permissions.forEach(p => permissions.add(p));
|
||||
|
||||
// 递归添加父角色的权限
|
||||
if (this.config.enableInheritance && role.parentRoleId) {
|
||||
collectPermissions(role.parentRoleId);
|
||||
}
|
||||
};
|
||||
|
||||
collectPermissions(roleId);
|
||||
return Array.from(permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为角色添加权限
|
||||
*/
|
||||
async addPermissionToRole(roleId: string, permission: Permission): Promise<boolean> {
|
||||
const role = this.roles.get(roleId);
|
||||
if (!role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!role.permissions.includes(permission)) {
|
||||
role.permissions.push(permission);
|
||||
this.clearPermissionCache();
|
||||
console.log(`Permission "${permission}" added to role "${roleId}"`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从角色移除权限
|
||||
*/
|
||||
async removePermissionFromRole(roleId: string, permission: Permission): Promise<boolean> {
|
||||
const role = this.roles.get(roleId);
|
||||
if (!role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const index = role.permissions.indexOf(permission);
|
||||
if (index !== -1) {
|
||||
role.permissions.splice(index, 1);
|
||||
this.clearPermissionCache();
|
||||
console.log(`Permission "${permission}" removed from role "${roleId}"`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有指定角色
|
||||
*/
|
||||
hasRole(user: UserInfo, roleId: string): boolean {
|
||||
return user.roles.includes(roleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为用户添加角色
|
||||
*/
|
||||
async addRoleToUser(user: UserInfo, roleId: string): Promise<boolean> {
|
||||
if (!this.roles.has(roleId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!user.roles.includes(roleId)) {
|
||||
user.roles.push(roleId);
|
||||
this.clearUserPermissionCache(user.id);
|
||||
console.log(`Role "${roleId}" added to user "${user.id}"`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从用户移除角色
|
||||
*/
|
||||
async removeRoleFromUser(user: UserInfo, roleId: string): Promise<boolean> {
|
||||
const index = user.roles.indexOf(roleId);
|
||||
if (index !== -1) {
|
||||
user.roles.splice(index, 1);
|
||||
this.clearUserPermissionCache(user.id);
|
||||
console.log(`Role "${roleId}" removed from user "${user.id}"`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除权限缓存
|
||||
*/
|
||||
clearPermissionCache(): void {
|
||||
this.permissionCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定用户的权限缓存
|
||||
*/
|
||||
clearUserPermissionCache(userId: string): void {
|
||||
const keysToDelete: string[] = [];
|
||||
|
||||
for (const [key] of this.permissionCache) {
|
||||
if (key.startsWith(`${userId}:`)) {
|
||||
keysToDelete.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
keysToDelete.forEach(key => this.permissionCache.delete(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁权限管理器
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.cleanupTimer) {
|
||||
clearInterval(this.cleanupTimer);
|
||||
this.cleanupTimer = null;
|
||||
}
|
||||
|
||||
this.roles.clear();
|
||||
this.permissionCache.clear();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
private initialize(): void {
|
||||
// 创建系统角色
|
||||
this.createSystemRoles();
|
||||
|
||||
// 启动缓存清理定时器(每30分钟清理一次)
|
||||
if (this.config.enableCache) {
|
||||
this.cleanupTimer = setInterval(() => {
|
||||
this.cleanupCache();
|
||||
}, 30 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建系统角色
|
||||
*/
|
||||
private createSystemRoles(): void {
|
||||
// 管理员角色
|
||||
const adminRole: Role = {
|
||||
id: SystemRoles.ADMIN,
|
||||
name: 'Administrator',
|
||||
description: 'Full system access',
|
||||
permissions: Object.values(Permissions),
|
||||
isSystemRole: true,
|
||||
metadata: {},
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
// 版主角色
|
||||
const moderatorRole: Role = {
|
||||
id: SystemRoles.MODERATOR,
|
||||
name: 'Moderator',
|
||||
description: 'Room and user management',
|
||||
permissions: [
|
||||
Permissions.USER_READ,
|
||||
Permissions.ROOM_CREATE,
|
||||
Permissions.ROOM_JOIN,
|
||||
Permissions.ROOM_MANAGE,
|
||||
Permissions.ROOM_KICK_PLAYERS,
|
||||
Permissions.NETWORK_SEND_RPC,
|
||||
Permissions.NETWORK_SYNC_VARS,
|
||||
Permissions.CHAT_SEND,
|
||||
Permissions.CHAT_MODERATE,
|
||||
Permissions.CHAT_PRIVATE
|
||||
],
|
||||
parentRoleId: SystemRoles.USER,
|
||||
isSystemRole: true,
|
||||
metadata: {},
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
// 普通用户角色
|
||||
const userRole: Role = {
|
||||
id: SystemRoles.USER,
|
||||
name: 'User',
|
||||
description: 'Basic user permissions',
|
||||
permissions: [
|
||||
Permissions.ROOM_JOIN,
|
||||
Permissions.ROOM_LEAVE,
|
||||
Permissions.NETWORK_SEND_RPC,
|
||||
Permissions.NETWORK_SYNC_VARS,
|
||||
Permissions.CHAT_SEND,
|
||||
Permissions.FILE_DOWNLOAD
|
||||
],
|
||||
parentRoleId: SystemRoles.GUEST,
|
||||
isSystemRole: true,
|
||||
metadata: {},
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
// 访客角色
|
||||
const guestRole: Role = {
|
||||
id: SystemRoles.GUEST,
|
||||
name: 'Guest',
|
||||
description: 'Limited access for guests',
|
||||
permissions: [
|
||||
Permissions.ROOM_JOIN
|
||||
],
|
||||
isSystemRole: true,
|
||||
metadata: {},
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
this.roles.set(adminRole.id, adminRole);
|
||||
this.roles.set(moderatorRole.id, moderatorRole);
|
||||
this.roles.set(userRole.id, userRole);
|
||||
this.roles.set(guestRole.id, guestRole);
|
||||
|
||||
console.log('System roles created');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行权限检查
|
||||
*/
|
||||
private async performPermissionCheck(context: PermissionContext): Promise<PermissionResult> {
|
||||
// 获取用户的所有角色权限
|
||||
const userPermissions = new Set<Permission>();
|
||||
|
||||
for (const roleId of context.userRoles) {
|
||||
const rolePermissions = await this.getRolePermissions(roleId);
|
||||
rolePermissions.forEach(p => userPermissions.add(p));
|
||||
}
|
||||
|
||||
// 直接权限匹配
|
||||
if (userPermissions.has(context.permission)) {
|
||||
return {
|
||||
granted: true,
|
||||
reason: 'Direct permission match',
|
||||
usedPermission: context.permission
|
||||
};
|
||||
}
|
||||
|
||||
// 通配符权限匹配
|
||||
const wildcardPermissions = Array.from(userPermissions)
|
||||
.filter(p => p.endsWith('*'));
|
||||
|
||||
for (const wildcardPerm of wildcardPermissions) {
|
||||
const prefix = wildcardPerm.slice(0, -1);
|
||||
if (context.permission.startsWith(prefix)) {
|
||||
return {
|
||||
granted: true,
|
||||
reason: 'Wildcard permission match',
|
||||
usedPermission: wildcardPerm
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有匹配的权限
|
||||
return {
|
||||
granted: this.config.defaultPolicy === 'allow',
|
||||
reason: this.config.defaultPolicy === 'allow'
|
||||
? 'Default allow policy'
|
||||
: 'No matching permissions found'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存键
|
||||
*/
|
||||
private getCacheKey(context: PermissionContext): string {
|
||||
const roleString = context.userRoles.sort().join(',');
|
||||
const resourcePart = context.resourceId ? `:${context.resourceId}` : '';
|
||||
return `${context.userId}:${roleString}:${context.permission}${resourcePart}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期缓存
|
||||
*/
|
||||
private cleanupCache(): void {
|
||||
const now = new Date();
|
||||
let cleanedCount = 0;
|
||||
|
||||
for (const [key, item] of this.permissionCache.entries()) {
|
||||
if (item.expiresAt < now) {
|
||||
this.permissionCache.delete(key);
|
||||
cleanedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedCount > 0) {
|
||||
console.log(`Permission cache cleanup: ${cleanedCount} entries removed`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
override on<K extends keyof AuthorizationEvents>(event: K, listener: AuthorizationEvents[K]): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
override emit<K extends keyof AuthorizationEvents>(event: K, ...args: Parameters<AuthorizationEvents[K]>): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* 认证系统导出
|
||||
*/
|
||||
|
||||
export * from './AuthenticationManager';
|
||||
export * from './AuthorizationManager';
|
||||
@@ -1,478 +0,0 @@
|
||||
/**
|
||||
* 客户端连接管理
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { NetworkValue, NetworkMessage } from '@esengine/ecs-framework-network-shared';
|
||||
import { TransportMessage } from './Transport';
|
||||
|
||||
/**
|
||||
* 客户端连接状态
|
||||
*/
|
||||
export enum ClientConnectionState {
|
||||
/** 连接中 */
|
||||
CONNECTING = 'connecting',
|
||||
/** 已连接 */
|
||||
CONNECTED = 'connected',
|
||||
/** 认证中 */
|
||||
AUTHENTICATING = 'authenticating',
|
||||
/** 已认证 */
|
||||
AUTHENTICATED = 'authenticated',
|
||||
/** 断开连接中 */
|
||||
DISCONNECTING = 'disconnecting',
|
||||
/** 已断开 */
|
||||
DISCONNECTED = 'disconnected',
|
||||
/** 错误状态 */
|
||||
ERROR = 'error'
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端权限
|
||||
*/
|
||||
export interface ClientPermissions {
|
||||
/** 是否可以加入房间 */
|
||||
canJoinRooms?: boolean;
|
||||
/** 是否可以创建房间 */
|
||||
canCreateRooms?: boolean;
|
||||
/** 是否可以发送RPC */
|
||||
canSendRpc?: boolean;
|
||||
/** 是否可以同步变量 */
|
||||
canSyncVars?: boolean;
|
||||
/** 自定义权限 */
|
||||
customPermissions?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端连接事件
|
||||
*/
|
||||
export interface ClientConnectionEvents {
|
||||
/** 状态变化 */
|
||||
'state-changed': (oldState: ClientConnectionState, newState: ClientConnectionState) => void;
|
||||
/** 收到消息 */
|
||||
'message': (message: TransportMessage) => void;
|
||||
/** 连接错误 */
|
||||
'error': (error: Error) => void;
|
||||
/** 连接超时 */
|
||||
'timeout': () => void;
|
||||
/** 身份验证成功 */
|
||||
'authenticated': (userData: Record<string, NetworkValue>) => void;
|
||||
/** 身份验证失败 */
|
||||
'authentication-failed': (reason: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端统计信息
|
||||
*/
|
||||
export interface ClientStats {
|
||||
/** 消息发送数 */
|
||||
messagesSent: number;
|
||||
/** 消息接收数 */
|
||||
messagesReceived: number;
|
||||
/** 字节发送数 */
|
||||
bytesSent: number;
|
||||
/** 字节接收数 */
|
||||
bytesReceived: number;
|
||||
/** 最后活跃时间 */
|
||||
lastActivity: Date;
|
||||
/** 连接时长(毫秒) */
|
||||
connectionDuration: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端连接管理类
|
||||
*/
|
||||
export class ClientConnection extends EventEmitter {
|
||||
/** 连接ID */
|
||||
public readonly id: string;
|
||||
|
||||
/** 客户端IP地址 */
|
||||
public readonly remoteAddress: string;
|
||||
|
||||
/** 连接创建时间 */
|
||||
public readonly connectedAt: Date;
|
||||
|
||||
/** 当前状态 */
|
||||
private _state: ClientConnectionState = ClientConnectionState.CONNECTING;
|
||||
|
||||
/** 用户数据 */
|
||||
private _userData: Record<string, NetworkValue> = {};
|
||||
|
||||
/** 权限信息 */
|
||||
private _permissions: ClientPermissions = {};
|
||||
|
||||
/** 所在房间ID */
|
||||
private _currentRoomId: string | null = null;
|
||||
|
||||
/** 统计信息 */
|
||||
private _stats: ClientStats;
|
||||
|
||||
/** 最后活跃时间 */
|
||||
private _lastActivity: Date;
|
||||
|
||||
/** 超时定时器 */
|
||||
private _timeoutTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
/** 连接超时时间(毫秒) */
|
||||
private _connectionTimeout: number;
|
||||
|
||||
/** 发送消息回调 */
|
||||
private _sendMessageCallback: (message: TransportMessage) => Promise<boolean>;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
remoteAddress: string,
|
||||
sendMessageCallback: (message: TransportMessage) => Promise<boolean>,
|
||||
options: {
|
||||
connectionTimeout?: number;
|
||||
userData?: Record<string, NetworkValue>;
|
||||
permissions?: ClientPermissions;
|
||||
} = {}
|
||||
) {
|
||||
super();
|
||||
|
||||
this.id = id;
|
||||
this.remoteAddress = remoteAddress;
|
||||
this.connectedAt = new Date();
|
||||
this._lastActivity = new Date();
|
||||
this._connectionTimeout = options.connectionTimeout || 60000; // 1分钟
|
||||
this._sendMessageCallback = sendMessageCallback;
|
||||
|
||||
if (options.userData) {
|
||||
this._userData = { ...options.userData };
|
||||
}
|
||||
|
||||
if (options.permissions) {
|
||||
this._permissions = { ...options.permissions };
|
||||
}
|
||||
|
||||
this._stats = {
|
||||
messagesSent: 0,
|
||||
messagesReceived: 0,
|
||||
bytesSent: 0,
|
||||
bytesReceived: 0,
|
||||
lastActivity: this._lastActivity,
|
||||
connectionDuration: 0
|
||||
};
|
||||
|
||||
this.setState(ClientConnectionState.CONNECTED);
|
||||
this.startTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前状态
|
||||
*/
|
||||
get state(): ClientConnectionState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户数据
|
||||
*/
|
||||
get userData(): Readonly<Record<string, NetworkValue>> {
|
||||
return this._userData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限信息
|
||||
*/
|
||||
get permissions(): Readonly<ClientPermissions> {
|
||||
return this._permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前房间ID
|
||||
*/
|
||||
get currentRoomId(): string | null {
|
||||
return this._currentRoomId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
get stats(): Readonly<ClientStats> {
|
||||
this._stats.connectionDuration = Date.now() - this.connectedAt.getTime();
|
||||
this._stats.lastActivity = this._lastActivity;
|
||||
return this._stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最后活跃时间
|
||||
*/
|
||||
get lastActivity(): Date {
|
||||
return this._lastActivity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已连接
|
||||
*/
|
||||
get isConnected(): boolean {
|
||||
return this._state === ClientConnectionState.CONNECTED ||
|
||||
this._state === ClientConnectionState.AUTHENTICATED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已认证
|
||||
*/
|
||||
get isAuthenticated(): boolean {
|
||||
return this._state === ClientConnectionState.AUTHENTICATED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
async sendMessage(message: TransportMessage): Promise<boolean> {
|
||||
if (!this.isConnected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const success = await this._sendMessageCallback(message);
|
||||
if (success) {
|
||||
this._stats.messagesSent++;
|
||||
const messageSize = JSON.stringify(message).length;
|
||||
this._stats.bytesSent += messageSize;
|
||||
this.updateActivity();
|
||||
}
|
||||
return success;
|
||||
} catch (error) {
|
||||
this.handleError(error as Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收到的消息
|
||||
*/
|
||||
handleMessage(message: TransportMessage): void {
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._stats.messagesReceived++;
|
||||
const messageSize = JSON.stringify(message).length;
|
||||
this._stats.bytesReceived += messageSize;
|
||||
this.updateActivity();
|
||||
|
||||
this.emit('message', message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户数据
|
||||
*/
|
||||
setUserData(key: string, value: NetworkValue): void {
|
||||
this._userData[key] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户数据
|
||||
*/
|
||||
getUserData<T extends NetworkValue = NetworkValue>(key: string): T | undefined {
|
||||
return this._userData[key] as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置用户数据
|
||||
*/
|
||||
setUserDataBatch(data: Record<string, NetworkValue>): void {
|
||||
Object.assign(this._userData, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置权限
|
||||
*/
|
||||
setPermission(permission: keyof ClientPermissions, value: boolean): void {
|
||||
(this._permissions as any)[permission] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查权限
|
||||
*/
|
||||
hasPermission(permission: keyof ClientPermissions): boolean {
|
||||
return (this._permissions as any)[permission] || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置自定义权限
|
||||
*/
|
||||
setCustomPermission(permission: string, value: boolean): void {
|
||||
if (!this._permissions.customPermissions) {
|
||||
this._permissions.customPermissions = {};
|
||||
}
|
||||
this._permissions.customPermissions[permission] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查自定义权限
|
||||
*/
|
||||
hasCustomPermission(permission: string): boolean {
|
||||
return this._permissions.customPermissions?.[permission] || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 进行身份认证
|
||||
*/
|
||||
async authenticate(credentials: Record<string, NetworkValue>): Promise<boolean> {
|
||||
if (this._state !== ClientConnectionState.CONNECTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.setState(ClientConnectionState.AUTHENTICATING);
|
||||
|
||||
try {
|
||||
// 这里可以添加实际的认证逻辑
|
||||
// 目前简单地认为所有认证都成功
|
||||
|
||||
this.setUserDataBatch(credentials);
|
||||
this.setState(ClientConnectionState.AUTHENTICATED);
|
||||
this.emit('authenticated', credentials);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.setState(ClientConnectionState.CONNECTED);
|
||||
this.emit('authentication-failed', (error as Error).message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入房间
|
||||
*/
|
||||
joinRoom(roomId: string): void {
|
||||
this._currentRoomId = roomId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 离开房间
|
||||
*/
|
||||
leaveRoom(): void {
|
||||
this._currentRoomId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
disconnect(reason?: string): void {
|
||||
if (this._state === ClientConnectionState.DISCONNECTED) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(ClientConnectionState.DISCONNECTING);
|
||||
this.stopTimeout();
|
||||
|
||||
// 发送断开连接消息
|
||||
this.sendMessage({
|
||||
type: 'system',
|
||||
data: {
|
||||
action: 'disconnect',
|
||||
reason: reason || 'server-disconnect'
|
||||
}
|
||||
}).finally(() => {
|
||||
this.setState(ClientConnectionState.DISCONNECTED);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新活跃时间
|
||||
*/
|
||||
updateActivity(): void {
|
||||
this._lastActivity = new Date();
|
||||
this.resetTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置连接状态
|
||||
*/
|
||||
private setState(newState: ClientConnectionState): void {
|
||||
const oldState = this._state;
|
||||
if (oldState !== newState) {
|
||||
this._state = newState;
|
||||
this.emit('state-changed', oldState, newState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理错误
|
||||
*/
|
||||
private handleError(error: Error): void {
|
||||
this.setState(ClientConnectionState.ERROR);
|
||||
this.emit('error', error);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动超时检测
|
||||
*/
|
||||
private startTimeout(): void {
|
||||
this.resetTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置超时定时器
|
||||
*/
|
||||
private resetTimeout(): void {
|
||||
this.stopTimeout();
|
||||
|
||||
if (this._connectionTimeout > 0) {
|
||||
this._timeoutTimer = setTimeout(() => {
|
||||
this.handleTimeout();
|
||||
}, this._connectionTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止超时检测
|
||||
*/
|
||||
private stopTimeout(): void {
|
||||
if (this._timeoutTimer) {
|
||||
clearTimeout(this._timeoutTimer);
|
||||
this._timeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理超时
|
||||
*/
|
||||
private handleTimeout(): void {
|
||||
this.emit('timeout');
|
||||
this.disconnect('timeout');
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁连接
|
||||
*/
|
||||
destroy(): void {
|
||||
this.stopTimeout();
|
||||
this.removeAllListeners();
|
||||
this.setState(ClientConnectionState.DISCONNECTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
override on<K extends keyof ClientConnectionEvents>(event: K, listener: ClientConnectionEvents[K]): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
override emit<K extends keyof ClientConnectionEvents>(event: K, ...args: Parameters<ClientConnectionEvents[K]>): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化连接信息
|
||||
*/
|
||||
toJSON(): object {
|
||||
return {
|
||||
id: this.id,
|
||||
remoteAddress: this.remoteAddress,
|
||||
state: this._state,
|
||||
connectedAt: this.connectedAt.toISOString(),
|
||||
lastActivity: this._lastActivity.toISOString(),
|
||||
currentRoomId: this._currentRoomId,
|
||||
userData: this._userData,
|
||||
permissions: this._permissions,
|
||||
stats: this.stats
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,602 +0,0 @@
|
||||
/**
|
||||
* HTTP 传输层实现
|
||||
*
|
||||
* 用于处理 REST API 请求和长轮询连接
|
||||
*/
|
||||
|
||||
import { createServer, IncomingMessage, ServerResponse, Server as HttpServer } from 'http';
|
||||
import { parse as parseUrl } from 'url';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Transport, TransportConfig, ClientConnectionInfo, TransportMessage } from './Transport';
|
||||
|
||||
/**
|
||||
* HTTP 传输配置
|
||||
*/
|
||||
export interface HttpTransportConfig extends TransportConfig {
|
||||
/** API 路径前缀 */
|
||||
apiPrefix?: string;
|
||||
/** 最大请求大小(字节) */
|
||||
maxRequestSize?: number;
|
||||
/** 长轮询超时(毫秒) */
|
||||
longPollTimeout?: number;
|
||||
/** 是否启用 CORS */
|
||||
enableCors?: boolean;
|
||||
/** 允许的域名 */
|
||||
corsOrigins?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 请求上下文
|
||||
*/
|
||||
interface HttpRequestContext {
|
||||
/** 请求ID */
|
||||
id: string;
|
||||
/** HTTP 请求 */
|
||||
request: IncomingMessage;
|
||||
/** HTTP 响应 */
|
||||
response: ServerResponse;
|
||||
/** 解析后的URL */
|
||||
parsedUrl: any;
|
||||
/** 请求体数据 */
|
||||
body?: string;
|
||||
/** 查询参数 */
|
||||
query: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 客户端连接信息(用于长轮询)
|
||||
*/
|
||||
interface HttpConnectionInfo extends ClientConnectionInfo {
|
||||
/** 长轮询响应对象 */
|
||||
longPollResponse?: ServerResponse;
|
||||
/** 消息队列 */
|
||||
messageQueue: TransportMessage[];
|
||||
/** 长轮询超时定时器 */
|
||||
longPollTimer?: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 传输层实现
|
||||
*/
|
||||
export class HttpTransport extends Transport {
|
||||
private httpServer: HttpServer | null = null;
|
||||
private httpConnections = new Map<string, HttpConnectionInfo>();
|
||||
|
||||
protected override config: HttpTransportConfig;
|
||||
|
||||
constructor(config: HttpTransportConfig) {
|
||||
super(config);
|
||||
this.config = {
|
||||
apiPrefix: '/api',
|
||||
maxRequestSize: 1024 * 1024, // 1MB
|
||||
longPollTimeout: 30000, // 30秒
|
||||
enableCors: true,
|
||||
corsOrigins: ['*'],
|
||||
heartbeatInterval: 60000,
|
||||
connectionTimeout: 120000,
|
||||
maxConnections: 1000,
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动 HTTP 服务器
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
if (this.isRunning) {
|
||||
throw new Error('HTTP transport is already running');
|
||||
}
|
||||
|
||||
try {
|
||||
this.httpServer = createServer((req, res) => {
|
||||
this.handleHttpRequest(req, res);
|
||||
});
|
||||
|
||||
this.httpServer.on('error', (error: Error) => {
|
||||
this.handleError(error);
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this.httpServer!.listen(this.config.port, this.config.host, (error?: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
this.isRunning = true;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.emit('server-started', this.config);
|
||||
} catch (error) {
|
||||
await this.cleanup();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止 HTTP 服务器
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
if (!this.isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRunning = false;
|
||||
|
||||
// 断开所有长轮询连接
|
||||
for (const [connectionId] of this.httpConnections) {
|
||||
this.disconnectClient(connectionId, 'server-shutdown');
|
||||
}
|
||||
|
||||
await this.cleanup();
|
||||
this.emit('server-stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给指定客户端
|
||||
*/
|
||||
async sendToClient(connectionId: string, message: TransportMessage): Promise<boolean> {
|
||||
const connection = this.httpConnections.get(connectionId);
|
||||
if (!connection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果有长轮询连接,直接发送
|
||||
if (connection.longPollResponse && !connection.longPollResponse.headersSent) {
|
||||
this.sendLongPollResponse(connection, [message]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 否则加入消息队列
|
||||
connection.messageQueue.push(message);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播消息给所有客户端
|
||||
*/
|
||||
async broadcast(message: TransportMessage, excludeId?: string): Promise<number> {
|
||||
let sentCount = 0;
|
||||
|
||||
for (const [connectionId, connection] of this.httpConnections) {
|
||||
if (excludeId && connectionId === excludeId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await this.sendToClient(connectionId, message)) {
|
||||
sentCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return sentCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给指定客户端列表
|
||||
*/
|
||||
async sendToClients(connectionIds: string[], message: TransportMessage): Promise<number> {
|
||||
let sentCount = 0;
|
||||
|
||||
for (const connectionId of connectionIds) {
|
||||
if (await this.sendToClient(connectionId, message)) {
|
||||
sentCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return sentCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开指定客户端连接
|
||||
*/
|
||||
async disconnectClient(connectionId: string, reason?: string): Promise<void> {
|
||||
const connection = this.httpConnections.get(connectionId);
|
||||
if (connection) {
|
||||
this.cleanupConnection(connectionId);
|
||||
this.removeConnection(connectionId, reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 HTTP 请求
|
||||
*/
|
||||
private async handleHttpRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
||||
try {
|
||||
// 设置 CORS 头
|
||||
if (this.config.enableCors) {
|
||||
this.setCorsHeaders(res);
|
||||
}
|
||||
|
||||
// 处理 OPTIONS 请求
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedUrl = parseUrl(req.url || '', true);
|
||||
const pathname = parsedUrl.pathname || '';
|
||||
|
||||
// 检查是否为 API 请求
|
||||
if (!pathname.startsWith(this.config.apiPrefix!)) {
|
||||
this.sendErrorResponse(res, 404, 'Not Found');
|
||||
return;
|
||||
}
|
||||
|
||||
const context: HttpRequestContext = {
|
||||
id: uuidv4(),
|
||||
request: req,
|
||||
response: res,
|
||||
parsedUrl,
|
||||
query: parsedUrl.query as Record<string, string>,
|
||||
};
|
||||
|
||||
// 读取请求体
|
||||
if (req.method === 'POST' || req.method === 'PUT') {
|
||||
context.body = await this.readRequestBody(req);
|
||||
}
|
||||
|
||||
// 路由处理
|
||||
const apiPath = pathname.substring(this.config.apiPrefix!.length);
|
||||
await this.routeApiRequest(context, apiPath);
|
||||
|
||||
} catch (error) {
|
||||
this.handleError(error as Error);
|
||||
this.sendErrorResponse(res, 500, 'Internal Server Error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API 路由处理
|
||||
*/
|
||||
private async routeApiRequest(context: HttpRequestContext, apiPath: string): Promise<void> {
|
||||
const { request, response } = context;
|
||||
|
||||
switch (apiPath) {
|
||||
case '/connect':
|
||||
if (request.method === 'POST') {
|
||||
await this.handleConnect(context);
|
||||
} else {
|
||||
this.sendErrorResponse(response, 405, 'Method Not Allowed');
|
||||
}
|
||||
break;
|
||||
|
||||
case '/disconnect':
|
||||
if (request.method === 'POST') {
|
||||
await this.handleDisconnect(context);
|
||||
} else {
|
||||
this.sendErrorResponse(response, 405, 'Method Not Allowed');
|
||||
}
|
||||
break;
|
||||
|
||||
case '/poll':
|
||||
if (request.method === 'GET') {
|
||||
await this.handleLongPoll(context);
|
||||
} else {
|
||||
this.sendErrorResponse(response, 405, 'Method Not Allowed');
|
||||
}
|
||||
break;
|
||||
|
||||
case '/send':
|
||||
if (request.method === 'POST') {
|
||||
await this.handleSendMessage(context);
|
||||
} else {
|
||||
this.sendErrorResponse(response, 405, 'Method Not Allowed');
|
||||
}
|
||||
break;
|
||||
|
||||
case '/status':
|
||||
if (request.method === 'GET') {
|
||||
await this.handleStatus(context);
|
||||
} else {
|
||||
this.sendErrorResponse(response, 405, 'Method Not Allowed');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
this.sendErrorResponse(response, 404, 'API endpoint not found');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理连接请求
|
||||
*/
|
||||
private async handleConnect(context: HttpRequestContext): Promise<void> {
|
||||
const { request, response } = context;
|
||||
|
||||
try {
|
||||
// 检查连接数限制
|
||||
if (this.config.maxConnections && this.httpConnections.size >= this.config.maxConnections) {
|
||||
this.sendErrorResponse(response, 429, 'Too many connections');
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionId = uuidv4();
|
||||
const remoteAddress = request.socket.remoteAddress || request.headers['x-forwarded-for'] || 'unknown';
|
||||
|
||||
const connectionInfo: HttpConnectionInfo = {
|
||||
id: connectionId,
|
||||
remoteAddress: Array.isArray(remoteAddress) ? remoteAddress[0] : remoteAddress,
|
||||
connectedAt: new Date(),
|
||||
lastActivity: new Date(),
|
||||
userData: {},
|
||||
messageQueue: []
|
||||
};
|
||||
|
||||
this.httpConnections.set(connectionId, connectionInfo);
|
||||
this.addConnection(connectionInfo);
|
||||
|
||||
this.sendJsonResponse(response, 200, {
|
||||
success: true,
|
||||
connectionId,
|
||||
serverTime: Date.now()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
this.handleError(error as Error);
|
||||
this.sendErrorResponse(response, 500, 'Failed to create connection');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理断开连接请求
|
||||
*/
|
||||
private async handleDisconnect(context: HttpRequestContext): Promise<void> {
|
||||
const { response, query } = context;
|
||||
|
||||
const connectionId = query.connectionId;
|
||||
if (!connectionId) {
|
||||
this.sendErrorResponse(response, 400, 'Missing connectionId');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.disconnectClient(connectionId, 'client-disconnect');
|
||||
|
||||
this.sendJsonResponse(response, 200, {
|
||||
success: true,
|
||||
message: 'Disconnected successfully'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理长轮询请求
|
||||
*/
|
||||
private async handleLongPoll(context: HttpRequestContext): Promise<void> {
|
||||
const { response, query } = context;
|
||||
|
||||
const connectionId = query.connectionId;
|
||||
if (!connectionId) {
|
||||
this.sendErrorResponse(response, 400, 'Missing connectionId');
|
||||
return;
|
||||
}
|
||||
|
||||
const connection = this.httpConnections.get(connectionId);
|
||||
if (!connection) {
|
||||
this.sendErrorResponse(response, 404, 'Connection not found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateClientActivity(connectionId);
|
||||
|
||||
// 如果有排队的消息,立即返回
|
||||
if (connection.messageQueue.length > 0) {
|
||||
const messages = connection.messageQueue.splice(0);
|
||||
this.sendLongPollResponse(connection, messages);
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置长轮询
|
||||
connection.longPollResponse = response;
|
||||
|
||||
// 设置超时
|
||||
connection.longPollTimer = setTimeout(() => {
|
||||
this.sendLongPollResponse(connection, []);
|
||||
}, this.config.longPollTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理发送消息请求
|
||||
*/
|
||||
private async handleSendMessage(context: HttpRequestContext): Promise<void> {
|
||||
const { response, query, body } = context;
|
||||
|
||||
const connectionId = query.connectionId;
|
||||
if (!connectionId) {
|
||||
this.sendErrorResponse(response, 400, 'Missing connectionId');
|
||||
return;
|
||||
}
|
||||
|
||||
const connection = this.httpConnections.get(connectionId);
|
||||
if (!connection) {
|
||||
this.sendErrorResponse(response, 404, 'Connection not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!body) {
|
||||
this.sendErrorResponse(response, 400, 'Missing message body');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const message = JSON.parse(body) as TransportMessage;
|
||||
message.senderId = connectionId;
|
||||
|
||||
this.handleMessage(connectionId, message);
|
||||
|
||||
this.sendJsonResponse(response, 200, {
|
||||
success: true,
|
||||
message: 'Message sent successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
this.sendErrorResponse(response, 400, 'Invalid message format');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理状态请求
|
||||
*/
|
||||
private async handleStatus(context: HttpRequestContext): Promise<void> {
|
||||
const { response } = context;
|
||||
|
||||
this.sendJsonResponse(response, 200, {
|
||||
success: true,
|
||||
status: 'running',
|
||||
connections: this.httpConnections.size,
|
||||
uptime: process.uptime(),
|
||||
serverTime: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取请求体
|
||||
*/
|
||||
private readRequestBody(req: IncomingMessage): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = '';
|
||||
let totalSize = 0;
|
||||
|
||||
req.on('data', (chunk: Buffer) => {
|
||||
totalSize += chunk.length;
|
||||
if (totalSize > this.config.maxRequestSize!) {
|
||||
reject(new Error('Request body too large'));
|
||||
return;
|
||||
}
|
||||
body += chunk.toString();
|
||||
});
|
||||
|
||||
req.on('end', () => {
|
||||
resolve(body);
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送长轮询响应
|
||||
*/
|
||||
private sendLongPollResponse(connection: HttpConnectionInfo, messages: TransportMessage[]): void {
|
||||
if (!connection.longPollResponse || connection.longPollResponse.headersSent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 清理定时器
|
||||
if (connection.longPollTimer) {
|
||||
clearTimeout(connection.longPollTimer);
|
||||
connection.longPollTimer = undefined;
|
||||
}
|
||||
|
||||
this.sendJsonResponse(connection.longPollResponse, 200, {
|
||||
success: true,
|
||||
messages
|
||||
});
|
||||
|
||||
connection.longPollResponse = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 CORS 头
|
||||
*/
|
||||
private setCorsHeaders(res: ServerResponse): void {
|
||||
const origins = this.config.corsOrigins!;
|
||||
const origin = origins.includes('*') ? '*' : origins[0];
|
||||
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
res.setHeader('Access-Control-Max-Age', '86400');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 JSON 响应
|
||||
*/
|
||||
private sendJsonResponse(res: ServerResponse, statusCode: number, data: any): void {
|
||||
if (res.headersSent) return;
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.writeHead(statusCode);
|
||||
res.end(JSON.stringify(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送错误响应
|
||||
*/
|
||||
private sendErrorResponse(res: ServerResponse, statusCode: number, message: string): void {
|
||||
if (res.headersSent) return;
|
||||
|
||||
this.sendJsonResponse(res, statusCode, {
|
||||
success: false,
|
||||
error: message,
|
||||
code: statusCode
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理连接资源
|
||||
*/
|
||||
private cleanupConnection(connectionId: string): void {
|
||||
const connection = this.httpConnections.get(connectionId);
|
||||
if (connection) {
|
||||
if (connection.longPollTimer) {
|
||||
clearTimeout(connection.longPollTimer);
|
||||
}
|
||||
if (connection.longPollResponse && !connection.longPollResponse.headersSent) {
|
||||
this.sendJsonResponse(connection.longPollResponse, 200, {
|
||||
success: true,
|
||||
messages: [],
|
||||
disconnected: true
|
||||
});
|
||||
}
|
||||
this.httpConnections.delete(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有资源
|
||||
*/
|
||||
private async cleanup(): Promise<void> {
|
||||
// 清理所有连接
|
||||
for (const connectionId of this.httpConnections.keys()) {
|
||||
this.cleanupConnection(connectionId);
|
||||
}
|
||||
this.clearConnections();
|
||||
|
||||
// 关闭 HTTP 服务器
|
||||
if (this.httpServer) {
|
||||
await new Promise<void>((resolve) => {
|
||||
this.httpServer!.close(() => resolve());
|
||||
});
|
||||
this.httpServer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 HTTP 连接统计信息
|
||||
*/
|
||||
getHttpStats(): {
|
||||
totalConnections: number;
|
||||
activeLongPolls: number;
|
||||
queuedMessages: number;
|
||||
} {
|
||||
let activeLongPolls = 0;
|
||||
let queuedMessages = 0;
|
||||
|
||||
for (const connection of this.httpConnections.values()) {
|
||||
if (connection.longPollResponse && !connection.longPollResponse.headersSent) {
|
||||
activeLongPolls++;
|
||||
}
|
||||
queuedMessages += connection.messageQueue.length;
|
||||
}
|
||||
|
||||
return {
|
||||
totalConnections: this.httpConnections.size,
|
||||
activeLongPolls,
|
||||
queuedMessages
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,452 +0,0 @@
|
||||
/**
|
||||
* 网络服务器主类
|
||||
*
|
||||
* 整合 WebSocket 和 HTTP 传输,提供统一的网络服务接口
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { Transport, TransportConfig, TransportMessage } from './Transport';
|
||||
import { WebSocketTransport, WebSocketTransportConfig } from './WebSocketTransport';
|
||||
import { HttpTransport, HttpTransportConfig } from './HttpTransport';
|
||||
import { ClientConnection, ClientConnectionState, ClientPermissions } from './ClientConnection';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
|
||||
/**
|
||||
* 网络服务器配置
|
||||
*/
|
||||
export interface NetworkServerConfig {
|
||||
/** 服务器名称 */
|
||||
name?: string;
|
||||
/** WebSocket 配置 */
|
||||
websocket?: WebSocketTransportConfig;
|
||||
/** HTTP 配置 */
|
||||
http?: HttpTransportConfig;
|
||||
/** 默认客户端权限 */
|
||||
defaultPermissions?: ClientPermissions;
|
||||
/** 最大客户端连接数 */
|
||||
maxConnections?: number;
|
||||
/** 客户端认证超时(毫秒) */
|
||||
authenticationTimeout?: number;
|
||||
/** 是否启用统计 */
|
||||
enableStats?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务器统计信息
|
||||
*/
|
||||
export interface ServerStats {
|
||||
/** 总连接数 */
|
||||
totalConnections: number;
|
||||
/** 当前活跃连接数 */
|
||||
activeConnections: number;
|
||||
/** 已认证连接数 */
|
||||
authenticatedConnections: number;
|
||||
/** 消息总数 */
|
||||
totalMessages: number;
|
||||
/** 错误总数 */
|
||||
totalErrors: number;
|
||||
/** 服务器启动时间 */
|
||||
startTime: Date;
|
||||
/** 服务器运行时间(毫秒) */
|
||||
uptime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络服务器事件
|
||||
*/
|
||||
export interface NetworkServerEvents {
|
||||
/** 服务器启动 */
|
||||
'server-started': () => void;
|
||||
/** 服务器停止 */
|
||||
'server-stopped': () => void;
|
||||
/** 客户端连接 */
|
||||
'client-connected': (client: ClientConnection) => void;
|
||||
/** 客户端断开连接 */
|
||||
'client-disconnected': (clientId: string, reason?: string) => void;
|
||||
/** 客户端认证成功 */
|
||||
'client-authenticated': (client: ClientConnection) => void;
|
||||
/** 收到消息 */
|
||||
'message': (client: ClientConnection, message: TransportMessage) => void;
|
||||
/** 服务器错误 */
|
||||
'error': (error: Error, clientId?: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络服务器主类
|
||||
*/
|
||||
export class NetworkServer extends EventEmitter {
|
||||
private config: NetworkServerConfig;
|
||||
private wsTransport: WebSocketTransport | null = null;
|
||||
private httpTransport: HttpTransport | null = null;
|
||||
private clients = new Map<string, ClientConnection>();
|
||||
private isRunning = false;
|
||||
private stats: ServerStats;
|
||||
|
||||
constructor(config: NetworkServerConfig) {
|
||||
super();
|
||||
|
||||
this.config = {
|
||||
name: 'NetworkServer',
|
||||
maxConnections: 1000,
|
||||
authenticationTimeout: 30000, // 30秒
|
||||
enableStats: true,
|
||||
defaultPermissions: {
|
||||
canJoinRooms: true,
|
||||
canCreateRooms: false,
|
||||
canSendRpc: true,
|
||||
canSyncVars: true
|
||||
},
|
||||
...config
|
||||
};
|
||||
|
||||
this.stats = {
|
||||
totalConnections: 0,
|
||||
activeConnections: 0,
|
||||
authenticatedConnections: 0,
|
||||
totalMessages: 0,
|
||||
totalErrors: 0,
|
||||
startTime: new Date(),
|
||||
uptime: 0
|
||||
};
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动服务器
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
if (this.isRunning) {
|
||||
throw new Error('Server is already running');
|
||||
}
|
||||
|
||||
try {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
// 启动 WebSocket 传输
|
||||
if (this.config.websocket && this.wsTransport) {
|
||||
promises.push(this.wsTransport.start());
|
||||
}
|
||||
|
||||
// 启动 HTTP 传输
|
||||
if (this.config.http && this.httpTransport) {
|
||||
promises.push(this.httpTransport.start());
|
||||
}
|
||||
|
||||
if (promises.length === 0) {
|
||||
throw new Error('No transport configured. Please configure at least one transport (WebSocket or HTTP)');
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
this.isRunning = true;
|
||||
this.stats.startTime = new Date();
|
||||
|
||||
console.log(`Network Server "${this.config.name}" started successfully`);
|
||||
if (this.config.websocket) {
|
||||
console.log(`- WebSocket: ws://${this.config.websocket.host || 'localhost'}:${this.config.websocket.port}${this.config.websocket.path || '/ws'}`);
|
||||
}
|
||||
if (this.config.http) {
|
||||
console.log(`- HTTP: http://${this.config.http.host || 'localhost'}:${this.config.http.port}${this.config.http.apiPrefix || '/api'}`);
|
||||
}
|
||||
|
||||
this.emit('server-started');
|
||||
|
||||
} catch (error) {
|
||||
await this.stop();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止服务器
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
if (!this.isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRunning = false;
|
||||
|
||||
// 断开所有客户端
|
||||
const clients = Array.from(this.clients.values());
|
||||
for (const client of clients) {
|
||||
client.disconnect('server-shutdown');
|
||||
}
|
||||
|
||||
// 停止传输层
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
if (this.wsTransport) {
|
||||
promises.push(this.wsTransport.stop());
|
||||
}
|
||||
|
||||
if (this.httpTransport) {
|
||||
promises.push(this.httpTransport.stop());
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
console.log(`Network Server "${this.config.name}" stopped`);
|
||||
this.emit('server-stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器配置
|
||||
*/
|
||||
getConfig(): Readonly<NetworkServerConfig> {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器统计信息
|
||||
*/
|
||||
getStats(): ServerStats {
|
||||
this.stats.uptime = Date.now() - this.stats.startTime.getTime();
|
||||
this.stats.activeConnections = this.clients.size;
|
||||
this.stats.authenticatedConnections = Array.from(this.clients.values())
|
||||
.filter(client => client.isAuthenticated).length;
|
||||
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有客户端连接
|
||||
*/
|
||||
getClients(): ClientConnection[] {
|
||||
return Array.from(this.clients.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定客户端连接
|
||||
*/
|
||||
getClient(clientId: string): ClientConnection | undefined {
|
||||
return this.clients.get(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查客户端是否存在
|
||||
*/
|
||||
hasClient(clientId: string): boolean {
|
||||
return this.clients.has(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端数量
|
||||
*/
|
||||
getClientCount(): number {
|
||||
return this.clients.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给指定客户端
|
||||
*/
|
||||
async sendToClient(clientId: string, message: TransportMessage): Promise<boolean> {
|
||||
const client = this.clients.get(clientId);
|
||||
if (!client) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await client.sendMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播消息给所有客户端
|
||||
*/
|
||||
async broadcast(message: TransportMessage, excludeId?: string): Promise<number> {
|
||||
const promises = Array.from(this.clients.entries())
|
||||
.filter(([clientId]) => clientId !== excludeId)
|
||||
.map(([, client]) => client.sendMessage(message));
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
return results.filter(result => result.status === 'fulfilled' && result.value).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给指定房间的所有客户端
|
||||
*/
|
||||
async broadcastToRoom(roomId: string, message: TransportMessage, excludeId?: string): Promise<number> {
|
||||
const roomClients = Array.from(this.clients.values())
|
||||
.filter(client => client.currentRoomId === roomId && client.id !== excludeId);
|
||||
|
||||
const promises = roomClients.map(client => client.sendMessage(message));
|
||||
const results = await Promise.allSettled(promises);
|
||||
|
||||
return results.filter(result => result.status === 'fulfilled' && result.value).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开指定客户端连接
|
||||
*/
|
||||
async disconnectClient(clientId: string, reason?: string): Promise<void> {
|
||||
const client = this.clients.get(clientId);
|
||||
if (client) {
|
||||
client.disconnect(reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取在指定房间的客户端列表
|
||||
*/
|
||||
getClientsInRoom(roomId: string): ClientConnection[] {
|
||||
return Array.from(this.clients.values())
|
||||
.filter(client => client.currentRoomId === roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查服务器是否正在运行
|
||||
*/
|
||||
isServerRunning(): boolean {
|
||||
return this.isRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化服务器
|
||||
*/
|
||||
private initialize(): void {
|
||||
// 初始化 WebSocket 传输
|
||||
if (this.config.websocket) {
|
||||
this.wsTransport = new WebSocketTransport(this.config.websocket);
|
||||
this.setupTransportEvents(this.wsTransport);
|
||||
}
|
||||
|
||||
// 初始化 HTTP 传输
|
||||
if (this.config.http) {
|
||||
this.httpTransport = new HttpTransport(this.config.http);
|
||||
this.setupTransportEvents(this.httpTransport);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置传输层事件监听
|
||||
*/
|
||||
private setupTransportEvents(transport: Transport): void {
|
||||
transport.on('client-connected', (connectionInfo) => {
|
||||
this.handleClientConnected(connectionInfo.id, connectionInfo.remoteAddress || 'unknown', transport);
|
||||
});
|
||||
|
||||
transport.on('client-disconnected', (connectionId, reason) => {
|
||||
this.handleClientDisconnected(connectionId, reason);
|
||||
});
|
||||
|
||||
transport.on('message', (connectionId, message) => {
|
||||
this.handleTransportMessage(connectionId, message);
|
||||
});
|
||||
|
||||
transport.on('error', (error, connectionId) => {
|
||||
this.handleTransportError(error, connectionId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端连接
|
||||
*/
|
||||
private handleClientConnected(connectionId: string, remoteAddress: string, transport: Transport): void {
|
||||
// 检查连接数限制
|
||||
if (this.config.maxConnections && this.clients.size >= this.config.maxConnections) {
|
||||
transport.disconnectClient(connectionId, 'Max connections reached');
|
||||
return;
|
||||
}
|
||||
|
||||
const client = new ClientConnection(
|
||||
connectionId,
|
||||
remoteAddress,
|
||||
(message) => transport.sendToClient(connectionId, message),
|
||||
{
|
||||
connectionTimeout: this.config.authenticationTimeout,
|
||||
permissions: this.config.defaultPermissions
|
||||
}
|
||||
);
|
||||
|
||||
// 设置客户端事件监听
|
||||
this.setupClientEvents(client);
|
||||
|
||||
this.clients.set(connectionId, client);
|
||||
this.stats.totalConnections++;
|
||||
|
||||
console.log(`Client connected: ${connectionId} from ${remoteAddress}`);
|
||||
this.emit('client-connected', client);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端断开连接
|
||||
*/
|
||||
private handleClientDisconnected(connectionId: string, reason?: string): void {
|
||||
const client = this.clients.get(connectionId);
|
||||
if (client) {
|
||||
client.destroy();
|
||||
this.clients.delete(connectionId);
|
||||
|
||||
console.log(`Client disconnected: ${connectionId}, reason: ${reason || 'unknown'}`);
|
||||
this.emit('client-disconnected', connectionId, reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理传输层消息
|
||||
*/
|
||||
private handleTransportMessage(connectionId: string, message: TransportMessage): void {
|
||||
const client = this.clients.get(connectionId);
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.handleMessage(message);
|
||||
this.stats.totalMessages++;
|
||||
|
||||
this.emit('message', client, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理传输层错误
|
||||
*/
|
||||
private handleTransportError(error: Error, connectionId?: string): void {
|
||||
this.stats.totalErrors++;
|
||||
|
||||
console.error(`Transport error${connectionId ? ` (client: ${connectionId})` : ''}:`, error.message);
|
||||
this.emit('error', error, connectionId);
|
||||
|
||||
// 如果是特定客户端的错误,断开该客户端
|
||||
if (connectionId) {
|
||||
this.disconnectClient(connectionId, 'transport-error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置客户端事件监听
|
||||
*/
|
||||
private setupClientEvents(client: ClientConnection): void {
|
||||
client.on('authenticated', (userData) => {
|
||||
console.log(`Client authenticated: ${client.id}`, userData);
|
||||
this.emit('client-authenticated', client);
|
||||
});
|
||||
|
||||
client.on('error', (error) => {
|
||||
console.error(`Client error (${client.id}):`, error.message);
|
||||
this.emit('error', error, client.id);
|
||||
});
|
||||
|
||||
client.on('timeout', () => {
|
||||
console.log(`Client timeout: ${client.id}`);
|
||||
this.disconnectClient(client.id, 'timeout');
|
||||
});
|
||||
|
||||
client.on('state-changed', (oldState, newState) => {
|
||||
console.log(`Client ${client.id} state changed: ${oldState} -> ${newState}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
override on<K extends keyof NetworkServerEvents>(event: K, listener: NetworkServerEvents[K]): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
override emit<K extends keyof NetworkServerEvents>(event: K, ...args: Parameters<NetworkServerEvents[K]>): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
/**
|
||||
* 网络传输层抽象接口
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { NetworkMessage, NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
|
||||
/**
|
||||
* 传输层配置
|
||||
*/
|
||||
export interface TransportConfig {
|
||||
/** 服务器端口 */
|
||||
port: number;
|
||||
/** 主机地址 */
|
||||
host?: string;
|
||||
/** 最大连接数 */
|
||||
maxConnections?: number;
|
||||
/** 心跳间隔(毫秒) */
|
||||
heartbeatInterval?: number;
|
||||
/** 连接超时(毫秒) */
|
||||
connectionTimeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端连接信息
|
||||
*/
|
||||
export interface ClientConnectionInfo {
|
||||
/** 连接ID */
|
||||
id: string;
|
||||
/** 客户端IP */
|
||||
remoteAddress?: string;
|
||||
/** 连接时间 */
|
||||
connectedAt: Date;
|
||||
/** 最后活跃时间 */
|
||||
lastActivity: Date;
|
||||
/** 用户数据 */
|
||||
userData?: Record<string, NetworkValue>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络消息包装
|
||||
*/
|
||||
export interface TransportMessage {
|
||||
/** 消息类型 */
|
||||
type: 'rpc' | 'syncvar' | 'system' | 'custom';
|
||||
/** 消息数据 */
|
||||
data: NetworkValue;
|
||||
/** 发送者ID */
|
||||
senderId?: string;
|
||||
/** 目标客户端ID(可选,用于单播) */
|
||||
targetId?: string;
|
||||
/** 是否可靠传输 */
|
||||
reliable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络传输层事件
|
||||
*/
|
||||
export interface TransportEvents {
|
||||
/** 客户端连接 */
|
||||
'client-connected': (connectionInfo: ClientConnectionInfo) => void;
|
||||
/** 客户端断开连接 */
|
||||
'client-disconnected': (connectionId: string, reason?: string) => void;
|
||||
/** 收到消息 */
|
||||
'message': (connectionId: string, message: TransportMessage) => void;
|
||||
/** 传输错误 */
|
||||
'error': (error: Error, connectionId?: string) => void;
|
||||
/** 服务器启动 */
|
||||
'server-started': (config: TransportConfig) => void;
|
||||
/** 服务器关闭 */
|
||||
'server-stopped': () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络传输层抽象类
|
||||
*/
|
||||
export abstract class Transport extends EventEmitter {
|
||||
protected config: TransportConfig;
|
||||
protected isRunning = false;
|
||||
protected connections = new Map<string, ClientConnectionInfo>();
|
||||
|
||||
constructor(config: TransportConfig) {
|
||||
super();
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动传输层服务
|
||||
*/
|
||||
abstract start(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 停止传输层服务
|
||||
*/
|
||||
abstract stop(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 发送消息给指定客户端
|
||||
*/
|
||||
abstract sendToClient(connectionId: string, message: TransportMessage): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 广播消息给所有客户端
|
||||
*/
|
||||
abstract broadcast(message: TransportMessage, excludeId?: string): Promise<number>;
|
||||
|
||||
/**
|
||||
* 广播消息给指定客户端列表
|
||||
*/
|
||||
abstract sendToClients(connectionIds: string[], message: TransportMessage): Promise<number>;
|
||||
|
||||
/**
|
||||
* 断开指定客户端连接
|
||||
*/
|
||||
abstract disconnectClient(connectionId: string, reason?: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取在线客户端数量
|
||||
*/
|
||||
getConnectionCount(): number {
|
||||
return this.connections.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有连接信息
|
||||
*/
|
||||
getConnections(): ClientConnectionInfo[] {
|
||||
return Array.from(this.connections.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定连接信息
|
||||
*/
|
||||
getConnection(connectionId: string): ClientConnectionInfo | undefined {
|
||||
return this.connections.get(connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查连接是否存在
|
||||
*/
|
||||
hasConnection(connectionId: string): boolean {
|
||||
return this.connections.has(connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务器是否正在运行
|
||||
*/
|
||||
isServerRunning(): boolean {
|
||||
return this.isRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取传输层配置
|
||||
*/
|
||||
getConfig(): TransportConfig {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新客户端最后活跃时间
|
||||
*/
|
||||
protected updateClientActivity(connectionId: string): void {
|
||||
const connection = this.connections.get(connectionId);
|
||||
if (connection) {
|
||||
connection.lastActivity = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加客户端连接
|
||||
*/
|
||||
protected addConnection(connectionInfo: ClientConnectionInfo): void {
|
||||
this.connections.set(connectionInfo.id, connectionInfo);
|
||||
this.emit('client-connected', connectionInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除客户端连接
|
||||
*/
|
||||
protected removeConnection(connectionId: string, reason?: string): void {
|
||||
if (this.connections.delete(connectionId)) {
|
||||
this.emit('client-disconnected', connectionId, reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收到的消息
|
||||
*/
|
||||
protected handleMessage(connectionId: string, message: TransportMessage): void {
|
||||
this.updateClientActivity(connectionId);
|
||||
this.emit('message', connectionId, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理传输错误
|
||||
*/
|
||||
protected handleError(error: Error, connectionId?: string): void {
|
||||
this.emit('error', error, connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有连接
|
||||
*/
|
||||
protected clearConnections(): void {
|
||||
const connectionIds = Array.from(this.connections.keys());
|
||||
for (const id of connectionIds) {
|
||||
this.removeConnection(id, 'server-shutdown');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
override on<K extends keyof TransportEvents>(event: K, listener: TransportEvents[K]): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
override emit<K extends keyof TransportEvents>(event: K, ...args: Parameters<TransportEvents[K]>): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,406 +0,0 @@
|
||||
/**
|
||||
* WebSocket 传输层实现
|
||||
*/
|
||||
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import { createServer, Server as HttpServer } from 'http';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Transport, TransportConfig, ClientConnectionInfo, TransportMessage } from './Transport';
|
||||
|
||||
/**
|
||||
* WebSocket 传输配置
|
||||
*/
|
||||
export interface WebSocketTransportConfig extends TransportConfig {
|
||||
/** WebSocket 路径 */
|
||||
path?: string;
|
||||
/** 是否启用压缩 */
|
||||
compression?: boolean;
|
||||
/** 最大消息大小(字节) */
|
||||
maxMessageSize?: number;
|
||||
/** ping 间隔(毫秒) */
|
||||
pingInterval?: number;
|
||||
/** pong 超时(毫秒) */
|
||||
pongTimeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 客户端连接扩展信息
|
||||
*/
|
||||
interface WebSocketConnectionInfo extends ClientConnectionInfo {
|
||||
/** WebSocket 实例 */
|
||||
socket: WebSocket;
|
||||
/** ping 定时器 */
|
||||
pingTimer?: NodeJS.Timeout;
|
||||
/** pong 超时定时器 */
|
||||
pongTimer?: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 传输层实现
|
||||
*/
|
||||
export class WebSocketTransport extends Transport {
|
||||
private httpServer: HttpServer | null = null;
|
||||
private wsServer: WebSocketServer | null = null;
|
||||
private wsConnections = new Map<string, WebSocketConnectionInfo>();
|
||||
|
||||
protected override config: WebSocketTransportConfig;
|
||||
|
||||
constructor(config: WebSocketTransportConfig) {
|
||||
super(config);
|
||||
this.config = {
|
||||
path: '/ws',
|
||||
compression: true,
|
||||
maxMessageSize: 1024 * 1024, // 1MB
|
||||
pingInterval: 30000, // 30秒
|
||||
pongTimeout: 5000, // 5秒
|
||||
heartbeatInterval: 30000,
|
||||
connectionTimeout: 60000,
|
||||
maxConnections: 1000,
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动 WebSocket 服务器
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
if (this.isRunning) {
|
||||
throw new Error('WebSocket transport is already running');
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建 HTTP 服务器
|
||||
this.httpServer = createServer();
|
||||
|
||||
// 创建 WebSocket 服务器
|
||||
this.wsServer = new WebSocketServer({
|
||||
server: this.httpServer,
|
||||
path: this.config.path,
|
||||
maxPayload: this.config.maxMessageSize,
|
||||
perMessageDeflate: this.config.compression
|
||||
});
|
||||
|
||||
// 设置事件监听
|
||||
this.setupEventListeners();
|
||||
|
||||
// 启动服务器
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this.httpServer!.listen(this.config.port, this.config.host, (error?: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
this.isRunning = true;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.emit('server-started', this.config);
|
||||
} catch (error) {
|
||||
await this.cleanup();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止 WebSocket 服务器
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
if (!this.isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRunning = false;
|
||||
|
||||
// 断开所有客户端连接
|
||||
for (const [connectionId, connection] of this.wsConnections) {
|
||||
this.disconnectClient(connectionId, 'server-shutdown');
|
||||
}
|
||||
|
||||
await this.cleanup();
|
||||
this.emit('server-stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给指定客户端
|
||||
*/
|
||||
async sendToClient(connectionId: string, message: TransportMessage): Promise<boolean> {
|
||||
const connection = this.wsConnections.get(connectionId);
|
||||
if (!connection || connection.socket.readyState !== WebSocket.OPEN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.stringify(message);
|
||||
connection.socket.send(data);
|
||||
this.updateClientActivity(connectionId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.handleError(error as Error, connectionId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播消息给所有客户端
|
||||
*/
|
||||
async broadcast(message: TransportMessage, excludeId?: string): Promise<number> {
|
||||
const data = JSON.stringify(message);
|
||||
let sentCount = 0;
|
||||
|
||||
for (const [connectionId, connection] of this.wsConnections) {
|
||||
if (excludeId && connectionId === excludeId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (connection.socket.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
connection.socket.send(data);
|
||||
sentCount++;
|
||||
} catch (error) {
|
||||
this.handleError(error as Error, connectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sentCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给指定客户端列表
|
||||
*/
|
||||
async sendToClients(connectionIds: string[], message: TransportMessage): Promise<number> {
|
||||
const data = JSON.stringify(message);
|
||||
let sentCount = 0;
|
||||
|
||||
for (const connectionId of connectionIds) {
|
||||
const connection = this.wsConnections.get(connectionId);
|
||||
if (connection && connection.socket.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
connection.socket.send(data);
|
||||
sentCount++;
|
||||
} catch (error) {
|
||||
this.handleError(error as Error, connectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sentCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开指定客户端连接
|
||||
*/
|
||||
async disconnectClient(connectionId: string, reason?: string): Promise<void> {
|
||||
const connection = this.wsConnections.get(connectionId);
|
||||
if (connection) {
|
||||
this.cleanupConnection(connectionId);
|
||||
connection.socket.close(1000, reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件监听器
|
||||
*/
|
||||
private setupEventListeners(): void {
|
||||
if (!this.wsServer) return;
|
||||
|
||||
this.wsServer.on('connection', (socket: WebSocket, request) => {
|
||||
this.handleNewConnection(socket, request);
|
||||
});
|
||||
|
||||
this.wsServer.on('error', (error: Error) => {
|
||||
this.handleError(error);
|
||||
});
|
||||
|
||||
if (this.httpServer) {
|
||||
this.httpServer.on('error', (error: Error) => {
|
||||
this.handleError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理新连接
|
||||
*/
|
||||
private handleNewConnection(socket: WebSocket, request: any): void {
|
||||
// 检查连接数限制
|
||||
if (this.config.maxConnections && this.wsConnections.size >= this.config.maxConnections) {
|
||||
socket.close(1013, 'Too many connections');
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionId = uuidv4();
|
||||
const remoteAddress = request.socket.remoteAddress || request.headers['x-forwarded-for'] || 'unknown';
|
||||
|
||||
const connectionInfo: WebSocketConnectionInfo = {
|
||||
id: connectionId,
|
||||
socket,
|
||||
remoteAddress: Array.isArray(remoteAddress) ? remoteAddress[0] : remoteAddress,
|
||||
connectedAt: new Date(),
|
||||
lastActivity: new Date(),
|
||||
userData: {}
|
||||
};
|
||||
|
||||
this.wsConnections.set(connectionId, connectionInfo);
|
||||
this.addConnection(connectionInfo);
|
||||
|
||||
// 设置 socket 事件监听
|
||||
socket.on('message', (data: Buffer) => {
|
||||
this.handleClientMessage(connectionId, data);
|
||||
});
|
||||
|
||||
socket.on('close', (code: number, reason: Buffer) => {
|
||||
this.handleClientDisconnect(connectionId, code, reason.toString());
|
||||
});
|
||||
|
||||
socket.on('error', (error: Error) => {
|
||||
this.handleError(error, connectionId);
|
||||
this.handleClientDisconnect(connectionId, 1006, 'Socket error');
|
||||
});
|
||||
|
||||
socket.on('pong', () => {
|
||||
this.handlePong(connectionId);
|
||||
});
|
||||
|
||||
// 启动心跳检测
|
||||
this.startHeartbeat(connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端消息
|
||||
*/
|
||||
private handleClientMessage(connectionId: string, data: Buffer): void {
|
||||
try {
|
||||
const message = JSON.parse(data.toString()) as TransportMessage;
|
||||
message.senderId = connectionId;
|
||||
this.handleMessage(connectionId, message);
|
||||
} catch (error) {
|
||||
this.handleError(new Error(`Invalid message format from client ${connectionId}`), connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端断开连接
|
||||
*/
|
||||
private handleClientDisconnect(connectionId: string, code: number, reason: string): void {
|
||||
this.cleanupConnection(connectionId);
|
||||
this.removeConnection(connectionId, `${code}: ${reason}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动心跳检测
|
||||
*/
|
||||
private startHeartbeat(connectionId: string): void {
|
||||
const connection = this.wsConnections.get(connectionId);
|
||||
if (!connection) return;
|
||||
|
||||
if (this.config.pingInterval && this.config.pingInterval > 0) {
|
||||
connection.pingTimer = setInterval(() => {
|
||||
this.sendPing(connectionId);
|
||||
}, this.config.pingInterval);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 ping
|
||||
*/
|
||||
private sendPing(connectionId: string): void {
|
||||
const connection = this.wsConnections.get(connectionId);
|
||||
if (!connection || connection.socket.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
connection.socket.ping();
|
||||
|
||||
// 设置 pong 超时
|
||||
if (this.config.pongTimeout && this.config.pongTimeout > 0) {
|
||||
if (connection.pongTimer) {
|
||||
clearTimeout(connection.pongTimer);
|
||||
}
|
||||
|
||||
connection.pongTimer = setTimeout(() => {
|
||||
this.disconnectClient(connectionId, 'Pong timeout');
|
||||
}, this.config.pongTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 pong 响应
|
||||
*/
|
||||
private handlePong(connectionId: string): void {
|
||||
const connection = this.wsConnections.get(connectionId);
|
||||
if (connection && connection.pongTimer) {
|
||||
clearTimeout(connection.pongTimer);
|
||||
connection.pongTimer = undefined;
|
||||
}
|
||||
this.updateClientActivity(connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理连接资源
|
||||
*/
|
||||
private cleanupConnection(connectionId: string): void {
|
||||
const connection = this.wsConnections.get(connectionId);
|
||||
if (connection) {
|
||||
if (connection.pingTimer) {
|
||||
clearInterval(connection.pingTimer);
|
||||
}
|
||||
if (connection.pongTimer) {
|
||||
clearTimeout(connection.pongTimer);
|
||||
}
|
||||
this.wsConnections.delete(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有资源
|
||||
*/
|
||||
private async cleanup(): Promise<void> {
|
||||
// 清理所有连接
|
||||
for (const connectionId of this.wsConnections.keys()) {
|
||||
this.cleanupConnection(connectionId);
|
||||
}
|
||||
this.clearConnections();
|
||||
|
||||
// 关闭 WebSocket 服务器
|
||||
if (this.wsServer) {
|
||||
this.wsServer.close();
|
||||
this.wsServer = null;
|
||||
}
|
||||
|
||||
// 关闭 HTTP 服务器
|
||||
if (this.httpServer) {
|
||||
await new Promise<void>((resolve) => {
|
||||
this.httpServer!.close(() => resolve());
|
||||
});
|
||||
this.httpServer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebSocket 连接统计信息
|
||||
*/
|
||||
getWebSocketStats(): {
|
||||
totalConnections: number;
|
||||
activeConnections: number;
|
||||
inactiveConnections: number;
|
||||
} {
|
||||
let activeConnections = 0;
|
||||
let inactiveConnections = 0;
|
||||
|
||||
for (const connection of this.wsConnections.values()) {
|
||||
if (connection.socket.readyState === WebSocket.OPEN) {
|
||||
activeConnections++;
|
||||
} else {
|
||||
inactiveConnections++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalConnections: this.wsConnections.size,
|
||||
activeConnections,
|
||||
inactiveConnections
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* 核心模块导出
|
||||
*/
|
||||
|
||||
export * from './Transport';
|
||||
export * from './WebSocketTransport';
|
||||
export * from './HttpTransport';
|
||||
export * from './ClientConnection';
|
||||
export * from './NetworkServer';
|
||||
@@ -1,79 +1,26 @@
|
||||
/**
|
||||
* ECS Framework Network Server
|
||||
*
|
||||
* 提供完整的网络服务端功能,包括:
|
||||
* - WebSocket 和 HTTP 传输层
|
||||
* - 客户端连接管理
|
||||
* - 房间系统
|
||||
* - 身份验证和权限管理
|
||||
* - SyncVar 和 RPC 系统
|
||||
* - 消息验证
|
||||
* @esengine/network-server
|
||||
* ECS Framework网络层 - 服务端实现
|
||||
*/
|
||||
|
||||
// 核心模块
|
||||
export * from './core';
|
||||
// 核心服务器 (待实现)
|
||||
// export * from './core/NetworkServer';
|
||||
// export * from './core/ClientConnection';
|
||||
|
||||
// 房间系统
|
||||
export * from './rooms';
|
||||
// 传输层 (待实现)
|
||||
// export * from './transport/WebSocketTransport';
|
||||
// export * from './transport/HttpTransport';
|
||||
|
||||
// 认证系统
|
||||
export * from './auth';
|
||||
// 系统层 (待实现)
|
||||
// export * from './systems/SyncVarSystem';
|
||||
// export * from './systems/RpcSystem';
|
||||
|
||||
// 网络系统
|
||||
export * from './systems';
|
||||
// 房间管理 (待实现)
|
||||
// export * from './rooms/Room';
|
||||
// export * from './rooms/RoomManager';
|
||||
|
||||
// 验证系统
|
||||
export * from './validation';
|
||||
// 认证授权 (待实现)
|
||||
// export * from './auth/AuthManager';
|
||||
|
||||
// 版本信息
|
||||
export const VERSION = '1.0.0';
|
||||
|
||||
// 导出常用组合配置
|
||||
export interface ServerConfigPreset {
|
||||
/** 服务器名称 */
|
||||
name: string;
|
||||
/** WebSocket 端口 */
|
||||
wsPort: number;
|
||||
/** HTTP 端口(可选) */
|
||||
httpPort?: number;
|
||||
/** 最大连接数 */
|
||||
maxConnections: number;
|
||||
/** 是否启用认证 */
|
||||
enableAuth: boolean;
|
||||
/** 是否启用房间系统 */
|
||||
enableRooms: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预定义服务器配置
|
||||
*/
|
||||
export const ServerPresets = {
|
||||
/** 开发环境配置 */
|
||||
Development: {
|
||||
name: 'Development Server',
|
||||
wsPort: 8080,
|
||||
httpPort: 3000,
|
||||
maxConnections: 100,
|
||||
enableAuth: false,
|
||||
enableRooms: true
|
||||
} as ServerConfigPreset,
|
||||
|
||||
/** 生产环境配置 */
|
||||
Production: {
|
||||
name: 'Production Server',
|
||||
wsPort: 443,
|
||||
httpPort: 80,
|
||||
maxConnections: 10000,
|
||||
enableAuth: true,
|
||||
enableRooms: true
|
||||
} as ServerConfigPreset,
|
||||
|
||||
/** 测试环境配置 */
|
||||
Testing: {
|
||||
name: 'Test Server',
|
||||
wsPort: 9090,
|
||||
maxConnections: 10,
|
||||
enableAuth: false,
|
||||
enableRooms: false
|
||||
} as ServerConfigPreset
|
||||
};
|
||||
// 重新导出shared包的类型
|
||||
export * from '@esengine/network-shared';
|
||||
@@ -1,637 +0,0 @@
|
||||
/**
|
||||
* 房间管理
|
||||
*
|
||||
* 类似于 Unity Mirror 的 Scene 概念,管理一组客户端和网络对象
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { Entity, Scene } from '@esengine/ecs-framework';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { ClientConnection } from '../core/ClientConnection';
|
||||
import { TransportMessage } from '../core/Transport';
|
||||
|
||||
/**
|
||||
* 房间状态
|
||||
*/
|
||||
export enum RoomState {
|
||||
/** 创建中 */
|
||||
CREATING = 'creating',
|
||||
/** 活跃状态 */
|
||||
ACTIVE = 'active',
|
||||
/** 暂停状态 */
|
||||
PAUSED = 'paused',
|
||||
/** 关闭中 */
|
||||
CLOSING = 'closing',
|
||||
/** 已关闭 */
|
||||
CLOSED = 'closed'
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间配置
|
||||
*/
|
||||
export interface RoomConfig {
|
||||
/** 房间ID */
|
||||
id: string;
|
||||
/** 房间名称 */
|
||||
name: string;
|
||||
/** 房间描述 */
|
||||
description?: string;
|
||||
/** 最大玩家数 */
|
||||
maxPlayers: number;
|
||||
/** 是否私有房间 */
|
||||
isPrivate?: boolean;
|
||||
/** 房间密码 */
|
||||
password?: string;
|
||||
/** 房间元数据 */
|
||||
metadata?: Record<string, NetworkValue>;
|
||||
/** 是否持久化 */
|
||||
persistent?: boolean;
|
||||
/** 房间过期时间(毫秒) */
|
||||
expirationTime?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家数据
|
||||
*/
|
||||
export interface PlayerData {
|
||||
/** 客户端连接 */
|
||||
client: ClientConnection;
|
||||
/** 加入时间 */
|
||||
joinedAt: Date;
|
||||
/** 是否为房主 */
|
||||
isOwner: boolean;
|
||||
/** 玩家自定义数据 */
|
||||
customData: Record<string, NetworkValue>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间统计信息
|
||||
*/
|
||||
export interface RoomStats {
|
||||
/** 当前玩家数 */
|
||||
currentPlayers: number;
|
||||
/** 最大玩家数 */
|
||||
maxPlayers: number;
|
||||
/** 总加入过的玩家数 */
|
||||
totalPlayersJoined: number;
|
||||
/** 消息总数 */
|
||||
totalMessages: number;
|
||||
/** 创建时间 */
|
||||
createdAt: Date;
|
||||
/** 房间存活时间(毫秒) */
|
||||
lifetime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间事件
|
||||
*/
|
||||
export interface RoomEvents {
|
||||
/** 玩家加入 */
|
||||
'player-joined': (player: PlayerData) => void;
|
||||
/** 玩家离开 */
|
||||
'player-left': (clientId: string, reason?: string) => void;
|
||||
/** 房主变更 */
|
||||
'owner-changed': (newOwnerId: string, oldOwnerId?: string) => void;
|
||||
/** 房间状态变化 */
|
||||
'state-changed': (oldState: RoomState, newState: RoomState) => void;
|
||||
/** 收到消息 */
|
||||
'message': (clientId: string, message: TransportMessage) => void;
|
||||
/** 房间更新 */
|
||||
'room-updated': (updatedFields: Partial<RoomConfig>) => void;
|
||||
/** 房间错误 */
|
||||
'error': (error: Error, clientId?: string) => void;
|
||||
/** 房间即将关闭 */
|
||||
'closing': (reason: string) => void;
|
||||
/** 房间已关闭 */
|
||||
'closed': (reason: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间类
|
||||
*/
|
||||
export class Room extends EventEmitter {
|
||||
private config: RoomConfig;
|
||||
private state: RoomState = RoomState.CREATING;
|
||||
private players = new Map<string, PlayerData>();
|
||||
private ownerId: string | null = null;
|
||||
private ecsScene: Scene | null = null;
|
||||
private stats: RoomStats;
|
||||
private expirationTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(config: RoomConfig) {
|
||||
super();
|
||||
|
||||
this.config = { ...config };
|
||||
this.stats = {
|
||||
currentPlayers: 0,
|
||||
maxPlayers: config.maxPlayers,
|
||||
totalPlayersJoined: 0,
|
||||
totalMessages: 0,
|
||||
createdAt: new Date(),
|
||||
lifetime: 0
|
||||
};
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间ID
|
||||
*/
|
||||
get id(): string {
|
||||
return this.config.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间名称
|
||||
*/
|
||||
get name(): string {
|
||||
return this.config.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间状态
|
||||
*/
|
||||
get currentState(): RoomState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间配置
|
||||
*/
|
||||
getConfig(): Readonly<RoomConfig> {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间统计信息
|
||||
*/
|
||||
getStats(): RoomStats {
|
||||
this.stats.lifetime = Date.now() - this.stats.createdAt.getTime();
|
||||
this.stats.currentPlayers = this.players.size;
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有玩家
|
||||
*/
|
||||
getPlayers(): PlayerData[] {
|
||||
return Array.from(this.players.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定玩家
|
||||
*/
|
||||
getPlayer(clientId: string): PlayerData | undefined {
|
||||
return this.players.get(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查玩家是否在房间中
|
||||
*/
|
||||
hasPlayer(clientId: string): boolean {
|
||||
return this.players.has(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前玩家数量
|
||||
*/
|
||||
getPlayerCount(): number {
|
||||
return this.players.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查房间是否已满
|
||||
*/
|
||||
isFull(): boolean {
|
||||
return this.players.size >= this.config.maxPlayers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查房间是否为空
|
||||
*/
|
||||
isEmpty(): boolean {
|
||||
return this.players.size === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房主
|
||||
*/
|
||||
getOwner(): PlayerData | undefined {
|
||||
return this.ownerId ? this.players.get(this.ownerId) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 ECS 场景
|
||||
*/
|
||||
getEcsScene(): Scene | null {
|
||||
return this.ecsScene;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家加入房间
|
||||
*/
|
||||
async addPlayer(client: ClientConnection, customData: Record<string, NetworkValue> = {}): Promise<boolean> {
|
||||
if (this.state !== RoomState.ACTIVE) {
|
||||
throw new Error(`Cannot join room in state: ${this.state}`);
|
||||
}
|
||||
|
||||
if (this.hasPlayer(client.id)) {
|
||||
throw new Error(`Player ${client.id} is already in the room`);
|
||||
}
|
||||
|
||||
if (this.isFull()) {
|
||||
throw new Error('Room is full');
|
||||
}
|
||||
|
||||
// 检查房间密码
|
||||
if (this.config.isPrivate && this.config.password) {
|
||||
const providedPassword = customData.password as string;
|
||||
if (providedPassword !== this.config.password) {
|
||||
throw new Error('Invalid room password');
|
||||
}
|
||||
}
|
||||
|
||||
const isFirstPlayer = this.isEmpty();
|
||||
const playerData: PlayerData = {
|
||||
client,
|
||||
joinedAt: new Date(),
|
||||
isOwner: isFirstPlayer,
|
||||
customData: { ...customData }
|
||||
};
|
||||
|
||||
this.players.set(client.id, playerData);
|
||||
client.joinRoom(this.id);
|
||||
|
||||
// 设置房主
|
||||
if (isFirstPlayer) {
|
||||
this.ownerId = client.id;
|
||||
}
|
||||
|
||||
this.stats.totalPlayersJoined++;
|
||||
|
||||
// 通知其他玩家
|
||||
await this.broadcast({
|
||||
type: 'system',
|
||||
data: {
|
||||
action: 'player-joined',
|
||||
playerId: client.id,
|
||||
playerData: {
|
||||
id: client.id,
|
||||
joinedAt: playerData.joinedAt.toISOString(),
|
||||
isOwner: playerData.isOwner,
|
||||
customData: playerData.customData
|
||||
}
|
||||
}
|
||||
}, client.id);
|
||||
|
||||
console.log(`Player ${client.id} joined room ${this.id}`);
|
||||
this.emit('player-joined', playerData);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家离开房间
|
||||
*/
|
||||
async removePlayer(clientId: string, reason?: string): Promise<boolean> {
|
||||
const player = this.players.get(clientId);
|
||||
if (!player) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.players.delete(clientId);
|
||||
player.client.leaveRoom();
|
||||
|
||||
// 如果离开的是房主,转移房主权限
|
||||
if (this.ownerId === clientId) {
|
||||
await this.transferOwnership();
|
||||
}
|
||||
|
||||
// 通知其他玩家
|
||||
await this.broadcast({
|
||||
type: 'system',
|
||||
data: {
|
||||
action: 'player-left',
|
||||
playerId: clientId,
|
||||
reason: reason || 'unknown'
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Player ${clientId} left room ${this.id}, reason: ${reason || 'unknown'}`);
|
||||
this.emit('player-left', clientId, reason);
|
||||
|
||||
// 如果房间为空,考虑关闭
|
||||
if (this.isEmpty() && !this.config.persistent) {
|
||||
await this.close('empty-room');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转移房主权限
|
||||
*/
|
||||
async transferOwnership(newOwnerId?: string): Promise<boolean> {
|
||||
const oldOwnerId = this.ownerId;
|
||||
|
||||
if (newOwnerId) {
|
||||
const newOwner = this.players.get(newOwnerId);
|
||||
if (!newOwner) {
|
||||
return false;
|
||||
}
|
||||
this.ownerId = newOwnerId;
|
||||
newOwner.isOwner = true;
|
||||
} else {
|
||||
// 自动选择下一个玩家作为房主
|
||||
const players = Array.from(this.players.values());
|
||||
if (players.length > 0) {
|
||||
const newOwner = players[0];
|
||||
this.ownerId = newOwner.client.id;
|
||||
newOwner.isOwner = true;
|
||||
} else {
|
||||
this.ownerId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新旧房主状态
|
||||
if (oldOwnerId) {
|
||||
const oldOwner = this.players.get(oldOwnerId);
|
||||
if (oldOwner) {
|
||||
oldOwner.isOwner = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 通知所有玩家房主变更
|
||||
if (this.ownerId) {
|
||||
await this.broadcast({
|
||||
type: 'system',
|
||||
data: {
|
||||
action: 'owner-changed',
|
||||
newOwnerId: this.ownerId,
|
||||
oldOwnerId: oldOwnerId || ''
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Room ${this.id} ownership transferred from ${oldOwnerId || 'none'} to ${this.ownerId}`);
|
||||
this.emit('owner-changed', this.ownerId, oldOwnerId || undefined);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播消息给房间内所有玩家
|
||||
*/
|
||||
async broadcast(message: TransportMessage, excludeClientId?: string): Promise<number> {
|
||||
const players = Array.from(this.players.values())
|
||||
.filter(player => player.client.id !== excludeClientId);
|
||||
|
||||
const promises = players.map(player => player.client.sendMessage(message));
|
||||
const results = await Promise.allSettled(promises);
|
||||
|
||||
return results.filter(result => result.status === 'fulfilled' && result.value).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给指定玩家
|
||||
*/
|
||||
async sendToPlayer(clientId: string, message: TransportMessage): Promise<boolean> {
|
||||
const player = this.players.get(clientId);
|
||||
if (!player) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await player.client.sendMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理玩家消息
|
||||
*/
|
||||
async handleMessage(clientId: string, message: TransportMessage): Promise<void> {
|
||||
if (!this.hasPlayer(clientId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stats.totalMessages++;
|
||||
this.emit('message', clientId, message);
|
||||
|
||||
// 根据消息类型进行处理
|
||||
switch (message.type) {
|
||||
case 'rpc':
|
||||
await this.handleRpcMessage(clientId, message);
|
||||
break;
|
||||
case 'syncvar':
|
||||
await this.handleSyncVarMessage(clientId, message);
|
||||
break;
|
||||
case 'system':
|
||||
await this.handleSystemMessage(clientId, message);
|
||||
break;
|
||||
default:
|
||||
// 转发自定义消息
|
||||
await this.broadcast(message, clientId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新房间配置
|
||||
*/
|
||||
async updateConfig(updates: Partial<RoomConfig>): Promise<void> {
|
||||
// 验证更新
|
||||
if (updates.maxPlayers !== undefined && updates.maxPlayers < this.players.size) {
|
||||
throw new Error('Cannot reduce maxPlayers below current player count');
|
||||
}
|
||||
|
||||
const oldConfig = { ...this.config };
|
||||
Object.assign(this.config, updates);
|
||||
|
||||
// 通知所有玩家房间更新
|
||||
await this.broadcast({
|
||||
type: 'system',
|
||||
data: {
|
||||
action: 'room-updated',
|
||||
updates
|
||||
}
|
||||
});
|
||||
|
||||
this.emit('room-updated', updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停房间
|
||||
*/
|
||||
async pause(): Promise<void> {
|
||||
if (this.state === RoomState.ACTIVE) {
|
||||
this.setState(RoomState.PAUSED);
|
||||
|
||||
await this.broadcast({
|
||||
type: 'system',
|
||||
data: {
|
||||
action: 'room-paused'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复房间
|
||||
*/
|
||||
async resume(): Promise<void> {
|
||||
if (this.state === RoomState.PAUSED) {
|
||||
this.setState(RoomState.ACTIVE);
|
||||
|
||||
await this.broadcast({
|
||||
type: 'system',
|
||||
data: {
|
||||
action: 'room-resumed'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭房间
|
||||
*/
|
||||
async close(reason: string = 'server-shutdown'): Promise<void> {
|
||||
if (this.state === RoomState.CLOSED || this.state === RoomState.CLOSING) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(RoomState.CLOSING);
|
||||
this.emit('closing', reason);
|
||||
|
||||
// 通知所有玩家房间即将关闭
|
||||
await this.broadcast({
|
||||
type: 'system',
|
||||
data: {
|
||||
action: 'room-closing',
|
||||
reason
|
||||
}
|
||||
});
|
||||
|
||||
// 移除所有玩家
|
||||
const playerIds = Array.from(this.players.keys());
|
||||
for (const clientId of playerIds) {
|
||||
await this.removePlayer(clientId, 'room-closed');
|
||||
}
|
||||
|
||||
this.cleanup();
|
||||
this.setState(RoomState.CLOSED);
|
||||
|
||||
console.log(`Room ${this.id} closed, reason: ${reason}`);
|
||||
this.emit('closed', reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化房间
|
||||
*/
|
||||
private initialize(): void {
|
||||
// 创建 ECS 场景
|
||||
this.ecsScene = new Scene();
|
||||
|
||||
// 设置过期定时器
|
||||
if (this.config.expirationTime && this.config.expirationTime > 0) {
|
||||
this.expirationTimer = setTimeout(() => {
|
||||
this.close('expired');
|
||||
}, this.config.expirationTime);
|
||||
}
|
||||
|
||||
this.setState(RoomState.ACTIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 RPC 消息
|
||||
*/
|
||||
private async handleRpcMessage(clientId: string, message: TransportMessage): Promise<void> {
|
||||
// RPC 消息处理逻辑
|
||||
// 这里可以添加权限检查、速率限制等
|
||||
await this.broadcast(message, clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SyncVar 消息
|
||||
*/
|
||||
private async handleSyncVarMessage(clientId: string, message: TransportMessage): Promise<void> {
|
||||
// SyncVar 消息处理逻辑
|
||||
// 这里可以添加权限检查、数据验证等
|
||||
await this.broadcast(message, clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理系统消息
|
||||
*/
|
||||
private async handleSystemMessage(clientId: string, message: TransportMessage): Promise<void> {
|
||||
const data = message.data as any;
|
||||
|
||||
switch (data.action) {
|
||||
case 'request-ownership':
|
||||
// 处理房主权限转移请求
|
||||
if (this.ownerId === clientId) {
|
||||
await this.transferOwnership(data.newOwnerId);
|
||||
}
|
||||
break;
|
||||
// 其他系统消息处理...
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置房间状态
|
||||
*/
|
||||
private setState(newState: RoomState): void {
|
||||
const oldState = this.state;
|
||||
if (oldState !== newState) {
|
||||
this.state = newState;
|
||||
this.emit('state-changed', oldState, newState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
private cleanup(): void {
|
||||
if (this.expirationTimer) {
|
||||
clearTimeout(this.expirationTimer);
|
||||
this.expirationTimer = null;
|
||||
}
|
||||
|
||||
this.removeAllListeners();
|
||||
|
||||
if (this.ecsScene) {
|
||||
this.ecsScene = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
override on<K extends keyof RoomEvents>(event: K, listener: RoomEvents[K]): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
override emit<K extends keyof RoomEvents>(event: K, ...args: Parameters<RoomEvents[K]>): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化房间信息
|
||||
*/
|
||||
toJSON(): object {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
state: this.state,
|
||||
config: this.config,
|
||||
stats: this.getStats(),
|
||||
players: this.getPlayers().map(player => ({
|
||||
id: player.client.id,
|
||||
joinedAt: player.joinedAt.toISOString(),
|
||||
isOwner: player.isOwner,
|
||||
customData: player.customData
|
||||
})),
|
||||
ownerId: this.ownerId
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,499 +0,0 @@
|
||||
/**
|
||||
* 房间管理器
|
||||
*
|
||||
* 管理所有房间的创建、销毁、查找等操作
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { Room, RoomConfig, RoomState, PlayerData } from './Room';
|
||||
import { ClientConnection } from '../core/ClientConnection';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
|
||||
/**
|
||||
* 房间管理器配置
|
||||
*/
|
||||
export interface RoomManagerConfig {
|
||||
/** 最大房间数量 */
|
||||
maxRooms?: number;
|
||||
/** 默认房间过期时间(毫秒) */
|
||||
defaultExpirationTime?: number;
|
||||
/** 是否启用房间统计 */
|
||||
enableStats?: boolean;
|
||||
/** 房间清理间隔(毫秒) */
|
||||
cleanupInterval?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间查询选项
|
||||
*/
|
||||
export interface RoomQueryOptions {
|
||||
/** 房间名称模糊搜索 */
|
||||
namePattern?: string;
|
||||
/** 房间状态过滤 */
|
||||
state?: RoomState;
|
||||
/** 是否私有房间 */
|
||||
isPrivate?: boolean;
|
||||
/** 最小空位数 */
|
||||
minAvailableSlots?: number;
|
||||
/** 最大空位数 */
|
||||
maxAvailableSlots?: number;
|
||||
/** 元数据过滤 */
|
||||
metadata?: Record<string, NetworkValue>;
|
||||
/** 限制结果数量 */
|
||||
limit?: number;
|
||||
/** 跳过条数 */
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间管理器统计信息
|
||||
*/
|
||||
export interface RoomManagerStats {
|
||||
/** 总房间数 */
|
||||
totalRooms: number;
|
||||
/** 活跃房间数 */
|
||||
activeRooms: number;
|
||||
/** 总玩家数 */
|
||||
totalPlayers: number;
|
||||
/** 私有房间数 */
|
||||
privateRooms: number;
|
||||
/** 持久化房间数 */
|
||||
persistentRooms: number;
|
||||
/** 创建的房间总数 */
|
||||
roomsCreated: number;
|
||||
/** 关闭的房间总数 */
|
||||
roomsClosed: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间管理器事件
|
||||
*/
|
||||
export interface RoomManagerEvents {
|
||||
/** 房间创建 */
|
||||
'room-created': (room: Room) => void;
|
||||
/** 房间关闭 */
|
||||
'room-closed': (roomId: string, reason: string) => void;
|
||||
/** 玩家加入房间 */
|
||||
'player-joined-room': (roomId: string, player: PlayerData) => void;
|
||||
/** 玩家离开房间 */
|
||||
'player-left-room': (roomId: string, clientId: string, reason?: string) => void;
|
||||
/** 房间管理器错误 */
|
||||
'error': (error: Error, roomId?: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间管理器
|
||||
*/
|
||||
export class RoomManager extends EventEmitter {
|
||||
private config: RoomManagerConfig;
|
||||
private rooms = new Map<string, Room>();
|
||||
private stats: RoomManagerStats;
|
||||
private cleanupTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(config: RoomManagerConfig = {}) {
|
||||
super();
|
||||
|
||||
this.config = {
|
||||
maxRooms: 1000,
|
||||
defaultExpirationTime: 0, // 0 = 不过期
|
||||
enableStats: true,
|
||||
cleanupInterval: 60000, // 1分钟
|
||||
...config
|
||||
};
|
||||
|
||||
this.stats = {
|
||||
totalRooms: 0,
|
||||
activeRooms: 0,
|
||||
totalPlayers: 0,
|
||||
privateRooms: 0,
|
||||
persistentRooms: 0,
|
||||
roomsCreated: 0,
|
||||
roomsClosed: 0
|
||||
};
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间管理器配置
|
||||
*/
|
||||
getConfig(): Readonly<RoomManagerConfig> {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间管理器统计信息
|
||||
*/
|
||||
getStats(): RoomManagerStats {
|
||||
this.updateStats();
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建房间
|
||||
*/
|
||||
async createRoom(config: RoomConfig, creatorClient?: ClientConnection): Promise<Room> {
|
||||
// 检查房间数量限制
|
||||
if (this.config.maxRooms && this.rooms.size >= this.config.maxRooms) {
|
||||
throw new Error('Maximum number of rooms reached');
|
||||
}
|
||||
|
||||
// 检查房间ID是否已存在
|
||||
if (this.rooms.has(config.id)) {
|
||||
throw new Error(`Room with id "${config.id}" already exists`);
|
||||
}
|
||||
|
||||
// 应用默认过期时间
|
||||
const roomConfig: RoomConfig = {
|
||||
expirationTime: this.config.defaultExpirationTime,
|
||||
...config
|
||||
};
|
||||
|
||||
const room = new Room(roomConfig);
|
||||
|
||||
// 设置房间事件监听
|
||||
this.setupRoomEvents(room);
|
||||
|
||||
this.rooms.set(room.id, room);
|
||||
this.stats.roomsCreated++;
|
||||
|
||||
console.log(`Room created: ${room.id} by ${creatorClient?.id || 'system'}`);
|
||||
this.emit('room-created', room);
|
||||
|
||||
// 如果有创建者,自动加入房间
|
||||
if (creatorClient) {
|
||||
try {
|
||||
await room.addPlayer(creatorClient);
|
||||
} catch (error) {
|
||||
console.error(`Failed to add creator to room ${room.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return room;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间
|
||||
*/
|
||||
getRoom(roomId: string): Room | undefined {
|
||||
return this.rooms.get(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查房间是否存在
|
||||
*/
|
||||
hasRoom(roomId: string): boolean {
|
||||
return this.rooms.has(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有房间
|
||||
*/
|
||||
getAllRooms(): Room[] {
|
||||
return Array.from(this.rooms.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询房间
|
||||
*/
|
||||
findRooms(options: RoomQueryOptions = {}): Room[] {
|
||||
let rooms = Array.from(this.rooms.values());
|
||||
|
||||
// 状态过滤
|
||||
if (options.state !== undefined) {
|
||||
rooms = rooms.filter(room => room.currentState === options.state);
|
||||
}
|
||||
|
||||
// 私有房间过滤
|
||||
if (options.isPrivate !== undefined) {
|
||||
rooms = rooms.filter(room => room.getConfig().isPrivate === options.isPrivate);
|
||||
}
|
||||
|
||||
// 名称模糊搜索
|
||||
if (options.namePattern) {
|
||||
const pattern = options.namePattern.toLowerCase();
|
||||
rooms = rooms.filter(room =>
|
||||
room.getConfig().name.toLowerCase().includes(pattern)
|
||||
);
|
||||
}
|
||||
|
||||
// 空位数过滤
|
||||
if (options.minAvailableSlots !== undefined) {
|
||||
rooms = rooms.filter(room => {
|
||||
const available = room.getConfig().maxPlayers - room.getPlayerCount();
|
||||
return available >= options.minAvailableSlots!;
|
||||
});
|
||||
}
|
||||
|
||||
if (options.maxAvailableSlots !== undefined) {
|
||||
rooms = rooms.filter(room => {
|
||||
const available = room.getConfig().maxPlayers - room.getPlayerCount();
|
||||
return available <= options.maxAvailableSlots!;
|
||||
});
|
||||
}
|
||||
|
||||
// 元数据过滤
|
||||
if (options.metadata) {
|
||||
rooms = rooms.filter(room => {
|
||||
const roomMetadata = room.getConfig().metadata || {};
|
||||
return Object.entries(options.metadata!).every(([key, value]) =>
|
||||
roomMetadata[key] === value
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 排序(按创建时间,最新的在前)
|
||||
rooms.sort((a, b) =>
|
||||
b.getStats().createdAt.getTime() - a.getStats().createdAt.getTime()
|
||||
);
|
||||
|
||||
// 分页
|
||||
const offset = options.offset || 0;
|
||||
const limit = options.limit || rooms.length;
|
||||
return rooms.slice(offset, offset + limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭房间
|
||||
*/
|
||||
async closeRoom(roomId: string, reason: string = 'manual'): Promise<boolean> {
|
||||
const room = this.rooms.get(roomId);
|
||||
if (!room) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await room.close(reason);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.emit('error', error as Error, roomId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家加入房间
|
||||
*/
|
||||
async joinRoom(
|
||||
roomId: string,
|
||||
client: ClientConnection,
|
||||
customData: Record<string, NetworkValue> = {}
|
||||
): Promise<boolean> {
|
||||
const room = this.rooms.get(roomId);
|
||||
if (!room) {
|
||||
throw new Error(`Room "${roomId}" not found`);
|
||||
}
|
||||
|
||||
try {
|
||||
return await room.addPlayer(client, customData);
|
||||
} catch (error) {
|
||||
this.emit('error', error as Error, roomId);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家离开房间
|
||||
*/
|
||||
async leaveRoom(roomId: string, clientId: string, reason?: string): Promise<boolean> {
|
||||
const room = this.rooms.get(roomId);
|
||||
if (!room) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return await room.removePlayer(clientId, reason);
|
||||
} catch (error) {
|
||||
this.emit('error', error as Error, roomId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家离开所有房间
|
||||
*/
|
||||
async leaveAllRooms(clientId: string, reason?: string): Promise<number> {
|
||||
let leftCount = 0;
|
||||
|
||||
for (const room of this.rooms.values()) {
|
||||
if (room.hasPlayer(clientId)) {
|
||||
try {
|
||||
await room.removePlayer(clientId, reason);
|
||||
leftCount++;
|
||||
} catch (error) {
|
||||
console.error(`Error removing player ${clientId} from room ${room.id}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return leftCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家所在的房间
|
||||
*/
|
||||
getPlayerRooms(clientId: string): Room[] {
|
||||
return Array.from(this.rooms.values())
|
||||
.filter(room => room.hasPlayer(clientId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间数量
|
||||
*/
|
||||
getRoomCount(): number {
|
||||
return this.rooms.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取总玩家数量
|
||||
*/
|
||||
getTotalPlayerCount(): number {
|
||||
return Array.from(this.rooms.values())
|
||||
.reduce((total, room) => total + room.getPlayerCount(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理空闲房间
|
||||
*/
|
||||
async cleanupRooms(): Promise<number> {
|
||||
let cleanedCount = 0;
|
||||
const now = Date.now();
|
||||
|
||||
for (const room of this.rooms.values()) {
|
||||
const config = room.getConfig();
|
||||
const stats = room.getStats();
|
||||
|
||||
// 清理条件:
|
||||
// 1. 非持久化的空房间
|
||||
// 2. 已过期的房间
|
||||
// 3. 已关闭的房间
|
||||
let shouldClean = false;
|
||||
let reason = '';
|
||||
|
||||
if (room.currentState === RoomState.CLOSED) {
|
||||
shouldClean = true;
|
||||
reason = 'room-closed';
|
||||
} else if (!config.persistent && room.isEmpty()) {
|
||||
shouldClean = true;
|
||||
reason = 'empty-room';
|
||||
} else if (config.expirationTime && config.expirationTime > 0) {
|
||||
const expireTime = stats.createdAt.getTime() + config.expirationTime;
|
||||
if (now >= expireTime) {
|
||||
shouldClean = true;
|
||||
reason = 'expired';
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldClean) {
|
||||
try {
|
||||
if (room.currentState !== RoomState.CLOSED) {
|
||||
await room.close(reason);
|
||||
}
|
||||
this.rooms.delete(room.id);
|
||||
cleanedCount++;
|
||||
console.log(`Cleaned up room: ${room.id}, reason: ${reason}`);
|
||||
} catch (error) {
|
||||
console.error(`Error cleaning up room ${room.id}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cleanedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有房间
|
||||
*/
|
||||
async closeAllRooms(reason: string = 'shutdown'): Promise<void> {
|
||||
const rooms = Array.from(this.rooms.values());
|
||||
const promises = rooms.map(room => room.close(reason));
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
this.rooms.clear();
|
||||
|
||||
console.log(`Closed ${rooms.length} rooms, reason: ${reason}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁房间管理器
|
||||
*/
|
||||
async destroy(): Promise<void> {
|
||||
// 停止清理定时器
|
||||
if (this.cleanupTimer) {
|
||||
clearInterval(this.cleanupTimer);
|
||||
this.cleanupTimer = null;
|
||||
}
|
||||
|
||||
// 关闭所有房间
|
||||
await this.closeAllRooms('manager-destroyed');
|
||||
|
||||
// 移除所有事件监听器
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化房间管理器
|
||||
*/
|
||||
private initialize(): void {
|
||||
// 启动清理定时器
|
||||
if (this.config.cleanupInterval && this.config.cleanupInterval > 0) {
|
||||
this.cleanupTimer = setInterval(() => {
|
||||
this.cleanupRooms().catch(error => {
|
||||
console.error('Error during room cleanup:', error);
|
||||
});
|
||||
}, this.config.cleanupInterval);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置房间事件监听
|
||||
*/
|
||||
private setupRoomEvents(room: Room): void {
|
||||
room.on('player-joined', (player) => {
|
||||
this.emit('player-joined-room', room.id, player);
|
||||
});
|
||||
|
||||
room.on('player-left', (clientId, reason) => {
|
||||
this.emit('player-left-room', room.id, clientId, reason);
|
||||
});
|
||||
|
||||
room.on('closed', (reason) => {
|
||||
this.rooms.delete(room.id);
|
||||
this.stats.roomsClosed++;
|
||||
console.log(`Room ${room.id} removed from manager, reason: ${reason}`);
|
||||
this.emit('room-closed', room.id, reason);
|
||||
});
|
||||
|
||||
room.on('error', (error) => {
|
||||
this.emit('error', error, room.id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新统计信息
|
||||
*/
|
||||
private updateStats(): void {
|
||||
this.stats.totalRooms = this.rooms.size;
|
||||
this.stats.activeRooms = Array.from(this.rooms.values())
|
||||
.filter(room => room.currentState === RoomState.ACTIVE).length;
|
||||
this.stats.totalPlayers = this.getTotalPlayerCount();
|
||||
this.stats.privateRooms = Array.from(this.rooms.values())
|
||||
.filter(room => room.getConfig().isPrivate).length;
|
||||
this.stats.persistentRooms = Array.from(this.rooms.values())
|
||||
.filter(room => room.getConfig().persistent).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
override on<K extends keyof RoomManagerEvents>(event: K, listener: RoomManagerEvents[K]): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
override emit<K extends keyof RoomManagerEvents>(event: K, ...args: Parameters<RoomManagerEvents[K]>): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* 房间系统导出
|
||||
*/
|
||||
|
||||
export * from './Room';
|
||||
export * from './RoomManager';
|
||||
@@ -1,762 +0,0 @@
|
||||
/**
|
||||
* RPC 系统
|
||||
*
|
||||
* 处理服务端的 RPC 调用、权限验证、参数验证等
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
NetworkValue,
|
||||
RpcMetadata
|
||||
} from '@esengine/ecs-framework-network-shared';
|
||||
import { ClientConnection } from '../core/ClientConnection';
|
||||
import { Room } from '../rooms/Room';
|
||||
import { TransportMessage } from '../core/Transport';
|
||||
|
||||
/**
|
||||
* RPC 调用记录
|
||||
*/
|
||||
export interface RpcCall {
|
||||
/** 调用ID */
|
||||
id: string;
|
||||
/** 网络对象ID */
|
||||
networkId: number;
|
||||
/** 组件类型 */
|
||||
componentType: string;
|
||||
/** 方法名 */
|
||||
methodName: string;
|
||||
/** 参数 */
|
||||
parameters: NetworkValue[];
|
||||
/** 元数据 */
|
||||
metadata: RpcMetadata;
|
||||
/** 发送者客户端ID */
|
||||
senderId: string;
|
||||
/** 目标客户端IDs(用于 ClientRpc) */
|
||||
targetClientIds?: string[];
|
||||
/** 是否需要响应 */
|
||||
requiresResponse: boolean;
|
||||
/** 时间戳 */
|
||||
timestamp: Date;
|
||||
/** 过期时间 */
|
||||
expiresAt?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 响应
|
||||
*/
|
||||
export interface RpcResponse {
|
||||
/** 调用ID */
|
||||
callId: string;
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
/** 返回值 */
|
||||
result?: NetworkValue;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
/** 错误代码 */
|
||||
errorCode?: string;
|
||||
/** 时间戳 */
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 系统配置
|
||||
*/
|
||||
export interface RpcSystemConfig {
|
||||
/** RPC 调用超时时间(毫秒) */
|
||||
callTimeout?: number;
|
||||
/** 最大并发 RPC 调用数 */
|
||||
maxConcurrentCalls?: number;
|
||||
/** 是否启用权限检查 */
|
||||
enablePermissionCheck?: boolean;
|
||||
/** 是否启用参数验证 */
|
||||
enableParameterValidation?: boolean;
|
||||
/** 是否启用频率限制 */
|
||||
enableRateLimit?: boolean;
|
||||
/** 最大 RPC 频率(调用/秒) */
|
||||
maxRpcRate?: number;
|
||||
/** 单个参数最大大小(字节) */
|
||||
maxParameterSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 系统事件
|
||||
*/
|
||||
export interface RpcSystemEvents {
|
||||
/** ClientRpc 调用 */
|
||||
'client-rpc-called': (call: RpcCall) => void;
|
||||
/** ServerRpc 调用 */
|
||||
'server-rpc-called': (call: RpcCall) => void;
|
||||
/** RPC 调用完成 */
|
||||
'rpc-completed': (call: RpcCall, response?: RpcResponse) => void;
|
||||
/** RPC 调用超时 */
|
||||
'rpc-timeout': (callId: string) => void;
|
||||
/** 权限验证失败 */
|
||||
'permission-denied': (clientId: string, call: RpcCall) => void;
|
||||
/** 参数验证失败 */
|
||||
'parameter-validation-failed': (clientId: string, call: RpcCall, reason: string) => void;
|
||||
/** 频率限制触发 */
|
||||
'rate-limit-exceeded': (clientId: string) => void;
|
||||
/** RPC 错误 */
|
||||
'rpc-error': (error: Error, callId?: string, clientId?: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端 RPC 状态
|
||||
*/
|
||||
interface ClientRpcState {
|
||||
/** 客户端ID */
|
||||
clientId: string;
|
||||
/** 活跃的调用 */
|
||||
activeCalls: Map<string, RpcCall>;
|
||||
/** RPC 调用计数 */
|
||||
rpcCount: number;
|
||||
/** 频率重置时间 */
|
||||
rateResetTime: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 待处理的 RPC 响应
|
||||
*/
|
||||
interface PendingRpcResponse {
|
||||
/** 调用信息 */
|
||||
call: RpcCall;
|
||||
/** 超时定时器 */
|
||||
timeoutTimer: NodeJS.Timeout;
|
||||
/** 响应回调 */
|
||||
responseCallback: (response: RpcResponse) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 系统
|
||||
*/
|
||||
export class RpcSystem extends EventEmitter {
|
||||
private config: RpcSystemConfig;
|
||||
private clientStates = new Map<string, ClientRpcState>();
|
||||
private pendingCalls = new Map<string, PendingRpcResponse>();
|
||||
private cleanupTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(config: RpcSystemConfig = {}) {
|
||||
super();
|
||||
|
||||
this.config = {
|
||||
callTimeout: 30000, // 30秒
|
||||
maxConcurrentCalls: 10,
|
||||
enablePermissionCheck: true,
|
||||
enableParameterValidation: true,
|
||||
enableRateLimit: true,
|
||||
maxRpcRate: 30, // 30次/秒
|
||||
maxParameterSize: 65536, // 64KB
|
||||
...config
|
||||
};
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 ClientRpc 调用
|
||||
*/
|
||||
async handleClientRpcCall(
|
||||
client: ClientConnection,
|
||||
message: TransportMessage,
|
||||
room: Room
|
||||
): Promise<void> {
|
||||
try {
|
||||
const data = message.data as any;
|
||||
const {
|
||||
networkId,
|
||||
componentType,
|
||||
methodName,
|
||||
parameters = [],
|
||||
metadata,
|
||||
targetFilter = 'all'
|
||||
} = data;
|
||||
|
||||
// 创建 RPC 调用记录
|
||||
const rpcCall: RpcCall = {
|
||||
id: uuidv4(),
|
||||
networkId,
|
||||
componentType,
|
||||
methodName,
|
||||
parameters,
|
||||
metadata,
|
||||
senderId: client.id,
|
||||
requiresResponse: metadata?.requiresResponse || false,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
// 权限检查
|
||||
if (this.config.enablePermissionCheck) {
|
||||
if (!this.checkRpcPermission(client, rpcCall, 'client-rpc')) {
|
||||
this.emit('permission-denied', client.id, rpcCall);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 频率限制检查
|
||||
if (this.config.enableRateLimit && !this.checkRpcRate(client.id)) {
|
||||
this.emit('rate-limit-exceeded', client.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// 参数验证
|
||||
if (this.config.enableParameterValidation) {
|
||||
const validationResult = this.validateRpcParameters(rpcCall);
|
||||
if (!validationResult.valid) {
|
||||
this.emit('parameter-validation-failed', client.id, rpcCall, validationResult.reason!);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 确定目标客户端
|
||||
const targetClientIds = this.getClientRpcTargets(room, client.id, targetFilter);
|
||||
rpcCall.targetClientIds = targetClientIds;
|
||||
|
||||
// 记录活跃调用
|
||||
this.recordActiveCall(client.id, rpcCall);
|
||||
|
||||
// 触发事件
|
||||
this.emit('client-rpc-called', rpcCall);
|
||||
|
||||
// 发送到目标客户端
|
||||
await this.sendClientRpc(room, rpcCall, targetClientIds);
|
||||
|
||||
// 如果不需要响应,立即标记完成
|
||||
if (!rpcCall.requiresResponse) {
|
||||
this.completeRpcCall(rpcCall);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.emit('rpc-error', error as Error, undefined, client.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 ServerRpc 调用
|
||||
*/
|
||||
async handleServerRpcCall(
|
||||
client: ClientConnection,
|
||||
message: TransportMessage,
|
||||
room: Room
|
||||
): Promise<void> {
|
||||
try {
|
||||
const data = message.data as any;
|
||||
const {
|
||||
networkId,
|
||||
componentType,
|
||||
methodName,
|
||||
parameters = [],
|
||||
metadata
|
||||
} = data;
|
||||
|
||||
// 创建 RPC 调用记录
|
||||
const rpcCall: RpcCall = {
|
||||
id: uuidv4(),
|
||||
networkId,
|
||||
componentType,
|
||||
methodName,
|
||||
parameters,
|
||||
metadata,
|
||||
senderId: client.id,
|
||||
requiresResponse: metadata?.requiresResponse || false,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
// 权限检查
|
||||
if (this.config.enablePermissionCheck) {
|
||||
if (!this.checkRpcPermission(client, rpcCall, 'server-rpc')) {
|
||||
this.emit('permission-denied', client.id, rpcCall);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 频率限制检查
|
||||
if (this.config.enableRateLimit && !this.checkRpcRate(client.id)) {
|
||||
this.emit('rate-limit-exceeded', client.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// 参数验证
|
||||
if (this.config.enableParameterValidation) {
|
||||
const validationResult = this.validateRpcParameters(rpcCall);
|
||||
if (!validationResult.valid) {
|
||||
this.emit('parameter-validation-failed', client.id, rpcCall, validationResult.reason!);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 记录活跃调用
|
||||
this.recordActiveCall(client.id, rpcCall);
|
||||
|
||||
// 触发事件
|
||||
this.emit('server-rpc-called', rpcCall);
|
||||
|
||||
// ServerRpc 在服务端执行,这里需要实际的执行逻辑
|
||||
// 在实际使用中,应该通过事件或回调来执行具体的方法
|
||||
const response = await this.executeServerRpc(rpcCall);
|
||||
|
||||
// 发送响应(如果需要)
|
||||
if (rpcCall.requiresResponse && response) {
|
||||
await this.sendRpcResponse(client, response);
|
||||
}
|
||||
|
||||
this.completeRpcCall(rpcCall, response || undefined);
|
||||
|
||||
} catch (error) {
|
||||
this.emit('rpc-error', error as Error, undefined, client.id);
|
||||
|
||||
// 发送错误响应
|
||||
if (message.data && (message.data as any).requiresResponse) {
|
||||
const errorResponse: RpcResponse = {
|
||||
callId: (message.data as any).callId || uuidv4(),
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
errorCode: 'EXECUTION_ERROR',
|
||||
timestamp: new Date()
|
||||
};
|
||||
await this.sendRpcResponse(client, errorResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 RPC 响应
|
||||
*/
|
||||
async handleRpcResponse(
|
||||
client: ClientConnection,
|
||||
message: TransportMessage
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response = message.data as any as RpcResponse;
|
||||
const pendingCall = this.pendingCalls.get(response.callId);
|
||||
|
||||
if (pendingCall) {
|
||||
// 清除超时定时器
|
||||
clearTimeout(pendingCall.timeoutTimer);
|
||||
this.pendingCalls.delete(response.callId);
|
||||
|
||||
// 调用响应回调
|
||||
pendingCall.responseCallback(response);
|
||||
|
||||
// 完成调用
|
||||
this.completeRpcCall(pendingCall.call, response);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.emit('rpc-error', error as Error, undefined, client.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 ClientRpc(从服务端向客户端发送)
|
||||
*/
|
||||
async callClientRpc(
|
||||
room: Room,
|
||||
networkId: number,
|
||||
componentType: string,
|
||||
methodName: string,
|
||||
parameters: NetworkValue[] = [],
|
||||
options: {
|
||||
targetFilter?: 'all' | 'others' | 'owner' | string[];
|
||||
requiresResponse?: boolean;
|
||||
timeout?: number;
|
||||
} = {}
|
||||
): Promise<RpcResponse[]> {
|
||||
const rpcCall: RpcCall = {
|
||||
id: uuidv4(),
|
||||
networkId,
|
||||
componentType,
|
||||
methodName,
|
||||
parameters,
|
||||
metadata: {
|
||||
methodName,
|
||||
rpcType: 'client-rpc',
|
||||
requiresAuth: false,
|
||||
reliable: true,
|
||||
requiresResponse: options.requiresResponse || false
|
||||
},
|
||||
senderId: 'server',
|
||||
requiresResponse: options.requiresResponse || false,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
// 确定目标客户端
|
||||
const targetClientIds = typeof options.targetFilter === 'string'
|
||||
? this.getClientRpcTargets(room, 'server', options.targetFilter)
|
||||
: options.targetFilter || [];
|
||||
|
||||
rpcCall.targetClientIds = targetClientIds;
|
||||
|
||||
// 发送到目标客户端
|
||||
await this.sendClientRpc(room, rpcCall, targetClientIds);
|
||||
|
||||
// 如果需要响应,等待响应
|
||||
if (options.requiresResponse) {
|
||||
return await this.waitForRpcResponses(rpcCall, targetClientIds, options.timeout);
|
||||
}
|
||||
|
||||
this.completeRpcCall(rpcCall);
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端统计信息
|
||||
*/
|
||||
getClientRpcStats(clientId: string): {
|
||||
activeCalls: number;
|
||||
totalCalls: number;
|
||||
} {
|
||||
const state = this.clientStates.get(clientId);
|
||||
return {
|
||||
activeCalls: state?.activeCalls.size || 0,
|
||||
totalCalls: state?.rpcCount || 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有客户端的 RPC 调用
|
||||
*/
|
||||
cancelClientRpcs(clientId: string): number {
|
||||
const state = this.clientStates.get(clientId);
|
||||
if (!state) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const cancelledCount = state.activeCalls.size;
|
||||
|
||||
// 取消所有活跃调用
|
||||
for (const call of state.activeCalls.values()) {
|
||||
this.completeRpcCall(call);
|
||||
}
|
||||
|
||||
state.activeCalls.clear();
|
||||
return cancelledCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁 RPC 系统
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.cleanupTimer) {
|
||||
clearInterval(this.cleanupTimer);
|
||||
this.cleanupTimer = null;
|
||||
}
|
||||
|
||||
// 清除所有待处理的调用
|
||||
for (const pending of this.pendingCalls.values()) {
|
||||
clearTimeout(pending.timeoutTimer);
|
||||
}
|
||||
|
||||
this.clientStates.clear();
|
||||
this.pendingCalls.clear();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化系统
|
||||
*/
|
||||
private initialize(): void {
|
||||
// 启动清理定时器(每分钟清理一次)
|
||||
this.cleanupTimer = setInterval(() => {
|
||||
this.cleanup();
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 RPC 权限
|
||||
*/
|
||||
private checkRpcPermission(
|
||||
client: ClientConnection,
|
||||
call: RpcCall,
|
||||
rpcType: 'client-rpc' | 'server-rpc'
|
||||
): boolean {
|
||||
// 基本权限检查
|
||||
if (!client.hasPermission('canSendRpc')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ServerRpc 额外权限检查
|
||||
if (rpcType === 'server-rpc' && call.metadata.requiresAuth) {
|
||||
if (!client.isAuthenticated) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 可以添加更多特定的权限检查逻辑
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 RPC 频率
|
||||
*/
|
||||
private checkRpcRate(clientId: string): boolean {
|
||||
if (!this.config.maxRpcRate || this.config.maxRpcRate <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
let state = this.clientStates.get(clientId);
|
||||
|
||||
if (!state) {
|
||||
state = {
|
||||
clientId,
|
||||
activeCalls: new Map(),
|
||||
rpcCount: 1,
|
||||
rateResetTime: new Date(now.getTime() + 1000)
|
||||
};
|
||||
this.clientStates.set(clientId, state);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否需要重置计数
|
||||
if (now >= state.rateResetTime) {
|
||||
state.rpcCount = 1;
|
||||
state.rateResetTime = new Date(now.getTime() + 1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查频率限制
|
||||
if (state.rpcCount >= this.config.maxRpcRate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.rpcCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 RPC 参数
|
||||
*/
|
||||
private validateRpcParameters(call: RpcCall): { valid: boolean; reason?: string } {
|
||||
// 检查参数数量
|
||||
if (call.parameters.length > 10) {
|
||||
return { valid: false, reason: 'Too many parameters' };
|
||||
}
|
||||
|
||||
// 检查每个参数的大小
|
||||
for (let i = 0; i < call.parameters.length; i++) {
|
||||
const param = call.parameters[i];
|
||||
try {
|
||||
const serialized = JSON.stringify(param);
|
||||
if (serialized.length > this.config.maxParameterSize!) {
|
||||
return { valid: false, reason: `Parameter ${i} is too large` };
|
||||
}
|
||||
} catch (error) {
|
||||
return { valid: false, reason: `Parameter ${i} is not serializable` };
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 ClientRpc 目标客户端
|
||||
*/
|
||||
private getClientRpcTargets(
|
||||
room: Room,
|
||||
senderId: string,
|
||||
targetFilter: string
|
||||
): string[] {
|
||||
const players = room.getPlayers();
|
||||
|
||||
switch (targetFilter) {
|
||||
case 'all':
|
||||
return players.map(p => p.client.id);
|
||||
|
||||
case 'others':
|
||||
return players
|
||||
.filter(p => p.client.id !== senderId)
|
||||
.map(p => p.client.id);
|
||||
|
||||
case 'owner':
|
||||
const owner = room.getOwner();
|
||||
return owner ? [owner.client.id] : [];
|
||||
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 ClientRpc
|
||||
*/
|
||||
private async sendClientRpc(
|
||||
room: Room,
|
||||
call: RpcCall,
|
||||
targetClientIds: string[]
|
||||
): Promise<void> {
|
||||
const message: TransportMessage = {
|
||||
type: 'rpc',
|
||||
data: {
|
||||
action: 'client-rpc',
|
||||
callId: call.id,
|
||||
networkId: call.networkId,
|
||||
componentType: call.componentType,
|
||||
methodName: call.methodName,
|
||||
parameters: call.parameters,
|
||||
metadata: call.metadata as any,
|
||||
requiresResponse: call.requiresResponse,
|
||||
timestamp: call.timestamp.getTime()
|
||||
} as any
|
||||
};
|
||||
|
||||
// 发送给目标客户端
|
||||
const promises = targetClientIds.map(clientId =>
|
||||
room.sendToPlayer(clientId, message)
|
||||
);
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 ServerRpc
|
||||
*/
|
||||
private async executeServerRpc(call: RpcCall): Promise<RpcResponse | null> {
|
||||
// 这里应该是实际的服务端方法执行逻辑
|
||||
// 在实际实现中,可能需要通过事件或回调来执行具体的方法
|
||||
|
||||
// 示例响应
|
||||
const response: RpcResponse = {
|
||||
callId: call.id,
|
||||
success: true,
|
||||
result: undefined, // 实际执行结果
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 RPC 响应
|
||||
*/
|
||||
private async sendRpcResponse(
|
||||
client: ClientConnection,
|
||||
response: RpcResponse
|
||||
): Promise<void> {
|
||||
const message: TransportMessage = {
|
||||
type: 'rpc',
|
||||
data: {
|
||||
action: 'rpc-response',
|
||||
...response
|
||||
} as any
|
||||
};
|
||||
|
||||
await client.sendMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待 RPC 响应
|
||||
*/
|
||||
private async waitForRpcResponses(
|
||||
call: RpcCall,
|
||||
targetClientIds: string[],
|
||||
timeout?: number
|
||||
): Promise<RpcResponse[]> {
|
||||
return new Promise((resolve) => {
|
||||
const responses: RpcResponse[] = [];
|
||||
const responseTimeout = timeout || this.config.callTimeout!;
|
||||
let responseCount = 0;
|
||||
|
||||
const responseCallback = (response: RpcResponse) => {
|
||||
responses.push(response);
|
||||
responseCount++;
|
||||
|
||||
// 如果收到所有响应,立即resolve
|
||||
if (responseCount >= targetClientIds.length) {
|
||||
resolve(responses);
|
||||
}
|
||||
};
|
||||
|
||||
// 设置超时
|
||||
const timeoutTimer = setTimeout(() => {
|
||||
resolve(responses); // 返回已收到的响应
|
||||
this.emit('rpc-timeout', call.id);
|
||||
}, responseTimeout);
|
||||
|
||||
// 注册待处理的响应
|
||||
this.pendingCalls.set(call.id, {
|
||||
call,
|
||||
timeoutTimer,
|
||||
responseCallback
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录活跃调用
|
||||
*/
|
||||
private recordActiveCall(clientId: string, call: RpcCall): void {
|
||||
let state = this.clientStates.get(clientId);
|
||||
if (!state) {
|
||||
state = {
|
||||
clientId,
|
||||
activeCalls: new Map(),
|
||||
rpcCount: 0,
|
||||
rateResetTime: new Date()
|
||||
};
|
||||
this.clientStates.set(clientId, state);
|
||||
}
|
||||
|
||||
state.activeCalls.set(call.id, call);
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成 RPC 调用
|
||||
*/
|
||||
private completeRpcCall(call: RpcCall, response?: RpcResponse): void {
|
||||
// 从活跃调用中移除
|
||||
const state = this.clientStates.get(call.senderId);
|
||||
if (state) {
|
||||
state.activeCalls.delete(call.id);
|
||||
}
|
||||
|
||||
// 触发完成事件
|
||||
this.emit('rpc-completed', call, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的调用和状态
|
||||
*/
|
||||
private cleanup(): void {
|
||||
const now = new Date();
|
||||
let cleanedCalls = 0;
|
||||
let cleanedStates = 0;
|
||||
|
||||
// 清理过期的待处理调用
|
||||
for (const [callId, pending] of this.pendingCalls.entries()) {
|
||||
if (pending.call.expiresAt && pending.call.expiresAt < now) {
|
||||
clearTimeout(pending.timeoutTimer);
|
||||
this.pendingCalls.delete(callId);
|
||||
cleanedCalls++;
|
||||
}
|
||||
}
|
||||
|
||||
// 清理空的客户端状态
|
||||
for (const [clientId, state] of this.clientStates.entries()) {
|
||||
if (state.activeCalls.size === 0 &&
|
||||
now.getTime() - state.rateResetTime.getTime() > 60000) {
|
||||
this.clientStates.delete(clientId);
|
||||
cleanedStates++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedCalls > 0 || cleanedStates > 0) {
|
||||
console.log(`RPC cleanup: ${cleanedCalls} calls, ${cleanedStates} states`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
override on<K extends keyof RpcSystemEvents>(event: K, listener: RpcSystemEvents[K]): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
override emit<K extends keyof RpcSystemEvents>(event: K, ...args: Parameters<RpcSystemEvents[K]>): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,587 +0,0 @@
|
||||
/**
|
||||
* SyncVar 同步系统
|
||||
*
|
||||
* 处理服务端的 SyncVar 同步逻辑、权限验证、数据传播等
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import {
|
||||
NetworkValue,
|
||||
SyncVarMetadata,
|
||||
NetworkSerializer
|
||||
} from '@esengine/ecs-framework-network-shared';
|
||||
import { ClientConnection } from '../core/ClientConnection';
|
||||
import { Room } from '../rooms/Room';
|
||||
import { TransportMessage } from '../core/Transport';
|
||||
|
||||
/**
|
||||
* SyncVar 更改记录
|
||||
*/
|
||||
export interface SyncVarChange {
|
||||
/** 网络对象ID */
|
||||
networkId: number;
|
||||
/** 组件类型 */
|
||||
componentType: string;
|
||||
/** 属性名 */
|
||||
propertyName: string;
|
||||
/** 旧值 */
|
||||
oldValue: NetworkValue;
|
||||
/** 新值 */
|
||||
newValue: NetworkValue;
|
||||
/** 元数据 */
|
||||
metadata: SyncVarMetadata;
|
||||
/** 发送者客户端ID */
|
||||
senderId: string;
|
||||
/** 时间戳 */
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncVar 同步配置
|
||||
*/
|
||||
export interface SyncVarSystemConfig {
|
||||
/** 批量同步间隔(毫秒) */
|
||||
batchInterval?: number;
|
||||
/** 单次批量最大数量 */
|
||||
maxBatchSize?: number;
|
||||
/** 是否启用增量同步 */
|
||||
enableDeltaSync?: boolean;
|
||||
/** 是否启用权限检查 */
|
||||
enablePermissionCheck?: boolean;
|
||||
/** 是否启用数据验证 */
|
||||
enableDataValidation?: boolean;
|
||||
/** 最大同步频率(次/秒) */
|
||||
maxSyncRate?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络对象状态
|
||||
*/
|
||||
export interface NetworkObjectState {
|
||||
/** 网络对象ID */
|
||||
networkId: number;
|
||||
/** 拥有者客户端ID */
|
||||
ownerId: string;
|
||||
/** 组件状态 */
|
||||
components: Map<string, Map<string, NetworkValue>>;
|
||||
/** 最后更新时间 */
|
||||
lastUpdateTime: Date;
|
||||
/** 权威状态 */
|
||||
hasAuthority: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncVar 系统事件
|
||||
*/
|
||||
export interface SyncVarSystemEvents {
|
||||
/** SyncVar 值变化 */
|
||||
'syncvar-changed': (change: SyncVarChange) => void;
|
||||
/** 同步批次完成 */
|
||||
'batch-synced': (changes: SyncVarChange[], targetClients: string[]) => void;
|
||||
/** 权限验证失败 */
|
||||
'permission-denied': (clientId: string, change: SyncVarChange) => void;
|
||||
/** 数据验证失败 */
|
||||
'validation-failed': (clientId: string, change: SyncVarChange, reason: string) => void;
|
||||
/** 同步错误 */
|
||||
'sync-error': (error: Error, clientId?: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端同步状态
|
||||
*/
|
||||
interface ClientSyncState {
|
||||
/** 客户端ID */
|
||||
clientId: string;
|
||||
/** 待同步的变化列表 */
|
||||
pendingChanges: SyncVarChange[];
|
||||
/** 最后同步时间 */
|
||||
lastSyncTime: Date;
|
||||
/** 同步频率限制 */
|
||||
syncCount: number;
|
||||
/** 频率重置时间 */
|
||||
rateResetTime: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncVar 同步系统
|
||||
*/
|
||||
export class SyncVarSystem extends EventEmitter {
|
||||
private config: SyncVarSystemConfig;
|
||||
private networkObjects = new Map<number, NetworkObjectState>();
|
||||
private clientSyncStates = new Map<string, ClientSyncState>();
|
||||
private serializer: NetworkSerializer;
|
||||
private batchTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(config: SyncVarSystemConfig = {}) {
|
||||
super();
|
||||
|
||||
this.config = {
|
||||
batchInterval: 50, // 50ms批量间隔
|
||||
maxBatchSize: 100,
|
||||
enableDeltaSync: true,
|
||||
enablePermissionCheck: true,
|
||||
enableDataValidation: true,
|
||||
maxSyncRate: 60, // 60次/秒
|
||||
...config
|
||||
};
|
||||
|
||||
this.serializer = new NetworkSerializer();
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册网络对象
|
||||
*/
|
||||
registerNetworkObject(
|
||||
networkId: number,
|
||||
ownerId: string,
|
||||
hasAuthority: boolean = true
|
||||
): void {
|
||||
if (this.networkObjects.has(networkId)) {
|
||||
console.warn(`Network object ${networkId} is already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
const networkObject: NetworkObjectState = {
|
||||
networkId,
|
||||
ownerId,
|
||||
components: new Map(),
|
||||
lastUpdateTime: new Date(),
|
||||
hasAuthority
|
||||
};
|
||||
|
||||
this.networkObjects.set(networkId, networkObject);
|
||||
console.log(`Network object registered: ${networkId} owned by ${ownerId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销网络对象
|
||||
*/
|
||||
unregisterNetworkObject(networkId: number): boolean {
|
||||
const removed = this.networkObjects.delete(networkId);
|
||||
if (removed) {
|
||||
console.log(`Network object unregistered: ${networkId}`);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网络对象
|
||||
*/
|
||||
getNetworkObject(networkId: number): NetworkObjectState | undefined {
|
||||
return this.networkObjects.get(networkId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SyncVar 变化消息
|
||||
*/
|
||||
async handleSyncVarChange(
|
||||
client: ClientConnection,
|
||||
message: TransportMessage,
|
||||
room?: Room
|
||||
): Promise<void> {
|
||||
try {
|
||||
const data = message.data as any;
|
||||
const {
|
||||
networkId,
|
||||
componentType,
|
||||
propertyName,
|
||||
oldValue,
|
||||
newValue,
|
||||
metadata
|
||||
} = data;
|
||||
|
||||
// 创建变化记录
|
||||
const change: SyncVarChange = {
|
||||
networkId,
|
||||
componentType,
|
||||
propertyName,
|
||||
oldValue,
|
||||
newValue,
|
||||
metadata,
|
||||
senderId: client.id,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
// 权限检查
|
||||
if (this.config.enablePermissionCheck) {
|
||||
if (!this.checkSyncVarPermission(client, change)) {
|
||||
this.emit('permission-denied', client.id, change);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 频率限制检查
|
||||
if (!this.checkSyncRate(client.id)) {
|
||||
console.warn(`SyncVar rate limit exceeded for client ${client.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 数据验证
|
||||
if (this.config.enableDataValidation) {
|
||||
const validationResult = this.validateSyncVarData(change);
|
||||
if (!validationResult.valid) {
|
||||
this.emit('validation-failed', client.id, change, validationResult.reason!);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新网络对象状态
|
||||
this.updateNetworkObjectState(change);
|
||||
|
||||
// 触发变化事件
|
||||
this.emit('syncvar-changed', change);
|
||||
|
||||
// 添加到待同步列表
|
||||
if (room) {
|
||||
this.addToBatchSync(change, room);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.emit('sync-error', error as Error, client.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网络对象的完整状态
|
||||
*/
|
||||
getNetworkObjectSnapshot(networkId: number): Record<string, any> | null {
|
||||
const networkObject = this.networkObjects.get(networkId);
|
||||
if (!networkObject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const snapshot: Record<string, any> = {};
|
||||
|
||||
for (const [componentType, componentData] of networkObject.components) {
|
||||
snapshot[componentType] = {};
|
||||
for (const [propertyName, value] of componentData) {
|
||||
snapshot[componentType][propertyName] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向客户端发送网络对象快照
|
||||
*/
|
||||
async sendNetworkObjectSnapshot(
|
||||
client: ClientConnection,
|
||||
networkId: number
|
||||
): Promise<boolean> {
|
||||
const snapshot = this.getNetworkObjectSnapshot(networkId);
|
||||
if (!snapshot) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const message: TransportMessage = {
|
||||
type: 'syncvar',
|
||||
data: {
|
||||
action: 'snapshot',
|
||||
networkId,
|
||||
snapshot
|
||||
}
|
||||
};
|
||||
|
||||
return await client.sendMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步所有网络对象给新客户端
|
||||
*/
|
||||
async syncAllNetworkObjects(client: ClientConnection, room: Room): Promise<number> {
|
||||
let syncedCount = 0;
|
||||
|
||||
for (const networkObject of this.networkObjects.values()) {
|
||||
// 检查客户端是否有权限看到这个网络对象
|
||||
if (this.canClientSeeNetworkObject(client.id, networkObject)) {
|
||||
const success = await this.sendNetworkObjectSnapshot(client, networkObject.networkId);
|
||||
if (success) {
|
||||
syncedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Synced ${syncedCount} network objects to client ${client.id}`);
|
||||
return syncedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置网络对象拥有者
|
||||
*/
|
||||
setNetworkObjectOwner(networkId: number, newOwnerId: string): boolean {
|
||||
const networkObject = this.networkObjects.get(networkId);
|
||||
if (!networkObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldOwnerId = networkObject.ownerId;
|
||||
networkObject.ownerId = newOwnerId;
|
||||
networkObject.lastUpdateTime = new Date();
|
||||
|
||||
console.log(`Network object ${networkId} ownership changed: ${oldOwnerId} -> ${newOwnerId}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网络对象拥有者
|
||||
*/
|
||||
getNetworkObjectOwner(networkId: number): string | undefined {
|
||||
const networkObject = this.networkObjects.get(networkId);
|
||||
return networkObject?.ownerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁 SyncVar 系统
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.batchTimer) {
|
||||
clearInterval(this.batchTimer);
|
||||
this.batchTimer = null;
|
||||
}
|
||||
|
||||
this.networkObjects.clear();
|
||||
this.clientSyncStates.clear();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化系统
|
||||
*/
|
||||
private initialize(): void {
|
||||
// 启动批量同步定时器
|
||||
if (this.config.batchInterval && this.config.batchInterval > 0) {
|
||||
this.batchTimer = setInterval(() => {
|
||||
this.processBatchSync();
|
||||
}, this.config.batchInterval);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 SyncVar 权限
|
||||
*/
|
||||
private checkSyncVarPermission(client: ClientConnection, change: SyncVarChange): boolean {
|
||||
// 检查客户端是否有网络同步权限
|
||||
if (!client.hasPermission('canSyncVars')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取网络对象
|
||||
const networkObject = this.networkObjects.get(change.networkId);
|
||||
if (!networkObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查权威权限
|
||||
if (change.metadata.authorityOnly) {
|
||||
// 只有网络对象拥有者或有权威权限的客户端可以修改
|
||||
return networkObject.ownerId === client.id || networkObject.hasAuthority;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查同步频率
|
||||
*/
|
||||
private checkSyncRate(clientId: string): boolean {
|
||||
if (!this.config.maxSyncRate || this.config.maxSyncRate <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
let syncState = this.clientSyncStates.get(clientId);
|
||||
|
||||
if (!syncState) {
|
||||
syncState = {
|
||||
clientId,
|
||||
pendingChanges: [],
|
||||
lastSyncTime: now,
|
||||
syncCount: 1,
|
||||
rateResetTime: new Date(now.getTime() + 1000) // 1秒后重置
|
||||
};
|
||||
this.clientSyncStates.set(clientId, syncState);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否需要重置计数
|
||||
if (now >= syncState.rateResetTime) {
|
||||
syncState.syncCount = 1;
|
||||
syncState.rateResetTime = new Date(now.getTime() + 1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查频率限制
|
||||
if (syncState.syncCount >= this.config.maxSyncRate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
syncState.syncCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 SyncVar 数据
|
||||
*/
|
||||
private validateSyncVarData(change: SyncVarChange): { valid: boolean; reason?: string } {
|
||||
// 基本类型检查
|
||||
if (change.newValue === null || change.newValue === undefined) {
|
||||
return { valid: false, reason: 'Value cannot be null or undefined' };
|
||||
}
|
||||
|
||||
// 检查数据大小(防止过大的数据)
|
||||
try {
|
||||
const serialized = JSON.stringify(change.newValue);
|
||||
if (serialized.length > 65536) { // 64KB限制
|
||||
return { valid: false, reason: 'Data too large' };
|
||||
}
|
||||
} catch (error) {
|
||||
return { valid: false, reason: 'Data is not serializable' };
|
||||
}
|
||||
|
||||
// 可以添加更多特定的验证逻辑
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新网络对象状态
|
||||
*/
|
||||
private updateNetworkObjectState(change: SyncVarChange): void {
|
||||
let networkObject = this.networkObjects.get(change.networkId);
|
||||
|
||||
if (!networkObject) {
|
||||
// 如果网络对象不存在,创建一个新的(可能是客户端创建的)
|
||||
networkObject = {
|
||||
networkId: change.networkId,
|
||||
ownerId: change.senderId,
|
||||
components: new Map(),
|
||||
lastUpdateTime: new Date(),
|
||||
hasAuthority: true
|
||||
};
|
||||
this.networkObjects.set(change.networkId, networkObject);
|
||||
}
|
||||
|
||||
// 获取或创建组件数据
|
||||
let componentData = networkObject.components.get(change.componentType);
|
||||
if (!componentData) {
|
||||
componentData = new Map();
|
||||
networkObject.components.set(change.componentType, componentData);
|
||||
}
|
||||
|
||||
// 更新属性值
|
||||
componentData.set(change.propertyName, change.newValue);
|
||||
networkObject.lastUpdateTime = change.timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加到批量同步
|
||||
*/
|
||||
private addToBatchSync(change: SyncVarChange, room: Room): void {
|
||||
// 获取房间内需要同步的客户端
|
||||
const roomPlayers = room.getPlayers();
|
||||
const targetClientIds = roomPlayers
|
||||
.filter(player => player.client.id !== change.senderId) // 不发送给发送者
|
||||
.map(player => player.client.id);
|
||||
|
||||
// 为每个目标客户端添加变化记录
|
||||
for (const clientId of targetClientIds) {
|
||||
let syncState = this.clientSyncStates.get(clientId);
|
||||
if (!syncState) {
|
||||
syncState = {
|
||||
clientId,
|
||||
pendingChanges: [],
|
||||
lastSyncTime: new Date(),
|
||||
syncCount: 0,
|
||||
rateResetTime: new Date()
|
||||
};
|
||||
this.clientSyncStates.set(clientId, syncState);
|
||||
}
|
||||
|
||||
syncState.pendingChanges.push(change);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理批量同步
|
||||
*/
|
||||
private async processBatchSync(): Promise<void> {
|
||||
const syncPromises: Promise<void>[] = [];
|
||||
|
||||
for (const [clientId, syncState] of this.clientSyncStates.entries()) {
|
||||
if (syncState.pendingChanges.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取要同步的变化(限制批量大小)
|
||||
const changesToSync = syncState.pendingChanges.splice(
|
||||
0,
|
||||
this.config.maxBatchSize
|
||||
);
|
||||
|
||||
if (changesToSync.length > 0) {
|
||||
syncPromises.push(this.sendBatchChanges(clientId, changesToSync));
|
||||
}
|
||||
}
|
||||
|
||||
if (syncPromises.length > 0) {
|
||||
await Promise.allSettled(syncPromises);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送批量变化
|
||||
*/
|
||||
private async sendBatchChanges(clientId: string, changes: SyncVarChange[]): Promise<void> {
|
||||
try {
|
||||
// 这里需要获取客户端连接,实际实现中可能需要从外部传入
|
||||
// 为了简化,这里假设有一个方法可以获取客户端连接
|
||||
// 实际使用时,可能需要通过回调或事件来发送消息
|
||||
|
||||
const message: TransportMessage = {
|
||||
type: 'syncvar',
|
||||
data: {
|
||||
action: 'batch-update',
|
||||
changes: changes.map(change => ({
|
||||
networkId: change.networkId,
|
||||
componentType: change.componentType,
|
||||
propertyName: change.propertyName,
|
||||
newValue: change.newValue,
|
||||
metadata: change.metadata as any,
|
||||
timestamp: change.timestamp.getTime()
|
||||
}))
|
||||
} as any
|
||||
};
|
||||
|
||||
// 这里需要实际的发送逻辑
|
||||
// 在实际使用中,应该通过事件或回调来发送消息
|
||||
this.emit('batch-synced', changes, [clientId]);
|
||||
|
||||
} catch (error) {
|
||||
this.emit('sync-error', error as Error, clientId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查客户端是否可以看到网络对象
|
||||
*/
|
||||
private canClientSeeNetworkObject(clientId: string, networkObject: NetworkObjectState): boolean {
|
||||
// 基本实现:客户端可以看到自己拥有的对象和公共对象
|
||||
// 实际实现中可能需要更复杂的可见性逻辑
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
override on<K extends keyof SyncVarSystemEvents>(event: K, listener: SyncVarSystemEvents[K]): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
override emit<K extends keyof SyncVarSystemEvents>(event: K, ...args: Parameters<SyncVarSystemEvents[K]>): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* 系统模块导出
|
||||
*/
|
||||
|
||||
export * from './SyncVarSystem';
|
||||
export * from './RpcSystem';
|
||||
@@ -1,572 +0,0 @@
|
||||
/**
|
||||
* 消息验证器
|
||||
*
|
||||
* 验证网络消息的格式、大小、内容等
|
||||
*/
|
||||
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { TransportMessage } from '../core/Transport';
|
||||
|
||||
/**
|
||||
* 验证结果
|
||||
*/
|
||||
export interface ValidationResult {
|
||||
/** 是否有效 */
|
||||
valid: boolean;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
/** 错误代码 */
|
||||
errorCode?: string;
|
||||
/** 详细信息 */
|
||||
details?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配置
|
||||
*/
|
||||
export interface ValidationConfig {
|
||||
/** 最大消息大小(字节) */
|
||||
maxMessageSize?: number;
|
||||
/** 最大数组长度 */
|
||||
maxArrayLength?: number;
|
||||
/** 最大对象深度 */
|
||||
maxObjectDepth?: number;
|
||||
/** 最大字符串长度 */
|
||||
maxStringLength?: number;
|
||||
/** 允许的消息类型 */
|
||||
allowedMessageTypes?: string[];
|
||||
/** 是否允许null值 */
|
||||
allowNullValues?: boolean;
|
||||
/** 是否允许undefined值 */
|
||||
allowUndefinedValues?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证规则
|
||||
*/
|
||||
export interface ValidationRule {
|
||||
/** 规则名称 */
|
||||
name: string;
|
||||
/** 验证函数 */
|
||||
validate: (value: any, context: ValidationContext) => ValidationResult;
|
||||
/** 是否必需 */
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证上下文
|
||||
*/
|
||||
export interface ValidationContext {
|
||||
/** 当前路径 */
|
||||
path: string[];
|
||||
/** 当前深度 */
|
||||
depth: number;
|
||||
/** 配置 */
|
||||
config: ValidationConfig;
|
||||
/** 消息类型 */
|
||||
messageType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息验证器
|
||||
*/
|
||||
export class MessageValidator {
|
||||
private config: ValidationConfig;
|
||||
private customRules = new Map<string, ValidationRule>();
|
||||
|
||||
constructor(config: ValidationConfig = {}) {
|
||||
this.config = {
|
||||
maxMessageSize: 1024 * 1024, // 1MB
|
||||
maxArrayLength: 1000,
|
||||
maxObjectDepth: 10,
|
||||
maxStringLength: 10000,
|
||||
allowedMessageTypes: ['rpc', 'syncvar', 'system', 'custom'],
|
||||
allowNullValues: true,
|
||||
allowUndefinedValues: false,
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证传输消息
|
||||
*/
|
||||
validateMessage(message: TransportMessage): ValidationResult {
|
||||
try {
|
||||
// 基本结构验证
|
||||
const structureResult = this.validateMessageStructure(message);
|
||||
if (!structureResult.valid) {
|
||||
return structureResult;
|
||||
}
|
||||
|
||||
// 消息大小验证
|
||||
const sizeResult = this.validateMessageSize(message);
|
||||
if (!sizeResult.valid) {
|
||||
return sizeResult;
|
||||
}
|
||||
|
||||
// 消息类型验证
|
||||
const typeResult = this.validateMessageType(message);
|
||||
if (!typeResult.valid) {
|
||||
return typeResult;
|
||||
}
|
||||
|
||||
// 数据内容验证
|
||||
const dataResult = this.validateMessageData(message);
|
||||
if (!dataResult.valid) {
|
||||
return dataResult;
|
||||
}
|
||||
|
||||
// 自定义规则验证
|
||||
const customResult = this.validateCustomRules(message);
|
||||
if (!customResult.valid) {
|
||||
return customResult;
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
error: (error as Error).message,
|
||||
errorCode: 'VALIDATION_ERROR'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证网络值
|
||||
*/
|
||||
validateNetworkValue(value: NetworkValue, context?: Partial<ValidationContext>): ValidationResult {
|
||||
const fullContext: ValidationContext = {
|
||||
path: [],
|
||||
depth: 0,
|
||||
config: this.config,
|
||||
...context
|
||||
};
|
||||
|
||||
return this.validateValue(value, fullContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加自定义验证规则
|
||||
*/
|
||||
addValidationRule(rule: ValidationRule): void {
|
||||
this.customRules.set(rule.name, rule);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除自定义验证规则
|
||||
*/
|
||||
removeValidationRule(ruleName: string): boolean {
|
||||
return this.customRules.delete(ruleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有自定义规则
|
||||
*/
|
||||
getCustomRules(): ValidationRule[] {
|
||||
return Array.from(this.customRules.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证消息结构
|
||||
*/
|
||||
private validateMessageStructure(message: TransportMessage): ValidationResult {
|
||||
// 检查必需字段
|
||||
if (!message.type) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Message type is required',
|
||||
errorCode: 'MISSING_TYPE'
|
||||
};
|
||||
}
|
||||
|
||||
if (message.data === undefined) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Message data is required',
|
||||
errorCode: 'MISSING_DATA'
|
||||
};
|
||||
}
|
||||
|
||||
// 检查字段类型
|
||||
if (typeof message.type !== 'string') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Message type must be a string',
|
||||
errorCode: 'INVALID_TYPE_FORMAT'
|
||||
};
|
||||
}
|
||||
|
||||
// 检查可选字段
|
||||
if (message.senderId && typeof message.senderId !== 'string') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Sender ID must be a string',
|
||||
errorCode: 'INVALID_SENDER_ID'
|
||||
};
|
||||
}
|
||||
|
||||
if (message.targetId && typeof message.targetId !== 'string') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Target ID must be a string',
|
||||
errorCode: 'INVALID_TARGET_ID'
|
||||
};
|
||||
}
|
||||
|
||||
if (message.reliable !== undefined && typeof message.reliable !== 'boolean') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Reliable flag must be a boolean',
|
||||
errorCode: 'INVALID_RELIABLE_FLAG'
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证消息大小
|
||||
*/
|
||||
private validateMessageSize(message: TransportMessage): ValidationResult {
|
||||
try {
|
||||
const serialized = JSON.stringify(message);
|
||||
const size = new TextEncoder().encode(serialized).length;
|
||||
|
||||
if (size > this.config.maxMessageSize!) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Message size (${size} bytes) exceeds maximum (${this.config.maxMessageSize} bytes)`,
|
||||
errorCode: 'MESSAGE_TOO_LARGE',
|
||||
details: { actualSize: size, maxSize: this.config.maxMessageSize }
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Failed to serialize message for size validation',
|
||||
errorCode: 'SERIALIZATION_ERROR'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证消息类型
|
||||
*/
|
||||
private validateMessageType(message: TransportMessage): ValidationResult {
|
||||
if (!this.config.allowedMessageTypes!.includes(message.type)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Message type '${message.type}' is not allowed`,
|
||||
errorCode: 'INVALID_MESSAGE_TYPE',
|
||||
details: {
|
||||
messageType: message.type,
|
||||
allowedTypes: this.config.allowedMessageTypes
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证消息数据
|
||||
*/
|
||||
private validateMessageData(message: TransportMessage): ValidationResult {
|
||||
const context: ValidationContext = {
|
||||
path: ['data'],
|
||||
depth: 0,
|
||||
config: this.config,
|
||||
messageType: message.type
|
||||
};
|
||||
|
||||
return this.validateValue(message.data, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证值
|
||||
*/
|
||||
private validateValue(value: any, context: ValidationContext): ValidationResult {
|
||||
// 深度检查
|
||||
if (context.depth > this.config.maxObjectDepth!) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Object depth (${context.depth}) exceeds maximum (${this.config.maxObjectDepth})`,
|
||||
errorCode: 'OBJECT_TOO_DEEP',
|
||||
details: { path: context.path.join('.'), depth: context.depth }
|
||||
};
|
||||
}
|
||||
|
||||
// null/undefined 检查
|
||||
if (value === null) {
|
||||
if (!this.config.allowNullValues) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Null values are not allowed',
|
||||
errorCode: 'NULL_NOT_ALLOWED',
|
||||
details: { path: context.path.join('.') }
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
if (!this.config.allowUndefinedValues) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Undefined values are not allowed',
|
||||
errorCode: 'UNDEFINED_NOT_ALLOWED',
|
||||
details: { path: context.path.join('.') }
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// 根据类型验证
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
return this.validateString(value, context);
|
||||
|
||||
case 'number':
|
||||
return this.validateNumber(value, context);
|
||||
|
||||
case 'boolean':
|
||||
return { valid: true };
|
||||
|
||||
case 'object':
|
||||
if (Array.isArray(value)) {
|
||||
return this.validateArray(value, context);
|
||||
} else {
|
||||
return this.validateObject(value, context);
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
valid: false,
|
||||
error: `Unsupported value type: ${typeof value}`,
|
||||
errorCode: 'UNSUPPORTED_TYPE',
|
||||
details: { path: context.path.join('.'), type: typeof value }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证字符串
|
||||
*/
|
||||
private validateString(value: string, context: ValidationContext): ValidationResult {
|
||||
if (value.length > this.config.maxStringLength!) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `String length (${value.length}) exceeds maximum (${this.config.maxStringLength})`,
|
||||
errorCode: 'STRING_TOO_LONG',
|
||||
details: {
|
||||
path: context.path.join('.'),
|
||||
actualLength: value.length,
|
||||
maxLength: this.config.maxStringLength
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证数字
|
||||
*/
|
||||
private validateNumber(value: number, context: ValidationContext): ValidationResult {
|
||||
// 检查是否为有效数字
|
||||
if (!Number.isFinite(value)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Number must be finite',
|
||||
errorCode: 'INVALID_NUMBER',
|
||||
details: { path: context.path.join('.'), value }
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证数组
|
||||
*/
|
||||
private validateArray(value: any[], context: ValidationContext): ValidationResult {
|
||||
// 长度检查
|
||||
if (value.length > this.config.maxArrayLength!) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Array length (${value.length}) exceeds maximum (${this.config.maxArrayLength})`,
|
||||
errorCode: 'ARRAY_TOO_LONG',
|
||||
details: {
|
||||
path: context.path.join('.'),
|
||||
actualLength: value.length,
|
||||
maxLength: this.config.maxArrayLength
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 验证每个元素
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const elementContext: ValidationContext = {
|
||||
...context,
|
||||
path: [...context.path, `[${i}]`],
|
||||
depth: context.depth + 1
|
||||
};
|
||||
|
||||
const result = this.validateValue(value[i], elementContext);
|
||||
if (!result.valid) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证对象
|
||||
*/
|
||||
private validateObject(value: Record<string, any>, context: ValidationContext): ValidationResult {
|
||||
// 验证每个属性
|
||||
for (const [key, propertyValue] of Object.entries(value)) {
|
||||
const propertyContext: ValidationContext = {
|
||||
...context,
|
||||
path: [...context.path, key],
|
||||
depth: context.depth + 1
|
||||
};
|
||||
|
||||
const result = this.validateValue(propertyValue, propertyContext);
|
||||
if (!result.valid) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证自定义规则
|
||||
*/
|
||||
private validateCustomRules(message: TransportMessage): ValidationResult {
|
||||
for (const rule of this.customRules.values()) {
|
||||
const context: ValidationContext = {
|
||||
path: [],
|
||||
depth: 0,
|
||||
config: this.config,
|
||||
messageType: message.type
|
||||
};
|
||||
|
||||
const result = rule.validate(message, context);
|
||||
if (!result.valid) {
|
||||
return {
|
||||
...result,
|
||||
details: {
|
||||
...result.details,
|
||||
rule: rule.name
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预定义验证规则
|
||||
*/
|
||||
export const DefaultValidationRules = {
|
||||
/**
|
||||
* RPC 消息验证规则
|
||||
*/
|
||||
RpcMessage: {
|
||||
name: 'RpcMessage',
|
||||
validate: (message: TransportMessage, context: ValidationContext): ValidationResult => {
|
||||
if (message.type !== 'rpc') {
|
||||
return { valid: true }; // 不是 RPC 消息,跳过验证
|
||||
}
|
||||
|
||||
const data = message.data as any;
|
||||
|
||||
// 检查必需字段
|
||||
if (!data.networkId || typeof data.networkId !== 'number') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'RPC message must have a valid networkId',
|
||||
errorCode: 'RPC_INVALID_NETWORK_ID'
|
||||
};
|
||||
}
|
||||
|
||||
if (!data.componentType || typeof data.componentType !== 'string') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'RPC message must have a valid componentType',
|
||||
errorCode: 'RPC_INVALID_COMPONENT_TYPE'
|
||||
};
|
||||
}
|
||||
|
||||
if (!data.methodName || typeof data.methodName !== 'string') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'RPC message must have a valid methodName',
|
||||
errorCode: 'RPC_INVALID_METHOD_NAME'
|
||||
};
|
||||
}
|
||||
|
||||
// 检查参数数组
|
||||
if (data.parameters && !Array.isArray(data.parameters)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'RPC parameters must be an array',
|
||||
errorCode: 'RPC_INVALID_PARAMETERS'
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
} as ValidationRule,
|
||||
|
||||
/**
|
||||
* SyncVar 消息验证规则
|
||||
*/
|
||||
SyncVarMessage: {
|
||||
name: 'SyncVarMessage',
|
||||
validate: (message: TransportMessage, context: ValidationContext): ValidationResult => {
|
||||
if (message.type !== 'syncvar') {
|
||||
return { valid: true }; // 不是 SyncVar 消息,跳过验证
|
||||
}
|
||||
|
||||
const data = message.data as any;
|
||||
|
||||
// 检查必需字段
|
||||
if (!data.networkId || typeof data.networkId !== 'number') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'SyncVar message must have a valid networkId',
|
||||
errorCode: 'SYNCVAR_INVALID_NETWORK_ID'
|
||||
};
|
||||
}
|
||||
|
||||
if (!data.componentType || typeof data.componentType !== 'string') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'SyncVar message must have a valid componentType',
|
||||
errorCode: 'SYNCVAR_INVALID_COMPONENT_TYPE'
|
||||
};
|
||||
}
|
||||
|
||||
if (!data.propertyName || typeof data.propertyName !== 'string') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'SyncVar message must have a valid propertyName',
|
||||
errorCode: 'SYNCVAR_INVALID_PROPERTY_NAME'
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
} as ValidationRule
|
||||
};
|
||||
@@ -1,776 +0,0 @@
|
||||
/**
|
||||
* RPC 验证器
|
||||
*
|
||||
* 专门用于验证 RPC 调用的参数、权限、频率等
|
||||
*/
|
||||
|
||||
import { NetworkValue, RpcMetadata } from '@esengine/ecs-framework-network-shared';
|
||||
import { ClientConnection } from '../core/ClientConnection';
|
||||
import { ValidationResult } from './MessageValidator';
|
||||
|
||||
/**
|
||||
* RPC 验证配置
|
||||
*/
|
||||
export interface RpcValidationConfig {
|
||||
/** 最大参数数量 */
|
||||
maxParameterCount?: number;
|
||||
/** 单个参数最大大小(字节) */
|
||||
maxParameterSize?: number;
|
||||
/** 允许的参数类型 */
|
||||
allowedParameterTypes?: string[];
|
||||
/** 方法名黑名单 */
|
||||
blacklistedMethods?: string[];
|
||||
/** 方法名白名单 */
|
||||
whitelistedMethods?: string[];
|
||||
/** 是否启用参数类型检查 */
|
||||
enableTypeCheck?: boolean;
|
||||
/** 是否启用参数内容过滤 */
|
||||
enableContentFilter?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 调用上下文
|
||||
*/
|
||||
export interface RpcCallContext {
|
||||
/** 客户端连接 */
|
||||
client: ClientConnection;
|
||||
/** 网络对象ID */
|
||||
networkId: number;
|
||||
/** 组件类型 */
|
||||
componentType: string;
|
||||
/** 方法名 */
|
||||
methodName: string;
|
||||
/** 参数列表 */
|
||||
parameters: NetworkValue[];
|
||||
/** RPC 元数据 */
|
||||
metadata: RpcMetadata;
|
||||
/** RPC 类型 */
|
||||
rpcType: 'client-rpc' | 'server-rpc';
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数类型定义
|
||||
*/
|
||||
export interface ParameterTypeDefinition {
|
||||
/** 参数名 */
|
||||
name: string;
|
||||
/** 参数类型 */
|
||||
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'any';
|
||||
/** 是否必需 */
|
||||
required?: boolean;
|
||||
/** 最小值/长度 */
|
||||
min?: number;
|
||||
/** 最大值/长度 */
|
||||
max?: number;
|
||||
/** 允许的值列表 */
|
||||
allowedValues?: NetworkValue[];
|
||||
/** 正则表达式(仅用于字符串) */
|
||||
pattern?: RegExp;
|
||||
/** 自定义验证函数 */
|
||||
customValidator?: (value: NetworkValue) => ValidationResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法签名定义
|
||||
*/
|
||||
export interface MethodSignature {
|
||||
/** 方法名 */
|
||||
methodName: string;
|
||||
/** 组件类型 */
|
||||
componentType: string;
|
||||
/** 参数定义 */
|
||||
parameters: ParameterTypeDefinition[];
|
||||
/** 返回值类型 */
|
||||
returnType?: string;
|
||||
/** 是否需要权限验证 */
|
||||
requiresAuth?: boolean;
|
||||
/** 所需权限 */
|
||||
requiredPermissions?: string[];
|
||||
/** 频率限制(调用/分钟) */
|
||||
rateLimit?: number;
|
||||
/** 自定义验证函数 */
|
||||
customValidator?: (context: RpcCallContext) => ValidationResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 频率跟踪
|
||||
*/
|
||||
interface RpcRateTracker {
|
||||
/** 客户端ID */
|
||||
clientId: string;
|
||||
/** 方法调用计数 */
|
||||
methodCalls: Map<string, { count: number; resetTime: Date }>;
|
||||
/** 最后更新时间 */
|
||||
lastUpdate: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 验证器
|
||||
*/
|
||||
export class RpcValidator {
|
||||
private config: RpcValidationConfig;
|
||||
private methodSignatures = new Map<string, MethodSignature>();
|
||||
private rateTrackers = new Map<string, RpcRateTracker>();
|
||||
private cleanupTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(config: RpcValidationConfig = {}) {
|
||||
this.config = {
|
||||
maxParameterCount: 10,
|
||||
maxParameterSize: 65536, // 64KB
|
||||
allowedParameterTypes: ['string', 'number', 'boolean', 'object', 'array'],
|
||||
blacklistedMethods: [],
|
||||
whitelistedMethods: [],
|
||||
enableTypeCheck: true,
|
||||
enableContentFilter: true,
|
||||
...config
|
||||
};
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 RPC 调用
|
||||
*/
|
||||
validateRpcCall(context: RpcCallContext): ValidationResult {
|
||||
try {
|
||||
// 基本验证
|
||||
const basicResult = this.validateBasicRpcCall(context);
|
||||
if (!basicResult.valid) {
|
||||
return basicResult;
|
||||
}
|
||||
|
||||
// 方法名验证
|
||||
const methodResult = this.validateMethodName(context);
|
||||
if (!methodResult.valid) {
|
||||
return methodResult;
|
||||
}
|
||||
|
||||
// 权限验证
|
||||
const permissionResult = this.validateRpcPermissions(context);
|
||||
if (!permissionResult.valid) {
|
||||
return permissionResult;
|
||||
}
|
||||
|
||||
// 参数验证
|
||||
const parameterResult = this.validateParameters(context);
|
||||
if (!parameterResult.valid) {
|
||||
return parameterResult;
|
||||
}
|
||||
|
||||
// 频率限制验证
|
||||
const rateResult = this.validateRateLimit(context);
|
||||
if (!rateResult.valid) {
|
||||
return rateResult;
|
||||
}
|
||||
|
||||
// 签名验证(如果有定义)
|
||||
const signatureResult = this.validateMethodSignature(context);
|
||||
if (!signatureResult.valid) {
|
||||
return signatureResult;
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
error: (error as Error).message,
|
||||
errorCode: 'RPC_VALIDATION_ERROR'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册方法签名
|
||||
*/
|
||||
registerMethodSignature(signature: MethodSignature): void {
|
||||
const key = `${signature.componentType}.${signature.methodName}`;
|
||||
this.methodSignatures.set(key, signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除方法签名
|
||||
*/
|
||||
removeMethodSignature(componentType: string, methodName: string): boolean {
|
||||
const key = `${componentType}.${methodName}`;
|
||||
return this.methodSignatures.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取方法签名
|
||||
*/
|
||||
getMethodSignature(componentType: string, methodName: string): MethodSignature | undefined {
|
||||
const key = `${componentType}.${methodName}`;
|
||||
return this.methodSignatures.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加方法到黑名单
|
||||
*/
|
||||
addToBlacklist(methodName: string): void {
|
||||
if (!this.config.blacklistedMethods!.includes(methodName)) {
|
||||
this.config.blacklistedMethods!.push(methodName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从黑名单移除方法
|
||||
*/
|
||||
removeFromBlacklist(methodName: string): boolean {
|
||||
const index = this.config.blacklistedMethods!.indexOf(methodName);
|
||||
if (index !== -1) {
|
||||
this.config.blacklistedMethods!.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加方法到白名单
|
||||
*/
|
||||
addToWhitelist(methodName: string): void {
|
||||
if (!this.config.whitelistedMethods!.includes(methodName)) {
|
||||
this.config.whitelistedMethods!.push(methodName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端的 RPC 统计
|
||||
*/
|
||||
getClientRpcStats(clientId: string): {
|
||||
totalCalls: number;
|
||||
methodStats: Record<string, number>;
|
||||
} {
|
||||
const tracker = this.rateTrackers.get(clientId);
|
||||
if (!tracker) {
|
||||
return { totalCalls: 0, methodStats: {} };
|
||||
}
|
||||
|
||||
let totalCalls = 0;
|
||||
const methodStats: Record<string, number> = {};
|
||||
|
||||
for (const [method, data] of tracker.methodCalls) {
|
||||
totalCalls += data.count;
|
||||
methodStats[method] = data.count;
|
||||
}
|
||||
|
||||
return { totalCalls, methodStats };
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置客户端的频率限制
|
||||
*/
|
||||
resetClientRateLimit(clientId: string): boolean {
|
||||
const tracker = this.rateTrackers.get(clientId);
|
||||
if (!tracker) {
|
||||
return false;
|
||||
}
|
||||
|
||||
tracker.methodCalls.clear();
|
||||
tracker.lastUpdate = new Date();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁验证器
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.cleanupTimer) {
|
||||
clearInterval(this.cleanupTimer);
|
||||
this.cleanupTimer = null;
|
||||
}
|
||||
|
||||
this.methodSignatures.clear();
|
||||
this.rateTrackers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化验证器
|
||||
*/
|
||||
private initialize(): void {
|
||||
// 启动清理定时器(每5分钟清理一次)
|
||||
this.cleanupTimer = setInterval(() => {
|
||||
this.cleanupRateTrackers();
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 基本 RPC 调用验证
|
||||
*/
|
||||
private validateBasicRpcCall(context: RpcCallContext): ValidationResult {
|
||||
// 网络对象ID验证
|
||||
if (!Number.isInteger(context.networkId) || context.networkId <= 0) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Invalid network object ID',
|
||||
errorCode: 'INVALID_NETWORK_ID'
|
||||
};
|
||||
}
|
||||
|
||||
// 组件类型验证
|
||||
if (!context.componentType || typeof context.componentType !== 'string') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Invalid component type',
|
||||
errorCode: 'INVALID_COMPONENT_TYPE'
|
||||
};
|
||||
}
|
||||
|
||||
// 方法名验证
|
||||
if (!context.methodName || typeof context.methodName !== 'string') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Invalid method name',
|
||||
errorCode: 'INVALID_METHOD_NAME'
|
||||
};
|
||||
}
|
||||
|
||||
// 参数数组验证
|
||||
if (!Array.isArray(context.parameters)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Parameters must be an array',
|
||||
errorCode: 'INVALID_PARAMETERS_FORMAT'
|
||||
};
|
||||
}
|
||||
|
||||
// 参数数量检查
|
||||
if (context.parameters.length > this.config.maxParameterCount!) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Too many parameters: ${context.parameters.length} (max: ${this.config.maxParameterCount})`,
|
||||
errorCode: 'TOO_MANY_PARAMETERS'
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法名验证
|
||||
*/
|
||||
private validateMethodName(context: RpcCallContext): ValidationResult {
|
||||
const methodName = context.methodName;
|
||||
|
||||
// 黑名单检查
|
||||
if (this.config.blacklistedMethods!.length > 0) {
|
||||
if (this.config.blacklistedMethods!.includes(methodName)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Method '${methodName}' is blacklisted`,
|
||||
errorCode: 'METHOD_BLACKLISTED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 白名单检查
|
||||
if (this.config.whitelistedMethods!.length > 0) {
|
||||
if (!this.config.whitelistedMethods!.includes(methodName)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Method '${methodName}' is not whitelisted`,
|
||||
errorCode: 'METHOD_NOT_WHITELISTED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 危险方法名检查
|
||||
const dangerousPatterns = [
|
||||
/^__/, // 私有方法
|
||||
/constructor/i,
|
||||
/prototype/i,
|
||||
/eval/i,
|
||||
/function/i
|
||||
];
|
||||
|
||||
for (const pattern of dangerousPatterns) {
|
||||
if (pattern.test(methodName)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Potentially dangerous method name: '${methodName}'`,
|
||||
errorCode: 'DANGEROUS_METHOD_NAME'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 权限验证
|
||||
*/
|
||||
private validateRpcPermissions(context: RpcCallContext): ValidationResult {
|
||||
// 基本 RPC 权限检查
|
||||
if (!context.client.hasPermission('canSendRpc')) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Client does not have RPC permission',
|
||||
errorCode: 'RPC_PERMISSION_DENIED'
|
||||
};
|
||||
}
|
||||
|
||||
// ServerRpc 特殊权限检查
|
||||
if (context.rpcType === 'server-rpc') {
|
||||
if (context.metadata.requiresAuth && !context.client.isAuthenticated) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Authentication required for this RPC',
|
||||
errorCode: 'AUTHENTICATION_REQUIRED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 检查方法签名中的权限要求
|
||||
const signature = this.getMethodSignature(context.componentType, context.methodName);
|
||||
if (signature && signature.requiredPermissions) {
|
||||
for (const permission of signature.requiredPermissions) {
|
||||
if (!context.client.hasCustomPermission(permission)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Required permission '${permission}' not found`,
|
||||
errorCode: 'INSUFFICIENT_PERMISSIONS'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数验证
|
||||
*/
|
||||
private validateParameters(context: RpcCallContext): ValidationResult {
|
||||
// 参数大小检查
|
||||
for (let i = 0; i < context.parameters.length; i++) {
|
||||
const param = context.parameters[i];
|
||||
|
||||
try {
|
||||
const serialized = JSON.stringify(param);
|
||||
const size = new TextEncoder().encode(serialized).length;
|
||||
|
||||
if (size > this.config.maxParameterSize!) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${i} is too large: ${size} bytes (max: ${this.config.maxParameterSize})`,
|
||||
errorCode: 'PARAMETER_TOO_LARGE'
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${i} is not serializable`,
|
||||
errorCode: 'PARAMETER_NOT_SERIALIZABLE'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 参数类型检查
|
||||
if (this.config.enableTypeCheck) {
|
||||
const typeResult = this.validateParameterTypes(context);
|
||||
if (!typeResult.valid) {
|
||||
return typeResult;
|
||||
}
|
||||
}
|
||||
|
||||
// 参数内容过滤
|
||||
if (this.config.enableContentFilter) {
|
||||
const contentResult = this.validateParameterContent(context);
|
||||
if (!contentResult.valid) {
|
||||
return contentResult;
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数类型验证
|
||||
*/
|
||||
private validateParameterTypes(context: RpcCallContext): ValidationResult {
|
||||
for (let i = 0; i < context.parameters.length; i++) {
|
||||
const param = context.parameters[i];
|
||||
const paramType = this.getParameterType(param);
|
||||
|
||||
if (!this.config.allowedParameterTypes!.includes(paramType)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${i} type '${paramType}' is not allowed`,
|
||||
errorCode: 'INVALID_PARAMETER_TYPE'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数内容验证
|
||||
*/
|
||||
private validateParameterContent(context: RpcCallContext): ValidationResult {
|
||||
for (let i = 0; i < context.parameters.length; i++) {
|
||||
const param = context.parameters[i];
|
||||
|
||||
// 检查危险内容
|
||||
if (typeof param === 'string') {
|
||||
const dangerousPatterns = [
|
||||
/<script/i,
|
||||
/javascript:/i,
|
||||
/eval\(/i,
|
||||
/function\(/i,
|
||||
/__proto__/i
|
||||
];
|
||||
|
||||
for (const pattern of dangerousPatterns) {
|
||||
if (pattern.test(param)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${i} contains potentially dangerous content`,
|
||||
errorCode: 'DANGEROUS_PARAMETER_CONTENT'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 频率限制验证
|
||||
*/
|
||||
private validateRateLimit(context: RpcCallContext): ValidationResult {
|
||||
const signature = this.getMethodSignature(context.componentType, context.methodName);
|
||||
if (!signature || !signature.rateLimit) {
|
||||
return { valid: true }; // 没有频率限制
|
||||
}
|
||||
|
||||
const clientId = context.client.id;
|
||||
const methodKey = `${context.componentType}.${context.methodName}`;
|
||||
|
||||
let tracker = this.rateTrackers.get(clientId);
|
||||
if (!tracker) {
|
||||
tracker = {
|
||||
clientId,
|
||||
methodCalls: new Map(),
|
||||
lastUpdate: new Date()
|
||||
};
|
||||
this.rateTrackers.set(clientId, tracker);
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
let methodData = tracker.methodCalls.get(methodKey);
|
||||
|
||||
if (!methodData) {
|
||||
methodData = {
|
||||
count: 1,
|
||||
resetTime: new Date(now.getTime() + 60000) // 1分钟后重置
|
||||
};
|
||||
tracker.methodCalls.set(methodKey, methodData);
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// 检查是否需要重置
|
||||
if (now >= methodData.resetTime) {
|
||||
methodData.count = 1;
|
||||
methodData.resetTime = new Date(now.getTime() + 60000);
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// 检查频率限制
|
||||
if (methodData.count >= signature.rateLimit) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Rate limit exceeded for method '${methodKey}': ${methodData.count}/${signature.rateLimit} per minute`,
|
||||
errorCode: 'RATE_LIMIT_EXCEEDED'
|
||||
};
|
||||
}
|
||||
|
||||
methodData.count++;
|
||||
tracker.lastUpdate = now;
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法签名验证
|
||||
*/
|
||||
private validateMethodSignature(context: RpcCallContext): ValidationResult {
|
||||
const signature = this.getMethodSignature(context.componentType, context.methodName);
|
||||
if (!signature) {
|
||||
return { valid: true }; // 没有定义签名,跳过验证
|
||||
}
|
||||
|
||||
// 参数数量检查
|
||||
const requiredParams = signature.parameters.filter(p => p.required !== false);
|
||||
if (context.parameters.length < requiredParams.length) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Not enough parameters: expected at least ${requiredParams.length}, got ${context.parameters.length}`,
|
||||
errorCode: 'INSUFFICIENT_PARAMETERS'
|
||||
};
|
||||
}
|
||||
|
||||
if (context.parameters.length > signature.parameters.length) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Too many parameters: expected at most ${signature.parameters.length}, got ${context.parameters.length}`,
|
||||
errorCode: 'EXCESS_PARAMETERS'
|
||||
};
|
||||
}
|
||||
|
||||
// 参数类型和值验证
|
||||
for (let i = 0; i < Math.min(context.parameters.length, signature.parameters.length); i++) {
|
||||
const param = context.parameters[i];
|
||||
const paramDef = signature.parameters[i];
|
||||
|
||||
const paramResult = this.validateParameterDefinition(param, paramDef, i);
|
||||
if (!paramResult.valid) {
|
||||
return paramResult;
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义验证
|
||||
if (signature.customValidator) {
|
||||
const customResult = signature.customValidator(context);
|
||||
if (!customResult.valid) {
|
||||
return customResult;
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证参数定义
|
||||
*/
|
||||
private validateParameterDefinition(
|
||||
value: NetworkValue,
|
||||
definition: ParameterTypeDefinition,
|
||||
index: number
|
||||
): ValidationResult {
|
||||
// 类型检查
|
||||
const actualType = this.getParameterType(value);
|
||||
if (definition.type !== 'any' && actualType !== definition.type) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${index} type mismatch: expected '${definition.type}', got '${actualType}'`,
|
||||
errorCode: 'PARAMETER_TYPE_MISMATCH'
|
||||
};
|
||||
}
|
||||
|
||||
// 值范围检查
|
||||
if (typeof value === 'number' && (definition.min !== undefined || definition.max !== undefined)) {
|
||||
if (definition.min !== undefined && value < definition.min) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${index} value ${value} is less than minimum ${definition.min}`,
|
||||
errorCode: 'PARAMETER_BELOW_MINIMUM'
|
||||
};
|
||||
}
|
||||
|
||||
if (definition.max !== undefined && value > definition.max) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${index} value ${value} is greater than maximum ${definition.max}`,
|
||||
errorCode: 'PARAMETER_ABOVE_MAXIMUM'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 字符串长度检查
|
||||
if (typeof value === 'string' && (definition.min !== undefined || definition.max !== undefined)) {
|
||||
if (definition.min !== undefined && value.length < definition.min) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${index} string length ${value.length} is less than minimum ${definition.min}`,
|
||||
errorCode: 'STRING_TOO_SHORT'
|
||||
};
|
||||
}
|
||||
|
||||
if (definition.max !== undefined && value.length > definition.max) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${index} string length ${value.length} is greater than maximum ${definition.max}`,
|
||||
errorCode: 'STRING_TOO_LONG'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 允许值检查
|
||||
if (definition.allowedValues && definition.allowedValues.length > 0) {
|
||||
if (!definition.allowedValues.includes(value)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${index} value '${value}' is not in allowed values: ${definition.allowedValues.join(', ')}`,
|
||||
errorCode: 'VALUE_NOT_ALLOWED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 正则表达式检查(字符串)
|
||||
if (typeof value === 'string' && definition.pattern) {
|
||||
if (!definition.pattern.test(value)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${index} string '${value}' does not match required pattern`,
|
||||
errorCode: 'PATTERN_MISMATCH'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义验证
|
||||
if (definition.customValidator) {
|
||||
const customResult = definition.customValidator(value);
|
||||
if (!customResult.valid) {
|
||||
return {
|
||||
...customResult,
|
||||
error: `Parameter ${index} validation failed: ${customResult.error}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数类型
|
||||
*/
|
||||
private getParameterType(value: any): string {
|
||||
if (value === null || value === undefined) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return 'array';
|
||||
}
|
||||
|
||||
return typeof value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的频率跟踪器
|
||||
*/
|
||||
private cleanupRateTrackers(): void {
|
||||
const now = new Date();
|
||||
const expireTime = 10 * 60 * 1000; // 10分钟
|
||||
let cleanedCount = 0;
|
||||
|
||||
for (const [clientId, tracker] of this.rateTrackers.entries()) {
|
||||
if (now.getTime() - tracker.lastUpdate.getTime() > expireTime) {
|
||||
this.rateTrackers.delete(clientId);
|
||||
cleanedCount++;
|
||||
} else {
|
||||
// 清理过期的方法调用记录
|
||||
for (const [methodKey, methodData] of tracker.methodCalls.entries()) {
|
||||
if (now >= methodData.resetTime) {
|
||||
tracker.methodCalls.delete(methodKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedCount > 0) {
|
||||
console.log(`RPC validator cleanup: ${cleanedCount} rate trackers removed`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* 验证系统导出
|
||||
*/
|
||||
|
||||
export * from './MessageValidator';
|
||||
export * from './RpcValidator';
|
||||
@@ -1,9 +1,26 @@
|
||||
/**
|
||||
* Jest测试环境设置 - 服务端
|
||||
*/
|
||||
|
||||
// 导入reflect-metadata以支持装饰器
|
||||
import 'reflect-metadata';
|
||||
|
||||
global.beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// 全局测试配置
|
||||
beforeAll(() => {
|
||||
// 设置测试环境
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.NETWORK_ENV = 'server';
|
||||
});
|
||||
|
||||
global.afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
afterAll(() => {
|
||||
// 清理测试环境
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// 每个测试前的准备工作
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 每个测试后的清理工作
|
||||
// 清理可能的网络连接、定时器等
|
||||
});
|
||||
@@ -8,6 +8,7 @@
|
||||
"outDir": "./bin",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
@@ -41,5 +42,13 @@
|
||||
"bin",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
},
|
||||
{
|
||||
"path": "../network-shared"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
# @esengine/ecs-framework-network-shared
|
||||
|
||||
ECS Framework 网络库 - 共享组件和类型定义
|
||||
|
||||
## 概述
|
||||
|
||||
这是 ECS Framework 网络库的共享包,包含了客户端和服务端通用的:
|
||||
|
||||
- 装饰器定义 (`@SyncVar`, `@ClientRpc`, `@ServerRpc` 等)
|
||||
- 类型定义和接口
|
||||
- 序列化/反序列化工具
|
||||
- Protobuf 自动生成机制
|
||||
- 网络消息基类
|
||||
|
||||
## 特性
|
||||
|
||||
- **装饰器驱动**: 基于装饰器自动生成网络协议
|
||||
- **类型安全**: 完整的 TypeScript 支持
|
||||
- **自动序列化**: 基于 Protobuf 的高性能序列化
|
||||
- **零配置**: 无需手写 .proto 文件
|
||||
- **ECS 集成**: 深度集成 ECS 框架的特性
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework-network-shared
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
```typescript
|
||||
import { NetworkComponent, SyncVar, ClientRpc, ServerRpc } from '@esengine/ecs-framework-network-shared';
|
||||
|
||||
@NetworkComponent()
|
||||
class PlayerController extends Component {
|
||||
@SyncVar({ onChanged: 'onHealthChanged' })
|
||||
public health: number = 100;
|
||||
|
||||
@SyncVar()
|
||||
public playerName: string = '';
|
||||
|
||||
@ClientRpc()
|
||||
public showDamage(damage: number): void {
|
||||
// 客户端显示伤害效果
|
||||
}
|
||||
|
||||
@ServerRpc()
|
||||
public movePlayer(direction: Vector3): void {
|
||||
// 服务端处理玩家移动
|
||||
}
|
||||
|
||||
private onHealthChanged(oldValue: number, newValue: number): void {
|
||||
console.log(`生命值从 ${oldValue} 变为 ${newValue}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -2,27 +2,32 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
console.log('🚀 使用 Rollup 构建 network-shared 包...');
|
||||
console.log('🚀 使用 Rollup 构建 @esengine/network-shared 包...');
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// 清理旧的dist目录
|
||||
if (fs.existsSync('./dist')) {
|
||||
console.log('🧹 清理旧的构建文件...');
|
||||
execSync('rimraf ./dist', { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
// 执行Rollup构建
|
||||
console.log('📦 执行 Rollup 构建...');
|
||||
execSync('rollup -c rollup.config.cjs', { stdio: 'inherit' });
|
||||
execSync('npx rollup -c rollup.config.cjs', { stdio: 'inherit' });
|
||||
|
||||
// 生成package.json
|
||||
console.log('📋 生成 package.json...');
|
||||
generatePackageJson();
|
||||
|
||||
// 复制其他文件
|
||||
console.log('📁 复制必要文件...');
|
||||
copyFiles();
|
||||
|
||||
// 输出构建结果
|
||||
showBuildResults();
|
||||
|
||||
console.log('✅ network-shared 构建完成!');
|
||||
console.log('✅ @esengine/network-shared 构建完成!');
|
||||
console.log('\n🚀 发布命令:');
|
||||
console.log('cd dist && npm publish');
|
||||
|
||||
@@ -63,19 +68,20 @@ function generatePackageJson() {
|
||||
],
|
||||
keywords: [
|
||||
'ecs',
|
||||
'networking',
|
||||
'network',
|
||||
'multiplayer',
|
||||
'game',
|
||||
'shared',
|
||||
'decorators',
|
||||
'protobuf',
|
||||
'serialization',
|
||||
'game-engine',
|
||||
'typescript'
|
||||
'typescript',
|
||||
'cocos-creator',
|
||||
'laya'
|
||||
],
|
||||
author: sourcePackage.author,
|
||||
license: sourcePackage.license,
|
||||
repository: sourcePackage.repository,
|
||||
dependencies: sourcePackage.dependencies,
|
||||
peerDependencies: sourcePackage.peerDependencies,
|
||||
dependencies: sourcePackage.dependencies,
|
||||
publishConfig: sourcePackage.publishConfig,
|
||||
engines: {
|
||||
node: '>=16.0.0'
|
||||
},
|
||||
@@ -88,7 +94,7 @@ function generatePackageJson() {
|
||||
function copyFiles() {
|
||||
const filesToCopy = [
|
||||
{ src: './README.md', dest: './dist/README.md' },
|
||||
{ src: '../../LICENSE', dest: './dist/LICENSE' }
|
||||
{ src: './LICENSE', dest: './dist/LICENSE' }
|
||||
];
|
||||
|
||||
filesToCopy.forEach(({ src, dest }) => {
|
||||
|
||||
@@ -18,16 +18,10 @@ module.exports = {
|
||||
coverageReporters: ['text', 'lcov', 'html'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 60,
|
||||
branches: 70,
|
||||
functions: 70,
|
||||
lines: 70,
|
||||
statements: 70
|
||||
},
|
||||
'./src/decorators/': {
|
||||
branches: 70,
|
||||
functions: 80,
|
||||
lines: 80,
|
||||
statements: 80
|
||||
}
|
||||
},
|
||||
verbose: true,
|
||||
@@ -39,6 +33,7 @@ module.exports = {
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
'^@esengine/ecs-framework$': '<rootDir>/../core/src/index.ts',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
|
||||
@@ -49,5 +44,8 @@ module.exports = {
|
||||
'<rootDir>/bin/',
|
||||
'<rootDir>/dist/',
|
||||
'<rootDir>/node_modules/'
|
||||
],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(.*\\.mjs$|@esengine))'
|
||||
]
|
||||
};
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"name": "@esengine/ecs-framework-network-shared",
|
||||
"version": "1.0.15",
|
||||
"description": "ECS Framework 网络库 - 共享组件和类型定义",
|
||||
"type": "module",
|
||||
"name": "@esengine/network-shared",
|
||||
"version": "1.0.2",
|
||||
"description": "ECS Framework网络层 - 共享组件和协议",
|
||||
"main": "bin/index.js",
|
||||
"types": "bin/index.d.ts",
|
||||
"exports": {
|
||||
@@ -22,17 +21,15 @@
|
||||
],
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"networking",
|
||||
"shared",
|
||||
"decorators",
|
||||
"protobuf",
|
||||
"serialization",
|
||||
"game-engine",
|
||||
"typescript"
|
||||
"network",
|
||||
"multiplayer",
|
||||
"game",
|
||||
"typescript",
|
||||
"shared"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf bin dist",
|
||||
"build:ts": "tsc",
|
||||
"clean": "rimraf bin dist tsconfig.tsbuildinfo",
|
||||
"build:ts": "tsc --build",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "npm run build:ts",
|
||||
"build:watch": "tsc --watch",
|
||||
@@ -46,30 +43,19 @@
|
||||
"test": "jest --config jest.config.cjs",
|
||||
"test:watch": "jest --watch --config jest.config.cjs",
|
||||
"test:coverage": "jest --coverage --config jest.config.cjs",
|
||||
"test:ci": "jest --ci --coverage --config jest.config.cjs",
|
||||
"test:clear": "jest --clearCache"
|
||||
"test:ci": "jest --ci --coverage --config jest.config.cjs"
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"protobufjs": "^7.5.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@esengine/ecs-framework": ">=2.1.29"
|
||||
"@esengine/ecs-framework": "file:../core",
|
||||
"reflect-metadata": "^0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "*",
|
||||
"@rollup/plugin-commonjs": "^28.0.3",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.19.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"rollup": "^4.42.0",
|
||||
"rollup-plugin-dts": "^6.2.1",
|
||||
"ts-jest": "^29.4.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
|
||||
@@ -7,14 +7,15 @@ const { readFileSync } = require('fs');
|
||||
const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||
|
||||
const banner = `/**
|
||||
* @esengine/ecs-framework-network-shared v${pkg.version}
|
||||
* ECS Framework 网络库 - 共享组件和类型定义
|
||||
* @esengine/network-shared v${pkg.version}
|
||||
* ECS网络层共享组件和协议
|
||||
*
|
||||
* @author ${pkg.author}
|
||||
* @license ${pkg.license}
|
||||
*/`;
|
||||
|
||||
const external = ['reflect-metadata', 'protobufjs', 'uuid', '@esengine/ecs-framework'];
|
||||
// 外部依赖,不打包进bundle
|
||||
const external = ['@esengine/ecs-framework', 'reflect-metadata'];
|
||||
|
||||
const commonPlugins = [
|
||||
resolve({
|
||||
@@ -77,7 +78,7 @@ module.exports = [
|
||||
}
|
||||
},
|
||||
|
||||
// UMD构建
|
||||
// UMD构建 - 包含所有依赖,用于浏览器直接使用
|
||||
{
|
||||
input: 'bin/index.js',
|
||||
output: {
|
||||
@@ -88,10 +89,8 @@ module.exports = [
|
||||
sourcemap: true,
|
||||
exports: 'named',
|
||||
globals: {
|
||||
'reflect-metadata': 'ReflectMetadata',
|
||||
'protobufjs': 'protobuf',
|
||||
'uuid': 'uuid',
|
||||
'@esengine/ecs-framework': 'ECS'
|
||||
'@esengine/ecs-framework': 'ECS',
|
||||
'reflect-metadata': 'Reflect'
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
@@ -115,7 +114,7 @@ module.exports = [
|
||||
file: 'dist/index.d.ts',
|
||||
format: 'es',
|
||||
banner: `/**
|
||||
* @esengine/ecs-framework-network-shared v${pkg.version}
|
||||
* @esengine/network-shared v${pkg.version}
|
||||
* TypeScript definitions
|
||||
*/`
|
||||
},
|
||||
|
||||
317
packages/network-shared/src/components/NetworkIdentity.ts
Normal file
317
packages/network-shared/src/components/NetworkIdentity.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* 网络身份组件
|
||||
*/
|
||||
import { Component, Emitter } from '@esengine/ecs-framework';
|
||||
import { AuthorityType, NetworkScope } from '../types/NetworkTypes';
|
||||
import {
|
||||
NetworkEventType,
|
||||
NetworkIdentityEventData,
|
||||
NetworkEventUtils
|
||||
} from '../events/NetworkEvents';
|
||||
|
||||
/**
|
||||
* 网络身份组件
|
||||
*
|
||||
* 为实体提供网络同步能力的核心组件。
|
||||
* 每个需要网络同步的实体都必须拥有此组件。
|
||||
*
|
||||
* 集成了事件系统,当属性变化时自动发射事件用于网络同步。
|
||||
*/
|
||||
export class NetworkIdentity extends Component {
|
||||
/**
|
||||
* 事件发射器
|
||||
* 用于发射网络相关事件
|
||||
*/
|
||||
private eventEmitter = new Emitter<NetworkEventType, NetworkIdentity>();
|
||||
/**
|
||||
* 网络ID (全局唯一)
|
||||
* 用于在网络中标识实体
|
||||
*/
|
||||
public networkId: number = 0;
|
||||
|
||||
/**
|
||||
* 拥有者ID
|
||||
* 表示哪个客户端拥有此实体的控制权
|
||||
*/
|
||||
public ownerId: string = '';
|
||||
|
||||
/**
|
||||
* 权限类型
|
||||
* 决定哪一端对此实体有控制权
|
||||
*/
|
||||
public authority: AuthorityType = AuthorityType.Server;
|
||||
|
||||
/**
|
||||
* 同步频率 (Hz)
|
||||
* 每秒同步的次数
|
||||
*/
|
||||
public syncRate: number = 20;
|
||||
|
||||
/**
|
||||
* 网络作用域
|
||||
* 决定哪些客户端可以看到此实体
|
||||
*/
|
||||
public scope: NetworkScope = NetworkScope.Room;
|
||||
|
||||
/**
|
||||
* 是否是本地玩家
|
||||
* 标识此实体是否代表本地玩家
|
||||
*/
|
||||
public isLocalPlayer: boolean = false;
|
||||
|
||||
/**
|
||||
* 是否启用网络同步
|
||||
* 临时禁用/启用同步
|
||||
*/
|
||||
public syncEnabled: boolean = true;
|
||||
|
||||
/**
|
||||
* 同步优先级
|
||||
* 影响同步的顺序和频率,数值越高优先级越高
|
||||
*/
|
||||
public priority: number = 0;
|
||||
|
||||
/**
|
||||
* 距离阈值
|
||||
* 用于附近同步模式,超过此距离的客户端不会收到同步
|
||||
*/
|
||||
public distanceThreshold: number = 100;
|
||||
|
||||
/**
|
||||
* 最后同步时间
|
||||
* 记录上次同步的时间戳
|
||||
*/
|
||||
public lastSyncTime: number = 0;
|
||||
|
||||
/**
|
||||
* 是否可见
|
||||
* 控制实体是否对其他客户端可见
|
||||
*/
|
||||
public visible: boolean = true;
|
||||
|
||||
/**
|
||||
* 自定义同步过滤器
|
||||
* 用于自定义作用域的同步逻辑
|
||||
*/
|
||||
public customSyncFilter?: (clientId: string) => boolean;
|
||||
|
||||
/**
|
||||
* 获取实体的同步权重
|
||||
* 基于优先级和距离计算
|
||||
*/
|
||||
public getSyncWeight(distance?: number): number {
|
||||
let weight = this.priority;
|
||||
|
||||
if (distance !== undefined && this.scope === NetworkScope.Nearby) {
|
||||
// 距离越近权重越高
|
||||
const distanceFactor = Math.max(0, 1 - (distance / this.distanceThreshold));
|
||||
weight *= distanceFactor;
|
||||
}
|
||||
|
||||
return weight;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该同步给指定客户端
|
||||
*/
|
||||
public shouldSyncToClient(clientId: string, distance?: number): boolean {
|
||||
if (!this.syncEnabled || !this.visible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (this.scope) {
|
||||
case NetworkScope.Global:
|
||||
return true;
|
||||
|
||||
case NetworkScope.Room:
|
||||
return true; // 由房间管理器控制
|
||||
|
||||
case NetworkScope.Owner:
|
||||
return clientId === this.ownerId;
|
||||
|
||||
case NetworkScope.Nearby:
|
||||
return distance !== undefined && distance <= this.distanceThreshold;
|
||||
|
||||
case NetworkScope.Custom:
|
||||
return this.customSyncFilter ? this.customSyncFilter(clientId) : false;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查客户端是否有权限修改此实体
|
||||
*/
|
||||
public hasAuthority(clientId: string): boolean {
|
||||
switch (this.authority) {
|
||||
case AuthorityType.Server:
|
||||
return false; // 只有服务端有权限
|
||||
|
||||
case AuthorityType.Client:
|
||||
return clientId === this.ownerId;
|
||||
|
||||
case AuthorityType.Shared:
|
||||
return true; // 任何人都可以修改
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置拥有者
|
||||
*/
|
||||
public setOwner(clientId: string): void {
|
||||
const oldOwner = this.ownerId;
|
||||
this.ownerId = clientId;
|
||||
|
||||
// 发射拥有者变化事件
|
||||
this.emitEvent(
|
||||
NetworkEventType.IDENTITY_OWNER_CHANGED,
|
||||
NetworkEventUtils.createIdentityEventData(
|
||||
this.networkId,
|
||||
clientId,
|
||||
oldOwner,
|
||||
clientId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置权限类型
|
||||
*/
|
||||
public setAuthority(authority: AuthorityType): void {
|
||||
const oldAuthority = this.authority;
|
||||
this.authority = authority;
|
||||
|
||||
// 发射权限变化事件
|
||||
this.emitEvent(
|
||||
NetworkEventType.IDENTITY_AUTHORITY_CHANGED,
|
||||
NetworkEventUtils.createIdentityEventData(
|
||||
this.networkId,
|
||||
this.ownerId,
|
||||
oldAuthority,
|
||||
authority
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置同步状态
|
||||
*/
|
||||
public setSyncEnabled(enabled: boolean): void {
|
||||
const oldEnabled = this.syncEnabled;
|
||||
this.syncEnabled = enabled;
|
||||
|
||||
// 发射同步状态变化事件
|
||||
const eventType = enabled
|
||||
? NetworkEventType.IDENTITY_SYNC_ENABLED
|
||||
: NetworkEventType.IDENTITY_SYNC_DISABLED;
|
||||
|
||||
this.emitEvent(
|
||||
eventType,
|
||||
NetworkEventUtils.createIdentityEventData(
|
||||
this.networkId,
|
||||
this.ownerId,
|
||||
oldEnabled,
|
||||
enabled
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置同步频率
|
||||
*/
|
||||
public setSyncRate(rate: number): void {
|
||||
const oldRate = this.syncRate;
|
||||
this.syncRate = rate;
|
||||
|
||||
// 发射同步频率变化事件
|
||||
this.emitEvent(
|
||||
NetworkEventType.SYNC_RATE_CHANGED,
|
||||
NetworkEventUtils.createIdentityEventData(
|
||||
this.networkId,
|
||||
this.ownerId,
|
||||
oldRate,
|
||||
rate
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加事件监听器
|
||||
*/
|
||||
public addEventListener(eventType: NetworkEventType, handler: (data: NetworkIdentityEventData) => void): void {
|
||||
this.eventEmitter.addObserver(eventType, handler, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件监听器
|
||||
*/
|
||||
public removeEventListener(eventType: NetworkEventType, handler: (data: NetworkIdentityEventData) => void): void {
|
||||
this.eventEmitter.removeObserver(eventType, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发射事件
|
||||
* @private
|
||||
*/
|
||||
private emitEvent(eventType: NetworkEventType, data: NetworkIdentityEventData): void {
|
||||
this.eventEmitter.emit(eventType, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听属性变化事件
|
||||
*/
|
||||
public onPropertyChanged(handler: (data: NetworkIdentityEventData) => void): void {
|
||||
this.addEventListener(NetworkEventType.IDENTITY_PROPERTY_CHANGED, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听拥有者变化事件
|
||||
*/
|
||||
public onOwnerChanged(handler: (data: NetworkIdentityEventData) => void): void {
|
||||
this.addEventListener(NetworkEventType.IDENTITY_OWNER_CHANGED, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听权限变化事件
|
||||
*/
|
||||
public onAuthorityChanged(handler: (data: NetworkIdentityEventData) => void): void {
|
||||
this.addEventListener(NetworkEventType.IDENTITY_AUTHORITY_CHANGED, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听同步状态变化事件
|
||||
*/
|
||||
public onSyncStateChanged(handler: (data: NetworkIdentityEventData) => void): void {
|
||||
this.addEventListener(NetworkEventType.IDENTITY_SYNC_ENABLED, handler);
|
||||
this.addEventListener(NetworkEventType.IDENTITY_SYNC_DISABLED, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调试信息
|
||||
*/
|
||||
public getDebugInfo(): Record<string, any> {
|
||||
return {
|
||||
networkId: this.networkId,
|
||||
ownerId: this.ownerId,
|
||||
authority: this.authority,
|
||||
scope: this.scope,
|
||||
syncRate: this.syncRate,
|
||||
priority: this.priority,
|
||||
syncEnabled: this.syncEnabled,
|
||||
visible: this.visible,
|
||||
lastSyncTime: this.lastSyncTime
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件销毁时清理事件监听器
|
||||
*/
|
||||
public dispose(): void {
|
||||
// 清理所有事件监听器
|
||||
this.eventEmitter.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
/**
|
||||
* NetworkBehaviour 基类
|
||||
*
|
||||
* 所有网络组件的基类,提供网络功能的基础实现
|
||||
*/
|
||||
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import { INetworkComponent, INetworkObject, SyncVarMetadata, RpcMetadata, Constructor } from '../types/NetworkTypes';
|
||||
import { getSyncVarMetadata, getDirtySyncVars, clearAllDirtySyncVars } from '../decorators/SyncVar';
|
||||
import { getClientRpcMetadata } from '../decorators/ClientRpc';
|
||||
import { getServerRpcMetadata } from '../decorators/ServerRpc';
|
||||
|
||||
/**
|
||||
* NetworkBehaviour 基类
|
||||
*
|
||||
* 提供网络组件的基础功能:
|
||||
* - SyncVar 支持
|
||||
* - RPC 调用支持
|
||||
* - 网络身份管理
|
||||
* - 权限控制
|
||||
*/
|
||||
export abstract class NetworkBehaviour extends Component implements INetworkComponent {
|
||||
/** 索引签名以支持动态属性访问 */
|
||||
[key: string]: unknown;
|
||||
|
||||
/** 网络对象引用 */
|
||||
public networkObject: INetworkObject | null = null;
|
||||
|
||||
/** 网络ID */
|
||||
public get networkId(): number {
|
||||
return this.networkObject?.networkId || 0;
|
||||
}
|
||||
|
||||
/** 是否拥有权威 */
|
||||
public get hasAuthority(): boolean {
|
||||
return this.networkObject?.hasAuthority || false;
|
||||
}
|
||||
|
||||
/** 组件类型名 */
|
||||
public get componentType(): string {
|
||||
return this.constructor.name;
|
||||
}
|
||||
|
||||
/** 是否为服务端 */
|
||||
public get isServer(): boolean {
|
||||
// 这个方法会被具体的客户端/服务端库重写
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 是否为客户端 */
|
||||
public get isClient(): boolean {
|
||||
// 这个方法会被具体的客户端/服务端库重写
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 是否为本地对象 */
|
||||
public get isLocal(): boolean {
|
||||
return this.networkObject?.isLocal || false;
|
||||
}
|
||||
|
||||
/** 所有者客户端ID */
|
||||
public get ownerId(): number {
|
||||
return this.networkObject?.ownerId || 0;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.setupSyncVarNotification();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 SyncVar 变化通知
|
||||
*/
|
||||
private setupSyncVarNotification(): void {
|
||||
// 添加 SyncVar 变化通知方法
|
||||
(this as any).notifySyncVarChanged = (
|
||||
propertyName: string,
|
||||
oldValue: any,
|
||||
newValue: any,
|
||||
metadata: SyncVarMetadata
|
||||
) => {
|
||||
this.onSyncVarChanged(propertyName, oldValue, newValue, metadata);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncVar 变化处理
|
||||
*/
|
||||
protected onSyncVarChanged(
|
||||
propertyName: string,
|
||||
oldValue: any,
|
||||
newValue: any,
|
||||
metadata: SyncVarMetadata
|
||||
): void {
|
||||
// 权限检查
|
||||
if (metadata.authorityOnly && !this.hasAuthority) {
|
||||
console.warn(`Authority required for SyncVar: ${this.componentType}.${propertyName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 通知网络管理器
|
||||
this.notifyNetworkManager('syncvar-changed', {
|
||||
networkId: this.networkId,
|
||||
componentType: this.componentType,
|
||||
propertyName,
|
||||
oldValue,
|
||||
newValue,
|
||||
metadata
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送客户端 RPC
|
||||
*/
|
||||
protected sendClientRpc(methodName: string, args: any[], options?: any, metadata?: RpcMetadata): any {
|
||||
if (!this.hasAuthority && !this.isServer) {
|
||||
console.warn(`Authority required for ClientRpc: ${this.componentType}.${methodName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
return this.notifyNetworkManager('client-rpc', {
|
||||
networkId: this.networkId,
|
||||
componentType: this.componentType,
|
||||
methodName,
|
||||
args,
|
||||
options,
|
||||
metadata
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送服务端 RPC
|
||||
*/
|
||||
protected sendServerRpc(methodName: string, args: any[], options?: any, metadata?: RpcMetadata): any {
|
||||
if (!this.isClient) {
|
||||
console.warn(`ServerRpc can only be called from client: ${this.componentType}.${methodName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
return this.notifyNetworkManager('server-rpc', {
|
||||
networkId: this.networkId,
|
||||
componentType: this.componentType,
|
||||
methodName,
|
||||
args,
|
||||
options,
|
||||
metadata
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知网络管理器
|
||||
*/
|
||||
private notifyNetworkManager(eventType: string, data: any): any {
|
||||
// 这个方法会被具体的客户端/服务端库重写
|
||||
// 用于与网络管理器通信
|
||||
if (typeof (globalThis as any).NetworkManager !== 'undefined') {
|
||||
return (globalThis as any).NetworkManager.handleNetworkEvent?.(eventType, data);
|
||||
}
|
||||
|
||||
console.warn(`NetworkManager not found for event: ${eventType}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有 SyncVar 元数据
|
||||
*/
|
||||
public getSyncVars(): SyncVarMetadata[] {
|
||||
return getSyncVarMetadata(this.constructor as Constructor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有客户端 RPC 元数据
|
||||
*/
|
||||
public getClientRpcs(): RpcMetadata[] {
|
||||
return getClientRpcMetadata(this.constructor as Constructor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有服务端 RPC 元数据
|
||||
*/
|
||||
public getServerRpcs(): RpcMetadata[] {
|
||||
return getServerRpcMetadata(this.constructor as Constructor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有脏的 SyncVar
|
||||
*/
|
||||
public getDirtySyncVars() {
|
||||
return getDirtySyncVars(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有脏标记
|
||||
*/
|
||||
public clearDirtySyncVars(): void {
|
||||
clearAllDirtySyncVars(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化组件状态
|
||||
*/
|
||||
public serializeState(): any {
|
||||
const syncVars = this.getSyncVars();
|
||||
const state: any = {};
|
||||
|
||||
for (const syncVar of syncVars) {
|
||||
const value = (this as any)[`_${syncVar.propertyName}`];
|
||||
if (value !== undefined) {
|
||||
state[syncVar.propertyName] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化组件状态
|
||||
*/
|
||||
public deserializeState(state: any): void {
|
||||
const syncVars = this.getSyncVars();
|
||||
|
||||
for (const syncVar of syncVars) {
|
||||
if (state.hasOwnProperty(syncVar.propertyName)) {
|
||||
// 直接设置内部值,跳过权限检查
|
||||
(this as any)[`_${syncVar.propertyName}`] = state[syncVar.propertyName];
|
||||
|
||||
// 调用变化回调
|
||||
if (syncVar.onChanged && typeof (this as any)[syncVar.onChanged] === 'function') {
|
||||
(this as any)[syncVar.onChanged](undefined, state[syncVar.propertyName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有权限执行操作
|
||||
*/
|
||||
protected checkAuthority(requiresOwnership = false): boolean {
|
||||
if (requiresOwnership && this.ownerId !== this.getLocalClientId()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.hasAuthority;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本地客户端ID
|
||||
* 这个方法会被具体实现重写
|
||||
*/
|
||||
protected getLocalClientId(): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件销毁时的清理
|
||||
*/
|
||||
public onDestroy(): void {
|
||||
this.networkObject = null;
|
||||
// 清理网络资源(基类销毁由框架处理)
|
||||
}
|
||||
}
|
||||
@@ -1,324 +0,0 @@
|
||||
/**
|
||||
* NetworkIdentity 类
|
||||
*
|
||||
* 标识网络对象的唯一身份,管理网络组件和权威性
|
||||
*/
|
||||
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import { INetworkObject, INetworkComponent } from '../types/NetworkTypes';
|
||||
import { NetworkBehaviour } from './NetworkBehaviour';
|
||||
|
||||
/**
|
||||
* NetworkIdentity 组件
|
||||
*
|
||||
* 所有需要网络同步的实体都必须拥有此组件
|
||||
*/
|
||||
export class NetworkIdentity extends Component implements INetworkObject {
|
||||
/** 网络对象的唯一标识符 */
|
||||
public networkId: number = 0;
|
||||
|
||||
/** 所有者客户端ID,0 表示服务端拥有 */
|
||||
public ownerId: number = 0;
|
||||
|
||||
/** 是否拥有权威,权威端可以修改 SyncVar 和发送 RPC */
|
||||
public hasAuthority: boolean = false;
|
||||
|
||||
/** 是否为本地对象 */
|
||||
public isLocal: boolean = false;
|
||||
|
||||
/** 是否为本地玩家对象 */
|
||||
public isLocalPlayer: boolean = false;
|
||||
|
||||
/** 预制体名称(用于网络生成) */
|
||||
public prefabName: string = '';
|
||||
|
||||
/** 场景对象ID(用于场景中已存在的对象) */
|
||||
public sceneId: number = 0;
|
||||
|
||||
/** 挂载的网络组件列表 */
|
||||
public networkComponents: INetworkComponent[] = [];
|
||||
|
||||
/** 是否已在网络中生成 */
|
||||
public isSpawned: boolean = false;
|
||||
|
||||
/** 可见性距离(用于网络LOD) */
|
||||
public visibilityDistance: number = 100;
|
||||
|
||||
/** 网络更新频率覆盖(0 = 使用全局设置) */
|
||||
public updateRate: number = 0;
|
||||
|
||||
/** 是否总是相关(不受距离限制) */
|
||||
public alwaysRelevant: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件启动时初始化
|
||||
*/
|
||||
public override onEnabled(): void {
|
||||
super.onEnabled();
|
||||
this.gatherNetworkComponents();
|
||||
this.registerToNetworkManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集实体上的所有网络组件
|
||||
*/
|
||||
private gatherNetworkComponents(): void {
|
||||
if (!this.entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 清空现有列表
|
||||
this.networkComponents = [];
|
||||
|
||||
// 获取实体上的所有组件
|
||||
// 获取实体上的所有组件,简化类型处理
|
||||
const components = (this.entity as any).getComponents();
|
||||
|
||||
for (const component of components) {
|
||||
if (component instanceof NetworkBehaviour) {
|
||||
this.addNetworkComponent(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加网络组件
|
||||
*/
|
||||
public addNetworkComponent(component: INetworkComponent): void {
|
||||
if (this.networkComponents.includes(component)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.networkComponents.push(component);
|
||||
component.networkObject = this;
|
||||
|
||||
// 如果已经注册到网络,通知网络管理器
|
||||
if (this.isSpawned) {
|
||||
this.notifyNetworkManager('component-added', {
|
||||
networkId: this.networkId,
|
||||
componentType: component.componentType,
|
||||
component
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除网络组件
|
||||
*/
|
||||
public removeNetworkComponent(component: INetworkComponent): void {
|
||||
const index = this.networkComponents.indexOf(component);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.networkComponents.splice(index, 1);
|
||||
component.networkObject = null;
|
||||
|
||||
// 如果已经注册到网络,通知网络管理器
|
||||
if (this.isSpawned) {
|
||||
this.notifyNetworkManager('component-removed', {
|
||||
networkId: this.networkId,
|
||||
componentType: component.componentType,
|
||||
component
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置权威性
|
||||
*/
|
||||
public setAuthority(hasAuthority: boolean, ownerId: number = 0): void {
|
||||
const oldAuthority = this.hasAuthority;
|
||||
const oldOwner = this.ownerId;
|
||||
|
||||
this.hasAuthority = hasAuthority;
|
||||
this.ownerId = ownerId;
|
||||
this.isLocal = this.checkIsLocal();
|
||||
|
||||
// 如果权威性发生变化,通知相关系统
|
||||
if (oldAuthority !== hasAuthority || oldOwner !== ownerId) {
|
||||
this.onAuthorityChanged(oldAuthority, hasAuthority, oldOwner, ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置为本地玩家
|
||||
*/
|
||||
public setAsLocalPlayer(): void {
|
||||
this.isLocalPlayer = true;
|
||||
this.hasAuthority = true;
|
||||
this.isLocal = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为本地对象
|
||||
*/
|
||||
private checkIsLocal(): boolean {
|
||||
const localClientId = this.getLocalClientId();
|
||||
return this.ownerId === localClientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本地客户端ID
|
||||
*/
|
||||
private getLocalClientId(): number {
|
||||
// 这个方法会被具体实现重写
|
||||
if (typeof (globalThis as any).NetworkManager !== 'undefined') {
|
||||
return (globalThis as any).NetworkManager.getLocalClientId?.() || 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 权威性变化处理
|
||||
*/
|
||||
private onAuthorityChanged(
|
||||
oldAuthority: boolean,
|
||||
newAuthority: boolean,
|
||||
oldOwner: number,
|
||||
newOwner: number
|
||||
): void {
|
||||
// 通知网络管理器
|
||||
this.notifyNetworkManager('authority-changed', {
|
||||
networkId: this.networkId,
|
||||
oldAuthority,
|
||||
newAuthority,
|
||||
oldOwner,
|
||||
newOwner
|
||||
});
|
||||
|
||||
// 通知所有网络组件
|
||||
for (const component of this.networkComponents) {
|
||||
if ('onAuthorityChanged' in component && typeof component.onAuthorityChanged === 'function') {
|
||||
component.onAuthorityChanged(newAuthority);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定类型的网络组件
|
||||
*/
|
||||
public getNetworkComponent<T extends INetworkComponent>(type: new (...args: any[]) => T): T | null {
|
||||
return this.networkComponents.find(c => c instanceof type) as T || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有指定类型的网络组件
|
||||
*/
|
||||
public getNetworkComponents<T extends INetworkComponent>(type: new (...args: any[]) => T): T[] {
|
||||
return this.networkComponents.filter(c => c instanceof type) as T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化网络身份
|
||||
*/
|
||||
public serialize(): any {
|
||||
return {
|
||||
networkId: this.networkId,
|
||||
ownerId: this.ownerId,
|
||||
hasAuthority: this.hasAuthority,
|
||||
isLocal: this.isLocal,
|
||||
isLocalPlayer: this.isLocalPlayer,
|
||||
prefabName: this.prefabName,
|
||||
sceneId: this.sceneId,
|
||||
visibilityDistance: this.visibilityDistance,
|
||||
updateRate: this.updateRate,
|
||||
alwaysRelevant: this.alwaysRelevant
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化网络身份
|
||||
*/
|
||||
public deserialize(data: any): void {
|
||||
this.networkId = data.networkId || 0;
|
||||
this.ownerId = data.ownerId || 0;
|
||||
this.hasAuthority = data.hasAuthority || false;
|
||||
this.isLocal = data.isLocal || false;
|
||||
this.isLocalPlayer = data.isLocalPlayer || false;
|
||||
this.prefabName = data.prefabName || '';
|
||||
this.sceneId = data.sceneId || 0;
|
||||
this.visibilityDistance = data.visibilityDistance || 100;
|
||||
this.updateRate = data.updateRate || 0;
|
||||
this.alwaysRelevant = data.alwaysRelevant || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册到网络管理器
|
||||
*/
|
||||
private registerToNetworkManager(): void {
|
||||
this.notifyNetworkManager('register-network-object', {
|
||||
networkIdentity: this,
|
||||
networkId: this.networkId,
|
||||
components: this.networkComponents
|
||||
});
|
||||
this.isSpawned = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从网络管理器注销
|
||||
*/
|
||||
private unregisterFromNetworkManager(): void {
|
||||
this.notifyNetworkManager('unregister-network-object', {
|
||||
networkIdentity: this,
|
||||
networkId: this.networkId
|
||||
});
|
||||
this.isSpawned = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知网络管理器
|
||||
*/
|
||||
private notifyNetworkManager(eventType: string, data: any): void {
|
||||
if (typeof (globalThis as any).NetworkManager !== 'undefined') {
|
||||
(globalThis as any).NetworkManager.handleNetworkEvent?.(eventType, data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查对象是否对指定客户端可见
|
||||
*/
|
||||
public isVisibleTo(clientId: number, clientPosition?: { x: number; y: number; z?: number }): boolean {
|
||||
// 如果总是相关,则对所有客户端可见
|
||||
if (this.alwaysRelevant) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果没有提供客户端位置,默认可见
|
||||
// 简单的可见性检查,暂时不依赖Transform组件
|
||||
if (!clientPosition) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 基于距离的可见性检查(需要自定义位置获取逻辑)
|
||||
const position = { x: 0, y: 0, z: 0 }; // 占位符
|
||||
const dx = position.x - clientPosition.x;
|
||||
const dy = position.y - clientPosition.y;
|
||||
const dz = (position.z || 0) - (clientPosition.z || 0);
|
||||
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
|
||||
return distance <= this.visibilityDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件销毁时的清理
|
||||
*/
|
||||
public destroy(): void {
|
||||
// 从网络管理器注销
|
||||
if (this.isSpawned) {
|
||||
this.unregisterFromNetworkManager();
|
||||
}
|
||||
|
||||
// 清理所有网络组件的引用
|
||||
for (const component of this.networkComponents) {
|
||||
component.networkObject = null;
|
||||
}
|
||||
this.networkComponents = [];
|
||||
|
||||
// 清理网络资源(基类销毁由框架处理)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* 核心类导出
|
||||
*/
|
||||
|
||||
export * from './NetworkBehaviour';
|
||||
export * from './NetworkIdentity';
|
||||
@@ -1,177 +0,0 @@
|
||||
/**
|
||||
* ClientRpc 装饰器
|
||||
*
|
||||
* 用于标记可以在服务端调用,在客户端执行的方法
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { RpcMetadata, DecoratorTarget, Constructor, RpcParameterType, RpcReturnType } from '../types/NetworkTypes';
|
||||
import { getNetworkComponentMetadata } from './NetworkComponent';
|
||||
|
||||
/**
|
||||
* ClientRpc 装饰器选项
|
||||
*/
|
||||
export interface ClientRpcOptions {
|
||||
/** 是否需要权限验证 */
|
||||
requiresAuth?: boolean;
|
||||
/** 是否可靠传输,默认为 true */
|
||||
reliable?: boolean;
|
||||
/** 是否需要响应 */
|
||||
requiresResponse?: boolean;
|
||||
/** 目标客户端筛选器 */
|
||||
targetFilter?: 'all' | 'others' | 'owner' | 'specific';
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储 ClientRpc 元数据的 Symbol
|
||||
*/
|
||||
export const CLIENT_RPC_METADATA_KEY = Symbol('client_rpc_metadata');
|
||||
|
||||
/**
|
||||
* ClientRpc 装饰器
|
||||
*
|
||||
* @param options 装饰器选项
|
||||
* @returns 方法装饰器函数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @NetworkComponent()
|
||||
* class PlayerController extends Component {
|
||||
* @ClientRpc({ targetFilter: 'all' })
|
||||
* public showDamageEffect(damage: number, position: Vector3): void {
|
||||
* // 在所有客户端显示伤害效果
|
||||
* console.log(`Showing damage: ${damage} at ${position}`);
|
||||
* }
|
||||
*
|
||||
* @ClientRpc({ targetFilter: 'owner', reliable: false })
|
||||
* public updateUI(data: UIData): void {
|
||||
* // 只在拥有者客户端更新UI,使用不可靠传输
|
||||
* }
|
||||
*
|
||||
* @ClientRpc({ requiresResponse: true })
|
||||
* public requestClientData(): ClientData {
|
||||
* // 请求客户端数据并等待响应
|
||||
* return this.getClientData();
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function ClientRpc(options: ClientRpcOptions = {}): MethodDecorator {
|
||||
return function (target: unknown, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
|
||||
if (typeof propertyKey !== 'string') {
|
||||
throw new Error('ClientRpc can only be applied to string method names');
|
||||
}
|
||||
|
||||
// 获取或创建元数据数组
|
||||
let metadata: RpcMetadata[] = Reflect.getMetadata(CLIENT_RPC_METADATA_KEY, (target as { constructor: Constructor }).constructor);
|
||||
if (!metadata) {
|
||||
metadata = [];
|
||||
Reflect.defineMetadata(CLIENT_RPC_METADATA_KEY, metadata, (target as { constructor: Constructor }).constructor);
|
||||
}
|
||||
|
||||
// 创建 RPC 元数据
|
||||
const rpcMetadata: RpcMetadata = {
|
||||
methodName: propertyKey,
|
||||
rpcType: 'client-rpc',
|
||||
requiresAuth: options.requiresAuth || false,
|
||||
reliable: options.reliable !== false,
|
||||
requiresResponse: options.requiresResponse || false
|
||||
};
|
||||
|
||||
metadata.push(rpcMetadata);
|
||||
|
||||
// 更新 NetworkComponent 元数据
|
||||
const componentMetadata = getNetworkComponentMetadata((target as { constructor: Constructor }).constructor);
|
||||
if (componentMetadata) {
|
||||
const existingIndex = componentMetadata.rpcs.findIndex(rpc => rpc.methodName === propertyKey);
|
||||
if (existingIndex >= 0) {
|
||||
componentMetadata.rpcs[existingIndex] = rpcMetadata;
|
||||
} else {
|
||||
componentMetadata.rpcs.push(rpcMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存原方法
|
||||
const originalMethod = descriptor.value;
|
||||
if (typeof originalMethod !== 'function') {
|
||||
throw new Error(`ClientRpc can only be applied to methods, got ${typeof originalMethod}`);
|
||||
}
|
||||
|
||||
// 包装方法以添加网络调用逻辑
|
||||
descriptor.value = function (this: Record<string, unknown> & {
|
||||
isServer?: () => boolean;
|
||||
sendClientRpc?: (methodName: string, args: RpcParameterType[], options: ClientRpcOptions, metadata: RpcMetadata) => RpcReturnType;
|
||||
}, ...args: RpcParameterType[]): RpcReturnType {
|
||||
// 如果在服务端调用,发送到客户端
|
||||
const isServer = this.isServer?.() || (typeof window === 'undefined' && typeof process !== 'undefined');
|
||||
if (isServer) {
|
||||
return this.sendClientRpc?.(propertyKey, args, options, rpcMetadata) as RpcReturnType;
|
||||
}
|
||||
|
||||
// 如果在客户端,直接执行本地方法
|
||||
return (originalMethod as (...args: RpcParameterType[]) => RpcReturnType).apply(this, args);
|
||||
};
|
||||
|
||||
// 保存原方法的引用,供直接调用
|
||||
const decoratedFunction = descriptor.value as typeof descriptor.value & {
|
||||
__originalMethod: typeof originalMethod;
|
||||
__rpcMetadata: RpcMetadata;
|
||||
__rpcOptions: ClientRpcOptions;
|
||||
};
|
||||
decoratedFunction.__originalMethod = originalMethod;
|
||||
decoratedFunction.__rpcMetadata = rpcMetadata;
|
||||
decoratedFunction.__rpcOptions = options;
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类的 ClientRpc 元数据
|
||||
*/
|
||||
export function getClientRpcMetadata(target: Constructor): RpcMetadata[] {
|
||||
return Reflect.getMetadata(CLIENT_RPC_METADATA_KEY, target) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查方法是否为 ClientRpc
|
||||
*/
|
||||
export function isClientRpc(target: Constructor, methodName: string): boolean {
|
||||
const metadata = getClientRpcMetadata(target);
|
||||
return metadata.some(m => m.methodName === methodName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取特定方法的 ClientRpc 元数据
|
||||
*/
|
||||
export function getClientRpcMethodMetadata(target: Constructor, methodName: string): RpcMetadata | null {
|
||||
const metadata = getClientRpcMetadata(target);
|
||||
return metadata.find(m => m.methodName === methodName) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接调用原方法(跳过网络逻辑)
|
||||
*/
|
||||
export function invokeClientRpcLocally(instance: Record<string, unknown>, methodName: string, args: RpcParameterType[]): RpcReturnType {
|
||||
const method = instance[methodName] as { __originalMethod?: (...args: RpcParameterType[]) => RpcReturnType } | undefined;
|
||||
if (method && typeof method.__originalMethod === 'function') {
|
||||
return method.__originalMethod.apply(instance, args);
|
||||
}
|
||||
throw new Error(`Method ${methodName} is not a valid ClientRpc or original method not found`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 ClientRpc 是否需要响应
|
||||
*/
|
||||
export function clientRpcRequiresResponse(instance: Record<string, unknown>, methodName: string): boolean {
|
||||
const method = instance[methodName] as { __rpcMetadata?: RpcMetadata } | undefined;
|
||||
return method?.__rpcMetadata?.requiresResponse || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 ClientRpc 的选项
|
||||
*/
|
||||
export function getClientRpcOptions(instance: Record<string, unknown>, methodName: string): ClientRpcOptions | null {
|
||||
const method = instance[methodName] as { __rpcOptions?: ClientRpcOptions } | undefined;
|
||||
return method?.__rpcOptions || null;
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/**
|
||||
* NetworkComponent 装饰器
|
||||
*
|
||||
* 用于标记网络组件,自动注册到网络系统
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { NetworkComponentMetadata } from '../types/NetworkTypes';
|
||||
|
||||
/**
|
||||
* NetworkComponent 装饰器选项
|
||||
*/
|
||||
export interface NetworkComponentOptions {
|
||||
/** 是否自动生成 protobuf 协议 */
|
||||
autoGenerateProtocol?: boolean;
|
||||
/** 自定义组件类型名 */
|
||||
typeName?: string;
|
||||
/** 是否仅服务端存在 */
|
||||
serverOnly?: boolean;
|
||||
/** 是否仅客户端存在 */
|
||||
clientOnly?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储 NetworkComponent 元数据的 Symbol
|
||||
*/
|
||||
export const NETWORK_COMPONENT_METADATA_KEY = Symbol('network_component_metadata');
|
||||
|
||||
/**
|
||||
* NetworkComponent 装饰器
|
||||
*
|
||||
* @param options 装饰器选项
|
||||
* @returns 类装饰器函数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @NetworkComponent({ autoGenerateProtocol: true })
|
||||
* class PlayerController extends Component implements INetworkComponent {
|
||||
* networkObject: INetworkObject | null = null;
|
||||
* networkId: number = 0;
|
||||
* hasAuthority: boolean = false;
|
||||
* componentType: string = 'PlayerController';
|
||||
*
|
||||
* @SyncVar()
|
||||
* public health: number = 100;
|
||||
*
|
||||
* @ClientRpc()
|
||||
* public showDamage(damage: number): void {
|
||||
* // 显示伤害效果
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function NetworkComponent(options: NetworkComponentOptions = {}): ClassDecorator {
|
||||
return function <T extends Function>(target: T) {
|
||||
const metadata: NetworkComponentMetadata = {
|
||||
componentType: options.typeName || target.name,
|
||||
syncVars: [],
|
||||
rpcs: [],
|
||||
autoGenerateProtocol: options.autoGenerateProtocol !== false,
|
||||
};
|
||||
|
||||
// 存储元数据
|
||||
Reflect.defineMetadata(NETWORK_COMPONENT_METADATA_KEY, metadata, target);
|
||||
|
||||
// 注册到全局组件注册表
|
||||
NetworkComponentRegistry.register(target as any, metadata);
|
||||
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类的 NetworkComponent 元数据
|
||||
*/
|
||||
export function getNetworkComponentMetadata(target: any): NetworkComponentMetadata | null {
|
||||
return Reflect.getMetadata(NETWORK_COMPONENT_METADATA_KEY, target) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查类是否为 NetworkComponent
|
||||
*/
|
||||
export function isNetworkComponent(target: any): boolean {
|
||||
return Reflect.hasMetadata(NETWORK_COMPONENT_METADATA_KEY, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络组件注册表
|
||||
*/
|
||||
class NetworkComponentRegistry {
|
||||
private static components = new Map<string, {
|
||||
constructor: any;
|
||||
metadata: NetworkComponentMetadata;
|
||||
}>();
|
||||
|
||||
/**
|
||||
* 注册网络组件
|
||||
*/
|
||||
static register(constructor: any, metadata: NetworkComponentMetadata): void {
|
||||
this.components.set(metadata.componentType, {
|
||||
constructor,
|
||||
metadata
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件信息
|
||||
*/
|
||||
static getComponent(typeName: string) {
|
||||
return this.components.get(typeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有组件
|
||||
*/
|
||||
static getAllComponents() {
|
||||
return Array.from(this.components.entries()).map(([typeName, info]) => ({
|
||||
typeName,
|
||||
...info
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查组件是否已注册
|
||||
*/
|
||||
static hasComponent(typeName: string): boolean {
|
||||
return this.components.has(typeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空注册表 (主要用于测试)
|
||||
*/
|
||||
static clear(): void {
|
||||
this.components.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export { NetworkComponentRegistry };
|
||||
@@ -1,178 +0,0 @@
|
||||
/**
|
||||
* ServerRpc 装饰器
|
||||
*
|
||||
* 用于标记可以在客户端调用,在服务端执行的方法
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { RpcMetadata, DecoratorTarget, Constructor, RpcParameterType, RpcReturnType } from '../types/NetworkTypes';
|
||||
import { getNetworkComponentMetadata } from './NetworkComponent';
|
||||
|
||||
/**
|
||||
* ServerRpc 装饰器选项
|
||||
*/
|
||||
export interface ServerRpcOptions {
|
||||
/** 是否需要权限验证 */
|
||||
requiresAuth?: boolean;
|
||||
/** 是否可靠传输,默认为 true */
|
||||
reliable?: boolean;
|
||||
/** 是否需要响应 */
|
||||
requiresResponse?: boolean;
|
||||
/** 是否需要拥有者权限 */
|
||||
requiresOwnership?: boolean;
|
||||
/** 调用频率限制 (调用/秒) */
|
||||
rateLimit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储 ServerRpc 元数据的 Symbol
|
||||
*/
|
||||
export const SERVER_RPC_METADATA_KEY = Symbol('server_rpc_metadata');
|
||||
|
||||
/**
|
||||
* ServerRpc 装饰器
|
||||
*
|
||||
* @param options 装饰器选项
|
||||
* @returns 方法装饰器函数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @NetworkComponent()
|
||||
* class PlayerController extends Component {
|
||||
* @ServerRpc({ requiresOwnership: true, rateLimit: 10 })
|
||||
* public movePlayer(direction: Vector3): void {
|
||||
* // 在服务端处理玩家移动,需要拥有者权限,限制每秒10次调用
|
||||
* this.transform.position.add(direction);
|
||||
* }
|
||||
*
|
||||
* @ServerRpc({ requiresAuth: true })
|
||||
* public purchaseItem(itemId: string): boolean {
|
||||
* // 购买物品,需要认证
|
||||
* return this.inventory.tryPurchase(itemId);
|
||||
* }
|
||||
*
|
||||
* @ServerRpc({ requiresResponse: true })
|
||||
* public getPlayerStats(): PlayerStats {
|
||||
* // 获取玩家统计数据并返回给客户端
|
||||
* return this.stats.toObject();
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function ServerRpc(options: ServerRpcOptions = {}): MethodDecorator {
|
||||
return function (target: unknown, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
|
||||
if (typeof propertyKey !== 'string') {
|
||||
throw new Error('ServerRpc can only be applied to string method names');
|
||||
}
|
||||
|
||||
// 获取或创建元数据数组
|
||||
const targetConstructor = (target as { constructor: Constructor }).constructor;
|
||||
let metadata: RpcMetadata[] = Reflect.getMetadata(SERVER_RPC_METADATA_KEY, targetConstructor);
|
||||
if (!metadata) {
|
||||
metadata = [];
|
||||
Reflect.defineMetadata(SERVER_RPC_METADATA_KEY, metadata, targetConstructor);
|
||||
}
|
||||
|
||||
// 创建 RPC 元数据
|
||||
const rpcMetadata: RpcMetadata = {
|
||||
methodName: propertyKey,
|
||||
rpcType: 'server-rpc',
|
||||
requiresAuth: options.requiresAuth || false,
|
||||
reliable: options.reliable !== false,
|
||||
requiresResponse: options.requiresResponse || false
|
||||
};
|
||||
|
||||
metadata.push(rpcMetadata);
|
||||
|
||||
// 更新 NetworkComponent 元数据
|
||||
const componentMetadata = getNetworkComponentMetadata(targetConstructor);
|
||||
if (componentMetadata) {
|
||||
const existingIndex = componentMetadata.rpcs.findIndex(rpc => rpc.methodName === propertyKey);
|
||||
if (existingIndex >= 0) {
|
||||
componentMetadata.rpcs[existingIndex] = rpcMetadata;
|
||||
} else {
|
||||
componentMetadata.rpcs.push(rpcMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存原方法
|
||||
const originalMethod = descriptor.value;
|
||||
if (typeof originalMethod !== 'function') {
|
||||
throw new Error(`ServerRpc can only be applied to methods, got ${typeof originalMethod}`);
|
||||
}
|
||||
|
||||
// 包装方法以添加网络调用逻辑
|
||||
descriptor.value = function (this: any, ...args: any[]) {
|
||||
// 如果在客户端调用,发送到服务端
|
||||
const isClient = this.isClient?.() || (typeof window !== 'undefined');
|
||||
if (isClient) {
|
||||
return this.sendServerRpc?.(propertyKey, args, options, rpcMetadata);
|
||||
}
|
||||
|
||||
// 如果在服务端,直接执行本地方法
|
||||
return originalMethod.apply(this, args);
|
||||
};
|
||||
|
||||
// 保存原方法的引用,供直接调用
|
||||
(descriptor.value as any).__originalMethod = originalMethod;
|
||||
(descriptor.value as any).__rpcMetadata = rpcMetadata;
|
||||
(descriptor.value as any).__rpcOptions = options;
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Command 装饰器 (ServerRpc 的别名,用于兼容性)
|
||||
*/
|
||||
export const Command = ServerRpc;
|
||||
|
||||
/**
|
||||
* 获取类的 ServerRpc 元数据
|
||||
*/
|
||||
export function getServerRpcMetadata(target: Constructor): RpcMetadata[] {
|
||||
return Reflect.getMetadata(SERVER_RPC_METADATA_KEY, target) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查方法是否为 ServerRpc
|
||||
*/
|
||||
export function isServerRpc(target: Constructor, methodName: string): boolean {
|
||||
const metadata = getServerRpcMetadata(target);
|
||||
return metadata.some(m => m.methodName === methodName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取特定方法的 ServerRpc 元数据
|
||||
*/
|
||||
export function getServerRpcMethodMetadata(target: Constructor, methodName: string): RpcMetadata | null {
|
||||
const metadata = getServerRpcMetadata(target);
|
||||
return metadata.find(m => m.methodName === methodName) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接调用原方法(跳过网络逻辑)
|
||||
*/
|
||||
export function invokeServerRpcLocally(instance: any, methodName: string, args: any[]): any {
|
||||
const method = instance[methodName];
|
||||
if (method && typeof method.__originalMethod === 'function') {
|
||||
return method.__originalMethod.apply(instance, args);
|
||||
}
|
||||
throw new Error(`Method ${methodName} is not a valid ServerRpc or original method not found`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 ServerRpc 是否需要响应
|
||||
*/
|
||||
export function serverRpcRequiresResponse(instance: any, methodName: string): boolean {
|
||||
const method = instance[methodName];
|
||||
return method?.__rpcMetadata?.requiresResponse || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 ServerRpc 的选项
|
||||
*/
|
||||
export function getServerRpcOptions(instance: any, methodName: string): ServerRpcOptions | null {
|
||||
const method = instance[methodName];
|
||||
return method?.__rpcOptions || null;
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
/**
|
||||
* SyncVar 装饰器
|
||||
*
|
||||
* 用于标记需要在网络间自动同步的属性
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { SyncVarMetadata, NetworkValue, DecoratorTarget, Constructor } from '../types/NetworkTypes';
|
||||
import { getNetworkComponentMetadata } from './NetworkComponent';
|
||||
|
||||
/**
|
||||
* SyncVar 装饰器选项
|
||||
*/
|
||||
export interface SyncVarOptions {
|
||||
/** 是否仅权威端可修改,默认为 true */
|
||||
authorityOnly?: boolean;
|
||||
/** 变化回调函数名 */
|
||||
onChanged?: string;
|
||||
/** 序列化类型提示 */
|
||||
serializeType?: string;
|
||||
/** 是否使用增量同步 */
|
||||
deltaSync?: boolean;
|
||||
/** 同步优先级,数值越大优先级越高 */
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储 SyncVar 元数据的 Symbol
|
||||
*/
|
||||
export const SYNCVAR_METADATA_KEY = Symbol('syncvar_metadata');
|
||||
|
||||
/**
|
||||
* SyncVar 装饰器
|
||||
*
|
||||
* @param options 装饰器选项
|
||||
* @returns 属性装饰器函数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @NetworkComponent()
|
||||
* class PlayerController extends Component {
|
||||
* @SyncVar({ onChanged: 'onHealthChanged', priority: 10 })
|
||||
* public health: number = 100;
|
||||
*
|
||||
* @SyncVar({ authorityOnly: false })
|
||||
* public playerName: string = '';
|
||||
*
|
||||
* @SyncVar({ deltaSync: true })
|
||||
* public inventory: Item[] = [];
|
||||
*
|
||||
* private onHealthChanged(oldValue: number, newValue: number): void {
|
||||
* console.log(`Health changed from ${oldValue} to ${newValue}`);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function SyncVar<T extends NetworkValue = NetworkValue>(options: SyncVarOptions = {}): PropertyDecorator {
|
||||
return function (target: unknown, propertyKey: string | symbol) {
|
||||
if (typeof propertyKey !== 'string') {
|
||||
throw new Error('SyncVar can only be applied to string property keys');
|
||||
}
|
||||
|
||||
// 获取或创建元数据数组
|
||||
const targetConstructor = (target as { constructor: Constructor }).constructor;
|
||||
let metadata: SyncVarMetadata[] = Reflect.getMetadata(SYNCVAR_METADATA_KEY, targetConstructor);
|
||||
if (!metadata) {
|
||||
metadata = [];
|
||||
Reflect.defineMetadata(SYNCVAR_METADATA_KEY, metadata, targetConstructor);
|
||||
}
|
||||
|
||||
// 创建 SyncVar 元数据
|
||||
const syncVarMetadata: SyncVarMetadata = {
|
||||
propertyName: propertyKey,
|
||||
authorityOnly: options.authorityOnly !== false,
|
||||
onChanged: options.onChanged,
|
||||
serializeType: options.serializeType,
|
||||
deltaSync: options.deltaSync || false,
|
||||
priority: options.priority || 0
|
||||
};
|
||||
|
||||
metadata.push(syncVarMetadata);
|
||||
|
||||
// 更新 NetworkComponent 元数据
|
||||
const componentMetadata = getNetworkComponentMetadata(targetConstructor);
|
||||
if (componentMetadata) {
|
||||
const existingIndex = componentMetadata.syncVars.findIndex(sv => sv.propertyName === propertyKey);
|
||||
if (existingIndex >= 0) {
|
||||
componentMetadata.syncVars[existingIndex] = syncVarMetadata;
|
||||
} else {
|
||||
componentMetadata.syncVars.push(syncVarMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建属性的内部存储和变化跟踪
|
||||
const internalKey = `_${propertyKey}`;
|
||||
const dirtyKey = `_${propertyKey}_dirty`;
|
||||
const previousKey = `_${propertyKey}_previous`;
|
||||
|
||||
// 重新定义属性的 getter 和 setter
|
||||
Object.defineProperty(target, propertyKey, {
|
||||
get: function (this: Record<string, unknown>): T {
|
||||
return this[internalKey] as T;
|
||||
},
|
||||
set: function (this: Record<string, unknown>, newValue: T) {
|
||||
const oldValue = this[internalKey] as T;
|
||||
|
||||
// 检查值是否真的发生了变化
|
||||
if (oldValue === newValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 对于复杂对象,进行深度比较
|
||||
if (typeof newValue === 'object' && newValue !== null &&
|
||||
typeof oldValue === 'object' && oldValue !== null) {
|
||||
if (JSON.stringify(oldValue) === JSON.stringify(newValue)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存旧值用于回调
|
||||
this[previousKey] = oldValue;
|
||||
this[internalKey] = newValue;
|
||||
this[dirtyKey] = true;
|
||||
|
||||
// 调用变化回调
|
||||
if (options.onChanged && typeof (this[options.onChanged] as unknown) === 'function') {
|
||||
(this[options.onChanged] as (oldValue: T, newValue: T) => void)(oldValue, newValue);
|
||||
}
|
||||
|
||||
// 通知网络同步系统
|
||||
(this as { notifySyncVarChanged?: (key: string, oldValue: T, newValue: T, metadata: SyncVarMetadata) => void }).notifySyncVarChanged?.(propertyKey, oldValue, newValue, syncVarMetadata);
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// 初始化内部属性
|
||||
const targetRecord = target as Record<string, unknown>;
|
||||
if (targetRecord[internalKey] === undefined) {
|
||||
targetRecord[internalKey] = targetRecord[propertyKey];
|
||||
}
|
||||
targetRecord[dirtyKey] = false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类的 SyncVar 元数据
|
||||
*/
|
||||
export function getSyncVarMetadata(target: Constructor): SyncVarMetadata[] {
|
||||
return Reflect.getMetadata(SYNCVAR_METADATA_KEY, target) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查属性是否为 SyncVar
|
||||
*/
|
||||
export function isSyncVar(target: Constructor, propertyName: string): boolean {
|
||||
const metadata = getSyncVarMetadata(target);
|
||||
return metadata.some(m => m.propertyName === propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 SyncVar 的脏标记
|
||||
*/
|
||||
export function isSyncVarDirty(instance: Record<string, unknown>, propertyName: string): boolean {
|
||||
return (instance[`_${propertyName}_dirty`] as boolean) || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除 SyncVar 的脏标记
|
||||
*/
|
||||
export function clearSyncVarDirty(instance: Record<string, unknown>, propertyName: string): void {
|
||||
instance[`_${propertyName}_dirty`] = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 SyncVar 的前一个值
|
||||
*/
|
||||
export function getSyncVarPreviousValue<T extends NetworkValue = NetworkValue>(instance: Record<string, unknown>, propertyName: string): T | undefined {
|
||||
return instance[`_${propertyName}_previous`] as T | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制设置 SyncVar 值(跳过权限检查和变化检测)
|
||||
*/
|
||||
export function setSyncVarValue<T extends NetworkValue = NetworkValue>(instance: Record<string, unknown>, propertyName: string, value: T, skipCallback = false): void {
|
||||
const internalKey = `_${propertyName}`;
|
||||
const dirtyKey = `_${propertyName}_dirty`;
|
||||
const previousKey = `_${propertyName}_previous`;
|
||||
|
||||
const oldValue = instance[internalKey] as T;
|
||||
instance[previousKey] = oldValue;
|
||||
instance[internalKey] = value;
|
||||
instance[dirtyKey] = false; // 网络接收的值不标记为脏
|
||||
|
||||
// 可选择性调用回调
|
||||
if (!skipCallback) {
|
||||
const metadata = getSyncVarMetadata((instance as { constructor: Constructor }).constructor);
|
||||
const syncVarMeta = metadata.find(m => m.propertyName === propertyName);
|
||||
|
||||
if (syncVarMeta?.onChanged && typeof (instance[syncVarMeta.onChanged] as unknown) === 'function') {
|
||||
(instance[syncVarMeta.onChanged] as (oldValue: T, newValue: T) => void)(oldValue, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取所有脏的 SyncVar
|
||||
*/
|
||||
export function getDirtySyncVars(instance: Record<string, unknown>): Array<{
|
||||
propertyName: string;
|
||||
oldValue: NetworkValue;
|
||||
newValue: NetworkValue;
|
||||
metadata: SyncVarMetadata;
|
||||
}> {
|
||||
const metadata = getSyncVarMetadata((instance as { constructor: Constructor }).constructor);
|
||||
const dirtyVars: Array<{
|
||||
propertyName: string;
|
||||
oldValue: NetworkValue;
|
||||
newValue: NetworkValue;
|
||||
metadata: SyncVarMetadata;
|
||||
}> = [];
|
||||
|
||||
for (const syncVar of metadata) {
|
||||
if (isSyncVarDirty(instance, syncVar.propertyName)) {
|
||||
const oldValue = getSyncVarPreviousValue(instance, syncVar.propertyName);
|
||||
const newValue = instance[`_${syncVar.propertyName}`] as NetworkValue;
|
||||
|
||||
dirtyVars.push({
|
||||
propertyName: syncVar.propertyName,
|
||||
oldValue: oldValue ?? newValue, // 使用空合并运算符处理undefined
|
||||
newValue: newValue,
|
||||
metadata: syncVar
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 按优先级排序,优先级高的先处理
|
||||
return dirtyVars.sort((a, b) => (b.metadata.priority || 0) - (a.metadata.priority || 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量清除所有脏标记
|
||||
*/
|
||||
export function clearAllDirtySyncVars(instance: Record<string, unknown>): void {
|
||||
const metadata = getSyncVarMetadata((instance as { constructor: Constructor }).constructor);
|
||||
for (const syncVar of metadata) {
|
||||
clearSyncVarDirty(instance, syncVar.propertyName);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* 装饰器导出
|
||||
*/
|
||||
|
||||
export * from './NetworkComponent';
|
||||
export * from './SyncVar';
|
||||
export * from './ClientRpc';
|
||||
export * from './ServerRpc';
|
||||
287
packages/network-shared/src/events/NetworkEvents.ts
Normal file
287
packages/network-shared/src/events/NetworkEvents.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* 网络事件类型枚举
|
||||
* 定义网络层中的所有事件类型
|
||||
*/
|
||||
export enum NetworkEventType {
|
||||
// 连接相关事件
|
||||
CONNECTION_ESTABLISHED = 'network:connection:established',
|
||||
CONNECTION_LOST = 'network:connection:lost',
|
||||
CONNECTION_ERROR = 'network:connection:error',
|
||||
CONNECTION_TIMEOUT = 'network:connection:timeout',
|
||||
RECONNECTION_STARTED = 'network:reconnection:started',
|
||||
RECONNECTION_SUCCEEDED = 'network:reconnection:succeeded',
|
||||
RECONNECTION_FAILED = 'network:reconnection:failed',
|
||||
|
||||
// 网络身份相关事件
|
||||
IDENTITY_CREATED = 'network:identity:created',
|
||||
IDENTITY_DESTROYED = 'network:identity:destroyed',
|
||||
IDENTITY_OWNER_CHANGED = 'network:identity:owner:changed',
|
||||
IDENTITY_AUTHORITY_CHANGED = 'network:identity:authority:changed',
|
||||
IDENTITY_SYNC_ENABLED = 'network:identity:sync:enabled',
|
||||
IDENTITY_SYNC_DISABLED = 'network:identity:sync:disabled',
|
||||
IDENTITY_PROPERTY_CHANGED = 'network:identity:property:changed',
|
||||
IDENTITY_VISIBLE_CHANGED = 'network:identity:visible:changed',
|
||||
|
||||
// 同步相关事件
|
||||
SYNC_STARTED = 'network:sync:started',
|
||||
SYNC_COMPLETED = 'network:sync:completed',
|
||||
SYNC_FAILED = 'network:sync:failed',
|
||||
SYNC_RATE_CHANGED = 'network:sync:rate:changed',
|
||||
SYNC_PRIORITY_CHANGED = 'network:sync:priority:changed',
|
||||
|
||||
// RPC相关事件
|
||||
RPC_CALL_SENT = 'network:rpc:call:sent',
|
||||
RPC_CALL_RECEIVED = 'network:rpc:call:received',
|
||||
RPC_RESPONSE_SENT = 'network:rpc:response:sent',
|
||||
RPC_RESPONSE_RECEIVED = 'network:rpc:response:received',
|
||||
RPC_ERROR = 'network:rpc:error',
|
||||
RPC_TIMEOUT = 'network:rpc:timeout',
|
||||
|
||||
// 消息相关事件
|
||||
MESSAGE_SENT = 'network:message:sent',
|
||||
MESSAGE_RECEIVED = 'network:message:received',
|
||||
MESSAGE_QUEUED = 'network:message:queued',
|
||||
MESSAGE_DROPPED = 'network:message:dropped',
|
||||
MESSAGE_RETRY = 'network:message:retry',
|
||||
MESSAGE_ACKNOWLEDGED = 'network:message:acknowledged',
|
||||
|
||||
// 房间相关事件
|
||||
ROOM_JOINED = 'network:room:joined',
|
||||
ROOM_LEFT = 'network:room:left',
|
||||
ROOM_CREATED = 'network:room:created',
|
||||
ROOM_DESTROYED = 'network:room:destroyed',
|
||||
ROOM_PLAYER_JOINED = 'network:room:player:joined',
|
||||
ROOM_PLAYER_LEFT = 'network:room:player:left',
|
||||
|
||||
// 客户端相关事件
|
||||
CLIENT_CONNECTED = 'network:client:connected',
|
||||
CLIENT_DISCONNECTED = 'network:client:disconnected',
|
||||
CLIENT_AUTHENTICATED = 'network:client:authenticated',
|
||||
CLIENT_KICKED = 'network:client:kicked',
|
||||
CLIENT_TIMEOUT = 'network:client:timeout',
|
||||
|
||||
// 服务器相关事件
|
||||
SERVER_STARTED = 'network:server:started',
|
||||
SERVER_STOPPED = 'network:server:stopped',
|
||||
SERVER_ERROR = 'network:server:error',
|
||||
SERVER_OVERLOADED = 'network:server:overloaded',
|
||||
|
||||
// 数据相关事件
|
||||
DATA_SYNCHRONIZED = 'network:data:synchronized',
|
||||
DATA_CONFLICT = 'network:data:conflict',
|
||||
DATA_CORRUPTED = 'network:data:corrupted',
|
||||
DATA_VALIDATED = 'network:data:validated',
|
||||
|
||||
// 性能相关事件
|
||||
BANDWIDTH_WARNING = 'network:bandwidth:warning',
|
||||
LATENCY_HIGH = 'network:latency:high',
|
||||
PACKET_LOSS_DETECTED = 'network:packet:loss:detected',
|
||||
PERFORMANCE_DEGRADED = 'network:performance:degraded'
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络事件优先级
|
||||
*/
|
||||
export enum NetworkEventPriority {
|
||||
LOW = 10,
|
||||
NORMAL = 20,
|
||||
HIGH = 30,
|
||||
CRITICAL = 40,
|
||||
EMERGENCY = 50
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络事件数据基础接口
|
||||
*/
|
||||
export interface NetworkEventData {
|
||||
timestamp: number;
|
||||
networkId?: number;
|
||||
clientId?: string;
|
||||
roomId?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络身份事件数据
|
||||
*/
|
||||
export interface NetworkIdentityEventData extends NetworkEventData {
|
||||
networkId: number;
|
||||
ownerId: string;
|
||||
oldValue?: any;
|
||||
newValue?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC事件数据
|
||||
*/
|
||||
export interface RpcEventData extends NetworkEventData {
|
||||
rpcId: string;
|
||||
methodName: string;
|
||||
parameters?: any[];
|
||||
result?: any;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息事件数据
|
||||
*/
|
||||
export interface MessageEventData extends NetworkEventData {
|
||||
messageId: string;
|
||||
messageType: string;
|
||||
payload: any;
|
||||
reliable: boolean;
|
||||
size: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接事件数据
|
||||
*/
|
||||
export interface ConnectionEventData extends NetworkEventData {
|
||||
clientId: string;
|
||||
address?: string;
|
||||
reason?: string;
|
||||
reconnectAttempt?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间事件数据
|
||||
*/
|
||||
export interface RoomEventData extends NetworkEventData {
|
||||
roomId: string;
|
||||
playerId?: string;
|
||||
playerCount?: number;
|
||||
maxPlayers?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能事件数据
|
||||
*/
|
||||
export interface PerformanceEventData extends NetworkEventData {
|
||||
metric: string;
|
||||
value: number;
|
||||
threshold?: number;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络事件工具类
|
||||
*/
|
||||
export class NetworkEventUtils {
|
||||
/**
|
||||
* 创建网络身份事件数据
|
||||
*/
|
||||
static createIdentityEventData(
|
||||
networkId: number,
|
||||
ownerId: string,
|
||||
oldValue?: any,
|
||||
newValue?: any
|
||||
): NetworkIdentityEventData {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
networkId,
|
||||
ownerId,
|
||||
oldValue,
|
||||
newValue
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建RPC事件数据
|
||||
*/
|
||||
static createRpcEventData(
|
||||
rpcId: string,
|
||||
methodName: string,
|
||||
clientId?: string,
|
||||
parameters?: any[],
|
||||
result?: any,
|
||||
error?: string
|
||||
): RpcEventData {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
clientId,
|
||||
rpcId,
|
||||
methodName,
|
||||
parameters,
|
||||
result,
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建消息事件数据
|
||||
*/
|
||||
static createMessageEventData(
|
||||
messageId: string,
|
||||
messageType: string,
|
||||
payload: any,
|
||||
reliable: boolean = true,
|
||||
clientId?: string
|
||||
): MessageEventData {
|
||||
const size = JSON.stringify(payload).length;
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
clientId,
|
||||
messageId,
|
||||
messageType,
|
||||
payload,
|
||||
reliable,
|
||||
size
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建连接事件数据
|
||||
*/
|
||||
static createConnectionEventData(
|
||||
clientId: string,
|
||||
address?: string,
|
||||
reason?: string,
|
||||
reconnectAttempt?: number
|
||||
): ConnectionEventData {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
clientId,
|
||||
address,
|
||||
reason,
|
||||
reconnectAttempt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建房间事件数据
|
||||
*/
|
||||
static createRoomEventData(
|
||||
roomId: string,
|
||||
playerId?: string,
|
||||
playerCount?: number,
|
||||
maxPlayers?: number
|
||||
): RoomEventData {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
roomId,
|
||||
playerId,
|
||||
playerCount,
|
||||
maxPlayers
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建性能事件数据
|
||||
*/
|
||||
static createPerformanceEventData(
|
||||
metric: string,
|
||||
value: number,
|
||||
threshold?: number,
|
||||
duration?: number,
|
||||
clientId?: string
|
||||
): PerformanceEventData {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
clientId,
|
||||
metric,
|
||||
value,
|
||||
threshold,
|
||||
duration
|
||||
};
|
||||
}
|
||||
}
|
||||
1
packages/network-shared/src/events/index.ts
Normal file
1
packages/network-shared/src/events/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './NetworkEvents';
|
||||
@@ -1,43 +1,26 @@
|
||||
/**
|
||||
* ECS Framework Network Shared
|
||||
*
|
||||
* 共享的网络组件、装饰器和类型定义
|
||||
* @esengine/network-shared
|
||||
* ECS Framework网络层 - 共享组件和协议
|
||||
*/
|
||||
|
||||
// 确保 reflect-metadata 被导入
|
||||
import 'reflect-metadata';
|
||||
|
||||
// 类型定义
|
||||
export * from './types';
|
||||
export * from './types/NetworkTypes';
|
||||
export * from './types/TransportTypes';
|
||||
|
||||
// 装饰器
|
||||
export * from './decorators';
|
||||
// 协议消息
|
||||
export * from './protocols/MessageTypes';
|
||||
|
||||
// 核心类
|
||||
export * from './core';
|
||||
// 核心组件
|
||||
export * from './components/NetworkIdentity';
|
||||
|
||||
// 序列化工具
|
||||
export * from './serialization';
|
||||
// 装饰器系统 (待实现)
|
||||
// export * from './decorators/SyncVar';
|
||||
// export * from './decorators/ServerRpc';
|
||||
// export * from './decorators/ClientRpc';
|
||||
// export * from './decorators/NetworkComponent';
|
||||
|
||||
// 协议编译器
|
||||
export * from './protocol';
|
||||
// 事件系统
|
||||
export * from './events/NetworkEvents';
|
||||
|
||||
// 工具函数
|
||||
export * from './utils';
|
||||
|
||||
// 版本信息
|
||||
export const VERSION = '1.0.0';
|
||||
|
||||
// 默认配置
|
||||
export const DEFAULT_NETWORK_CONFIG = {
|
||||
port: 7777,
|
||||
host: 'localhost',
|
||||
maxConnections: 100,
|
||||
syncRate: 20,
|
||||
snapshotRate: 5,
|
||||
compression: true,
|
||||
encryption: false,
|
||||
timeout: 30000,
|
||||
maxReconnectAttempts: 3,
|
||||
reconnectInterval: 5000
|
||||
};
|
||||
// 序列化系统 (待实现)
|
||||
// export * from './serialization/NetworkSerializer';
|
||||
@@ -1,663 +0,0 @@
|
||||
/**
|
||||
* TypeScript 协议分析器
|
||||
*
|
||||
* 负责解析 TypeScript 代码中的网络组件装饰器,
|
||||
* 提取类型信息并构建协议定义
|
||||
*/
|
||||
|
||||
// TypeScript编译器API - 开发时依赖
|
||||
declare const require: any;
|
||||
|
||||
let ts: any;
|
||||
let path: any;
|
||||
let fs: any;
|
||||
|
||||
try {
|
||||
ts = require('typescript');
|
||||
path = require('path');
|
||||
fs = require('fs');
|
||||
} catch (e) {
|
||||
// 在运行时如果没有这些依赖,使用占位符
|
||||
ts = {
|
||||
ScriptTarget: { ES2020: 99 },
|
||||
ModuleKind: { ES2020: 99 },
|
||||
createProgram: () => ({ getSourceFiles: () => [] }),
|
||||
isClassDeclaration: () => false,
|
||||
isDecorator: () => false,
|
||||
isIdentifier: () => false,
|
||||
isCallExpression: () => false,
|
||||
forEachChild: () => {}
|
||||
};
|
||||
path = { join: (...args: string[]) => args.join('/') };
|
||||
fs = { existsSync: () => false, readFileSync: () => '{}' };
|
||||
}
|
||||
|
||||
import {
|
||||
ComponentProtocol,
|
||||
ProtocolField,
|
||||
ProtocolRpc,
|
||||
RpcParameter,
|
||||
SerializeType,
|
||||
ProtocolAnalysisResult,
|
||||
ProtocolError,
|
||||
ProtocolWarning,
|
||||
ProtocolCompilerConfig
|
||||
} from '../types/ProtocolTypes';
|
||||
|
||||
/**
|
||||
* TypeScript 协议分析器
|
||||
*/
|
||||
export class TypeScriptAnalyzer {
|
||||
private program: ts.Program;
|
||||
private typeChecker: ts.TypeChecker;
|
||||
private config: ProtocolCompilerConfig;
|
||||
|
||||
private components: ComponentProtocol[] = [];
|
||||
private errors: ProtocolError[] = [];
|
||||
private warnings: ProtocolWarning[] = [];
|
||||
private dependencies: Map<string, string[]> = new Map();
|
||||
|
||||
constructor(config: ProtocolCompilerConfig) {
|
||||
this.config = config;
|
||||
this.initializeTypeScript();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 TypeScript 编译器
|
||||
*/
|
||||
private initializeTypeScript(): void {
|
||||
if (!ts || !path || !fs) {
|
||||
throw new Error('TypeScript analyzer requires typescript, path, and fs modules');
|
||||
}
|
||||
|
||||
const configPath = this.config.tsconfigPath || path.join(this.config.inputDir, 'tsconfig.json');
|
||||
|
||||
let compilerOptions: ts.CompilerOptions = {
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
module: ts.ModuleKind.ES2020,
|
||||
lib: ['ES2020'],
|
||||
experimentalDecorators: true,
|
||||
emitDecoratorMetadata: true,
|
||||
strict: true
|
||||
};
|
||||
|
||||
// 加载 tsconfig.json
|
||||
if (fs.existsSync(configPath)) {
|
||||
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
|
||||
if (configFile.error) {
|
||||
this.addError('syntax', `Failed to read tsconfig.json: ${configFile.error.messageText}`);
|
||||
} else {
|
||||
const parsedConfig = ts.parseJsonConfigFileContent(
|
||||
configFile.config,
|
||||
ts.sys,
|
||||
path.dirname(configPath)
|
||||
);
|
||||
compilerOptions = { ...compilerOptions, ...parsedConfig.options };
|
||||
}
|
||||
}
|
||||
|
||||
// 收集所有 TypeScript 文件
|
||||
const files = this.collectTypeScriptFiles(this.config.inputDir);
|
||||
|
||||
this.program = ts.createProgram(files, compilerOptions);
|
||||
this.typeChecker = this.program.getTypeChecker();
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集 TypeScript 文件
|
||||
*/
|
||||
private collectTypeScriptFiles(dir: string): string[] {
|
||||
const files: string[] = [];
|
||||
const excludePatterns = this.config.excludePatterns || ['**/*.test.ts', '**/*.spec.ts', '**/node_modules/**'];
|
||||
|
||||
function collectFiles(currentDir: string): void {
|
||||
const items = fs.readdirSync(currentDir);
|
||||
|
||||
for (const item of items) {
|
||||
const fullPath = path.join(currentDir, item);
|
||||
const stat = fs.statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
// 检查是否应该排除此目录
|
||||
const shouldExclude = excludePatterns.some(pattern =>
|
||||
fullPath.includes(pattern.replace('**/', '').replace('/**', ''))
|
||||
);
|
||||
|
||||
if (!shouldExclude) {
|
||||
collectFiles(fullPath);
|
||||
}
|
||||
} else if (item.endsWith('.ts') || item.endsWith('.tsx')) {
|
||||
// 检查是否应该排除此文件
|
||||
const shouldExclude = excludePatterns.some(pattern => {
|
||||
if (pattern.includes('**')) {
|
||||
const regex = new RegExp(pattern.replace('**/', '.*').replace('*', '.*'));
|
||||
return regex.test(fullPath);
|
||||
}
|
||||
return fullPath.endsWith(pattern.replace('*', ''));
|
||||
});
|
||||
|
||||
if (!shouldExclude) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collectFiles(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析网络协议
|
||||
*/
|
||||
public analyze(): ProtocolAnalysisResult {
|
||||
this.components = [];
|
||||
this.errors = [];
|
||||
this.warnings = [];
|
||||
this.dependencies.clear();
|
||||
|
||||
const sourceFiles = this.program.getSourceFiles().filter(sf =>
|
||||
!sf.isDeclarationFile && sf.fileName.includes(this.config.inputDir)
|
||||
);
|
||||
|
||||
// 分析每个源文件
|
||||
for (const sourceFile of sourceFiles) {
|
||||
this.analyzeSourceFile(sourceFile);
|
||||
}
|
||||
|
||||
// 检查依赖关系
|
||||
this.validateDependencies();
|
||||
|
||||
return {
|
||||
files: sourceFiles.map(sf => sf.fileName),
|
||||
components: this.components,
|
||||
dependencies: this.dependencies,
|
||||
errors: this.errors,
|
||||
warnings: this.warnings
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析单个源文件
|
||||
*/
|
||||
private analyzeSourceFile(sourceFile: any): void {
|
||||
const visit = (node: any): void => {
|
||||
if (ts.isClassDeclaration(node) && this.isNetworkComponent(node)) {
|
||||
this.analyzeNetworkComponent(node, sourceFile);
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
|
||||
visit(sourceFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为网络组件
|
||||
*/
|
||||
private isNetworkComponent(node: any): boolean {
|
||||
if (!node.modifiers) return false;
|
||||
|
||||
return node.modifiers.some((modifier: any) => {
|
||||
if (ts.isDecorator(modifier)) {
|
||||
const expression = modifier.expression;
|
||||
if (ts.isCallExpression(expression) || ts.isIdentifier(expression)) {
|
||||
const decoratorName = this.getDecoratorName(expression);
|
||||
return decoratorName === 'NetworkComponent';
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取装饰器名称
|
||||
*/
|
||||
private getDecoratorName(expression: ts.Expression): string | null {
|
||||
if (ts.isIdentifier(expression)) {
|
||||
return expression.text;
|
||||
}
|
||||
if (ts.isCallExpression(expression) && ts.isIdentifier(expression.expression)) {
|
||||
return expression.expression.text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析网络组件
|
||||
*/
|
||||
private analyzeNetworkComponent(node: any, sourceFile: any): void {
|
||||
const className = node.name?.text;
|
||||
if (!className) {
|
||||
this.addError('syntax', 'NetworkComponent class must have a name', sourceFile, node);
|
||||
return;
|
||||
}
|
||||
|
||||
const componentProtocol: ComponentProtocol = {
|
||||
typeName: className,
|
||||
version: 1,
|
||||
syncVars: [],
|
||||
rpcs: [],
|
||||
batchEnabled: false,
|
||||
deltaEnabled: false
|
||||
};
|
||||
|
||||
// 分析类成员
|
||||
for (const member of node.members) {
|
||||
if (ts.isPropertyDeclaration(member)) {
|
||||
const syncVar = this.analyzeSyncVar(member, sourceFile);
|
||||
if (syncVar) {
|
||||
componentProtocol.syncVars.push(syncVar);
|
||||
}
|
||||
} else if (ts.isMethodDeclaration(member)) {
|
||||
const rpc = this.analyzeRpc(member, sourceFile);
|
||||
if (rpc) {
|
||||
componentProtocol.rpcs.push(rpc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分析装饰器选项
|
||||
this.analyzeComponentDecorator(node, componentProtocol, sourceFile);
|
||||
|
||||
this.components.push(componentProtocol);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析 SyncVar 属性
|
||||
*/
|
||||
private analyzeSyncVar(node: ts.PropertyDeclaration, sourceFile: ts.SourceFile): ProtocolField | null {
|
||||
if (!this.hasSyncVarDecorator(node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const propertyName = this.getPropertyName(node);
|
||||
if (!propertyName) {
|
||||
this.addError('syntax', 'SyncVar property must have a name', sourceFile, node);
|
||||
return null;
|
||||
}
|
||||
|
||||
const type = this.typeChecker.getTypeAtLocation(node);
|
||||
const serializeType = this.inferSerializeType(type, node, sourceFile);
|
||||
|
||||
if (!serializeType) {
|
||||
this.addError('type', `Cannot infer serialize type for property: ${propertyName}`, sourceFile, node);
|
||||
return null;
|
||||
}
|
||||
|
||||
const field: ProtocolField = {
|
||||
name: propertyName,
|
||||
type: serializeType,
|
||||
id: this.generateFieldId(propertyName),
|
||||
optional: this.isOptionalProperty(node),
|
||||
repeated: this.isArrayType(type)
|
||||
};
|
||||
|
||||
// 分析装饰器选项
|
||||
this.analyzeSyncVarDecorator(node, field, sourceFile);
|
||||
|
||||
return field;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析 RPC 方法
|
||||
*/
|
||||
private analyzeRpc(node: ts.MethodDeclaration, sourceFile: ts.SourceFile): ProtocolRpc | null {
|
||||
const rpcType = this.getRpcType(node);
|
||||
if (!rpcType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const methodName = this.getMethodName(node);
|
||||
if (!methodName) {
|
||||
this.addError('syntax', 'RPC method must have a name', sourceFile, node);
|
||||
return null;
|
||||
}
|
||||
|
||||
const parameters: RpcParameter[] = [];
|
||||
|
||||
// 分析参数
|
||||
if (node.parameters) {
|
||||
for (const param of node.parameters) {
|
||||
const paramName = param.name.getText();
|
||||
const paramType = this.typeChecker.getTypeAtLocation(param);
|
||||
const serializeType = this.inferSerializeType(paramType, param, sourceFile);
|
||||
|
||||
if (serializeType === null) {
|
||||
this.addError('type', `Cannot infer type for parameter: ${paramName}`, sourceFile, param);
|
||||
continue;
|
||||
}
|
||||
|
||||
parameters.push({
|
||||
name: paramName,
|
||||
type: serializeType,
|
||||
optional: param.questionToken !== undefined,
|
||||
isArray: this.isArrayType(paramType)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 分析返回类型
|
||||
let returnType: SerializeType | undefined;
|
||||
if (node.type && !this.isVoidType(node.type)) {
|
||||
const returnTypeNode = this.typeChecker.getTypeAtLocation(node.type);
|
||||
returnType = this.inferSerializeType(returnTypeNode, node.type, sourceFile);
|
||||
}
|
||||
|
||||
const rpc: ProtocolRpc = {
|
||||
name: methodName,
|
||||
id: this.generateRpcId(methodName),
|
||||
type: rpcType,
|
||||
parameters,
|
||||
returnType
|
||||
};
|
||||
|
||||
// 分析装饰器选项
|
||||
this.analyzeRpcDecorator(node, rpc, sourceFile);
|
||||
|
||||
return rpc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有 SyncVar 装饰器
|
||||
*/
|
||||
private hasSyncVarDecorator(node: ts.PropertyDeclaration): boolean {
|
||||
return this.hasDecorator(node, 'SyncVar');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 RPC 类型
|
||||
*/
|
||||
private getRpcType(node: ts.MethodDeclaration): 'client-rpc' | 'server-rpc' | null {
|
||||
if (this.hasDecorator(node, 'ClientRpc')) {
|
||||
return 'client-rpc';
|
||||
}
|
||||
if (this.hasDecorator(node, 'ServerRpc') || this.hasDecorator(node, 'Command')) {
|
||||
return 'server-rpc';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有特定装饰器
|
||||
*/
|
||||
private hasDecorator(node: ts.Node, decoratorName: string): boolean {
|
||||
if (!ts.canHaveModifiers(node) || !ts.getModifiers(node)) return false;
|
||||
|
||||
const modifiers = ts.getModifiers(node)!;
|
||||
return modifiers.some(modifier => {
|
||||
if (ts.isDecorator(modifier)) {
|
||||
const name = this.getDecoratorName(modifier.expression);
|
||||
return name === decoratorName;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 推导序列化类型
|
||||
*/
|
||||
private inferSerializeType(type: ts.Type, node: ts.Node, sourceFile: ts.SourceFile): SerializeType | null {
|
||||
const typeString = this.typeChecker.typeToString(type);
|
||||
|
||||
// 自定义类型映射
|
||||
if (this.config.typeMapping?.has(typeString)) {
|
||||
return this.config.typeMapping.get(typeString)!;
|
||||
}
|
||||
|
||||
// 基础类型推导
|
||||
if (type.flags & ts.TypeFlags.Boolean) return SerializeType.BOOLEAN;
|
||||
if (type.flags & ts.TypeFlags.Number) return SerializeType.FLOAT64;
|
||||
if (type.flags & ts.TypeFlags.String) return SerializeType.STRING;
|
||||
|
||||
// 对象类型推导
|
||||
if (type.flags & ts.TypeFlags.Object) {
|
||||
// 检查是否为数组
|
||||
if (this.typeChecker.isArrayType(type)) {
|
||||
return SerializeType.ARRAY;
|
||||
}
|
||||
|
||||
// 检查常见游戏类型
|
||||
if (typeString.includes('Vector2')) return SerializeType.VECTOR2;
|
||||
if (typeString.includes('Vector3')) return SerializeType.VECTOR3;
|
||||
if (typeString.includes('Quaternion')) return SerializeType.QUATERNION;
|
||||
if (typeString.includes('Color')) return SerializeType.COLOR;
|
||||
|
||||
// 默认为对象类型
|
||||
return SerializeType.OBJECT;
|
||||
}
|
||||
|
||||
this.addWarning('performance', `Unknown type: ${typeString}, falling back to JSON`, sourceFile, node);
|
||||
return SerializeType.JSON;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性名
|
||||
*/
|
||||
private getPropertyName(node: ts.PropertyDeclaration): string | null {
|
||||
if (ts.isIdentifier(node.name)) {
|
||||
return node.name.text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取方法名
|
||||
*/
|
||||
private getMethodName(node: ts.MethodDeclaration): string | null {
|
||||
if (ts.isIdentifier(node.name)) {
|
||||
return node.name.text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为可选属性
|
||||
*/
|
||||
private isOptionalProperty(node: ts.PropertyDeclaration): boolean {
|
||||
return node.questionToken !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为数组类型
|
||||
*/
|
||||
private isArrayType(type: ts.Type): boolean {
|
||||
return this.typeChecker.isArrayType(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为 void 类型
|
||||
*/
|
||||
private isVoidType(node: ts.TypeNode): boolean {
|
||||
return ts.isTypeReferenceNode(node) && node.typeName.getText() === 'void';
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成字段 ID
|
||||
*/
|
||||
private generateFieldId(fieldName: string): number {
|
||||
// 简单的哈希函数生成字段 ID
|
||||
let hash = 0;
|
||||
for (let i = 0; i < fieldName.length; i++) {
|
||||
const char = fieldName.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // 转换为 32位整数
|
||||
}
|
||||
return Math.abs(hash) % 10000 + 1; // 确保 ID 为正数且在合理范围内
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 RPC ID
|
||||
*/
|
||||
private generateRpcId(rpcName: string): number {
|
||||
return this.generateFieldId(rpcName) + 10000; // RPC ID 从 10000 开始
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析组件装饰器选项
|
||||
*/
|
||||
private analyzeComponentDecorator(
|
||||
node: ts.ClassDeclaration,
|
||||
protocol: ComponentProtocol,
|
||||
sourceFile: ts.SourceFile
|
||||
): void {
|
||||
const decorator = this.findDecorator(node, 'NetworkComponent');
|
||||
if (decorator && ts.isCallExpression(decorator.expression)) {
|
||||
const args = decorator.expression.arguments;
|
||||
if (args.length > 0 && ts.isObjectLiteralExpression(args[0])) {
|
||||
const options = args[0];
|
||||
|
||||
for (const prop of options.properties) {
|
||||
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
||||
const propName = prop.name.text;
|
||||
|
||||
if (propName === 'batchEnabled' && this.isBooleanLiteral(prop.initializer)) {
|
||||
protocol.batchEnabled = (prop.initializer as ts.BooleanLiteral).token === ts.SyntaxKind.TrueKeyword;
|
||||
}
|
||||
|
||||
if (propName === 'deltaEnabled' && this.isBooleanLiteral(prop.initializer)) {
|
||||
protocol.deltaEnabled = (prop.initializer as ts.BooleanLiteral).token === ts.SyntaxKind.TrueKeyword;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析 SyncVar 装饰器选项
|
||||
*/
|
||||
private analyzeSyncVarDecorator(
|
||||
node: ts.PropertyDeclaration,
|
||||
field: ProtocolField,
|
||||
sourceFile: ts.SourceFile
|
||||
): void {
|
||||
const decorator = this.findDecorator(node, 'SyncVar');
|
||||
if (decorator && ts.isCallExpression(decorator.expression)) {
|
||||
const args = decorator.expression.arguments;
|
||||
if (args.length > 0 && ts.isObjectLiteralExpression(args[0])) {
|
||||
const options = args[0];
|
||||
|
||||
for (const prop of options.properties) {
|
||||
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
||||
const propName = prop.name.text;
|
||||
|
||||
if (propName === 'serialize' && ts.isStringLiteral(prop.initializer)) {
|
||||
const serializeType = prop.initializer.text as SerializeType;
|
||||
if (Object.values(SerializeType).includes(serializeType)) {
|
||||
field.type = serializeType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析 RPC 装饰器选项
|
||||
*/
|
||||
private analyzeRpcDecorator(
|
||||
node: ts.MethodDeclaration,
|
||||
rpc: ProtocolRpc,
|
||||
sourceFile: ts.SourceFile
|
||||
): void {
|
||||
const decoratorName = rpc.type === 'client-rpc' ? 'ClientRpc' : 'ServerRpc';
|
||||
const decorator = this.findDecorator(node, decoratorName) || this.findDecorator(node, 'Command');
|
||||
|
||||
if (decorator && ts.isCallExpression(decorator.expression)) {
|
||||
const args = decorator.expression.arguments;
|
||||
if (args.length > 0 && ts.isObjectLiteralExpression(args[0])) {
|
||||
const options = args[0];
|
||||
|
||||
for (const prop of options.properties) {
|
||||
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
||||
const propName = prop.name.text;
|
||||
|
||||
if (propName === 'requiresAuth' && this.isBooleanLiteral(prop.initializer)) {
|
||||
rpc.requiresAuth = (prop.initializer as ts.BooleanLiteral).token === ts.SyntaxKind.TrueKeyword;
|
||||
}
|
||||
|
||||
if (propName === 'reliable' && this.isBooleanLiteral(prop.initializer)) {
|
||||
rpc.reliable = (prop.initializer as ts.BooleanLiteral).token === ts.SyntaxKind.TrueKeyword;
|
||||
}
|
||||
|
||||
if (propName === 'rateLimit' && ts.isNumericLiteral(prop.initializer)) {
|
||||
rpc.rateLimit = parseInt(prop.initializer.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找装饰器
|
||||
*/
|
||||
private findDecorator(node: ts.Node, decoratorName: string): ts.Decorator | null {
|
||||
if (!ts.canHaveModifiers(node) || !ts.getModifiers(node)) return null;
|
||||
|
||||
const modifiers = ts.getModifiers(node)!;
|
||||
for (const modifier of modifiers) {
|
||||
if (ts.isDecorator(modifier)) {
|
||||
const name = this.getDecoratorName(modifier.expression);
|
||||
if (name === decoratorName) {
|
||||
return modifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为布尔字面量
|
||||
*/
|
||||
private isBooleanLiteral(node: ts.Node): node is ts.BooleanLiteral {
|
||||
return node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证依赖关系
|
||||
*/
|
||||
private validateDependencies(): void {
|
||||
// 检查循环依赖等问题
|
||||
// 这里可以添加更复杂的依赖分析逻辑
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加错误
|
||||
*/
|
||||
private addError(
|
||||
type: ProtocolError['type'],
|
||||
message: string,
|
||||
sourceFile?: ts.SourceFile,
|
||||
node?: ts.Node
|
||||
): void {
|
||||
const error: ProtocolError = {
|
||||
type,
|
||||
message,
|
||||
file: sourceFile?.fileName,
|
||||
line: node ? ts.getLineAndCharacterOfPosition(sourceFile!, node.getStart()).line + 1 : undefined,
|
||||
column: node ? ts.getLineAndCharacterOfPosition(sourceFile!, node.getStart()).character + 1 : undefined
|
||||
};
|
||||
this.errors.push(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加警告
|
||||
*/
|
||||
private addWarning(
|
||||
type: ProtocolWarning['type'],
|
||||
message: string,
|
||||
sourceFile?: ts.SourceFile,
|
||||
node?: ts.Node
|
||||
): void {
|
||||
const warning: ProtocolWarning = {
|
||||
type,
|
||||
message,
|
||||
file: sourceFile?.fileName,
|
||||
line: node ? ts.getLineAndCharacterOfPosition(sourceFile!, node.getStart()).line + 1 : undefined,
|
||||
column: node ? ts.getLineAndCharacterOfPosition(sourceFile!, node.getStart()).character + 1 : undefined
|
||||
};
|
||||
this.warnings.push(warning);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
/**
|
||||
* 协议分析器导出
|
||||
*/
|
||||
|
||||
export * from './TypeScriptAnalyzer';
|
||||
@@ -1,576 +0,0 @@
|
||||
/**
|
||||
* 协议推导引擎
|
||||
*
|
||||
* 负责从分析结果推导出最优的序列化协议,
|
||||
* 包括类型优化、字段重排序、兼容性检查等
|
||||
*/
|
||||
|
||||
import {
|
||||
ComponentProtocol,
|
||||
ProtocolField,
|
||||
ProtocolRpc,
|
||||
SerializeType,
|
||||
ProtocolSchema,
|
||||
ProtocolError,
|
||||
ProtocolWarning
|
||||
} from '../types/ProtocolTypes';
|
||||
|
||||
/**
|
||||
* 优化选项
|
||||
*/
|
||||
export interface InferenceOptions {
|
||||
/** 是否启用字段重排序优化 */
|
||||
enableFieldReordering?: boolean;
|
||||
/** 是否启用类型提升优化 */
|
||||
enableTypePromotion?: boolean;
|
||||
/** 是否启用批量处理优化 */
|
||||
enableBatchOptimization?: boolean;
|
||||
/** 是否启用向后兼容检查 */
|
||||
enableCompatibilityCheck?: boolean;
|
||||
/** 最大字段数量限制 */
|
||||
maxFieldCount?: number;
|
||||
/** 最大 RPC 数量限制 */
|
||||
maxRpcCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 协议推导引擎
|
||||
*/
|
||||
export class ProtocolInferenceEngine {
|
||||
private options: Required<InferenceOptions>;
|
||||
private errors: ProtocolError[] = [];
|
||||
private warnings: ProtocolWarning[] = [];
|
||||
|
||||
constructor(options: InferenceOptions = {}) {
|
||||
this.options = {
|
||||
enableFieldReordering: true,
|
||||
enableTypePromotion: true,
|
||||
enableBatchOptimization: true,
|
||||
enableCompatibilityCheck: true,
|
||||
maxFieldCount: 100,
|
||||
maxRpcCount: 50,
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 推导协议模式
|
||||
*/
|
||||
public inferSchema(components: ComponentProtocol[], version: string = '1.0.0'): ProtocolSchema {
|
||||
this.errors = [];
|
||||
this.warnings = [];
|
||||
|
||||
const optimizedComponents = new Map<string, ComponentProtocol>();
|
||||
const globalTypes = new Map<string, ProtocolField[]>();
|
||||
|
||||
// 第一遍:基础优化和验证
|
||||
for (const component of components) {
|
||||
const optimized = this.optimizeComponent(component);
|
||||
if (optimized) {
|
||||
optimizedComponents.set(component.typeName, optimized);
|
||||
}
|
||||
}
|
||||
|
||||
// 第二遍:跨组件优化
|
||||
this.performCrossComponentOptimizations(optimizedComponents);
|
||||
|
||||
// 提取全局类型
|
||||
this.extractGlobalTypes(optimizedComponents, globalTypes);
|
||||
|
||||
const schema: ProtocolSchema = {
|
||||
version,
|
||||
components: optimizedComponents,
|
||||
types: globalTypes,
|
||||
compatibility: {
|
||||
minVersion: version,
|
||||
maxVersion: version
|
||||
}
|
||||
};
|
||||
|
||||
// 最终验证
|
||||
this.validateSchema(schema);
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化单个组件
|
||||
*/
|
||||
private optimizeComponent(component: ComponentProtocol): ComponentProtocol | null {
|
||||
// 验证组件
|
||||
if (!this.validateComponent(component)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const optimized: ComponentProtocol = {
|
||||
...component,
|
||||
syncVars: [...component.syncVars],
|
||||
rpcs: [...component.rpcs]
|
||||
};
|
||||
|
||||
// 优化 SyncVar 字段
|
||||
if (this.options.enableFieldReordering) {
|
||||
optimized.syncVars = this.optimizeFieldOrdering(optimized.syncVars);
|
||||
}
|
||||
|
||||
if (this.options.enableTypePromotion) {
|
||||
optimized.syncVars = this.optimizeFieldTypes(optimized.syncVars);
|
||||
}
|
||||
|
||||
// 优化 RPC 方法
|
||||
optimized.rpcs = this.optimizeRpcs(optimized.rpcs);
|
||||
|
||||
// 启用批量处理优化
|
||||
if (this.options.enableBatchOptimization) {
|
||||
this.inferBatchOptimization(optimized);
|
||||
}
|
||||
|
||||
return optimized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证组件
|
||||
*/
|
||||
private validateComponent(component: ComponentProtocol): boolean {
|
||||
let isValid = true;
|
||||
|
||||
// 检查字段数量限制
|
||||
if (component.syncVars.length > this.options.maxFieldCount) {
|
||||
this.addError('semantic', `Component ${component.typeName} has too many SyncVars (${component.syncVars.length}/${this.options.maxFieldCount})`);
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 检查 RPC 数量限制
|
||||
if (component.rpcs.length > this.options.maxRpcCount) {
|
||||
this.addError('semantic', `Component ${component.typeName} has too many RPCs (${component.rpcs.length}/${this.options.maxRpcCount})`);
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 检查字段名冲突
|
||||
const fieldNames = new Set<string>();
|
||||
for (const field of component.syncVars) {
|
||||
if (fieldNames.has(field.name)) {
|
||||
this.addError('semantic', `Duplicate SyncVar name: ${field.name} in ${component.typeName}`);
|
||||
isValid = false;
|
||||
}
|
||||
fieldNames.add(field.name);
|
||||
}
|
||||
|
||||
// 检查 RPC 名冲突
|
||||
const rpcNames = new Set<string>();
|
||||
for (const rpc of component.rpcs) {
|
||||
if (rpcNames.has(rpc.name)) {
|
||||
this.addError('semantic', `Duplicate RPC name: ${rpc.name} in ${component.typeName}`);
|
||||
isValid = false;
|
||||
}
|
||||
rpcNames.add(rpc.name);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化字段顺序
|
||||
* 将频繁变化的字段和固定大小的字段排在前面,以提高序列化效率
|
||||
*/
|
||||
private optimizeFieldOrdering(fields: ProtocolField[]): ProtocolField[] {
|
||||
const optimized = [...fields];
|
||||
|
||||
// 按照优化策略排序
|
||||
optimized.sort((a, b) => {
|
||||
// 优先级高的在前
|
||||
const priorityA = this.getFieldPriority(a);
|
||||
const priorityB = this.getFieldPriority(b);
|
||||
|
||||
if (priorityA !== priorityB) {
|
||||
return priorityB - priorityA; // 优先级高的在前
|
||||
}
|
||||
|
||||
// 固定大小类型在前
|
||||
const fixedA = this.isFixedSizeType(a.type) ? 1 : 0;
|
||||
const fixedB = this.isFixedSizeType(b.type) ? 1 : 0;
|
||||
|
||||
if (fixedA !== fixedB) {
|
||||
return fixedB - fixedA;
|
||||
}
|
||||
|
||||
// 按类型大小排序,小的在前
|
||||
const sizeA = this.getTypeSize(a.type);
|
||||
const sizeB = this.getTypeSize(b.type);
|
||||
|
||||
return sizeA - sizeB;
|
||||
});
|
||||
|
||||
// 重新分配字段 ID(保持顺序)
|
||||
optimized.forEach((field, index) => {
|
||||
field.id = index + 1;
|
||||
});
|
||||
|
||||
return optimized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化字段类型
|
||||
* 将通用类型提升为更高效的序列化类型
|
||||
*/
|
||||
private optimizeFieldTypes(fields: ProtocolField[]): ProtocolField[] {
|
||||
return fields.map(field => {
|
||||
const optimized = { ...field };
|
||||
|
||||
// 类型提升规则
|
||||
switch (field.type) {
|
||||
case SerializeType.FLOAT64:
|
||||
// 检查是否可以使用 float32
|
||||
if (this.canUseFloat32(field)) {
|
||||
optimized.type = SerializeType.FLOAT32;
|
||||
this.addWarning('performance', `Promoted field ${field.name} from float64 to float32`);
|
||||
}
|
||||
break;
|
||||
|
||||
case SerializeType.INT64:
|
||||
// 检查是否可以使用 int32
|
||||
if (this.canUseInt32(field)) {
|
||||
optimized.type = SerializeType.INT32;
|
||||
this.addWarning('performance', `Promoted field ${field.name} from int64 to int32`);
|
||||
}
|
||||
break;
|
||||
|
||||
case SerializeType.JSON:
|
||||
// 检查是否可以使用更高效的类型
|
||||
const betterType = this.inferBetterType(field);
|
||||
if (betterType && betterType !== SerializeType.JSON) {
|
||||
optimized.type = betterType;
|
||||
this.addWarning('performance', `Promoted field ${field.name} from JSON to ${betterType}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return optimized;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化 RPC 方法
|
||||
*/
|
||||
private optimizeRpcs(rpcs: ProtocolRpc[]): ProtocolRpc[] {
|
||||
return rpcs.map(rpc => {
|
||||
const optimized = { ...rpc };
|
||||
|
||||
// 优化参数类型
|
||||
optimized.parameters = rpc.parameters.map(param => ({
|
||||
...param,
|
||||
type: this.optimizeParameterType(param.type)
|
||||
}));
|
||||
|
||||
// 设置默认选项
|
||||
if (optimized.reliable === undefined) {
|
||||
optimized.reliable = rpc.type === 'server-rpc'; // 服务端 RPC 默认可靠
|
||||
}
|
||||
|
||||
return optimized;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 推导批量处理优化
|
||||
*/
|
||||
private inferBatchOptimization(component: ComponentProtocol): void {
|
||||
// 检查是否适合批量处理
|
||||
const hasManyInstances = this.estimateInstanceCount(component) > 10;
|
||||
const hasSimpleTypes = component.syncVars.every(field =>
|
||||
this.isSimpleType(field.type) && !field.repeated
|
||||
);
|
||||
|
||||
if (hasManyInstances && hasSimpleTypes) {
|
||||
component.batchEnabled = true;
|
||||
this.addWarning('performance', `Enabled batch optimization for ${component.typeName}`);
|
||||
}
|
||||
|
||||
// 检查是否适合增量同步
|
||||
const hasLargeData = component.syncVars.some(field =>
|
||||
this.isLargeDataType(field.type) || field.repeated
|
||||
);
|
||||
|
||||
if (hasLargeData) {
|
||||
component.deltaEnabled = true;
|
||||
this.addWarning('performance', `Enabled delta synchronization for ${component.typeName}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跨组件优化
|
||||
*/
|
||||
private performCrossComponentOptimizations(components: Map<string, ComponentProtocol>): void {
|
||||
// 检查重复字段模式,提取为全局类型
|
||||
const fieldPatterns = this.findCommonFieldPatterns(Array.from(components.values()));
|
||||
|
||||
for (const [pattern, count] of fieldPatterns) {
|
||||
if (count >= 3) { // 如果有3个或更多组件使用相同模式
|
||||
this.addWarning('style', `Common field pattern found: ${pattern} (used ${count} times). Consider extracting to a shared type.`);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 ID 冲突
|
||||
this.validateIdUniqueness(Array.from(components.values()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取全局类型
|
||||
*/
|
||||
private extractGlobalTypes(
|
||||
components: Map<string, ComponentProtocol>,
|
||||
globalTypes: Map<string, ProtocolField[]>
|
||||
): void {
|
||||
// 预定义常用游戏类型
|
||||
globalTypes.set('Vector2', [
|
||||
{ name: 'x', type: SerializeType.FLOAT32, id: 1 },
|
||||
{ name: 'y', type: SerializeType.FLOAT32, id: 2 }
|
||||
]);
|
||||
|
||||
globalTypes.set('Vector3', [
|
||||
{ name: 'x', type: SerializeType.FLOAT32, id: 1 },
|
||||
{ name: 'y', type: SerializeType.FLOAT32, id: 2 },
|
||||
{ name: 'z', type: SerializeType.FLOAT32, id: 3 }
|
||||
]);
|
||||
|
||||
globalTypes.set('Quaternion', [
|
||||
{ name: 'x', type: SerializeType.FLOAT32, id: 1 },
|
||||
{ name: 'y', type: SerializeType.FLOAT32, id: 2 },
|
||||
{ name: 'z', type: SerializeType.FLOAT32, id: 3 },
|
||||
{ name: 'w', type: SerializeType.FLOAT32, id: 4 }
|
||||
]);
|
||||
|
||||
globalTypes.set('Color', [
|
||||
{ name: 'r', type: SerializeType.FLOAT32, id: 1 },
|
||||
{ name: 'g', type: SerializeType.FLOAT32, id: 2 },
|
||||
{ name: 'b', type: SerializeType.FLOAT32, id: 3 },
|
||||
{ name: 'a', type: SerializeType.FLOAT32, id: 4, optional: true, defaultValue: 1.0 }
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证协议模式
|
||||
*/
|
||||
private validateSchema(schema: ProtocolSchema): void {
|
||||
// 检查版本兼容性
|
||||
if (this.options.enableCompatibilityCheck) {
|
||||
this.validateCompatibility(schema);
|
||||
}
|
||||
|
||||
// 检查全局一致性
|
||||
this.validateGlobalConsistency(schema);
|
||||
}
|
||||
|
||||
// 辅助方法
|
||||
|
||||
private getFieldPriority(field: ProtocolField): number {
|
||||
// 根据字段名推断优先级
|
||||
const highPriorityNames = ['position', 'rotation', 'health', 'transform'];
|
||||
const mediumPriorityNames = ['velocity', 'speed', 'direction'];
|
||||
|
||||
const fieldName = field.name.toLowerCase();
|
||||
|
||||
if (highPriorityNames.some(name => fieldName.includes(name))) {
|
||||
return 10;
|
||||
}
|
||||
if (mediumPriorityNames.some(name => fieldName.includes(name))) {
|
||||
return 5;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private isFixedSizeType(type: SerializeType): boolean {
|
||||
const fixedTypes = [
|
||||
SerializeType.BOOLEAN,
|
||||
SerializeType.INT8, SerializeType.UINT8,
|
||||
SerializeType.INT16, SerializeType.UINT16,
|
||||
SerializeType.INT32, SerializeType.UINT32,
|
||||
SerializeType.INT64, SerializeType.UINT64,
|
||||
SerializeType.FLOAT32, SerializeType.FLOAT64,
|
||||
SerializeType.VECTOR2, SerializeType.VECTOR3,
|
||||
SerializeType.QUATERNION, SerializeType.COLOR
|
||||
];
|
||||
return fixedTypes.includes(type);
|
||||
}
|
||||
|
||||
private getTypeSize(type: SerializeType): number {
|
||||
const sizes = {
|
||||
[SerializeType.BOOLEAN]: 1,
|
||||
[SerializeType.INT8]: 1,
|
||||
[SerializeType.UINT8]: 1,
|
||||
[SerializeType.INT16]: 2,
|
||||
[SerializeType.UINT16]: 2,
|
||||
[SerializeType.INT32]: 4,
|
||||
[SerializeType.UINT32]: 4,
|
||||
[SerializeType.INT64]: 8,
|
||||
[SerializeType.UINT64]: 8,
|
||||
[SerializeType.FLOAT32]: 4,
|
||||
[SerializeType.FLOAT64]: 8,
|
||||
[SerializeType.VECTOR2]: 8,
|
||||
[SerializeType.VECTOR3]: 12,
|
||||
[SerializeType.QUATERNION]: 16,
|
||||
[SerializeType.COLOR]: 16,
|
||||
[SerializeType.STRING]: 100, // 估算
|
||||
[SerializeType.BYTES]: 100,
|
||||
[SerializeType.ARRAY]: 200,
|
||||
[SerializeType.MAP]: 200,
|
||||
[SerializeType.OBJECT]: 500,
|
||||
[SerializeType.JSON]: 1000
|
||||
};
|
||||
return sizes[type] || 100;
|
||||
}
|
||||
|
||||
private canUseFloat32(field: ProtocolField): boolean {
|
||||
// 简单启发式:位置、旋转等游戏相关字段通常可以使用 float32
|
||||
const float32FriendlyNames = ['position', 'rotation', 'scale', 'velocity', 'speed'];
|
||||
return float32FriendlyNames.some(name => field.name.toLowerCase().includes(name));
|
||||
}
|
||||
|
||||
private canUseInt32(field: ProtocolField): boolean {
|
||||
// 大多数游戏中的整数值都可以用 int32 表示
|
||||
const int32FriendlyNames = ['id', 'count', 'level', 'score', 'health', 'mana'];
|
||||
return int32FriendlyNames.some(name => field.name.toLowerCase().includes(name));
|
||||
}
|
||||
|
||||
private inferBetterType(field: ProtocolField): SerializeType | null {
|
||||
// 根据字段名推断更好的类型
|
||||
const fieldName = field.name.toLowerCase();
|
||||
|
||||
if (fieldName.includes('position') || fieldName.includes('vector')) {
|
||||
return SerializeType.VECTOR3;
|
||||
}
|
||||
if (fieldName.includes('rotation') || fieldName.includes('quaternion')) {
|
||||
return SerializeType.QUATERNION;
|
||||
}
|
||||
if (fieldName.includes('color')) {
|
||||
return SerializeType.COLOR;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private optimizeParameterType(type: SerializeType): SerializeType {
|
||||
// RPC 参数类型优化
|
||||
if (type === SerializeType.FLOAT64) {
|
||||
return SerializeType.FLOAT32; // RPC 通常不需要高精度
|
||||
}
|
||||
if (type === SerializeType.INT64) {
|
||||
return SerializeType.INT32;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
private estimateInstanceCount(component: ComponentProtocol): number {
|
||||
// 基于组件名称估算实例数量
|
||||
const highVolumeNames = ['transform', 'position', 'movement', 'particle'];
|
||||
const mediumVolumeNames = ['player', 'enemy', 'bullet', 'item'];
|
||||
|
||||
const typeName = component.typeName.toLowerCase();
|
||||
|
||||
if (highVolumeNames.some(name => typeName.includes(name))) {
|
||||
return 100;
|
||||
}
|
||||
if (mediumVolumeNames.some(name => typeName.includes(name))) {
|
||||
return 20;
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
private isSimpleType(type: SerializeType): boolean {
|
||||
const simpleTypes = [
|
||||
SerializeType.BOOLEAN,
|
||||
SerializeType.INT32, SerializeType.UINT32,
|
||||
SerializeType.FLOAT32,
|
||||
SerializeType.VECTOR2, SerializeType.VECTOR3,
|
||||
SerializeType.QUATERNION
|
||||
];
|
||||
return simpleTypes.includes(type);
|
||||
}
|
||||
|
||||
private isLargeDataType(type: SerializeType): boolean {
|
||||
const largeTypes = [
|
||||
SerializeType.STRING,
|
||||
SerializeType.BYTES,
|
||||
SerializeType.ARRAY,
|
||||
SerializeType.MAP,
|
||||
SerializeType.OBJECT,
|
||||
SerializeType.JSON
|
||||
];
|
||||
return largeTypes.includes(type);
|
||||
}
|
||||
|
||||
private findCommonFieldPatterns(components: ComponentProtocol[]): Map<string, number> {
|
||||
const patterns = new Map<string, number>();
|
||||
|
||||
for (const component of components) {
|
||||
const pattern = component.syncVars
|
||||
.map(field => `${field.name}:${field.type}`)
|
||||
.sort()
|
||||
.join(',');
|
||||
|
||||
patterns.set(pattern, (patterns.get(pattern) || 0) + 1);
|
||||
}
|
||||
|
||||
return patterns;
|
||||
}
|
||||
|
||||
private validateIdUniqueness(components: ComponentProtocol[]): void {
|
||||
const fieldIds = new Map<number, string>();
|
||||
const rpcIds = new Map<number, string>();
|
||||
|
||||
for (const component of components) {
|
||||
// 检查字段 ID 冲突
|
||||
for (const field of component.syncVars) {
|
||||
const existing = fieldIds.get(field.id);
|
||||
if (existing && existing !== `${component.typeName}.${field.name}`) {
|
||||
this.addError('semantic', `Field ID conflict: ${field.id} used by both ${existing} and ${component.typeName}.${field.name}`);
|
||||
}
|
||||
fieldIds.set(field.id, `${component.typeName}.${field.name}`);
|
||||
}
|
||||
|
||||
// 检查 RPC ID 冲突
|
||||
for (const rpc of component.rpcs) {
|
||||
const existing = rpcIds.get(rpc.id);
|
||||
if (existing && existing !== `${component.typeName}.${rpc.name}`) {
|
||||
this.addError('semantic', `RPC ID conflict: ${rpc.id} used by both ${existing} and ${component.typeName}.${rpc.name}`);
|
||||
}
|
||||
rpcIds.set(rpc.id, `${component.typeName}.${rpc.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validateCompatibility(schema: ProtocolSchema): void {
|
||||
// 这里可以添加向后兼容性检查逻辑
|
||||
// 比如检查字段删除、类型变更等
|
||||
}
|
||||
|
||||
private validateGlobalConsistency(schema: ProtocolSchema): void {
|
||||
// 检查全局类型的一致性使用
|
||||
for (const [typeName, fields] of schema.types) {
|
||||
const usageCount = Array.from(schema.components.values())
|
||||
.flatMap(comp => comp.syncVars)
|
||||
.filter(field => field.type === typeName as SerializeType)
|
||||
.length;
|
||||
|
||||
if (usageCount === 0) {
|
||||
this.addWarning('style', `Global type ${typeName} is defined but not used`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addError(type: ProtocolError['type'], message: string): void {
|
||||
this.errors.push({ type, message });
|
||||
}
|
||||
|
||||
private addWarning(type: ProtocolWarning['type'], message: string): void {
|
||||
this.warnings.push({ type, message });
|
||||
}
|
||||
|
||||
public getErrors(): ProtocolError[] {
|
||||
return [...this.errors];
|
||||
}
|
||||
|
||||
public getWarnings(): ProtocolWarning[] {
|
||||
return [...this.warnings];
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
/**
|
||||
* 协议编译器导出
|
||||
*/
|
||||
|
||||
export * from './ProtocolInferenceEngine';
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* 协议编译器模块导出
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
// 协议分析器需要开发时依赖,暂时禁用
|
||||
// export * from './analyzer';
|
||||
export * from './compiler';
|
||||
@@ -1,289 +0,0 @@
|
||||
/**
|
||||
* 网络协议编译系统类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 序列化类型枚举
|
||||
*/
|
||||
export enum SerializeType {
|
||||
// 基础类型
|
||||
BOOLEAN = 'boolean',
|
||||
INT8 = 'int8',
|
||||
UINT8 = 'uint8',
|
||||
INT16 = 'int16',
|
||||
UINT16 = 'uint16',
|
||||
INT32 = 'int32',
|
||||
UINT32 = 'uint32',
|
||||
INT64 = 'int64',
|
||||
UINT64 = 'uint64',
|
||||
FLOAT32 = 'float32',
|
||||
FLOAT64 = 'float64',
|
||||
STRING = 'string',
|
||||
BYTES = 'bytes',
|
||||
|
||||
// 常用游戏类型
|
||||
VECTOR2 = 'Vector2',
|
||||
VECTOR3 = 'Vector3',
|
||||
QUATERNION = 'Quaternion',
|
||||
COLOR = 'Color',
|
||||
|
||||
// 容器类型
|
||||
ARRAY = 'array',
|
||||
MAP = 'map',
|
||||
|
||||
// 复杂类型
|
||||
OBJECT = 'object',
|
||||
JSON = 'json'
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段定义
|
||||
*/
|
||||
export interface ProtocolField {
|
||||
/** 字段名 */
|
||||
name: string;
|
||||
/** 序列化类型 */
|
||||
type: SerializeType;
|
||||
/** 字段ID(用于向后兼容) */
|
||||
id: number;
|
||||
/** 是否可选 */
|
||||
optional?: boolean;
|
||||
/** 是否重复(数组) */
|
||||
repeated?: boolean;
|
||||
/** 元素类型(用于数组和映射) */
|
||||
elementType?: SerializeType;
|
||||
/** 键类型(用于映射) */
|
||||
keyType?: SerializeType;
|
||||
/** 值类型(用于映射) */
|
||||
valueType?: SerializeType;
|
||||
/** 默认值 */
|
||||
defaultValue?: any;
|
||||
/** 自定义序列化器 */
|
||||
customSerializer?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 参数定义
|
||||
*/
|
||||
export interface RpcParameter {
|
||||
/** 参数名 */
|
||||
name: string;
|
||||
/** 参数类型 */
|
||||
type: SerializeType;
|
||||
/** 是否可选 */
|
||||
optional?: boolean;
|
||||
/** 是否为数组 */
|
||||
isArray?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 定义
|
||||
*/
|
||||
export interface ProtocolRpc {
|
||||
/** 方法名 */
|
||||
name: string;
|
||||
/** RPC ID */
|
||||
id: number;
|
||||
/** RPC 类型 */
|
||||
type: 'client-rpc' | 'server-rpc';
|
||||
/** 参数列表 */
|
||||
parameters: RpcParameter[];
|
||||
/** 返回类型 */
|
||||
returnType?: SerializeType;
|
||||
/** 是否需要权限 */
|
||||
requiresAuth?: boolean;
|
||||
/** 是否可靠传输 */
|
||||
reliable?: boolean;
|
||||
/** 频率限制 */
|
||||
rateLimit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络组件协议定义
|
||||
*/
|
||||
export interface ComponentProtocol {
|
||||
/** 组件类型名 */
|
||||
typeName: string;
|
||||
/** 协议版本 */
|
||||
version: number;
|
||||
/** SyncVar 字段 */
|
||||
syncVars: ProtocolField[];
|
||||
/** RPC 方法 */
|
||||
rpcs: ProtocolRpc[];
|
||||
/** 是否启用批量处理 */
|
||||
batchEnabled?: boolean;
|
||||
/** 是否启用增量同步 */
|
||||
deltaEnabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 协议模式定义
|
||||
*/
|
||||
export interface ProtocolSchema {
|
||||
/** 模式版本 */
|
||||
version: string;
|
||||
/** 组件协议映射 */
|
||||
components: Map<string, ComponentProtocol>;
|
||||
/** 全局类型定义 */
|
||||
types: Map<string, ProtocolField[]>;
|
||||
/** 协议兼容性信息 */
|
||||
compatibility: {
|
||||
minVersion: string;
|
||||
maxVersion: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化器接口
|
||||
*/
|
||||
export interface IProtocolSerializer {
|
||||
/** 序列化单个对象 */
|
||||
serialize(obj: any, type: SerializeType): Uint8Array;
|
||||
/** 反序列化单个对象 */
|
||||
deserialize(data: Uint8Array, type: SerializeType): any;
|
||||
/** 批量序列化 */
|
||||
serializeBatch(objects: any[], type: SerializeType): Uint8Array;
|
||||
/** 批量反序列化 */
|
||||
deserializeBatch(data: Uint8Array, type: SerializeType): any[];
|
||||
/** 增量序列化 */
|
||||
serializeDelta(oldObj: any, newObj: any, type: SerializeType): Uint8Array | null;
|
||||
/** 应用增量 */
|
||||
applyDelta(baseObj: any, delta: Uint8Array, type: SerializeType): any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 协议编译器配置
|
||||
*/
|
||||
export interface ProtocolCompilerConfig {
|
||||
/** 输入目录 */
|
||||
inputDir: string;
|
||||
/** 输出目录 */
|
||||
outputDir: string;
|
||||
/** TypeScript 配置文件路径 */
|
||||
tsconfigPath?: string;
|
||||
/** 是否启用优化 */
|
||||
optimize?: boolean;
|
||||
/** 是否生成调试信息 */
|
||||
debug?: boolean;
|
||||
/** 自定义类型映射 */
|
||||
typeMapping?: Map<string, SerializeType>;
|
||||
/** 排除的文件模式 */
|
||||
excludePatterns?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 协议分析结果
|
||||
*/
|
||||
export interface ProtocolAnalysisResult {
|
||||
/** 分析的文件列表 */
|
||||
files: string[];
|
||||
/** 发现的网络组件 */
|
||||
components: ComponentProtocol[];
|
||||
/** 类型依赖图 */
|
||||
dependencies: Map<string, string[]>;
|
||||
/** 分析错误 */
|
||||
errors: ProtocolError[];
|
||||
/** 分析警告 */
|
||||
warnings: ProtocolWarning[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 协议错误
|
||||
*/
|
||||
export interface ProtocolError {
|
||||
/** 错误类型 */
|
||||
type: 'syntax' | 'type' | 'semantic' | 'compatibility';
|
||||
/** 错误消息 */
|
||||
message: string;
|
||||
/** 文件路径 */
|
||||
file?: string;
|
||||
/** 行号 */
|
||||
line?: number;
|
||||
/** 列号 */
|
||||
column?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 协议警告
|
||||
*/
|
||||
export interface ProtocolWarning {
|
||||
/** 警告类型 */
|
||||
type: 'performance' | 'compatibility' | 'style';
|
||||
/** 警告消息 */
|
||||
message: string;
|
||||
/** 文件路径 */
|
||||
file?: string;
|
||||
/** 行号 */
|
||||
line?: number;
|
||||
/** 列号 */
|
||||
column?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 代码生成选项
|
||||
*/
|
||||
export interface CodeGenerationOptions {
|
||||
/** 目标平台 */
|
||||
platform: 'node' | 'browser' | 'universal';
|
||||
/** 代码风格 */
|
||||
style: 'typescript' | 'javascript';
|
||||
/** 是否生成类型定义 */
|
||||
generateTypes?: boolean;
|
||||
/** 是否生成文档 */
|
||||
generateDocs?: boolean;
|
||||
/** 模块格式 */
|
||||
moduleFormat?: 'es' | 'cjs' | 'umd';
|
||||
/** 压缩级别 */
|
||||
minification?: 'none' | 'basic' | 'aggressive';
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时协议信息
|
||||
*/
|
||||
export interface RuntimeProtocolInfo {
|
||||
/** 协议版本 */
|
||||
version: string;
|
||||
/** 组件数量 */
|
||||
componentCount: number;
|
||||
/** 总字段数 */
|
||||
fieldCount: number;
|
||||
/** 总 RPC 数 */
|
||||
rpcCount: number;
|
||||
/** 内存使用情况 */
|
||||
memoryUsage: {
|
||||
schemas: number;
|
||||
serializers: number;
|
||||
cache: number;
|
||||
};
|
||||
/** 性能统计 */
|
||||
performance: {
|
||||
serializeTime: number;
|
||||
deserializeTime: number;
|
||||
cacheHits: number;
|
||||
cacheMisses: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 协议事件类型
|
||||
*/
|
||||
export type ProtocolEventType =
|
||||
| 'protocol-loaded'
|
||||
| 'protocol-updated'
|
||||
| 'serializer-registered'
|
||||
| 'compatibility-check'
|
||||
| 'performance-warning';
|
||||
|
||||
/**
|
||||
* 协议事件数据
|
||||
*/
|
||||
export interface ProtocolEventData {
|
||||
type: ProtocolEventType;
|
||||
timestamp: number;
|
||||
data: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 协议事件处理器
|
||||
*/
|
||||
export type ProtocolEventHandler = (event: ProtocolEventData) => void;
|
||||
@@ -1,5 +0,0 @@
|
||||
/**
|
||||
* 协议类型定义导出
|
||||
*/
|
||||
|
||||
export * from './ProtocolTypes';
|
||||
283
packages/network-shared/src/protocols/MessageTypes.ts
Normal file
283
packages/network-shared/src/protocols/MessageTypes.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* 网络消息协议定义
|
||||
*/
|
||||
import { MessageType, INetworkMessage, AuthorityType, SyncMode, RpcTarget } from '../types/NetworkTypes';
|
||||
|
||||
/**
|
||||
* 连接请求消息
|
||||
*/
|
||||
export interface IConnectMessage extends INetworkMessage {
|
||||
type: MessageType.CONNECT;
|
||||
data: {
|
||||
/** 客户端版本 */
|
||||
clientVersion: string;
|
||||
/** 协议版本 */
|
||||
protocolVersion: string;
|
||||
/** 认证令牌 */
|
||||
authToken?: string;
|
||||
/** 客户端信息 */
|
||||
clientInfo: {
|
||||
name: string;
|
||||
platform: string;
|
||||
version: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接响应消息
|
||||
*/
|
||||
export interface IConnectResponseMessage extends INetworkMessage {
|
||||
type: MessageType.CONNECT;
|
||||
data: {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
/** 分配的客户端ID */
|
||||
clientId?: string;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
/** 服务器信息 */
|
||||
serverInfo?: {
|
||||
name: string;
|
||||
version: string;
|
||||
maxPlayers: number;
|
||||
currentPlayers: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 心跳消息
|
||||
*/
|
||||
export interface IHeartbeatMessage extends INetworkMessage {
|
||||
type: MessageType.HEARTBEAT;
|
||||
data: {
|
||||
/** 客户端时间戳 */
|
||||
clientTime: number;
|
||||
/** 服务器时间戳(响应时包含) */
|
||||
serverTime?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步变量消息
|
||||
*/
|
||||
export interface ISyncVarMessage extends INetworkMessage {
|
||||
type: MessageType.SYNC_VAR;
|
||||
data: {
|
||||
/** 网络实体ID */
|
||||
networkId: number;
|
||||
/** 组件类型名称 */
|
||||
componentType: string;
|
||||
/** 变化的属性 */
|
||||
changes: Record<string, any>;
|
||||
/** 同步模式 */
|
||||
syncMode: SyncMode;
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量同步消息
|
||||
*/
|
||||
export interface ISyncBatchMessage extends INetworkMessage {
|
||||
type: MessageType.SYNC_BATCH;
|
||||
data: {
|
||||
/** 同步数据列表 */
|
||||
syncData: Array<{
|
||||
networkId: number;
|
||||
componentType: string;
|
||||
changes: Record<string, any>;
|
||||
syncMode: SyncMode;
|
||||
}>;
|
||||
/** 批次时间戳 */
|
||||
batchTimestamp: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC调用消息
|
||||
*/
|
||||
export interface IRpcCallMessage extends INetworkMessage {
|
||||
type: MessageType.RPC_CALL;
|
||||
data: {
|
||||
/** 网络实体ID */
|
||||
networkId: number;
|
||||
/** 组件类型名称 */
|
||||
componentType: string;
|
||||
/** 方法名 */
|
||||
methodName: string;
|
||||
/** 参数列表 */
|
||||
args: any[];
|
||||
/** 调用ID(用于响应匹配) */
|
||||
callId?: string;
|
||||
/** RPC目标 */
|
||||
target: RpcTarget;
|
||||
/** 是否需要响应 */
|
||||
expectResponse?: boolean;
|
||||
/** 超时时间 */
|
||||
timeout?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC响应消息
|
||||
*/
|
||||
export interface IRpcResponseMessage extends INetworkMessage {
|
||||
type: MessageType.RPC_RESPONSE;
|
||||
data: {
|
||||
/** 调用ID */
|
||||
callId: string;
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
/** 返回值 */
|
||||
result?: any;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体创建消息
|
||||
*/
|
||||
export interface IEntityCreateMessage extends INetworkMessage {
|
||||
type: MessageType.ENTITY_CREATE;
|
||||
data: {
|
||||
/** 网络实体ID */
|
||||
networkId: number;
|
||||
/** 实体名称 */
|
||||
entityName: string;
|
||||
/** 拥有者ID */
|
||||
ownerId: string;
|
||||
/** 权限类型 */
|
||||
authority: AuthorityType;
|
||||
/** 初始组件数据 */
|
||||
components: Array<{
|
||||
type: string;
|
||||
data: any;
|
||||
}>;
|
||||
/** 位置信息 */
|
||||
position?: { x: number; y: number; z?: number };
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体销毁消息
|
||||
*/
|
||||
export interface IEntityDestroyMessage extends INetworkMessage {
|
||||
type: MessageType.ENTITY_DESTROY;
|
||||
data: {
|
||||
/** 网络实体ID */
|
||||
networkId: number;
|
||||
/** 销毁原因 */
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入房间消息
|
||||
*/
|
||||
export interface IJoinRoomMessage extends INetworkMessage {
|
||||
type: MessageType.JOIN_ROOM;
|
||||
data: {
|
||||
/** 房间ID */
|
||||
roomId: string;
|
||||
/** 密码(如果需要) */
|
||||
password?: string;
|
||||
/** 玩家信息 */
|
||||
playerInfo?: {
|
||||
name: string;
|
||||
avatar?: string;
|
||||
customData?: Record<string, any>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 离开房间消息
|
||||
*/
|
||||
export interface ILeaveRoomMessage extends INetworkMessage {
|
||||
type: MessageType.LEAVE_ROOM;
|
||||
data: {
|
||||
/** 房间ID */
|
||||
roomId: string;
|
||||
/** 离开原因 */
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间状态消息
|
||||
*/
|
||||
export interface IRoomStateMessage extends INetworkMessage {
|
||||
type: MessageType.ROOM_STATE;
|
||||
data: {
|
||||
/** 房间ID */
|
||||
roomId: string;
|
||||
/** 房间状态 */
|
||||
state: string;
|
||||
/** 玩家列表 */
|
||||
players: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
isHost: boolean;
|
||||
customData?: Record<string, any>;
|
||||
}>;
|
||||
/** 房间设置 */
|
||||
settings?: Record<string, any>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏事件消息
|
||||
*/
|
||||
export interface IGameEventMessage extends INetworkMessage {
|
||||
type: MessageType.GAME_EVENT;
|
||||
data: {
|
||||
/** 事件类型 */
|
||||
eventType: string;
|
||||
/** 事件数据 */
|
||||
eventData: any;
|
||||
/** 目标客户端 */
|
||||
target?: RpcTarget;
|
||||
/** 事件优先级 */
|
||||
priority?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误消息
|
||||
*/
|
||||
export interface IErrorMessage extends INetworkMessage {
|
||||
type: MessageType.ERROR;
|
||||
data: {
|
||||
/** 错误代码 */
|
||||
code: string;
|
||||
/** 错误消息 */
|
||||
message: string;
|
||||
/** 错误详情 */
|
||||
details?: any;
|
||||
/** 相关的消息ID */
|
||||
relatedMessageId?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息类型联合
|
||||
*/
|
||||
export type NetworkMessage =
|
||||
| IConnectMessage
|
||||
| IConnectResponseMessage
|
||||
| IHeartbeatMessage
|
||||
| ISyncVarMessage
|
||||
| ISyncBatchMessage
|
||||
| IRpcCallMessage
|
||||
| IRpcResponseMessage
|
||||
| IEntityCreateMessage
|
||||
| IEntityDestroyMessage
|
||||
| IJoinRoomMessage
|
||||
| ILeaveRoomMessage
|
||||
| IRoomStateMessage
|
||||
| IGameEventMessage
|
||||
| IErrorMessage;
|
||||
@@ -1,355 +0,0 @@
|
||||
/**
|
||||
* 网络序列化器
|
||||
*
|
||||
* 提供高效的网络消息序列化和反序列化
|
||||
*/
|
||||
|
||||
import { INetworkSerializer, NetworkValue, SerializationSchema } from '../types/NetworkTypes';
|
||||
|
||||
/**
|
||||
* 序列化类型映射
|
||||
*/
|
||||
interface SerializationTypeMap {
|
||||
[typeName: string]: SerializationSchema<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基础网络序列化器实现
|
||||
*/
|
||||
export class NetworkSerializer implements INetworkSerializer {
|
||||
private typeMap: SerializationTypeMap = {};
|
||||
|
||||
constructor() {
|
||||
this.registerBuiltinTypes();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册内置类型
|
||||
*/
|
||||
private registerBuiltinTypes(): void {
|
||||
// 基础类型
|
||||
this.registerType<string>('string', {
|
||||
serialize: (str: string) => new TextEncoder().encode(str),
|
||||
deserialize: (data: Uint8Array) => new TextDecoder().decode(data),
|
||||
getSize: (str: string) => new TextEncoder().encode(str).length
|
||||
});
|
||||
|
||||
this.registerType<number>('number', {
|
||||
serialize: (num: number) => {
|
||||
const buffer = new ArrayBuffer(8);
|
||||
const view = new DataView(buffer);
|
||||
view.setFloat64(0, num);
|
||||
return new Uint8Array(buffer);
|
||||
},
|
||||
deserialize: (data: Uint8Array) => {
|
||||
const view = new DataView(data.buffer);
|
||||
return view.getFloat64(0);
|
||||
},
|
||||
getSize: () => 8
|
||||
});
|
||||
|
||||
this.registerType<boolean>('boolean', {
|
||||
serialize: (bool: boolean) => new Uint8Array([bool ? 1 : 0]),
|
||||
deserialize: (data: Uint8Array) => data[0] === 1,
|
||||
getSize: () => 1
|
||||
});
|
||||
|
||||
this.registerType<number>('int32', {
|
||||
serialize: (num: number) => {
|
||||
const buffer = new ArrayBuffer(4);
|
||||
const view = new DataView(buffer);
|
||||
view.setInt32(0, num);
|
||||
return new Uint8Array(buffer);
|
||||
},
|
||||
deserialize: (data: Uint8Array) => {
|
||||
const view = new DataView(data.buffer);
|
||||
return view.getInt32(0);
|
||||
},
|
||||
getSize: () => 4
|
||||
});
|
||||
|
||||
this.registerType<number>('uint32', {
|
||||
serialize: (num: number) => {
|
||||
const buffer = new ArrayBuffer(4);
|
||||
const view = new DataView(buffer);
|
||||
view.setUint32(0, num);
|
||||
return new Uint8Array(buffer);
|
||||
},
|
||||
deserialize: (data: Uint8Array) => {
|
||||
const view = new DataView(data.buffer);
|
||||
return view.getUint32(0);
|
||||
},
|
||||
getSize: () => 4
|
||||
});
|
||||
|
||||
// Vector3 类型
|
||||
this.registerType<{x: number, y: number, z?: number}>('Vector3', {
|
||||
serialize: (vec: { x: number; y: number; z?: number }) => {
|
||||
const buffer = new ArrayBuffer(12);
|
||||
const view = new DataView(buffer);
|
||||
view.setFloat32(0, vec.x);
|
||||
view.setFloat32(4, vec.y);
|
||||
view.setFloat32(8, vec.z || 0);
|
||||
return new Uint8Array(buffer);
|
||||
},
|
||||
deserialize: (data: Uint8Array) => {
|
||||
const view = new DataView(data.buffer);
|
||||
return {
|
||||
x: view.getFloat32(0),
|
||||
y: view.getFloat32(4),
|
||||
z: view.getFloat32(8)
|
||||
};
|
||||
},
|
||||
getSize: () => 12
|
||||
});
|
||||
|
||||
// JSON 类型(用于复杂对象)
|
||||
this.registerType('json', {
|
||||
serialize: (obj: any) => {
|
||||
const jsonStr = JSON.stringify(obj);
|
||||
return new TextEncoder().encode(jsonStr);
|
||||
},
|
||||
deserialize: (data: Uint8Array) => {
|
||||
const jsonStr = new TextDecoder().decode(data);
|
||||
return JSON.parse(jsonStr);
|
||||
},
|
||||
getSize: (obj: any) => {
|
||||
const jsonStr = JSON.stringify(obj);
|
||||
return new TextEncoder().encode(jsonStr).length;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册序列化类型
|
||||
*/
|
||||
public registerType<T = NetworkValue>(typeName: string, typeSchema: SerializationSchema<T>): void {
|
||||
if (typeof typeSchema.serialize !== 'function' ||
|
||||
typeof typeSchema.deserialize !== 'function') {
|
||||
throw new Error(`Invalid type schema for ${typeName}: must have serialize and deserialize methods`);
|
||||
}
|
||||
|
||||
this.typeMap[typeName] = {
|
||||
serialize: typeSchema.serialize as any,
|
||||
deserialize: typeSchema.deserialize as any,
|
||||
getSize: typeSchema.getSize as any || ((obj: any) => this.serialize(obj, typeName).length)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化对象
|
||||
*/
|
||||
public serialize(obj: any, type?: string): Uint8Array {
|
||||
if (type && this.typeMap[type]) {
|
||||
return this.typeMap[type].serialize(obj);
|
||||
}
|
||||
|
||||
// 自动类型检测
|
||||
const detectedType = this.detectType(obj);
|
||||
if (this.typeMap[detectedType]) {
|
||||
return this.typeMap[detectedType].serialize(obj);
|
||||
}
|
||||
|
||||
// 默认使用 JSON 序列化
|
||||
const jsonHandler = this.typeMap['json'];
|
||||
if (jsonHandler?.serialize) {
|
||||
return jsonHandler.serialize(obj);
|
||||
}
|
||||
|
||||
// 最终回退方案
|
||||
return new TextEncoder().encode(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化对象
|
||||
*/
|
||||
public deserialize<T = any>(data: Uint8Array, type?: string): T {
|
||||
if (type && this.typeMap[type]) {
|
||||
return this.typeMap[type].deserialize(data);
|
||||
}
|
||||
|
||||
// 如果没有指定类型,尝试使用 JSON 反序列化
|
||||
try {
|
||||
const jsonHandler = this.typeMap['json'];
|
||||
if (jsonHandler?.deserialize) {
|
||||
return jsonHandler.deserialize(data);
|
||||
}
|
||||
|
||||
// 最终回退方案
|
||||
const jsonString = new TextDecoder().decode(data);
|
||||
return JSON.parse(jsonString);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to deserialize data: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取序列化后的大小
|
||||
*/
|
||||
public getSerializedSize(obj: any, type?: string): number {
|
||||
if (type && this.typeMap[type]?.getSize) {
|
||||
return this.typeMap[type].getSize(obj);
|
||||
}
|
||||
|
||||
const detectedType = this.detectType(obj);
|
||||
if (this.typeMap[detectedType]?.getSize) {
|
||||
return this.typeMap[detectedType].getSize(obj);
|
||||
}
|
||||
|
||||
const jsonHandler = this.typeMap['json'];
|
||||
return jsonHandler?.getSize ? jsonHandler.getSize(obj) : JSON.stringify(obj).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动检测对象类型
|
||||
*/
|
||||
private detectType(obj: any): string {
|
||||
if (typeof obj === 'string') return 'string';
|
||||
if (typeof obj === 'number') return 'number';
|
||||
if (typeof obj === 'boolean') return 'boolean';
|
||||
|
||||
if (obj && typeof obj === 'object') {
|
||||
// 检测 Vector3 类型
|
||||
if ('x' in obj && 'y' in obj && typeof obj.x === 'number' && typeof obj.y === 'number') {
|
||||
return 'Vector3';
|
||||
}
|
||||
}
|
||||
|
||||
return 'json';
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量序列化多个值
|
||||
*/
|
||||
public serializeBatch(values: Array<{ value: any; type?: string }>): Uint8Array {
|
||||
const serializedParts: Uint8Array[] = [];
|
||||
let totalSize = 0;
|
||||
|
||||
// 序列化每个值
|
||||
for (const item of values) {
|
||||
const serialized = this.serialize(item.value, item.type);
|
||||
serializedParts.push(serialized);
|
||||
totalSize += serialized.length + 4; // +4 为长度信息
|
||||
}
|
||||
|
||||
// 创建总缓冲区
|
||||
const result = new Uint8Array(totalSize + 4); // +4 为值的数量
|
||||
const view = new DataView(result.buffer);
|
||||
let offset = 0;
|
||||
|
||||
// 写入值的数量
|
||||
view.setUint32(offset, values.length);
|
||||
offset += 4;
|
||||
|
||||
// 写入每个序列化的值
|
||||
for (const serialized of serializedParts) {
|
||||
// 写入长度
|
||||
view.setUint32(offset, serialized.length);
|
||||
offset += 4;
|
||||
|
||||
// 写入数据
|
||||
result.set(serialized, offset);
|
||||
offset += serialized.length;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量反序列化
|
||||
*/
|
||||
public deserializeBatch(data: Uint8Array, types?: string[]): any[] {
|
||||
const view = new DataView(data.buffer);
|
||||
let offset = 0;
|
||||
|
||||
// 读取值的数量
|
||||
const count = view.getUint32(offset);
|
||||
offset += 4;
|
||||
|
||||
const results: any[] = [];
|
||||
|
||||
// 读取每个值
|
||||
for (let i = 0; i < count; i++) {
|
||||
// 读取长度
|
||||
const length = view.getUint32(offset);
|
||||
offset += 4;
|
||||
|
||||
// 读取数据
|
||||
const valueData = data.slice(offset, offset + length);
|
||||
offset += length;
|
||||
|
||||
// 反序列化
|
||||
const type = types?.[i];
|
||||
const value = this.deserialize(valueData, type);
|
||||
results.push(value);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩序列化数据
|
||||
*/
|
||||
public compress(data: Uint8Array): Uint8Array {
|
||||
// 这里可以集成压缩算法,如 LZ4、gzip 等
|
||||
// 目前返回原数据
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压缩数据
|
||||
*/
|
||||
public decompress(data: Uint8Array): Uint8Array {
|
||||
// 这里可以集成解压缩算法
|
||||
// 目前返回原数据
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建增量序列化数据
|
||||
*/
|
||||
public serializeDelta(oldValue: any, newValue: any, type?: string): Uint8Array | null {
|
||||
// 基础实现:如果值相同则返回 null,否则序列化新值
|
||||
if (this.isEqual(oldValue, newValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.serialize(newValue, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用增量数据
|
||||
*/
|
||||
public applyDelta(_baseValue: any, deltaData: Uint8Array, type?: string): any {
|
||||
// 基础实现:直接反序列化增量数据
|
||||
// baseValue 在更复杂的增量实现中会被使用
|
||||
return this.deserialize(deltaData, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查两个值是否相等
|
||||
*/
|
||||
private isEqual(a: any, b: any): boolean {
|
||||
if (a === b) return true;
|
||||
|
||||
if (typeof a === 'object' && typeof b === 'object' && a !== null && b !== null) {
|
||||
return JSON.stringify(a) === JSON.stringify(b);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已注册的类型列表
|
||||
*/
|
||||
public getRegisteredTypes(): string[] {
|
||||
return Object.keys(this.typeMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查类型是否已注册
|
||||
*/
|
||||
public hasType(typeName: string): boolean {
|
||||
return typeName in this.typeMap;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
/**
|
||||
* 序列化工具导出
|
||||
*/
|
||||
|
||||
export * from './NetworkSerializer';
|
||||
@@ -1,375 +1,251 @@
|
||||
/**
|
||||
* 网络库核心类型定义
|
||||
* 网络层核心类型定义
|
||||
*/
|
||||
|
||||
// 通用类型定义
|
||||
export type NetworkValue = string | number | boolean | NetworkValue[] | { [key: string]: NetworkValue };
|
||||
export type SerializableObject = Record<string, NetworkValue>;
|
||||
export type Constructor<T = {}> = new (...args: unknown[]) => T;
|
||||
export type MethodDecorator<T = unknown> = (target: unknown, propertyKey: string | symbol, descriptor: PropertyDescriptor) => void;
|
||||
/**
|
||||
* 网络消息类型枚举
|
||||
*/
|
||||
export enum MessageType {
|
||||
// 连接管理
|
||||
CONNECT = 'connect',
|
||||
DISCONNECT = 'disconnect',
|
||||
HEARTBEAT = 'heartbeat',
|
||||
|
||||
// 装饰器目标类型 - 使用更灵活的定义
|
||||
export interface DecoratorTarget extends Record<string, unknown> {
|
||||
constructor: Constructor;
|
||||
}
|
||||
// 数据同步
|
||||
SYNC_VAR = 'sync_var',
|
||||
SYNC_BATCH = 'sync_batch',
|
||||
SYNC_SNAPSHOT = 'sync_snapshot',
|
||||
|
||||
// 网络数据类型约束
|
||||
export interface SerializedData {
|
||||
type: string;
|
||||
data: Uint8Array;
|
||||
checksum?: string;
|
||||
}
|
||||
// RPC调用
|
||||
RPC_CALL = 'rpc_call',
|
||||
RPC_RESPONSE = 'rpc_response',
|
||||
|
||||
// RPC参数类型
|
||||
export type RpcParameterType = NetworkValue;
|
||||
export type RpcReturnType = NetworkValue | void | Promise<NetworkValue | void>;
|
||||
// 实体管理
|
||||
ENTITY_CREATE = 'entity_create',
|
||||
ENTITY_DESTROY = 'entity_destroy',
|
||||
ENTITY_UPDATE = 'entity_update',
|
||||
|
||||
// 序列化模式接口 - 使用泛型支持特定类型
|
||||
export interface SerializationSchema<T = NetworkValue> {
|
||||
serialize: (obj: T) => Uint8Array;
|
||||
deserialize: (data: Uint8Array) => T;
|
||||
getSize?: (obj: T) => number;
|
||||
// 房间管理
|
||||
JOIN_ROOM = 'join_room',
|
||||
LEAVE_ROOM = 'leave_room',
|
||||
ROOM_STATE = 'room_state',
|
||||
|
||||
// 游戏事件
|
||||
GAME_EVENT = 'game_event',
|
||||
|
||||
// 系统消息
|
||||
ERROR = 'error',
|
||||
WARNING = 'warning',
|
||||
INFO = 'info'
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络端类型
|
||||
* 网络消息基础接口
|
||||
*/
|
||||
export type NetworkSide = 'client' | 'server' | 'host';
|
||||
export interface INetworkMessage {
|
||||
/** 消息类型 */
|
||||
type: MessageType;
|
||||
/** 消息唯一ID */
|
||||
messageId: string;
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
/** 发送者ID */
|
||||
senderId: string;
|
||||
/** 消息数据 */
|
||||
data: any;
|
||||
/** 是否可靠传输 */
|
||||
reliable?: boolean;
|
||||
/** 消息优先级 */
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络连接状态
|
||||
* 同步权限类型
|
||||
*/
|
||||
export type NetworkConnectionState =
|
||||
| 'disconnected'
|
||||
| 'connecting'
|
||||
| 'connected'
|
||||
| 'disconnecting'
|
||||
| 'reconnecting'
|
||||
| 'failed';
|
||||
export enum AuthorityType {
|
||||
/** 服务端权限 */
|
||||
Server = 'server',
|
||||
/** 客户端权限 */
|
||||
Client = 'client',
|
||||
/** 共享权限 */
|
||||
Shared = 'shared'
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络消息类型
|
||||
* 网络作用域
|
||||
*/
|
||||
export type NetworkMessageType =
|
||||
| 'syncvar'
|
||||
| 'client-rpc'
|
||||
| 'server-rpc'
|
||||
| 'spawn'
|
||||
| 'destroy'
|
||||
| 'ownership'
|
||||
| 'scene-change'
|
||||
| 'snapshot'
|
||||
| 'ping'
|
||||
| 'custom';
|
||||
export enum NetworkScope {
|
||||
/** 全局可见 */
|
||||
Global = 'global',
|
||||
/** 房间内可见 */
|
||||
Room = 'room',
|
||||
/** 仅拥有者可见 */
|
||||
Owner = 'owner',
|
||||
/** 附近玩家可见 */
|
||||
Nearby = 'nearby',
|
||||
/** 自定义作用域 */
|
||||
Custom = 'custom'
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络配置
|
||||
* 同步模式
|
||||
*/
|
||||
export interface NetworkConfig {
|
||||
/** 端口号 */
|
||||
port: number;
|
||||
/** 主机地址 */
|
||||
host: string;
|
||||
/** 最大连接数 */
|
||||
maxConnections: number;
|
||||
/** 同步频率 (Hz) */
|
||||
syncRate: number;
|
||||
/** 快照频率 (Hz) */
|
||||
snapshotRate: number;
|
||||
/** 是否启用压缩 */
|
||||
compression: boolean;
|
||||
/** 是否启用加密 */
|
||||
encryption: boolean;
|
||||
/** 网络超时时间 (ms) */
|
||||
timeout: number;
|
||||
/** 重连尝试次数 */
|
||||
maxReconnectAttempts: number;
|
||||
/** 重连间隔 (ms) */
|
||||
reconnectInterval: number;
|
||||
export enum SyncMode {
|
||||
/** 同步给所有客户端 */
|
||||
All = 'all',
|
||||
/** 只同步给拥有者 */
|
||||
Owner = 'owner',
|
||||
/** 同步给除拥有者外的客户端 */
|
||||
Others = 'others',
|
||||
/** 同步给附近的客户端 */
|
||||
Nearby = 'nearby',
|
||||
/** 自定义同步逻辑 */
|
||||
Custom = 'custom'
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC目标
|
||||
*/
|
||||
export enum RpcTarget {
|
||||
/** 服务端 */
|
||||
Server = 'server',
|
||||
/** 客户端 */
|
||||
Client = 'client',
|
||||
/** 所有客户端 */
|
||||
All = 'all',
|
||||
/** 除发送者外的客户端 */
|
||||
Others = 'others',
|
||||
/** 拥有者客户端 */
|
||||
Owner = 'owner',
|
||||
/** 附近的客户端 */
|
||||
Nearby = 'nearby'
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端信息
|
||||
*/
|
||||
export interface IClientInfo {
|
||||
/** 客户端ID */
|
||||
id: string;
|
||||
/** 客户端名称 */
|
||||
name: string;
|
||||
/** 加入时间 */
|
||||
joinTime: number;
|
||||
/** 是否已认证 */
|
||||
authenticated: boolean;
|
||||
/** 延迟(毫秒) */
|
||||
latency?: number;
|
||||
/** 自定义数据 */
|
||||
userData?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间信息
|
||||
*/
|
||||
export interface IRoomInfo {
|
||||
/** 房间ID */
|
||||
id: string;
|
||||
/** 房间名称 */
|
||||
name: string;
|
||||
/** 当前玩家数量 */
|
||||
playerCount: number;
|
||||
/** 最大玩家数量 */
|
||||
maxPlayers: number;
|
||||
/** 房间状态 */
|
||||
state: RoomState;
|
||||
/** 自定义数据 */
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间状态
|
||||
*/
|
||||
export enum RoomState {
|
||||
/** 等待中 */
|
||||
Waiting = 'waiting',
|
||||
/** 游戏中 */
|
||||
Playing = 'playing',
|
||||
/** 已暂停 */
|
||||
Paused = 'paused',
|
||||
/** 已结束 */
|
||||
Finished = 'finished'
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络统计信息
|
||||
*/
|
||||
export interface NetworkStats {
|
||||
/** 连接数量 */
|
||||
connectionCount: number;
|
||||
/** 已发送字节数 */
|
||||
export interface INetworkStats {
|
||||
/** 总发送字节数 */
|
||||
bytesSent: number;
|
||||
/** 已接收字节数 */
|
||||
/** 总接收字节数 */
|
||||
bytesReceived: number;
|
||||
/** 已发送消息数 */
|
||||
/** 发送消息数 */
|
||||
messagesSent: number;
|
||||
/** 已接收消息数 */
|
||||
/** 接收消息数 */
|
||||
messagesReceived: number;
|
||||
/** 平均延迟 (ms) */
|
||||
/** 平均延迟 */
|
||||
averageLatency: number;
|
||||
/** 丢包率 (%) */
|
||||
/** 丢包率 */
|
||||
packetLoss: number;
|
||||
/** 带宽使用率 (bytes/s) */
|
||||
bandwidth: number;
|
||||
/** 连接时长 */
|
||||
connectionTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络消息基类
|
||||
* 向量2D
|
||||
*/
|
||||
export interface NetworkMessage {
|
||||
/** 消息类型 */
|
||||
type: NetworkMessageType;
|
||||
/** 网络对象ID */
|
||||
networkId: number;
|
||||
/** 消息数据 */
|
||||
data: SerializableObject;
|
||||
/** 时间戳 */
|
||||
export interface IVector2 {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向量3D
|
||||
*/
|
||||
export interface IVector3 extends IVector2 {
|
||||
z: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 四元数
|
||||
*/
|
||||
export interface IQuaternion {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
w: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 变换信息
|
||||
*/
|
||||
export interface ITransform {
|
||||
position: IVector3;
|
||||
rotation: IQuaternion;
|
||||
scale: IVector3;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络错误类型
|
||||
*/
|
||||
export enum NetworkErrorType {
|
||||
CONNECTION_FAILED = 'connection_failed',
|
||||
CONNECTION_LOST = 'connection_lost',
|
||||
AUTHENTICATION_FAILED = 'authentication_failed',
|
||||
PERMISSION_DENIED = 'permission_denied',
|
||||
RATE_LIMITED = 'rate_limited',
|
||||
INVALID_MESSAGE = 'invalid_message',
|
||||
TIMEOUT = 'timeout',
|
||||
UNKNOWN = 'unknown'
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络错误信息
|
||||
*/
|
||||
export interface INetworkError {
|
||||
type: NetworkErrorType;
|
||||
message: string;
|
||||
code?: number;
|
||||
details?: any;
|
||||
timestamp: number;
|
||||
/** 消息ID */
|
||||
messageId?: string;
|
||||
/** 发送者ID */
|
||||
senderId?: number;
|
||||
/** 接收者ID (可选,用于定向发送) */
|
||||
targetId?: number;
|
||||
/** 是否可靠传输 */
|
||||
reliable?: boolean;
|
||||
/** 优先级 */
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncVar 消息
|
||||
*/
|
||||
export interface SyncVarMessage extends NetworkMessage {
|
||||
type: 'syncvar';
|
||||
/** 组件类型名 */
|
||||
componentType: string;
|
||||
/** 属性名 */
|
||||
propertyName: string;
|
||||
/** 属性值 */
|
||||
value: NetworkValue;
|
||||
/** 变化类型 */
|
||||
changeType?: 'set' | 'add' | 'remove' | 'clear';
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 消息
|
||||
*/
|
||||
export interface RpcMessage extends NetworkMessage {
|
||||
type: 'client-rpc' | 'server-rpc';
|
||||
/** 组件类型名 */
|
||||
componentType: string;
|
||||
/** 方法名 */
|
||||
methodName: string;
|
||||
/** 参数列表 */
|
||||
args: RpcParameterType[];
|
||||
/** RPC ID (用于响应) */
|
||||
rpcId?: string;
|
||||
/** 是否需要响应 */
|
||||
requiresResponse?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对象生成消息
|
||||
*/
|
||||
export interface SpawnMessage extends NetworkMessage {
|
||||
type: 'spawn';
|
||||
/** 预制体名称或ID */
|
||||
prefabName: string;
|
||||
/** 生成位置 */
|
||||
position?: { x: number; y: number; z?: number };
|
||||
/** 生成旋转 */
|
||||
rotation?: { x: number; y: number; z: number; w: number };
|
||||
/** 所有者ID */
|
||||
ownerId: number;
|
||||
/** 初始数据 */
|
||||
initData?: SerializableObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对象销毁消息
|
||||
*/
|
||||
export interface DestroyMessage extends NetworkMessage {
|
||||
type: 'destroy';
|
||||
/** 销毁原因 */
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有权转移消息
|
||||
*/
|
||||
export interface OwnershipMessage extends NetworkMessage {
|
||||
type: 'ownership';
|
||||
/** 新所有者ID */
|
||||
newOwnerId: number;
|
||||
/** 旧所有者ID */
|
||||
oldOwnerId: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快照消息
|
||||
*/
|
||||
export interface SnapshotMessage extends NetworkMessage {
|
||||
type: 'snapshot';
|
||||
/** 快照ID */
|
||||
snapshotId: number;
|
||||
/** 快照数据 */
|
||||
snapshot: SerializableObject;
|
||||
/** 包含的网络对象ID列表 */
|
||||
networkIds: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncVar 元数据
|
||||
*/
|
||||
export interface SyncVarMetadata {
|
||||
/** 属性名 */
|
||||
propertyName: string;
|
||||
/** 是否仅权威端可修改 */
|
||||
authorityOnly: boolean;
|
||||
/** 变化回调函数名 */
|
||||
onChanged?: string;
|
||||
/** 序列化类型 */
|
||||
serializeType?: string;
|
||||
/** 是否使用增量同步 */
|
||||
deltaSync?: boolean;
|
||||
/** 同步优先级 */
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 元数据
|
||||
*/
|
||||
export interface RpcMetadata {
|
||||
/** 方法名 */
|
||||
methodName: string;
|
||||
/** RPC 类型 */
|
||||
rpcType: 'client-rpc' | 'server-rpc';
|
||||
/** 是否需要权限验证 */
|
||||
requiresAuth?: boolean;
|
||||
/** 是否可靠传输 */
|
||||
reliable?: boolean;
|
||||
/** 是否需要响应 */
|
||||
requiresResponse?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络组件元数据
|
||||
*/
|
||||
export interface NetworkComponentMetadata {
|
||||
/** 组件类型名 */
|
||||
componentType: string;
|
||||
/** SyncVar 列表 */
|
||||
syncVars: SyncVarMetadata[];
|
||||
/** RPC 列表 */
|
||||
rpcs: RpcMetadata[];
|
||||
/** 是否自动生成协议 */
|
||||
autoGenerateProtocol?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络对象接口
|
||||
*/
|
||||
export interface INetworkObject {
|
||||
/** 网络ID */
|
||||
networkId: number;
|
||||
/** 所有者客户端ID */
|
||||
ownerId: number;
|
||||
/** 是否拥有权威 */
|
||||
hasAuthority: boolean;
|
||||
/** 是否为本地对象 */
|
||||
isLocal: boolean;
|
||||
/** 网络组件列表 */
|
||||
networkComponents: INetworkComponent[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络组件接口
|
||||
*/
|
||||
export interface INetworkComponent {
|
||||
/** 网络对象引用 */
|
||||
networkObject: INetworkObject | null;
|
||||
/** 网络ID */
|
||||
networkId: number;
|
||||
/** 是否拥有权威 */
|
||||
hasAuthority: boolean;
|
||||
/** 组件类型名 */
|
||||
componentType: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络传输层接口
|
||||
*/
|
||||
export interface INetworkTransport {
|
||||
/** 启动服务端 */
|
||||
startServer(config: NetworkConfig): Promise<void>;
|
||||
/** 连接到服务端 */
|
||||
connectToServer(host: string, port: number): Promise<void>;
|
||||
/** 断开连接 */
|
||||
disconnect(): Promise<void>;
|
||||
/** 发送消息 */
|
||||
sendMessage(message: NetworkMessage, targetId?: number): Promise<void>;
|
||||
/** 广播消息 */
|
||||
broadcastMessage(message: NetworkMessage, excludeIds?: number[]): Promise<void>;
|
||||
/** 设置消息处理器 */
|
||||
onMessage(handler: (message: NetworkMessage, fromId?: number) => void): void;
|
||||
/** 设置连接事件处理器 */
|
||||
onConnection(handler: (clientId: number, isConnected: boolean) => void): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化器接口
|
||||
*/
|
||||
export interface INetworkSerializer {
|
||||
/** 序列化对象 */
|
||||
serialize(obj: NetworkValue, type?: string): Uint8Array;
|
||||
/** 反序列化对象 */
|
||||
deserialize<T extends NetworkValue = NetworkValue>(data: Uint8Array, type?: string): T;
|
||||
/** 注册类型 */
|
||||
registerType<T = NetworkValue>(typeName: string, typeSchema: SerializationSchema<T>): void;
|
||||
/** 获取序列化后的大小 */
|
||||
getSerializedSize(obj: NetworkValue, type?: string): number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络事件处理器
|
||||
*/
|
||||
export interface NetworkEventHandlers {
|
||||
/** 连接成功 */
|
||||
onConnected?: () => void;
|
||||
/** 连接断开 */
|
||||
onDisconnected?: (reason?: string) => void;
|
||||
/** 客户端连接 */
|
||||
onClientConnected?: (clientId: number) => void;
|
||||
/** 客户端断开 */
|
||||
onClientDisconnected?: (clientId: number, reason?: string) => void;
|
||||
/** 网络错误 */
|
||||
onError?: (error: Error) => void;
|
||||
/** 延迟变化 */
|
||||
onLatencyUpdate?: (latency: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络调试信息
|
||||
*/
|
||||
export interface NetworkDebugInfo {
|
||||
/** 连接信息 */
|
||||
connections: {
|
||||
[clientId: number]: {
|
||||
id: number;
|
||||
address: string;
|
||||
latency: number;
|
||||
connected: boolean;
|
||||
lastSeen: number;
|
||||
};
|
||||
};
|
||||
/** 网络对象列表 */
|
||||
networkObjects: {
|
||||
[networkId: number]: {
|
||||
id: number;
|
||||
ownerId: number;
|
||||
componentTypes: string[];
|
||||
syncVarCount: number;
|
||||
rpcCount: number;
|
||||
};
|
||||
};
|
||||
/** 统计信息 */
|
||||
stats: NetworkStats;
|
||||
}
|
||||
228
packages/network-shared/src/types/TransportTypes.ts
Normal file
228
packages/network-shared/src/types/TransportTypes.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* 传输层接口定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 传输层抽象接口
|
||||
*/
|
||||
export interface ITransport {
|
||||
/**
|
||||
* 启动传输层
|
||||
* @param port 端口号
|
||||
* @param host 主机地址
|
||||
*/
|
||||
start(port: number, host?: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 停止传输层
|
||||
*/
|
||||
stop(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 发送数据到指定客户端
|
||||
* @param clientId 客户端ID
|
||||
* @param data 数据
|
||||
*/
|
||||
send(clientId: string, data: Buffer | string): void;
|
||||
|
||||
/**
|
||||
* 广播数据到所有客户端
|
||||
* @param data 数据
|
||||
* @param exclude 排除的客户端ID列表
|
||||
*/
|
||||
broadcast(data: Buffer | string, exclude?: string[]): void;
|
||||
|
||||
/**
|
||||
* 监听客户端连接事件
|
||||
* @param handler 处理函数
|
||||
*/
|
||||
onConnect(handler: (clientInfo: ITransportClientInfo) => void): void;
|
||||
|
||||
/**
|
||||
* 监听客户端断开事件
|
||||
* @param handler 处理函数
|
||||
*/
|
||||
onDisconnect(handler: (clientId: string, reason?: string) => void): void;
|
||||
|
||||
/**
|
||||
* 监听消息接收事件
|
||||
* @param handler 处理函数
|
||||
*/
|
||||
onMessage(handler: (clientId: string, data: Buffer | string) => void): void;
|
||||
|
||||
/**
|
||||
* 监听错误事件
|
||||
* @param handler 处理函数
|
||||
*/
|
||||
onError(handler: (error: Error) => void): void;
|
||||
|
||||
/**
|
||||
* 获取连接的客户端数量
|
||||
*/
|
||||
getClientCount(): number;
|
||||
|
||||
/**
|
||||
* 检查客户端是否连接
|
||||
* @param clientId 客户端ID
|
||||
*/
|
||||
isClientConnected(clientId: string): boolean;
|
||||
|
||||
/**
|
||||
* 断开指定客户端
|
||||
* @param clientId 客户端ID
|
||||
* @param reason 断开原因
|
||||
*/
|
||||
disconnectClient(clientId: string, reason?: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端传输层接口
|
||||
*/
|
||||
export interface IClientTransport {
|
||||
/**
|
||||
* 连接到服务器
|
||||
* @param url 服务器URL
|
||||
* @param options 连接选项
|
||||
*/
|
||||
connect(url: string, options?: IConnectionOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
* @param reason 断开原因
|
||||
*/
|
||||
disconnect(reason?: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 发送数据到服务器
|
||||
* @param data 数据
|
||||
*/
|
||||
send(data: Buffer | string): void;
|
||||
|
||||
/**
|
||||
* 监听服务器消息
|
||||
* @param handler 处理函数
|
||||
*/
|
||||
onMessage(handler: (data: Buffer | string) => void): void;
|
||||
|
||||
/**
|
||||
* 监听连接状态变化
|
||||
* @param handler 处理函数
|
||||
*/
|
||||
onConnectionStateChange(handler: (state: ConnectionState) => void): void;
|
||||
|
||||
/**
|
||||
* 监听错误事件
|
||||
* @param handler 处理函数
|
||||
*/
|
||||
onError(handler: (error: Error) => void): void;
|
||||
|
||||
/**
|
||||
* 获取连接状态
|
||||
*/
|
||||
getConnectionState(): ConnectionState;
|
||||
|
||||
/**
|
||||
* 获取连接统计信息
|
||||
*/
|
||||
getStats(): IConnectionStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 传输层客户端信息
|
||||
*/
|
||||
export interface ITransportClientInfo {
|
||||
/** 客户端ID */
|
||||
id: string;
|
||||
/** 远程地址 */
|
||||
remoteAddress: string;
|
||||
/** 连接时间 */
|
||||
connectTime: number;
|
||||
/** 用户代理 */
|
||||
userAgent?: string;
|
||||
/** 自定义头信息 */
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接选项
|
||||
*/
|
||||
export interface IConnectionOptions {
|
||||
/** 连接超时时间(毫秒) */
|
||||
timeout?: number;
|
||||
/** 重连间隔(毫秒) */
|
||||
reconnectInterval?: number;
|
||||
/** 最大重连次数 */
|
||||
maxReconnectAttempts?: number;
|
||||
/** 是否自动重连 */
|
||||
autoReconnect?: boolean;
|
||||
/** 自定义头信息 */
|
||||
headers?: Record<string, string>;
|
||||
/** 协议版本 */
|
||||
protocolVersion?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接状态
|
||||
*/
|
||||
export enum ConnectionState {
|
||||
/** 断开连接 */
|
||||
Disconnected = 'disconnected',
|
||||
/** 连接中 */
|
||||
Connecting = 'connecting',
|
||||
/** 已连接 */
|
||||
Connected = 'connected',
|
||||
/** 重连中 */
|
||||
Reconnecting = 'reconnecting',
|
||||
/** 连接失败 */
|
||||
Failed = 'failed'
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接统计信息
|
||||
*/
|
||||
export interface IConnectionStats {
|
||||
/** 连接状态 */
|
||||
state: ConnectionState;
|
||||
/** 连接时间 */
|
||||
connectTime?: number;
|
||||
/** 断开时间 */
|
||||
disconnectTime?: number;
|
||||
/** 重连次数 */
|
||||
reconnectCount: number;
|
||||
/** 发送字节数 */
|
||||
bytesSent: number;
|
||||
/** 接收字节数 */
|
||||
bytesReceived: number;
|
||||
/** 发送消息数 */
|
||||
messagesSent: number;
|
||||
/** 接收消息数 */
|
||||
messagesReceived: number;
|
||||
/** 延迟(毫秒) */
|
||||
latency?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 传输层配置
|
||||
*/
|
||||
export interface ITransportConfig {
|
||||
/** 端口号 */
|
||||
port: number;
|
||||
/** 主机地址 */
|
||||
host?: string;
|
||||
/** 最大连接数 */
|
||||
maxConnections?: number;
|
||||
/** 心跳间隔(毫秒) */
|
||||
heartbeatInterval?: number;
|
||||
/** 连接超时时间(毫秒) */
|
||||
connectionTimeout?: number;
|
||||
/** 消息最大大小(字节) */
|
||||
maxMessageSize?: number;
|
||||
/** 是否启用压缩 */
|
||||
compression?: boolean;
|
||||
/** SSL配置 */
|
||||
ssl?: {
|
||||
enabled: boolean;
|
||||
cert?: string;
|
||||
key?: string;
|
||||
};
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* 类型定义导出
|
||||
*/
|
||||
export * from './NetworkTypes';
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* reflect-metadata 类型扩展
|
||||
*/
|
||||
|
||||
/// <reference types="reflect-metadata" />
|
||||
|
||||
declare namespace Reflect {
|
||||
function defineMetadata(metadataKey: any, metadataValue: any, target: any, propertyKey?: string | symbol): void;
|
||||
function getMetadata(metadataKey: any, target: any, propertyKey?: string | symbol): any;
|
||||
function getOwnMetadata(metadataKey: any, target: any, propertyKey?: string | symbol): any;
|
||||
function hasMetadata(metadataKey: any, target: any, propertyKey?: string | symbol): boolean;
|
||||
function hasOwnMetadata(metadataKey: any, target: any, propertyKey?: string | symbol): boolean;
|
||||
function deleteMetadata(metadataKey: any, target: any, propertyKey?: string | symbol): boolean;
|
||||
function getMetadataKeys(target: any, propertyKey?: string | symbol): any[];
|
||||
function getOwnMetadataKeys(target: any, propertyKey?: string | symbol): any[];
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
/**
|
||||
* 网络工具函数
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* 生成网络ID
|
||||
*/
|
||||
export function generateNetworkId(): number {
|
||||
return Math.floor(Math.random() * 0x7FFFFFFF) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成消息ID
|
||||
*/
|
||||
export function generateMessageId(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两点之间的距离
|
||||
*/
|
||||
export function calculateDistance(
|
||||
pos1: { x: number; y: number; z?: number },
|
||||
pos2: { x: number; y: number; z?: number }
|
||||
): number {
|
||||
const dx = pos1.x - pos2.x;
|
||||
const dy = pos1.y - pos2.y;
|
||||
const dz = (pos1.z || 0) - (pos2.z || 0);
|
||||
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查环境是否为 Node.js
|
||||
*/
|
||||
export function isNodeEnvironment(): boolean {
|
||||
return typeof process !== 'undefined' && process.versions && !!process.versions.node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查环境是否为浏览器
|
||||
*/
|
||||
export function isBrowserEnvironment(): boolean {
|
||||
return typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取时间戳(毫秒)
|
||||
*/
|
||||
export function getTimestamp(): number {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取高精度时间戳(如果可用)
|
||||
*/
|
||||
export function getHighResTimestamp(): number {
|
||||
if (typeof performance !== 'undefined' && performance.now) {
|
||||
return performance.now();
|
||||
}
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 限制调用频率
|
||||
*/
|
||||
export function throttle<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
limit: number
|
||||
): T {
|
||||
let inThrottle: boolean;
|
||||
let context: any;
|
||||
|
||||
return (function(this: any, ...args: any[]) {
|
||||
context = this;
|
||||
if (!inThrottle) {
|
||||
func.apply(context, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => (inThrottle = false), limit);
|
||||
}
|
||||
}) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
*/
|
||||
export function debounce<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
delay: number
|
||||
): T {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
let context: any;
|
||||
|
||||
return (function(this: any, ...args: any[]) {
|
||||
context = this;
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => func.apply(context, args), delay);
|
||||
}) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 深拷贝对象
|
||||
*/
|
||||
export function deepClone<T>(obj: T): T {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (obj instanceof Date) {
|
||||
return new Date(obj.getTime()) as T;
|
||||
}
|
||||
|
||||
if (obj instanceof Array) {
|
||||
return obj.map(item => deepClone(item)) as T;
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
const cloned = {} as T;
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
cloned[key] = deepClone(obj[key]);
|
||||
}
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查对象是否为空
|
||||
*/
|
||||
export function isEmpty(obj: any): boolean {
|
||||
if (obj === null || obj === undefined) return true;
|
||||
if (typeof obj === 'string' || Array.isArray(obj)) return obj.length === 0;
|
||||
if (typeof obj === 'object') return Object.keys(obj).length === 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化字节大小
|
||||
*/
|
||||
export function formatBytes(bytes: number, decimals = 2): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化延迟时间
|
||||
*/
|
||||
export function formatLatency(milliseconds: number): string {
|
||||
if (milliseconds < 1000) {
|
||||
return `${Math.round(milliseconds)}ms`;
|
||||
} else {
|
||||
return `${(milliseconds / 1000).toFixed(1)}s`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网络质量描述
|
||||
*/
|
||||
export function getNetworkQuality(latency: number, packetLoss: number): string {
|
||||
if (latency < 50 && packetLoss < 1) return 'Excellent';
|
||||
if (latency < 100 && packetLoss < 2) return 'Good';
|
||||
if (latency < 200 && packetLoss < 5) return 'Fair';
|
||||
if (latency < 500 && packetLoss < 10) return 'Poor';
|
||||
return 'Very Poor';
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算网络统计平均值
|
||||
*/
|
||||
export function calculateNetworkAverage(values: number[], maxSamples = 100): number {
|
||||
if (values.length === 0) return 0;
|
||||
|
||||
// 保留最近的样本
|
||||
const samples = values.slice(-maxSamples);
|
||||
const sum = samples.reduce((acc, val) => acc + val, 0);
|
||||
return sum / samples.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证网络配置
|
||||
*/
|
||||
export function validateNetworkConfig(config: any): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (typeof config.port !== 'number' || config.port <= 0 || config.port > 65535) {
|
||||
errors.push('Port must be a number between 1 and 65535');
|
||||
}
|
||||
|
||||
if (typeof config.host !== 'string' || config.host.length === 0) {
|
||||
errors.push('Host must be a non-empty string');
|
||||
}
|
||||
|
||||
if (typeof config.maxConnections !== 'number' || config.maxConnections <= 0) {
|
||||
errors.push('Max connections must be a positive number');
|
||||
}
|
||||
|
||||
if (typeof config.syncRate !== 'number' || config.syncRate <= 0) {
|
||||
errors.push('Sync rate must be a positive number');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试函数
|
||||
*/
|
||||
export async function retry<T>(
|
||||
fn: () => Promise<T>,
|
||||
maxAttempts: number,
|
||||
delay: number = 1000
|
||||
): Promise<T> {
|
||||
let lastError: Error;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
if (attempt === maxAttempts) {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay * attempt));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
/**
|
||||
* 工具函数导出
|
||||
*/
|
||||
|
||||
export * from './NetworkUtils';
|
||||
148
packages/network-shared/tests/NetworkIdentity.test.ts
Normal file
148
packages/network-shared/tests/NetworkIdentity.test.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* NetworkIdentity组件测试
|
||||
*/
|
||||
import { NetworkIdentity } from '../src/components/NetworkIdentity';
|
||||
import { AuthorityType, NetworkScope } from '../src/types/NetworkTypes';
|
||||
|
||||
describe('NetworkIdentity', () => {
|
||||
let networkIdentity: NetworkIdentity;
|
||||
|
||||
beforeEach(() => {
|
||||
networkIdentity = new NetworkIdentity();
|
||||
});
|
||||
|
||||
describe('基础属性', () => {
|
||||
test('应该有默认的网络ID', () => {
|
||||
expect(networkIdentity.networkId).toBe(0);
|
||||
});
|
||||
|
||||
test('应该有默认的权限类型', () => {
|
||||
expect(networkIdentity.authority).toBe(AuthorityType.Server);
|
||||
});
|
||||
|
||||
test('应该有默认的网络作用域', () => {
|
||||
expect(networkIdentity.scope).toBe(NetworkScope.Room);
|
||||
});
|
||||
|
||||
test('应该有默认的同步频率', () => {
|
||||
expect(networkIdentity.syncRate).toBe(20);
|
||||
});
|
||||
|
||||
test('应该默认启用同步', () => {
|
||||
expect(networkIdentity.syncEnabled).toBe(true);
|
||||
});
|
||||
|
||||
test('应该默认可见', () => {
|
||||
expect(networkIdentity.visible).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('权限检查', () => {
|
||||
test('服务端权限下客户端无权限', () => {
|
||||
networkIdentity.authority = AuthorityType.Server;
|
||||
expect(networkIdentity.hasAuthority('client1')).toBe(false);
|
||||
});
|
||||
|
||||
test('客户端权限下拥有者有权限', () => {
|
||||
networkIdentity.authority = AuthorityType.Client;
|
||||
networkIdentity.ownerId = 'client1';
|
||||
expect(networkIdentity.hasAuthority('client1')).toBe(true);
|
||||
expect(networkIdentity.hasAuthority('client2')).toBe(false);
|
||||
});
|
||||
|
||||
test('共享权限下所有人都有权限', () => {
|
||||
networkIdentity.authority = AuthorityType.Shared;
|
||||
expect(networkIdentity.hasAuthority('client1')).toBe(true);
|
||||
expect(networkIdentity.hasAuthority('client2')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('同步范围检查', () => {
|
||||
test('全局作用域下所有客户端都应该同步', () => {
|
||||
networkIdentity.scope = NetworkScope.Global;
|
||||
expect(networkIdentity.shouldSyncToClient('client1')).toBe(true);
|
||||
expect(networkIdentity.shouldSyncToClient('client2')).toBe(true);
|
||||
});
|
||||
|
||||
test('拥有者作用域下只有拥有者应该同步', () => {
|
||||
networkIdentity.scope = NetworkScope.Owner;
|
||||
networkIdentity.ownerId = 'client1';
|
||||
expect(networkIdentity.shouldSyncToClient('client1')).toBe(true);
|
||||
expect(networkIdentity.shouldSyncToClient('client2')).toBe(false);
|
||||
});
|
||||
|
||||
test('附近作用域下距离内的客户端应该同步', () => {
|
||||
networkIdentity.scope = NetworkScope.Nearby;
|
||||
networkIdentity.distanceThreshold = 100;
|
||||
|
||||
expect(networkIdentity.shouldSyncToClient('client1', 50)).toBe(true);
|
||||
expect(networkIdentity.shouldSyncToClient('client2', 150)).toBe(false);
|
||||
});
|
||||
|
||||
test('禁用同步时不应该同步给任何客户端', () => {
|
||||
networkIdentity.scope = NetworkScope.Global;
|
||||
networkIdentity.syncEnabled = false;
|
||||
expect(networkIdentity.shouldSyncToClient('client1')).toBe(false);
|
||||
});
|
||||
|
||||
test('不可见时不应该同步给任何客户端', () => {
|
||||
networkIdentity.scope = NetworkScope.Global;
|
||||
networkIdentity.visible = false;
|
||||
expect(networkIdentity.shouldSyncToClient('client1')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('同步权重计算', () => {
|
||||
test('应该基于优先级计算权重', () => {
|
||||
networkIdentity.priority = 10;
|
||||
expect(networkIdentity.getSyncWeight()).toBe(10);
|
||||
});
|
||||
|
||||
test('附近作用域应该基于距离调整权重', () => {
|
||||
networkIdentity.scope = NetworkScope.Nearby;
|
||||
networkIdentity.priority = 10;
|
||||
networkIdentity.distanceThreshold = 100;
|
||||
|
||||
// 距离为0时权重应该等于优先级
|
||||
expect(networkIdentity.getSyncWeight(0)).toBe(10);
|
||||
|
||||
// 距离为50时权重应该降低
|
||||
const weight50 = networkIdentity.getSyncWeight(50);
|
||||
expect(weight50).toBeGreaterThan(0);
|
||||
expect(weight50).toBeLessThan(10);
|
||||
|
||||
// 距离超过阈值时权重应该为0
|
||||
expect(networkIdentity.getSyncWeight(150)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('拥有者管理', () => {
|
||||
test('应该能够设置拥有者', () => {
|
||||
networkIdentity.setOwner('client1');
|
||||
expect(networkIdentity.ownerId).toBe('client1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('调试信息', () => {
|
||||
test('应该返回完整的调试信息', () => {
|
||||
networkIdentity.networkId = 123;
|
||||
networkIdentity.ownerId = 'client1';
|
||||
networkIdentity.priority = 5;
|
||||
|
||||
const debugInfo = networkIdentity.getDebugInfo();
|
||||
|
||||
expect(debugInfo).toMatchObject({
|
||||
networkId: 123,
|
||||
ownerId: 'client1',
|
||||
authority: AuthorityType.Server,
|
||||
scope: NetworkScope.Room,
|
||||
syncRate: 20,
|
||||
priority: 5,
|
||||
syncEnabled: true,
|
||||
visible: true
|
||||
});
|
||||
|
||||
expect(debugInfo).toHaveProperty('lastSyncTime');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,24 @@
|
||||
/**
|
||||
* Jest测试环境设置
|
||||
*/
|
||||
|
||||
// 导入reflect-metadata以支持装饰器
|
||||
import 'reflect-metadata';
|
||||
|
||||
global.beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// 全局测试配置
|
||||
beforeAll(() => {
|
||||
// 设置测试环境
|
||||
process.env.NODE_ENV = 'test';
|
||||
});
|
||||
|
||||
global.afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
afterAll(() => {
|
||||
// 清理测试环境
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// 每个测试前的准备工作
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 每个测试后的清理工作
|
||||
});
|
||||
57
packages/network-shared/tests/types.test.ts
Normal file
57
packages/network-shared/tests/types.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 类型定义测试
|
||||
*/
|
||||
import { MessageType, AuthorityType, NetworkScope, SyncMode, RpcTarget } from '../src/types/NetworkTypes';
|
||||
|
||||
describe('NetworkTypes', () => {
|
||||
describe('MessageType枚举', () => {
|
||||
test('应该包含所有必要的消息类型', () => {
|
||||
expect(MessageType.CONNECT).toBe('connect');
|
||||
expect(MessageType.DISCONNECT).toBe('disconnect');
|
||||
expect(MessageType.HEARTBEAT).toBe('heartbeat');
|
||||
expect(MessageType.SYNC_VAR).toBe('sync_var');
|
||||
expect(MessageType.RPC_CALL).toBe('rpc_call');
|
||||
expect(MessageType.ENTITY_CREATE).toBe('entity_create');
|
||||
expect(MessageType.ERROR).toBe('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AuthorityType枚举', () => {
|
||||
test('应该包含正确的权限类型', () => {
|
||||
expect(AuthorityType.Server).toBe('server');
|
||||
expect(AuthorityType.Client).toBe('client');
|
||||
expect(AuthorityType.Shared).toBe('shared');
|
||||
});
|
||||
});
|
||||
|
||||
describe('NetworkScope枚举', () => {
|
||||
test('应该包含正确的网络作用域', () => {
|
||||
expect(NetworkScope.Global).toBe('global');
|
||||
expect(NetworkScope.Room).toBe('room');
|
||||
expect(NetworkScope.Owner).toBe('owner');
|
||||
expect(NetworkScope.Nearby).toBe('nearby');
|
||||
expect(NetworkScope.Custom).toBe('custom');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SyncMode枚举', () => {
|
||||
test('应该包含正确的同步模式', () => {
|
||||
expect(SyncMode.All).toBe('all');
|
||||
expect(SyncMode.Owner).toBe('owner');
|
||||
expect(SyncMode.Others).toBe('others');
|
||||
expect(SyncMode.Nearby).toBe('nearby');
|
||||
expect(SyncMode.Custom).toBe('custom');
|
||||
});
|
||||
});
|
||||
|
||||
describe('RpcTarget枚举', () => {
|
||||
test('应该包含正确的RPC目标', () => {
|
||||
expect(RpcTarget.Server).toBe('server');
|
||||
expect(RpcTarget.Client).toBe('client');
|
||||
expect(RpcTarget.All).toBe('all');
|
||||
expect(RpcTarget.Others).toBe('others');
|
||||
expect(RpcTarget.Owner).toBe('owner');
|
||||
expect(RpcTarget.Nearby).toBe('nearby');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@
|
||||
"outDir": "./bin",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
@@ -40,7 +41,11 @@
|
||||
"node_modules",
|
||||
"bin",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"src/protocol/analyzer/**/*"
|
||||
"**/*.spec.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
}
|
||||
]
|
||||
}
|
||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"references": [
|
||||
{ "path": "./packages/core" },
|
||||
{ "path": "./packages/math" },
|
||||
{ "path": "./packages/ecs-network" }
|
||||
],
|
||||
"files": []
|
||||
}
|
||||
Reference in New Issue
Block a user