重构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"]
|
[submodule "thirdparty/ecs-astar"]
|
||||||
path = thirdparty/ecs-astar
|
path = thirdparty/ecs-astar
|
||||||
url = https://github.com/esengine/ecs-astar.git
|
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"]
|
[submodule "examples/lawn-mower-demo"]
|
||||||
path = examples/lawn-mower-demo
|
path = examples/lawn-mower-demo
|
||||||
url = https://github.com/esengine/lawn-mower-demo.git
|
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": {
|
"packages/network-client": {
|
||||||
"name": "@esengine/network-client",
|
"name": "@esengine/network-client",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@esengine/ecs-framework": "file:../core",
|
"@esengine/ecs-framework": "file:../core",
|
||||||
@@ -11622,7 +11622,7 @@
|
|||||||
},
|
},
|
||||||
"packages/network-server": {
|
"packages/network-server": {
|
||||||
"name": "@esengine/network-server",
|
"name": "@esengine/network-server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@esengine/ecs-framework": "file:../core",
|
"@esengine/ecs-framework": "file:../core",
|
||||||
@@ -11643,7 +11643,7 @@
|
|||||||
},
|
},
|
||||||
"packages/network-shared": {
|
"packages/network-shared": {
|
||||||
"name": "@esengine/network-shared",
|
"name": "@esengine/network-shared",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@esengine/ecs-framework": "file:../core",
|
"@esengine/ecs-framework": "file:../core",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"name": "@esengine/ecs-framework-math",
|
"name": "@esengine/ecs-framework-math",
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"description": "ECS框架2D数学库 - 提供向量、矩阵、几何形状和碰撞检测功能",
|
"description": "ECS框架2D数学库 - 提供向量、矩阵、几何形状和碰撞检测功能",
|
||||||
"type": "module",
|
|
||||||
"main": "bin/index.js",
|
"main": "bin/index.js",
|
||||||
"types": "bin/index.d.ts",
|
"types": "bin/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
@@ -22,7 +21,7 @@
|
|||||||
"typescript"
|
"typescript"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf bin dist",
|
"clean": "rimraf bin dist tsconfig.tsbuildinfo",
|
||||||
"build:ts": "tsc",
|
"build:ts": "tsc",
|
||||||
"prebuild": "npm run clean",
|
"prebuild": "npm run clean",
|
||||||
"build": "npm run build:ts",
|
"build": "npm run build:ts",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"declaration": true,
|
"declaration": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"composite": true,
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "node",
|
"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 path = require('path');
|
||||||
const { execSync } = require('child_process');
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
console.log('🚀 使用 Rollup 构建 network-client 包...');
|
console.log('🚀 使用 Rollup 构建 @esengine/network-client 包...');
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
|
// 清理旧的dist目录
|
||||||
if (fs.existsSync('./dist')) {
|
if (fs.existsSync('./dist')) {
|
||||||
console.log('🧹 清理旧的构建文件...');
|
console.log('🧹 清理旧的构建文件...');
|
||||||
execSync('rimraf ./dist', { stdio: 'inherit' });
|
execSync('rimraf ./dist', { stdio: 'inherit' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 执行Rollup构建
|
||||||
console.log('📦 执行 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...');
|
console.log('📋 生成 package.json...');
|
||||||
generatePackageJson();
|
generatePackageJson();
|
||||||
|
|
||||||
|
// 复制其他文件
|
||||||
console.log('📁 复制必要文件...');
|
console.log('📁 复制必要文件...');
|
||||||
copyFiles();
|
copyFiles();
|
||||||
|
|
||||||
|
// 输出构建结果
|
||||||
showBuildResults();
|
showBuildResults();
|
||||||
|
|
||||||
console.log('✅ network-client 构建完成!');
|
console.log('✅ @esengine/network-client 构建完成!');
|
||||||
console.log('\n🚀 发布命令:');
|
console.log('\n🚀 发布命令:');
|
||||||
console.log('cd dist && npm publish');
|
console.log('cd dist && npm publish');
|
||||||
|
|
||||||
@@ -63,18 +68,19 @@ function generatePackageJson() {
|
|||||||
],
|
],
|
||||||
keywords: [
|
keywords: [
|
||||||
'ecs',
|
'ecs',
|
||||||
'networking',
|
'network',
|
||||||
'client',
|
'client',
|
||||||
'prediction',
|
'multiplayer',
|
||||||
'interpolation',
|
'game',
|
||||||
'game-engine',
|
'browser',
|
||||||
|
'cocos',
|
||||||
'typescript'
|
'typescript'
|
||||||
],
|
],
|
||||||
author: sourcePackage.author,
|
author: sourcePackage.author,
|
||||||
license: sourcePackage.license,
|
license: sourcePackage.license,
|
||||||
repository: sourcePackage.repository,
|
repository: sourcePackage.repository,
|
||||||
dependencies: sourcePackage.dependencies,
|
dependencies: sourcePackage.dependencies,
|
||||||
peerDependencies: sourcePackage.peerDependencies,
|
publishConfig: sourcePackage.publishConfig,
|
||||||
engines: {
|
engines: {
|
||||||
node: '>=16.0.0'
|
node: '>=16.0.0'
|
||||||
},
|
},
|
||||||
@@ -87,7 +93,7 @@ function generatePackageJson() {
|
|||||||
function copyFiles() {
|
function copyFiles() {
|
||||||
const filesToCopy = [
|
const filesToCopy = [
|
||||||
{ src: './README.md', dest: './dist/README.md' },
|
{ src: './README.md', dest: './dist/README.md' },
|
||||||
{ src: '../../LICENSE', dest: './dist/LICENSE' }
|
{ src: './LICENSE', dest: './dist/LICENSE' }
|
||||||
];
|
];
|
||||||
|
|
||||||
filesToCopy.forEach(({ src, dest }) => {
|
filesToCopy.forEach(({ src, dest }) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
preset: 'ts-jest',
|
preset: 'ts-jest',
|
||||||
testEnvironment: 'jsdom', // 客户端库使用 jsdom 环境
|
testEnvironment: 'jsdom', // 客户端使用jsdom环境
|
||||||
roots: ['<rootDir>/tests'],
|
roots: ['<rootDir>/tests'],
|
||||||
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
|
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
|
||||||
testPathIgnorePatterns: ['/node_modules/'],
|
testPathIgnorePatterns: ['/node_modules/'],
|
||||||
@@ -18,16 +18,10 @@ module.exports = {
|
|||||||
coverageReporters: ['text', 'lcov', 'html'],
|
coverageReporters: ['text', 'lcov', 'html'],
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
branches: 60,
|
branches: 70,
|
||||||
functions: 70,
|
functions: 70,
|
||||||
lines: 70,
|
lines: 70,
|
||||||
statements: 70
|
statements: 70
|
||||||
},
|
|
||||||
'./src/core/': {
|
|
||||||
branches: 70,
|
|
||||||
functions: 80,
|
|
||||||
lines: 80,
|
|
||||||
statements: 80
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
verbose: true,
|
verbose: true,
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/ecs-framework-network-client",
|
"name": "@esengine/network-client",
|
||||||
"version": "1.0.17",
|
"version": "1.0.1",
|
||||||
"description": "ECS Framework 网络库 - 客户端实现",
|
"description": "ECS Framework网络层 - 客户端实现",
|
||||||
"type": "module",
|
|
||||||
"main": "bin/index.js",
|
"main": "bin/index.js",
|
||||||
"types": "bin/index.d.ts",
|
"types": "bin/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -22,15 +21,16 @@
|
|||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ecs",
|
"ecs",
|
||||||
"networking",
|
"network",
|
||||||
"client",
|
"client",
|
||||||
"prediction",
|
"multiplayer",
|
||||||
"interpolation",
|
"game",
|
||||||
"game-engine",
|
"browser",
|
||||||
|
"cocos",
|
||||||
"typescript"
|
"typescript"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf bin dist",
|
"clean": "rimraf bin dist tsconfig.tsbuildinfo",
|
||||||
"build:ts": "tsc",
|
"build:ts": "tsc",
|
||||||
"prebuild": "npm run clean",
|
"prebuild": "npm run clean",
|
||||||
"build": "npm run build:ts",
|
"build": "npm run build:ts",
|
||||||
@@ -45,32 +45,21 @@
|
|||||||
"test": "jest --config jest.config.cjs",
|
"test": "jest --config jest.config.cjs",
|
||||||
"test:watch": "jest --watch --config jest.config.cjs",
|
"test:watch": "jest --watch --config jest.config.cjs",
|
||||||
"test:coverage": "jest --coverage --config jest.config.cjs",
|
"test:coverage": "jest --coverage --config jest.config.cjs",
|
||||||
"test:ci": "jest --ci --coverage --config jest.config.cjs",
|
"test:ci": "jest --ci --coverage --config jest.config.cjs"
|
||||||
"test:clear": "jest --clearCache"
|
|
||||||
},
|
},
|
||||||
"author": "yhh",
|
"author": "yhh",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ws": "^8.18.0"
|
"@esengine/ecs-framework": "file:../core",
|
||||||
},
|
"@esengine/network-shared": "file:../network-shared",
|
||||||
"peerDependencies": {
|
"reflect-metadata": "^0.2.2"
|
||||||
"@esengine/ecs-framework": ">=2.1.29",
|
|
||||||
"@esengine/ecs-framework-network-shared": ">=1.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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/jest": "^29.5.14",
|
||||||
"@types/node": "^20.19.0",
|
"@types/node": "^20.19.0",
|
||||||
"@types/ws": "^8.5.13",
|
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"rimraf": "^5.0.0",
|
"rimraf": "^5.0.0",
|
||||||
"rollup": "^4.42.0",
|
|
||||||
"rollup-plugin-dts": "^6.2.1",
|
|
||||||
"ts-jest": "^29.4.0",
|
"ts-jest": "^29.4.0",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,17 +7,18 @@ const { readFileSync } = require('fs');
|
|||||||
const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));
|
const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||||
|
|
||||||
const banner = `/**
|
const banner = `/**
|
||||||
* @esengine/ecs-framework-network-client v${pkg.version}
|
* @esengine/network-client v${pkg.version}
|
||||||
* ECS Framework 网络库 - 客户端实现
|
* ECS网络层客户端实现
|
||||||
*
|
*
|
||||||
* @author ${pkg.author}
|
* @author ${pkg.author}
|
||||||
* @license ${pkg.license}
|
* @license ${pkg.license}
|
||||||
*/`;
|
*/`;
|
||||||
|
|
||||||
|
// 外部依赖,不打包进bundle
|
||||||
const external = [
|
const external = [
|
||||||
'ws',
|
|
||||||
'@esengine/ecs-framework',
|
'@esengine/ecs-framework',
|
||||||
'@esengine/ecs-framework-network-shared'
|
'@esengine/network-shared',
|
||||||
|
'reflect-metadata'
|
||||||
];
|
];
|
||||||
|
|
||||||
const commonPlugins = [
|
const commonPlugins = [
|
||||||
@@ -81,7 +82,7 @@ module.exports = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// UMD构建
|
// UMD构建 - 用于浏览器直接使用
|
||||||
{
|
{
|
||||||
input: 'bin/index.js',
|
input: 'bin/index.js',
|
||||||
output: {
|
output: {
|
||||||
@@ -92,10 +93,9 @@ module.exports = [
|
|||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
exports: 'named',
|
exports: 'named',
|
||||||
globals: {
|
globals: {
|
||||||
'ws': 'WebSocket',
|
|
||||||
'uuid': 'uuid',
|
|
||||||
'@esengine/ecs-framework': 'ECS',
|
'@esengine/ecs-framework': 'ECS',
|
||||||
'@esengine/ecs-framework-network-shared': 'ECSNetworkShared'
|
'@esengine/network-shared': 'ECSNetworkShared',
|
||||||
|
'reflect-metadata': 'Reflect'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -119,7 +119,7 @@ module.exports = [
|
|||||||
file: 'dist/index.d.ts',
|
file: 'dist/index.d.ts',
|
||||||
format: 'es',
|
format: 'es',
|
||||||
banner: `/**
|
banner: `/**
|
||||||
* @esengine/ecs-framework-network-client v${pkg.version}
|
* @esengine/network-client v${pkg.version}
|
||||||
* TypeScript definitions
|
* 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';
|
||||||
|
|
||||||
// 接口
|
// 重新导出shared包的类型
|
||||||
export * from './interfaces';
|
export * from '@esengine/network-shared';
|
||||||
|
|
||||||
// 版本信息
|
|
||||||
export const VERSION = '1.0.11';
|
|
||||||
@@ -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';
|
import 'reflect-metadata';
|
||||||
|
|
||||||
// Mock WebSocket for testing
|
// 模拟浏览器环境的WebSocket
|
||||||
(global as any).WebSocket = class MockWebSocket {
|
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;
|
onopen: ((event: Event) => void) | null = null;
|
||||||
onmessage: ((event: MessageEvent) => void) | null = null;
|
|
||||||
onclose: ((event: CloseEvent) => void) | null = null;
|
onclose: ((event: CloseEvent) => void) | null = null;
|
||||||
|
onmessage: ((event: MessageEvent) => void) | null = null;
|
||||||
onerror: ((event: Event) => 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) {
|
send(data: string | ArrayBuffer) {
|
||||||
// Mock implementation
|
// 模拟发送
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
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",
|
"module": "ES2020",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"allowImportingTsExtensions": false,
|
"allowImportingTsExtensions": false,
|
||||||
"lib": ["ES2020", "DOM"],
|
"lib": ["ES2020", "DOM", "WebWorker"],
|
||||||
"outDir": "./bin",
|
"outDir": "./bin",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"composite": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
@@ -41,5 +42,13 @@
|
|||||||
"bin",
|
"bin",
|
||||||
"**/*.test.ts",
|
"**/*.test.ts",
|
||||||
"**/*.spec.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 path = require('path');
|
||||||
const { execSync } = require('child_process');
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
console.log('🚀 使用 Rollup 构建 network-server 包...');
|
console.log('🚀 使用 Rollup 构建 @esengine/network-server 包...');
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
|
// 清理旧的dist目录
|
||||||
if (fs.existsSync('./dist')) {
|
if (fs.existsSync('./dist')) {
|
||||||
console.log('🧹 清理旧的构建文件...');
|
console.log('🧹 清理旧的构建文件...');
|
||||||
execSync('rimraf ./dist', { stdio: 'inherit' });
|
execSync('rimraf ./dist', { stdio: 'inherit' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 执行Rollup构建
|
||||||
console.log('📦 执行 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...');
|
console.log('📋 生成 package.json...');
|
||||||
generatePackageJson();
|
generatePackageJson();
|
||||||
|
|
||||||
|
// 复制其他文件
|
||||||
console.log('📁 复制必要文件...');
|
console.log('📁 复制必要文件...');
|
||||||
copyFiles();
|
copyFiles();
|
||||||
|
|
||||||
|
// 输出构建结果
|
||||||
showBuildResults();
|
showBuildResults();
|
||||||
|
|
||||||
console.log('✅ network-server 构建完成!');
|
console.log('✅ @esengine/network-server 构建完成!');
|
||||||
console.log('\n🚀 发布命令:');
|
console.log('\n🚀 发布命令:');
|
||||||
console.log('cd dist && npm publish');
|
console.log('cd dist && npm publish');
|
||||||
|
|
||||||
@@ -60,19 +65,18 @@ function generatePackageJson() {
|
|||||||
],
|
],
|
||||||
keywords: [
|
keywords: [
|
||||||
'ecs',
|
'ecs',
|
||||||
'networking',
|
'network',
|
||||||
'server',
|
'server',
|
||||||
'authority',
|
'multiplayer',
|
||||||
'validation',
|
'game',
|
||||||
'rooms',
|
'nodejs',
|
||||||
'game-server',
|
|
||||||
'typescript'
|
'typescript'
|
||||||
],
|
],
|
||||||
author: sourcePackage.author,
|
author: sourcePackage.author,
|
||||||
license: sourcePackage.license,
|
license: sourcePackage.license,
|
||||||
repository: sourcePackage.repository,
|
repository: sourcePackage.repository,
|
||||||
dependencies: sourcePackage.dependencies,
|
dependencies: sourcePackage.dependencies,
|
||||||
peerDependencies: sourcePackage.peerDependencies,
|
publishConfig: sourcePackage.publishConfig,
|
||||||
engines: {
|
engines: {
|
||||||
node: '>=16.0.0'
|
node: '>=16.0.0'
|
||||||
},
|
},
|
||||||
@@ -85,7 +89,7 @@ function generatePackageJson() {
|
|||||||
function copyFiles() {
|
function copyFiles() {
|
||||||
const filesToCopy = [
|
const filesToCopy = [
|
||||||
{ src: './README.md', dest: './dist/README.md' },
|
{ src: './README.md', dest: './dist/README.md' },
|
||||||
{ src: '../../LICENSE', dest: './dist/LICENSE' }
|
{ src: './LICENSE', dest: './dist/LICENSE' }
|
||||||
];
|
];
|
||||||
|
|
||||||
filesToCopy.forEach(({ src, dest }) => {
|
filesToCopy.forEach(({ src, dest }) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
preset: 'ts-jest',
|
preset: 'ts-jest',
|
||||||
testEnvironment: 'node', // 服务端库使用 node 环境
|
testEnvironment: 'node',
|
||||||
roots: ['<rootDir>/tests'],
|
roots: ['<rootDir>/tests'],
|
||||||
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
|
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
|
||||||
testPathIgnorePatterns: ['/node_modules/'],
|
testPathIgnorePatterns: ['/node_modules/'],
|
||||||
@@ -18,16 +18,10 @@ module.exports = {
|
|||||||
coverageReporters: ['text', 'lcov', 'html'],
|
coverageReporters: ['text', 'lcov', 'html'],
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
branches: 60,
|
branches: 70,
|
||||||
functions: 70,
|
functions: 70,
|
||||||
lines: 70,
|
lines: 70,
|
||||||
statements: 70
|
statements: 70
|
||||||
},
|
|
||||||
'./src/core/': {
|
|
||||||
branches: 70,
|
|
||||||
functions: 80,
|
|
||||||
lines: 80,
|
|
||||||
statements: 80
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
verbose: true,
|
verbose: true,
|
||||||
@@ -42,7 +36,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||||
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
|
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
|
||||||
testTimeout: 10000,
|
testTimeout: 15000, // 服务端测试可能需要更长时间
|
||||||
clearMocks: true,
|
clearMocks: true,
|
||||||
restoreMocks: true,
|
restoreMocks: true,
|
||||||
modulePathIgnorePatterns: [
|
modulePathIgnorePatterns: [
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/ecs-framework-network-server",
|
"name": "@esengine/network-server",
|
||||||
"version": "1.0.5",
|
"version": "1.0.1",
|
||||||
"description": "ECS Framework 网络库 - 服务端实现",
|
"description": "ECS Framework网络层 - 服务端实现",
|
||||||
"type": "module",
|
|
||||||
"main": "bin/index.js",
|
"main": "bin/index.js",
|
||||||
"types": "bin/index.d.ts",
|
"types": "bin/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -22,16 +21,15 @@
|
|||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ecs",
|
"ecs",
|
||||||
"networking",
|
"network",
|
||||||
"server",
|
"server",
|
||||||
"authority",
|
"multiplayer",
|
||||||
"validation",
|
"game",
|
||||||
"rooms",
|
"nodejs",
|
||||||
"game-server",
|
|
||||||
"typescript"
|
"typescript"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf bin dist",
|
"clean": "rimraf bin dist tsconfig.tsbuildinfo",
|
||||||
"build:ts": "tsc",
|
"build:ts": "tsc",
|
||||||
"prebuild": "npm run clean",
|
"prebuild": "npm run clean",
|
||||||
"build": "npm run build:ts",
|
"build": "npm run build:ts",
|
||||||
@@ -43,38 +41,29 @@
|
|||||||
"publish:minor": "npm version minor && npm run build:npm && cd dist && npm publish",
|
"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",
|
"publish:major": "npm version major && npm run build:npm && cd dist && npm publish",
|
||||||
"preversion": "npm run rebuild",
|
"preversion": "npm run rebuild",
|
||||||
|
"dev": "ts-node src/dev-server.ts",
|
||||||
|
"start": "node bin/index.js",
|
||||||
"test": "jest --config jest.config.cjs",
|
"test": "jest --config jest.config.cjs",
|
||||||
"test:watch": "jest --watch --config jest.config.cjs",
|
"test:watch": "jest --watch --config jest.config.cjs",
|
||||||
"test:coverage": "jest --coverage --config jest.config.cjs",
|
"test:coverage": "jest --coverage --config jest.config.cjs",
|
||||||
"test:ci": "jest --ci --coverage --config jest.config.cjs",
|
"test:ci": "jest --ci --coverage --config jest.config.cjs"
|
||||||
"test:clear": "jest --clearCache"
|
|
||||||
},
|
},
|
||||||
"author": "yhh",
|
"author": "yhh",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ws": "^8.18.0",
|
"@esengine/ecs-framework": "file:../core",
|
||||||
"uuid": "^10.0.0"
|
"@esengine/network-shared": "file:../network-shared",
|
||||||
},
|
"ws": "^8.18.2",
|
||||||
"peerDependencies": {
|
"reflect-metadata": "^0.2.2"
|
||||||
"@esengine/ecs-framework": ">=2.1.29",
|
|
||||||
"@esengine/ecs-framework-network-shared": ">=1.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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/jest": "^29.5.14",
|
||||||
"@types/node": "^20.19.0",
|
"@types/node": "^20.19.0",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/ws": "^8.18.1",
|
||||||
"@types/ws": "^8.5.13",
|
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-node": "^29.7.0",
|
|
||||||
"rimraf": "^5.0.0",
|
"rimraf": "^5.0.0",
|
||||||
"rollup": "^4.42.0",
|
|
||||||
"rollup-plugin-dts": "^6.2.1",
|
|
||||||
"ts-jest": "^29.4.0",
|
"ts-jest": "^29.4.0",
|
||||||
|
"ts-node": "^10.9.0",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -7,23 +7,33 @@ const { readFileSync } = require('fs');
|
|||||||
const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));
|
const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||||
|
|
||||||
const banner = `/**
|
const banner = `/**
|
||||||
* @esengine/ecs-framework-network-server v${pkg.version}
|
* @esengine/network-server v${pkg.version}
|
||||||
* ECS Framework 网络库 - 服务端实现
|
* ECS网络层服务端实现
|
||||||
*
|
*
|
||||||
* @author ${pkg.author}
|
* @author ${pkg.author}
|
||||||
* @license ${pkg.license}
|
* @license ${pkg.license}
|
||||||
*/`;
|
*/`;
|
||||||
|
|
||||||
|
// 外部依赖,不打包进bundle (Node.js环境,保持依赖外部化)
|
||||||
const external = [
|
const external = [
|
||||||
'ws',
|
|
||||||
'uuid',
|
|
||||||
'@esengine/ecs-framework',
|
'@esengine/ecs-framework',
|
||||||
'@esengine/ecs-framework-network-shared'
|
'@esengine/network-shared',
|
||||||
|
'ws',
|
||||||
|
'reflect-metadata',
|
||||||
|
'http',
|
||||||
|
'https',
|
||||||
|
'crypto',
|
||||||
|
'events',
|
||||||
|
'stream',
|
||||||
|
'util',
|
||||||
|
'fs',
|
||||||
|
'path'
|
||||||
];
|
];
|
||||||
|
|
||||||
const commonPlugins = [
|
const commonPlugins = [
|
||||||
resolve({
|
resolve({
|
||||||
preferBuiltins: true
|
preferBuiltins: true,
|
||||||
|
browser: false
|
||||||
}),
|
}),
|
||||||
commonjs({
|
commonjs({
|
||||||
include: /node_modules/
|
include: /node_modules/
|
||||||
@@ -57,7 +67,7 @@ module.exports = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// CommonJS构建
|
// CommonJS构建 (Node.js主要格式)
|
||||||
{
|
{
|
||||||
input: 'bin/index.js',
|
input: 'bin/index.js',
|
||||||
output: {
|
output: {
|
||||||
@@ -88,7 +98,7 @@ module.exports = [
|
|||||||
file: 'dist/index.d.ts',
|
file: 'dist/index.d.ts',
|
||||||
format: 'es',
|
format: 'es',
|
||||||
banner: `/**
|
banner: `/**
|
||||||
* @esengine/ecs-framework-network-server v${pkg.version}
|
* @esengine/network-server v${pkg.version}
|
||||||
* TypeScript definitions
|
* 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
|
* @esengine/network-server
|
||||||
*
|
* ECS Framework网络层 - 服务端实现
|
||||||
* 提供完整的网络服务端功能,包括:
|
|
||||||
* - WebSocket 和 HTTP 传输层
|
|
||||||
* - 客户端连接管理
|
|
||||||
* - 房间系统
|
|
||||||
* - 身份验证和权限管理
|
|
||||||
* - SyncVar 和 RPC 系统
|
|
||||||
* - 消息验证
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 核心模块
|
// 核心服务器 (待实现)
|
||||||
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';
|
||||||
|
|
||||||
// 版本信息
|
// 重新导出shared包的类型
|
||||||
export const VERSION = '1.0.0';
|
export * from '@esengine/network-shared';
|
||||||
|
|
||||||
// 导出常用组合配置
|
|
||||||
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
|
|
||||||
};
|
|
||||||
@@ -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';
|
import 'reflect-metadata';
|
||||||
|
|
||||||
global.beforeEach(() => {
|
// 全局测试配置
|
||||||
jest.clearAllMocks();
|
beforeAll(() => {
|
||||||
|
// 设置测试环境
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
|
process.env.NETWORK_ENV = 'server';
|
||||||
});
|
});
|
||||||
|
|
||||||
global.afterEach(() => {
|
afterAll(() => {
|
||||||
jest.restoreAllMocks();
|
// 清理测试环境
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// 每个测试前的准备工作
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// 每个测试后的清理工作
|
||||||
|
// 清理可能的网络连接、定时器等
|
||||||
});
|
});
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
"outDir": "./bin",
|
"outDir": "./bin",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"composite": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
@@ -41,5 +42,13 @@
|
|||||||
"bin",
|
"bin",
|
||||||
"**/*.test.ts",
|
"**/*.test.ts",
|
||||||
"**/*.spec.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 path = require('path');
|
||||||
const { execSync } = require('child_process');
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
console.log('🚀 使用 Rollup 构建 network-shared 包...');
|
console.log('🚀 使用 Rollup 构建 @esengine/network-shared 包...');
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
|
// 清理旧的dist目录
|
||||||
if (fs.existsSync('./dist')) {
|
if (fs.existsSync('./dist')) {
|
||||||
console.log('🧹 清理旧的构建文件...');
|
console.log('🧹 清理旧的构建文件...');
|
||||||
execSync('rimraf ./dist', { stdio: 'inherit' });
|
execSync('rimraf ./dist', { stdio: 'inherit' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 执行Rollup构建
|
||||||
console.log('📦 执行 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...');
|
console.log('📋 生成 package.json...');
|
||||||
generatePackageJson();
|
generatePackageJson();
|
||||||
|
|
||||||
|
// 复制其他文件
|
||||||
console.log('📁 复制必要文件...');
|
console.log('📁 复制必要文件...');
|
||||||
copyFiles();
|
copyFiles();
|
||||||
|
|
||||||
|
// 输出构建结果
|
||||||
showBuildResults();
|
showBuildResults();
|
||||||
|
|
||||||
console.log('✅ network-shared 构建完成!');
|
console.log('✅ @esengine/network-shared 构建完成!');
|
||||||
console.log('\n🚀 发布命令:');
|
console.log('\n🚀 发布命令:');
|
||||||
console.log('cd dist && npm publish');
|
console.log('cd dist && npm publish');
|
||||||
|
|
||||||
@@ -63,19 +68,20 @@ function generatePackageJson() {
|
|||||||
],
|
],
|
||||||
keywords: [
|
keywords: [
|
||||||
'ecs',
|
'ecs',
|
||||||
'networking',
|
'network',
|
||||||
|
'multiplayer',
|
||||||
|
'game',
|
||||||
'shared',
|
'shared',
|
||||||
'decorators',
|
'typescript',
|
||||||
'protobuf',
|
'cocos-creator',
|
||||||
'serialization',
|
'laya'
|
||||||
'game-engine',
|
|
||||||
'typescript'
|
|
||||||
],
|
],
|
||||||
author: sourcePackage.author,
|
author: sourcePackage.author,
|
||||||
license: sourcePackage.license,
|
license: sourcePackage.license,
|
||||||
repository: sourcePackage.repository,
|
repository: sourcePackage.repository,
|
||||||
dependencies: sourcePackage.dependencies,
|
|
||||||
peerDependencies: sourcePackage.peerDependencies,
|
peerDependencies: sourcePackage.peerDependencies,
|
||||||
|
dependencies: sourcePackage.dependencies,
|
||||||
|
publishConfig: sourcePackage.publishConfig,
|
||||||
engines: {
|
engines: {
|
||||||
node: '>=16.0.0'
|
node: '>=16.0.0'
|
||||||
},
|
},
|
||||||
@@ -88,7 +94,7 @@ function generatePackageJson() {
|
|||||||
function copyFiles() {
|
function copyFiles() {
|
||||||
const filesToCopy = [
|
const filesToCopy = [
|
||||||
{ src: './README.md', dest: './dist/README.md' },
|
{ src: './README.md', dest: './dist/README.md' },
|
||||||
{ src: '../../LICENSE', dest: './dist/LICENSE' }
|
{ src: './LICENSE', dest: './dist/LICENSE' }
|
||||||
];
|
];
|
||||||
|
|
||||||
filesToCopy.forEach(({ src, dest }) => {
|
filesToCopy.forEach(({ src, dest }) => {
|
||||||
|
|||||||
@@ -18,16 +18,10 @@ module.exports = {
|
|||||||
coverageReporters: ['text', 'lcov', 'html'],
|
coverageReporters: ['text', 'lcov', 'html'],
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
branches: 60,
|
branches: 70,
|
||||||
functions: 70,
|
functions: 70,
|
||||||
lines: 70,
|
lines: 70,
|
||||||
statements: 70
|
statements: 70
|
||||||
},
|
|
||||||
'./src/decorators/': {
|
|
||||||
branches: 70,
|
|
||||||
functions: 80,
|
|
||||||
lines: 80,
|
|
||||||
statements: 80
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
verbose: true,
|
verbose: true,
|
||||||
@@ -39,6 +33,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'^@/(.*)$': '<rootDir>/src/$1',
|
'^@/(.*)$': '<rootDir>/src/$1',
|
||||||
|
'^@esengine/ecs-framework$': '<rootDir>/../core/src/index.ts',
|
||||||
},
|
},
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||||
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
|
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
|
||||||
@@ -49,5 +44,8 @@ module.exports = {
|
|||||||
'<rootDir>/bin/',
|
'<rootDir>/bin/',
|
||||||
'<rootDir>/dist/',
|
'<rootDir>/dist/',
|
||||||
'<rootDir>/node_modules/'
|
'<rootDir>/node_modules/'
|
||||||
|
],
|
||||||
|
transformIgnorePatterns: [
|
||||||
|
'node_modules/(?!(.*\\.mjs$|@esengine))'
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/ecs-framework-network-shared",
|
"name": "@esengine/network-shared",
|
||||||
"version": "1.0.15",
|
"version": "1.0.2",
|
||||||
"description": "ECS Framework 网络库 - 共享组件和类型定义",
|
"description": "ECS Framework网络层 - 共享组件和协议",
|
||||||
"type": "module",
|
|
||||||
"main": "bin/index.js",
|
"main": "bin/index.js",
|
||||||
"types": "bin/index.d.ts",
|
"types": "bin/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -22,17 +21,15 @@
|
|||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ecs",
|
"ecs",
|
||||||
"networking",
|
"network",
|
||||||
"shared",
|
"multiplayer",
|
||||||
"decorators",
|
"game",
|
||||||
"protobuf",
|
"typescript",
|
||||||
"serialization",
|
"shared"
|
||||||
"game-engine",
|
|
||||||
"typescript"
|
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf bin dist",
|
"clean": "rimraf bin dist tsconfig.tsbuildinfo",
|
||||||
"build:ts": "tsc",
|
"build:ts": "tsc --build",
|
||||||
"prebuild": "npm run clean",
|
"prebuild": "npm run clean",
|
||||||
"build": "npm run build:ts",
|
"build": "npm run build:ts",
|
||||||
"build:watch": "tsc --watch",
|
"build:watch": "tsc --watch",
|
||||||
@@ -46,30 +43,19 @@
|
|||||||
"test": "jest --config jest.config.cjs",
|
"test": "jest --config jest.config.cjs",
|
||||||
"test:watch": "jest --watch --config jest.config.cjs",
|
"test:watch": "jest --watch --config jest.config.cjs",
|
||||||
"test:coverage": "jest --coverage --config jest.config.cjs",
|
"test:coverage": "jest --coverage --config jest.config.cjs",
|
||||||
"test:ci": "jest --ci --coverage --config jest.config.cjs",
|
"test:ci": "jest --ci --coverage --config jest.config.cjs"
|
||||||
"test:clear": "jest --clearCache"
|
|
||||||
},
|
},
|
||||||
"author": "yhh",
|
"author": "yhh",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"reflect-metadata": "^0.2.2",
|
"@esengine/ecs-framework": "file:../core",
|
||||||
"protobufjs": "^7.5.3"
|
"reflect-metadata": "^0.2.2"
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@esengine/ecs-framework": ">=2.1.29"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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/jest": "^29.5.14",
|
||||||
"@types/node": "^20.19.0",
|
"@types/node": "^20.19.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
|
||||||
"rimraf": "^5.0.0",
|
"rimraf": "^5.0.0",
|
||||||
"rollup": "^4.42.0",
|
|
||||||
"rollup-plugin-dts": "^6.2.1",
|
|
||||||
"ts-jest": "^29.4.0",
|
"ts-jest": "^29.4.0",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ const { readFileSync } = require('fs');
|
|||||||
const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));
|
const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||||
|
|
||||||
const banner = `/**
|
const banner = `/**
|
||||||
* @esengine/ecs-framework-network-shared v${pkg.version}
|
* @esengine/network-shared v${pkg.version}
|
||||||
* ECS Framework 网络库 - 共享组件和类型定义
|
* ECS网络层共享组件和协议
|
||||||
*
|
*
|
||||||
* @author ${pkg.author}
|
* @author ${pkg.author}
|
||||||
* @license ${pkg.license}
|
* @license ${pkg.license}
|
||||||
*/`;
|
*/`;
|
||||||
|
|
||||||
const external = ['reflect-metadata', 'protobufjs', 'uuid', '@esengine/ecs-framework'];
|
// 外部依赖,不打包进bundle
|
||||||
|
const external = ['@esengine/ecs-framework', 'reflect-metadata'];
|
||||||
|
|
||||||
const commonPlugins = [
|
const commonPlugins = [
|
||||||
resolve({
|
resolve({
|
||||||
@@ -77,7 +78,7 @@ module.exports = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// UMD构建
|
// UMD构建 - 包含所有依赖,用于浏览器直接使用
|
||||||
{
|
{
|
||||||
input: 'bin/index.js',
|
input: 'bin/index.js',
|
||||||
output: {
|
output: {
|
||||||
@@ -88,10 +89,8 @@ module.exports = [
|
|||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
exports: 'named',
|
exports: 'named',
|
||||||
globals: {
|
globals: {
|
||||||
'reflect-metadata': 'ReflectMetadata',
|
'@esengine/ecs-framework': 'ECS',
|
||||||
'protobufjs': 'protobuf',
|
'reflect-metadata': 'Reflect'
|
||||||
'uuid': 'uuid',
|
|
||||||
'@esengine/ecs-framework': 'ECS'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -115,7 +114,7 @@ module.exports = [
|
|||||||
file: 'dist/index.d.ts',
|
file: 'dist/index.d.ts',
|
||||||
format: 'es',
|
format: 'es',
|
||||||
banner: `/**
|
banner: `/**
|
||||||
* @esengine/ecs-framework-network-shared v${pkg.version}
|
* @esengine/network-shared v${pkg.version}
|
||||||
* TypeScript definitions
|
* 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 * from './serialization/NetworkSerializer';
|
||||||
|
|
||||||
// 版本信息
|
|
||||||
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
|
|
||||||
};
|
|
||||||
@@ -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 enum MessageType {
|
||||||
export type MethodDecorator<T = unknown> = (target: unknown, propertyKey: string | symbol, descriptor: PropertyDescriptor) => void;
|
// 连接管理
|
||||||
|
CONNECT = 'connect',
|
||||||
|
DISCONNECT = 'disconnect',
|
||||||
|
HEARTBEAT = 'heartbeat',
|
||||||
|
|
||||||
// 装饰器目标类型 - 使用更灵活的定义
|
// 数据同步
|
||||||
export interface DecoratorTarget extends Record<string, unknown> {
|
SYNC_VAR = 'sync_var',
|
||||||
constructor: Constructor;
|
SYNC_BATCH = 'sync_batch',
|
||||||
}
|
SYNC_SNAPSHOT = 'sync_snapshot',
|
||||||
|
|
||||||
// 网络数据类型约束
|
// RPC调用
|
||||||
export interface SerializedData {
|
RPC_CALL = 'rpc_call',
|
||||||
type: string;
|
RPC_RESPONSE = 'rpc_response',
|
||||||
data: Uint8Array;
|
|
||||||
checksum?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// RPC参数类型
|
// 实体管理
|
||||||
export type RpcParameterType = NetworkValue;
|
ENTITY_CREATE = 'entity_create',
|
||||||
export type RpcReturnType = NetworkValue | void | Promise<NetworkValue | void>;
|
ENTITY_DESTROY = 'entity_destroy',
|
||||||
|
ENTITY_UPDATE = 'entity_update',
|
||||||
|
|
||||||
// 序列化模式接口 - 使用泛型支持特定类型
|
// 房间管理
|
||||||
export interface SerializationSchema<T = NetworkValue> {
|
JOIN_ROOM = 'join_room',
|
||||||
serialize: (obj: T) => Uint8Array;
|
LEAVE_ROOM = 'leave_room',
|
||||||
deserialize: (data: Uint8Array) => T;
|
ROOM_STATE = 'room_state',
|
||||||
getSize?: (obj: T) => number;
|
|
||||||
|
// 游戏事件
|
||||||
|
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 =
|
export enum AuthorityType {
|
||||||
| 'disconnected'
|
/** 服务端权限 */
|
||||||
| 'connecting'
|
Server = 'server',
|
||||||
| 'connected'
|
/** 客户端权限 */
|
||||||
| 'disconnecting'
|
Client = 'client',
|
||||||
| 'reconnecting'
|
/** 共享权限 */
|
||||||
| 'failed';
|
Shared = 'shared'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 网络消息类型
|
* 网络作用域
|
||||||
*/
|
*/
|
||||||
export type NetworkMessageType =
|
export enum NetworkScope {
|
||||||
| 'syncvar'
|
/** 全局可见 */
|
||||||
| 'client-rpc'
|
Global = 'global',
|
||||||
| 'server-rpc'
|
/** 房间内可见 */
|
||||||
| 'spawn'
|
Room = 'room',
|
||||||
| 'destroy'
|
/** 仅拥有者可见 */
|
||||||
| 'ownership'
|
Owner = 'owner',
|
||||||
| 'scene-change'
|
/** 附近玩家可见 */
|
||||||
| 'snapshot'
|
Nearby = 'nearby',
|
||||||
| 'ping'
|
/** 自定义作用域 */
|
||||||
| 'custom';
|
Custom = 'custom'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 网络配置
|
* 同步模式
|
||||||
*/
|
*/
|
||||||
export interface NetworkConfig {
|
export enum SyncMode {
|
||||||
/** 端口号 */
|
/** 同步给所有客户端 */
|
||||||
port: number;
|
All = 'all',
|
||||||
/** 主机地址 */
|
/** 只同步给拥有者 */
|
||||||
host: string;
|
Owner = 'owner',
|
||||||
/** 最大连接数 */
|
/** 同步给除拥有者外的客户端 */
|
||||||
maxConnections: number;
|
Others = 'others',
|
||||||
/** 同步频率 (Hz) */
|
/** 同步给附近的客户端 */
|
||||||
syncRate: number;
|
Nearby = 'nearby',
|
||||||
/** 快照频率 (Hz) */
|
/** 自定义同步逻辑 */
|
||||||
snapshotRate: number;
|
Custom = 'custom'
|
||||||
/** 是否启用压缩 */
|
}
|
||||||
compression: boolean;
|
|
||||||
/** 是否启用加密 */
|
/**
|
||||||
encryption: boolean;
|
* RPC目标
|
||||||
/** 网络超时时间 (ms) */
|
*/
|
||||||
timeout: number;
|
export enum RpcTarget {
|
||||||
/** 重连尝试次数 */
|
/** 服务端 */
|
||||||
maxReconnectAttempts: number;
|
Server = 'server',
|
||||||
/** 重连间隔 (ms) */
|
/** 客户端 */
|
||||||
reconnectInterval: number;
|
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 {
|
export interface INetworkStats {
|
||||||
/** 连接数量 */
|
/** 总发送字节数 */
|
||||||
connectionCount: number;
|
|
||||||
/** 已发送字节数 */
|
|
||||||
bytesSent: number;
|
bytesSent: number;
|
||||||
/** 已接收字节数 */
|
/** 总接收字节数 */
|
||||||
bytesReceived: number;
|
bytesReceived: number;
|
||||||
/** 已发送消息数 */
|
/** 发送消息数 */
|
||||||
messagesSent: number;
|
messagesSent: number;
|
||||||
/** 已接收消息数 */
|
/** 接收消息数 */
|
||||||
messagesReceived: number;
|
messagesReceived: number;
|
||||||
/** 平均延迟 (ms) */
|
/** 平均延迟 */
|
||||||
averageLatency: number;
|
averageLatency: number;
|
||||||
/** 丢包率 (%) */
|
/** 丢包率 */
|
||||||
packetLoss: number;
|
packetLoss: number;
|
||||||
/** 带宽使用率 (bytes/s) */
|
/** 连接时长 */
|
||||||
bandwidth: number;
|
connectionTime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 网络消息基类
|
* 向量2D
|
||||||
*/
|
*/
|
||||||
export interface NetworkMessage {
|
export interface IVector2 {
|
||||||
/** 消息类型 */
|
x: number;
|
||||||
type: NetworkMessageType;
|
y: number;
|
||||||
/** 网络对象ID */
|
}
|
||||||
networkId: number;
|
|
||||||
/** 消息数据 */
|
/**
|
||||||
data: SerializableObject;
|
* 向量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;
|
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';
|
import 'reflect-metadata';
|
||||||
|
|
||||||
global.beforeEach(() => {
|
// 全局测试配置
|
||||||
jest.clearAllMocks();
|
beforeAll(() => {
|
||||||
|
// 设置测试环境
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
});
|
});
|
||||||
|
|
||||||
global.afterEach(() => {
|
afterAll(() => {
|
||||||
jest.restoreAllMocks();
|
// 清理测试环境
|
||||||
|
});
|
||||||
|
|
||||||
|
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",
|
"outDir": "./bin",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"composite": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
@@ -40,7 +41,11 @@
|
|||||||
"node_modules",
|
"node_modules",
|
||||||
"bin",
|
"bin",
|
||||||
"**/*.test.ts",
|
"**/*.test.ts",
|
||||||
"**/*.spec.ts",
|
"**/*.spec.ts"
|
||||||
"src/protocol/analyzer/**/*"
|
],
|
||||||
|
"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