@esengine/server 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-O3VN2QVN.js +338 -0
- package/dist/chunk-O3VN2QVN.js.map +1 -0
- package/dist/{chunk-7C6JZO4O.js → chunk-QWEIP5QH.js} +3 -335
- package/dist/chunk-QWEIP5QH.js.map +1 -0
- package/dist/decorators-DY8nZ8Nh.d.ts +26 -0
- package/dist/ecs/index.d.ts +159 -0
- package/dist/ecs/index.js +247 -0
- package/dist/ecs/index.js.map +1 -0
- package/dist/index.d.ts +2 -26
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/index.js.map +1 -1
- package/package.json +12 -3
- package/dist/chunk-7C6JZO4O.js.map +0 -1
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { Room } from '../chunk-O3VN2QVN.js';
|
|
2
|
+
export { onMessage } from '../chunk-O3VN2QVN.js';
|
|
3
|
+
import { __name, __publicField } from '../chunk-T626JPC7.js';
|
|
4
|
+
import { Core, encodeDespawn, encodeSnapshot, SyncOperation, encodeSpawn, SYNC_METADATA, CHANGE_TRACKER, initChangeTracker } from '@esengine/ecs-framework';
|
|
5
|
+
export { SyncOperation, clearChanges, getSyncMetadata, hasChanges, hasSyncFields, initChangeTracker, sync } from '@esengine/ecs-framework';
|
|
6
|
+
|
|
7
|
+
var DEFAULT_ECS_CONFIG = {
|
|
8
|
+
syncInterval: 50,
|
|
9
|
+
enableDeltaSync: true
|
|
10
|
+
};
|
|
11
|
+
var NETWORK_ENTITY_OWNER = /* @__PURE__ */ Symbol("NetworkEntityOwner");
|
|
12
|
+
var _ECSRoom = class _ECSRoom extends Room {
|
|
13
|
+
constructor(ecsConfig) {
|
|
14
|
+
super();
|
|
15
|
+
/**
|
|
16
|
+
* @zh ECS World(由 Core.worldManager 管理)
|
|
17
|
+
* @en ECS World (managed by Core.worldManager)
|
|
18
|
+
*/
|
|
19
|
+
__publicField(this, "world");
|
|
20
|
+
/**
|
|
21
|
+
* @zh World 在 WorldManager 中的 ID
|
|
22
|
+
* @en World ID in WorldManager
|
|
23
|
+
*/
|
|
24
|
+
__publicField(this, "worldId");
|
|
25
|
+
/**
|
|
26
|
+
* @zh 房间的主场景
|
|
27
|
+
* @en Room's main scene
|
|
28
|
+
*/
|
|
29
|
+
__publicField(this, "scene");
|
|
30
|
+
/**
|
|
31
|
+
* @zh ECS 配置
|
|
32
|
+
* @en ECS configuration
|
|
33
|
+
*/
|
|
34
|
+
__publicField(this, "ecsConfig");
|
|
35
|
+
/**
|
|
36
|
+
* @zh 玩家 ID 到实体的映射
|
|
37
|
+
* @en Player ID to Entity mapping
|
|
38
|
+
*/
|
|
39
|
+
__publicField(this, "_playerEntities", /* @__PURE__ */ new Map());
|
|
40
|
+
/**
|
|
41
|
+
* @zh 上次同步时间
|
|
42
|
+
* @en Last sync time
|
|
43
|
+
*/
|
|
44
|
+
__publicField(this, "_lastSyncTime", 0);
|
|
45
|
+
this.ecsConfig = {
|
|
46
|
+
...DEFAULT_ECS_CONFIG,
|
|
47
|
+
...ecsConfig
|
|
48
|
+
};
|
|
49
|
+
this.worldId = `room_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
50
|
+
this.world = Core.worldManager.createWorld(this.worldId);
|
|
51
|
+
this.scene = this.world.createScene("game");
|
|
52
|
+
this.world.setSceneActive("game", true);
|
|
53
|
+
this.world.start();
|
|
54
|
+
}
|
|
55
|
+
// =========================================================================
|
|
56
|
+
// Scene Management | 场景管理
|
|
57
|
+
// =========================================================================
|
|
58
|
+
/**
|
|
59
|
+
* @zh 添加系统到场景
|
|
60
|
+
* @en Add system to scene
|
|
61
|
+
*/
|
|
62
|
+
addSystem(system) {
|
|
63
|
+
this.scene.addSystem(system);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* @zh 创建实体
|
|
67
|
+
* @en Create entity
|
|
68
|
+
*/
|
|
69
|
+
createEntity(name) {
|
|
70
|
+
return this.scene.createEntity(name ?? `entity_${Date.now()}`);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* @zh 为玩家创建实体
|
|
74
|
+
* @en Create entity for player
|
|
75
|
+
*
|
|
76
|
+
* @param playerId - @zh 玩家 ID @en Player ID
|
|
77
|
+
* @param name - @zh 实体名称 @en Entity name
|
|
78
|
+
* @returns @zh 创建的实体 @en Created entity
|
|
79
|
+
*/
|
|
80
|
+
createPlayerEntity(playerId, name) {
|
|
81
|
+
const entityName = name ?? `player_${playerId}`;
|
|
82
|
+
const entity = this.scene.createEntity(entityName);
|
|
83
|
+
entity[NETWORK_ENTITY_OWNER] = playerId;
|
|
84
|
+
this._playerEntities.set(playerId, entity);
|
|
85
|
+
return entity;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* @zh 获取玩家的实体
|
|
89
|
+
* @en Get player's entity
|
|
90
|
+
*/
|
|
91
|
+
getPlayerEntity(playerId) {
|
|
92
|
+
return this._playerEntities.get(playerId);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* @zh 销毁玩家的实体
|
|
96
|
+
* @en Destroy player's entity
|
|
97
|
+
*/
|
|
98
|
+
destroyPlayerEntity(playerId) {
|
|
99
|
+
const entity = this._playerEntities.get(playerId);
|
|
100
|
+
if (entity) {
|
|
101
|
+
const despawnData = encodeDespawn(entity.id);
|
|
102
|
+
this.broadcastBinary(despawnData);
|
|
103
|
+
entity.destroy();
|
|
104
|
+
this._playerEntities.delete(playerId);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// =========================================================================
|
|
108
|
+
// State Sync | 状态同步
|
|
109
|
+
// =========================================================================
|
|
110
|
+
/**
|
|
111
|
+
* @zh 广播二进制数据
|
|
112
|
+
* @en Broadcast binary data
|
|
113
|
+
*/
|
|
114
|
+
broadcastBinary(data) {
|
|
115
|
+
for (const player of this.players) {
|
|
116
|
+
this.sendBinary(player, data);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* @zh 发送二进制数据给指定玩家
|
|
121
|
+
* @en Send binary data to specific player
|
|
122
|
+
*/
|
|
123
|
+
sendBinary(player, data) {
|
|
124
|
+
player.send("$sync", {
|
|
125
|
+
data: Array.from(data)
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* @zh 发送完整状态给玩家(用于玩家刚加入时)
|
|
130
|
+
* @en Send full state to player (for when player just joined)
|
|
131
|
+
*/
|
|
132
|
+
sendFullState(player) {
|
|
133
|
+
const entities = this._getSyncEntities();
|
|
134
|
+
if (entities.length === 0) return;
|
|
135
|
+
for (const entity of entities) {
|
|
136
|
+
this._initComponentTrackers(entity);
|
|
137
|
+
}
|
|
138
|
+
const data = encodeSnapshot(entities, SyncOperation.FULL);
|
|
139
|
+
this.sendBinary(player, data);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* @zh 广播实体生成
|
|
143
|
+
* @en Broadcast entity spawn
|
|
144
|
+
*/
|
|
145
|
+
broadcastSpawn(entity, prefabType) {
|
|
146
|
+
this._initComponentTrackers(entity);
|
|
147
|
+
const data = encodeSpawn(entity, prefabType);
|
|
148
|
+
this.broadcastBinary(data);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* @zh 广播增量状态更新
|
|
152
|
+
* @en Broadcast delta state update
|
|
153
|
+
*/
|
|
154
|
+
broadcastDelta() {
|
|
155
|
+
const entities = this._getSyncEntities();
|
|
156
|
+
const changedEntities = entities.filter((entity) => this._hasChanges(entity));
|
|
157
|
+
if (changedEntities.length === 0) return;
|
|
158
|
+
const data = encodeSnapshot(changedEntities, SyncOperation.DELTA);
|
|
159
|
+
this.broadcastBinary(data);
|
|
160
|
+
this._clearChangeTrackers(changedEntities);
|
|
161
|
+
}
|
|
162
|
+
// =========================================================================
|
|
163
|
+
// Lifecycle Overrides | 生命周期重载
|
|
164
|
+
// =========================================================================
|
|
165
|
+
/**
|
|
166
|
+
* @zh 游戏循环,处理状态同步
|
|
167
|
+
* @en Game tick, handles state sync
|
|
168
|
+
*/
|
|
169
|
+
onTick(_dt) {
|
|
170
|
+
if (this.ecsConfig.enableDeltaSync) {
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
if (now - this._lastSyncTime >= this.ecsConfig.syncInterval) {
|
|
173
|
+
this._lastSyncTime = now;
|
|
174
|
+
this.broadcastDelta();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* @zh 玩家离开时自动销毁其实体
|
|
180
|
+
* @en Auto destroy player entity when leaving
|
|
181
|
+
*/
|
|
182
|
+
async onLeave(player, reason) {
|
|
183
|
+
this.destroyPlayerEntity(player.id);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* @zh 房间销毁时从 WorldManager 移除 World
|
|
187
|
+
* @en Remove World from WorldManager when room is disposed
|
|
188
|
+
*/
|
|
189
|
+
onDispose() {
|
|
190
|
+
this._playerEntities.clear();
|
|
191
|
+
Core.worldManager.removeWorld(this.worldId);
|
|
192
|
+
}
|
|
193
|
+
// =========================================================================
|
|
194
|
+
// Internal | 内部方法
|
|
195
|
+
// =========================================================================
|
|
196
|
+
_getSyncEntities() {
|
|
197
|
+
const entities = [];
|
|
198
|
+
for (const entity of this.scene.entities.buffer) {
|
|
199
|
+
if (this._hasSyncComponents(entity)) {
|
|
200
|
+
entities.push(entity);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return entities;
|
|
204
|
+
}
|
|
205
|
+
_hasSyncComponents(entity) {
|
|
206
|
+
for (const component of entity.components) {
|
|
207
|
+
const metadata = component.constructor[SYNC_METADATA];
|
|
208
|
+
if (metadata && metadata.fields.length > 0) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
_hasChanges(entity) {
|
|
215
|
+
for (const component of entity.components) {
|
|
216
|
+
const tracker = component[CHANGE_TRACKER];
|
|
217
|
+
if (tracker?.hasChanges()) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
_initComponentTrackers(entity) {
|
|
224
|
+
for (const component of entity.components) {
|
|
225
|
+
const metadata = component.constructor[SYNC_METADATA];
|
|
226
|
+
if (metadata && metadata.fields.length > 0) {
|
|
227
|
+
initChangeTracker(component);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
_clearChangeTrackers(entities) {
|
|
232
|
+
for (const entity of entities) {
|
|
233
|
+
for (const component of entity.components) {
|
|
234
|
+
const tracker = component[CHANGE_TRACKER];
|
|
235
|
+
if (tracker) {
|
|
236
|
+
tracker.clear();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
__name(_ECSRoom, "ECSRoom");
|
|
243
|
+
var ECSRoom = _ECSRoom;
|
|
244
|
+
|
|
245
|
+
export { ECSRoom };
|
|
246
|
+
//# sourceMappingURL=index.js.map
|
|
247
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/ecs/ECSRoom.ts"],"names":["DEFAULT_ECS_CONFIG","syncInterval","enableDeltaSync","NETWORK_ENTITY_OWNER","ECSRoom","Room","ecsConfig","world","worldId","scene","_playerEntities","Map","_lastSyncTime","Date","now","Math","random","toString","slice","Core","worldManager","createWorld","createScene","setSceneActive","start","addSystem","system","createEntity","name","createPlayerEntity","playerId","entityName","entity","set","getPlayerEntity","get","destroyPlayerEntity","despawnData","encodeDespawn","id","broadcastBinary","destroy","delete","data","player","players","sendBinary","send","Array","from","sendFullState","entities","_getSyncEntities","length","_initComponentTrackers","encodeSnapshot","SyncOperation","FULL","broadcastSpawn","prefabType","encodeSpawn","broadcastDelta","changedEntities","filter","_hasChanges","DELTA","_clearChangeTrackers","onTick","_dt","onLeave","reason","onDispose","clear","removeWorld","buffer","_hasSyncComponents","push","component","components","metadata","SYNC_METADATA","fields","tracker","CHANGE_TRACKER","hasChanges","initChangeTracker"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @zh ECS 房间基类\n * @en ECS Room base class\n */\n\nimport {\n Core,\n Scene,\n World,\n Entity,\n EntitySystem,\n type Component,\n // Sync\n SyncOperation,\n SYNC_METADATA,\n CHANGE_TRACKER,\n type SyncMetadata,\n type ChangeTracker,\n encodeSnapshot,\n encodeSpawn,\n encodeDespawn,\n initChangeTracker,\n} from '@esengine/ecs-framework';\n\nimport { Room, type RoomOptions } from '../room/Room.js';\nimport type { Player } from '../room/Player.js';\n\n// =============================================================================\n// Types | 类型定义\n// =============================================================================\n\n/**\n * @zh ECS 房间配置\n * @en ECS room configuration\n */\nexport interface ECSRoomConfig {\n /**\n * @zh 状态同步间隔(毫秒)\n * @en State sync interval in milliseconds\n */\n syncInterval: number;\n\n /**\n * @zh 是否启用增量同步\n * @en Whether to enable delta sync\n */\n enableDeltaSync: boolean;\n}\n\nconst DEFAULT_ECS_CONFIG: ECSRoomConfig = {\n syncInterval: 50, // 20 Hz\n enableDeltaSync: true,\n};\n\n/**\n * @zh 网络实体标识组件\n * @en Network entity identity component\n */\nconst NETWORK_ENTITY_OWNER = Symbol('NetworkEntityOwner');\n\n// =============================================================================\n// ECSRoom | ECS 房间\n// =============================================================================\n\n/**\n * @zh ECS 房间基类,带有 ECS World 支持和自动状态同步\n * @en ECS Room base class with ECS World support and automatic state synchronization\n *\n * @example\n * ```typescript\n * // 服务端启动\n * Core.create();\n * setInterval(() => Core.update(1/60), 16);\n *\n * // 定义房间\n * class GameRoom extends ECSRoom {\n * onCreate() {\n * this.addSystem(new PhysicsSystem());\n * }\n *\n * onJoin(player: Player) {\n * const entity = this.createPlayerEntity(player.id);\n * entity.addComponent(new PlayerComponent());\n * }\n * }\n * ```\n */\nexport abstract class ECSRoom<TState = any, TPlayerData = Record<string, unknown>> extends Room<TState, TPlayerData> {\n /**\n * @zh ECS World(由 Core.worldManager 管理)\n * @en ECS World (managed by Core.worldManager)\n */\n protected readonly world: World;\n\n /**\n * @zh World 在 WorldManager 中的 ID\n * @en World ID in WorldManager\n */\n protected readonly worldId: string;\n\n /**\n * @zh 房间的主场景\n * @en Room's main scene\n */\n protected readonly scene: Scene;\n\n /**\n * @zh ECS 配置\n * @en ECS configuration\n */\n protected readonly ecsConfig: ECSRoomConfig;\n\n /**\n * @zh 玩家 ID 到实体的映射\n * @en Player ID to Entity mapping\n */\n private readonly _playerEntities: Map<string, Entity> = new Map();\n\n /**\n * @zh 上次同步时间\n * @en Last sync time\n */\n private _lastSyncTime: number = 0;\n\n constructor(ecsConfig?: Partial<ECSRoomConfig>) {\n super();\n this.ecsConfig = { ...DEFAULT_ECS_CONFIG, ...ecsConfig };\n\n this.worldId = `room_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;\n this.world = Core.worldManager.createWorld(this.worldId);\n this.scene = this.world.createScene('game');\n this.world.setSceneActive('game', true);\n this.world.start();\n }\n\n // =========================================================================\n // Scene Management | 场景管理\n // =========================================================================\n\n /**\n * @zh 添加系统到场景\n * @en Add system to scene\n */\n protected addSystem(system: EntitySystem): void {\n this.scene.addSystem(system);\n }\n\n /**\n * @zh 创建实体\n * @en Create entity\n */\n protected createEntity(name?: string): Entity {\n return this.scene.createEntity(name ?? `entity_${Date.now()}`);\n }\n\n /**\n * @zh 为玩家创建实体\n * @en Create entity for player\n *\n * @param playerId - @zh 玩家 ID @en Player ID\n * @param name - @zh 实体名称 @en Entity name\n * @returns @zh 创建的实体 @en Created entity\n */\n protected createPlayerEntity(playerId: string, name?: string): Entity {\n const entityName = name ?? `player_${playerId}`;\n const entity = this.scene.createEntity(entityName);\n (entity as any)[NETWORK_ENTITY_OWNER] = playerId;\n this._playerEntities.set(playerId, entity);\n return entity;\n }\n\n /**\n * @zh 获取玩家的实体\n * @en Get player's entity\n */\n protected getPlayerEntity(playerId: string): Entity | undefined {\n return this._playerEntities.get(playerId);\n }\n\n /**\n * @zh 销毁玩家的实体\n * @en Destroy player's entity\n */\n protected destroyPlayerEntity(playerId: string): void {\n const entity = this._playerEntities.get(playerId);\n if (entity) {\n const despawnData = encodeDespawn(entity.id);\n this.broadcastBinary(despawnData);\n entity.destroy();\n this._playerEntities.delete(playerId);\n }\n }\n\n // =========================================================================\n // State Sync | 状态同步\n // =========================================================================\n\n /**\n * @zh 广播二进制数据\n * @en Broadcast binary data\n */\n protected broadcastBinary(data: Uint8Array): void {\n for (const player of this.players) {\n this.sendBinary(player, data);\n }\n }\n\n /**\n * @zh 发送二进制数据给指定玩家\n * @en Send binary data to specific player\n */\n protected sendBinary(player: Player<TPlayerData>, data: Uint8Array): void {\n player.send('$sync', { data: Array.from(data) });\n }\n\n /**\n * @zh 发送完整状态给玩家(用于玩家刚加入时)\n * @en Send full state to player (for when player just joined)\n */\n protected sendFullState(player: Player<TPlayerData>): void {\n const entities = this._getSyncEntities();\n if (entities.length === 0) return;\n\n for (const entity of entities) {\n this._initComponentTrackers(entity);\n }\n\n const data = encodeSnapshot(entities, SyncOperation.FULL);\n this.sendBinary(player, data);\n }\n\n /**\n * @zh 广播实体生成\n * @en Broadcast entity spawn\n */\n protected broadcastSpawn(entity: Entity, prefabType?: string): void {\n this._initComponentTrackers(entity);\n const data = encodeSpawn(entity, prefabType);\n this.broadcastBinary(data);\n }\n\n /**\n * @zh 广播增量状态更新\n * @en Broadcast delta state update\n */\n protected broadcastDelta(): void {\n const entities = this._getSyncEntities();\n const changedEntities = entities.filter(entity => this._hasChanges(entity));\n\n if (changedEntities.length === 0) return;\n\n const data = encodeSnapshot(changedEntities, SyncOperation.DELTA);\n this.broadcastBinary(data);\n this._clearChangeTrackers(changedEntities);\n }\n\n // =========================================================================\n // Lifecycle Overrides | 生命周期重载\n // =========================================================================\n\n /**\n * @zh 游戏循环,处理状态同步\n * @en Game tick, handles state sync\n */\n override onTick(_dt: number): void {\n if (this.ecsConfig.enableDeltaSync) {\n const now = Date.now();\n if (now - this._lastSyncTime >= this.ecsConfig.syncInterval) {\n this._lastSyncTime = now;\n this.broadcastDelta();\n }\n }\n }\n\n /**\n * @zh 玩家离开时自动销毁其实体\n * @en Auto destroy player entity when leaving\n */\n override async onLeave(player: Player<TPlayerData>, reason?: string): Promise<void> {\n this.destroyPlayerEntity(player.id);\n }\n\n /**\n * @zh 房间销毁时从 WorldManager 移除 World\n * @en Remove World from WorldManager when room is disposed\n */\n override onDispose(): void {\n this._playerEntities.clear();\n Core.worldManager.removeWorld(this.worldId);\n }\n\n // =========================================================================\n // Internal | 内部方法\n // =========================================================================\n\n private _getSyncEntities(): Entity[] {\n const entities: Entity[] = [];\n for (const entity of this.scene.entities.buffer) {\n if (this._hasSyncComponents(entity)) {\n entities.push(entity);\n }\n }\n return entities;\n }\n\n private _hasSyncComponents(entity: Entity): boolean {\n for (const component of entity.components) {\n const metadata: SyncMetadata | undefined = (component.constructor as any)[SYNC_METADATA];\n if (metadata && metadata.fields.length > 0) {\n return true;\n }\n }\n return false;\n }\n\n private _hasChanges(entity: Entity): boolean {\n for (const component of entity.components) {\n const tracker = (component as any)[CHANGE_TRACKER] as ChangeTracker | undefined;\n if (tracker?.hasChanges()) {\n return true;\n }\n }\n return false;\n }\n\n private _initComponentTrackers(entity: Entity): void {\n for (const component of entity.components) {\n const metadata: SyncMetadata | undefined = (component.constructor as any)[SYNC_METADATA];\n if (metadata && metadata.fields.length > 0) {\n initChangeTracker(component);\n }\n }\n }\n\n private _clearChangeTrackers(entities: Entity[]): void {\n for (const entity of entities) {\n for (const component of entity.components) {\n const tracker = (component as any)[CHANGE_TRACKER] as ChangeTracker | undefined;\n if (tracker) {\n tracker.clear();\n }\n }\n }\n }\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { S as ServerConfig, G as GameServer, A as ApiDefinition, M as MsgDefinition } from './index-DgaJIm6-.js';
|
|
2
2
|
export { b as ApiContext, c as MsgContext, a as ServerConnection } from './index-DgaJIm6-.js';
|
|
3
3
|
export { I as IPlayer, P as Player, R as Room, a as RoomOptions } from './Room-BnKpl5Sj.js';
|
|
4
|
+
export { o as onMessage } from './decorators-DY8nZ8Nh.js';
|
|
4
5
|
export { ErrorCode, RpcError } from '@esengine/rpc';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -70,29 +71,4 @@ declare function defineApi<TReq, TRes, TData = Record<string, unknown>>(definiti
|
|
|
70
71
|
*/
|
|
71
72
|
declare function defineMsg<TMsg, TData = Record<string, unknown>>(definition: MsgDefinition<TMsg, TData>): MsgDefinition<TMsg, TData>;
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
* @zh 房间装饰器
|
|
75
|
-
* @en Room decorators
|
|
76
|
-
*/
|
|
77
|
-
/**
|
|
78
|
-
* @zh 消息处理器装饰器
|
|
79
|
-
* @en Message handler decorator
|
|
80
|
-
*
|
|
81
|
-
* @example
|
|
82
|
-
* ```typescript
|
|
83
|
-
* class GameRoom extends Room {
|
|
84
|
-
* @onMessage('Move')
|
|
85
|
-
* handleMove(data: { x: number, y: number }, player: Player) {
|
|
86
|
-
* // handle move
|
|
87
|
-
* }
|
|
88
|
-
*
|
|
89
|
-
* @onMessage('Chat')
|
|
90
|
-
* handleChat(data: { text: string }, player: Player) {
|
|
91
|
-
* this.broadcast('Chat', { from: player.id, text: data.text })
|
|
92
|
-
* }
|
|
93
|
-
* }
|
|
94
|
-
* ```
|
|
95
|
-
*/
|
|
96
|
-
declare function onMessage(type: string): MethodDecorator;
|
|
97
|
-
|
|
98
|
-
export { ApiDefinition, GameServer, MsgDefinition, ServerConfig, createServer, defineApi, defineMsg, onMessage };
|
|
74
|
+
export { ApiDefinition, GameServer, MsgDefinition, ServerConfig, createServer, defineApi, defineMsg };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { createServer } from './chunk-QWEIP5QH.js';
|
|
2
|
+
export { Player, Room, onMessage } from './chunk-O3VN2QVN.js';
|
|
2
3
|
import { __name } from './chunk-T626JPC7.js';
|
|
3
4
|
export { ErrorCode, RpcError } from '@esengine/rpc';
|
|
4
5
|
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/helpers/define.ts"],"names":["defineApi","definition","defineMsg"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/helpers/define.ts"],"names":["defineApi","definition","defineMsg"],"mappings":";;;;;;AAwBO,SAASA,UACZC,UAAAA,EAA4C;AAE5C,EAAA,OAAOA,UAAAA;AACX;AAJgBD,MAAAA,CAAAA,SAAAA,EAAAA,WAAAA,CAAAA;AAsBT,SAASE,UACZD,UAAAA,EAAsC;AAEtC,EAAA,OAAOA,UAAAA;AACX;AAJgBC,MAAAA,CAAAA,SAAAA,EAAAA,WAAAA,CAAAA","file":"index.js","sourcesContent":["/**\n * @zh API 和消息定义助手\n * @en API and message definition helpers\n */\n\nimport type { ApiDefinition, MsgDefinition } from '../types/index.js'\n\n/**\n * @zh 定义 API 处理器\n * @en Define API handler\n *\n * @example\n * ```typescript\n * // src/api/join.ts\n * import { defineApi } from '@esengine/server'\n *\n * export default defineApi<ReqJoin, ResJoin>({\n * handler(req, ctx) {\n * ctx.conn.data.playerId = generateId()\n * return { playerId: ctx.conn.data.playerId }\n * }\n * })\n * ```\n */\nexport function defineApi<TReq, TRes, TData = Record<string, unknown>>(\n definition: ApiDefinition<TReq, TRes, TData>\n): ApiDefinition<TReq, TRes, TData> {\n return definition\n}\n\n/**\n * @zh 定义消息处理器\n * @en Define message handler\n *\n * @example\n * ```typescript\n * // src/msg/input.ts\n * import { defineMsg } from '@esengine/server'\n *\n * export default defineMsg<MsgInput>({\n * handler(msg, ctx) {\n * console.log('Input from', ctx.conn.id, msg)\n * }\n * })\n * ```\n */\nexport function defineMsg<TMsg, TData = Record<string, unknown>>(\n definition: MsgDefinition<TMsg, TData>\n): MsgDefinition<TMsg, TData> {\n return definition\n}\n"]}
|
package/dist/testing/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createServer } from '../chunk-QWEIP5QH.js';
|
|
2
|
+
import { onMessage, Room } from '../chunk-O3VN2QVN.js';
|
|
2
3
|
import { __name, __publicField } from '../chunk-T626JPC7.js';
|
|
3
4
|
import WebSocket from 'ws';
|
|
4
5
|
import { json } from '@esengine/rpc/codec';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/testing/TestClient.ts","../../src/testing/TestServer.ts","../../src/testing/MockRoom.ts"],"names":["PacketType","ApiRequest","ApiResponse","ApiError","Message","TestClient","port","options","_port","_codec","_timeout","_connectTimeout","_ws","_callIdCounter","_connected","_currentRoomId","_currentPlayerId","_pendingCalls","Map","_msgHandlers","_receivedMessages","codec","json","timeout","connectTimeout","isConnected","roomId","playerId","receivedMessages","connect","Promise","resolve","reject","url","WebSocket","setTimeout","close","Error","on","clearTimeout","_rejectAllPending","err","data","_handleMessage","disconnect","readyState","CLOSED","once","joinRoom","roomType","result","call","joinRoomById","leaveRoom","sendToRoom","type","send","name","input","id","timer","delete","set","packet","encode","handler","handlers","get","Set","add","off","waitForMessage","timeoutMs","waitForRoomMessage","msg","hasReceivedMessage","some","m","getMessagesOfType","filter","map","getLastMessage","i","length","undefined","clearMessages","getMessageCount","raw","decode","_handleApiResponse","_handleApiError","_handleMsg","console","error","pending","code","message","push","timestamp","Date","now","reason","clear","getRandomPort","net","server","createServer","listen","address","createTestServer","silent","originalLog","log","tickRate","apiDir","msgDir","start","cleanup","stop","createTestEnv","serverCleanup","clients","createClient","clientOptions","client","createClients","count","newClients","all","c","catch","MockRoom","Room","state","messages","joinCount","leaveCount","onCreate","onJoin","player","broadcast","onLeave","handleAnyMessage","from","handleEcho","handleBroadcast","_player","handlePing","_data","EchoRoom","BroadcastRoom"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @zh 测试客户端\n * @en Test client for server testing\n */\n\nimport WebSocket from 'ws'\nimport { json } from '@esengine/rpc/codec'\nimport type { Codec } from '@esengine/rpc/codec'\n\n// ============================================================================\n// Types | 类型定义\n// ============================================================================\n\n/**\n * @zh 测试客户端配置\n * @en Test client options\n */\nexport interface TestClientOptions {\n /**\n * @zh 编解码器\n * @en Codec\n * @defaultValue json()\n */\n codec?: Codec\n\n /**\n * @zh API 调用超时(毫秒)\n * @en API call timeout in milliseconds\n * @defaultValue 5000\n */\n timeout?: number\n\n /**\n * @zh 连接超时(毫秒)\n * @en Connection timeout in milliseconds\n * @defaultValue 5000\n */\n connectTimeout?: number\n}\n\n/**\n * @zh 房间加入结果\n * @en Room join result\n */\nexport interface JoinRoomResult {\n roomId: string\n playerId: string\n}\n\n/**\n * @zh 收到的消息记录\n * @en Received message record\n */\nexport interface ReceivedMessage {\n type: string\n data: unknown\n timestamp: number\n}\n\n// ============================================================================\n// Constants | 常量\n// ============================================================================\n\nconst PacketType = {\n ApiRequest: 0,\n ApiResponse: 1,\n ApiError: 2,\n Message: 3,\n} as const\n\n// ============================================================================\n// TestClient Class | 测试客户端类\n// ============================================================================\n\ninterface PendingCall {\n resolve: (value: unknown) => void\n reject: (error: Error) => void\n timer: ReturnType<typeof setTimeout>\n}\n\n/**\n * @zh 测试客户端\n * @en Test client for server integration testing\n *\n * @zh 专为测试设计的客户端,提供便捷的断言方法和消息记录功能\n * @en Client designed for testing, with convenient assertion methods and message recording\n *\n * @example\n * ```typescript\n * const client = new TestClient(3000)\n * await client.connect()\n *\n * // 加入房间\n * const { roomId } = await client.joinRoom('game')\n *\n * // 发送消息\n * client.sendToRoom('Move', { x: 10, y: 20 })\n *\n * // 等待收到特定消息\n * const msg = await client.waitForMessage('PlayerMoved')\n *\n * // 断言收到消息\n * expect(client.hasReceivedMessage('PlayerMoved')).toBe(true)\n *\n * await client.disconnect()\n * ```\n */\nexport class TestClient {\n private readonly _port: number\n private readonly _codec: Codec\n private readonly _timeout: number\n private readonly _connectTimeout: number\n\n private _ws: WebSocket | null = null\n private _callIdCounter = 0\n private _connected = false\n private _currentRoomId: string | null = null\n private _currentPlayerId: string | null = null\n\n private readonly _pendingCalls = new Map<number, PendingCall>()\n private readonly _msgHandlers = new Map<string, Set<(data: unknown) => void>>()\n private readonly _receivedMessages: ReceivedMessage[] = []\n\n constructor(port: number, options: TestClientOptions = {}) {\n this._port = port\n this._codec = options.codec ?? json()\n this._timeout = options.timeout ?? 5000\n this._connectTimeout = options.connectTimeout ?? 5000\n }\n\n // ========================================================================\n // Properties | 属性\n // ========================================================================\n\n /**\n * @zh 是否已连接\n * @en Whether connected\n */\n get isConnected(): boolean {\n return this._connected\n }\n\n /**\n * @zh 当前房间 ID\n * @en Current room ID\n */\n get roomId(): string | null {\n return this._currentRoomId\n }\n\n /**\n * @zh 当前玩家 ID\n * @en Current player ID\n */\n get playerId(): string | null {\n return this._currentPlayerId\n }\n\n /**\n * @zh 收到的所有消息\n * @en All received messages\n */\n get receivedMessages(): ReadonlyArray<ReceivedMessage> {\n return this._receivedMessages\n }\n\n // ========================================================================\n // Connection | 连接管理\n // ========================================================================\n\n /**\n * @zh 连接到服务器\n * @en Connect to server\n */\n connect(): Promise<this> {\n return new Promise((resolve, reject) => {\n const url = `ws://localhost:${this._port}`\n this._ws = new WebSocket(url)\n\n const timeout = setTimeout(() => {\n this._ws?.close()\n reject(new Error(`Connection timeout after ${this._connectTimeout}ms`))\n }, this._connectTimeout)\n\n this._ws.on('open', () => {\n clearTimeout(timeout)\n this._connected = true\n resolve(this)\n })\n\n this._ws.on('close', () => {\n this._connected = false\n this._rejectAllPending('Connection closed')\n })\n\n this._ws.on('error', (err) => {\n clearTimeout(timeout)\n if (!this._connected) {\n reject(err)\n }\n })\n\n this._ws.on('message', (data: Buffer) => {\n this._handleMessage(data)\n })\n })\n }\n\n /**\n * @zh 断开连接\n * @en Disconnect from server\n */\n async disconnect(): Promise<void> {\n return new Promise((resolve) => {\n if (!this._ws || this._ws.readyState === WebSocket.CLOSED) {\n resolve()\n return\n }\n\n this._ws.once('close', () => {\n this._connected = false\n this._ws = null\n resolve()\n })\n\n this._ws.close()\n })\n }\n\n // ========================================================================\n // Room Operations | 房间操作\n // ========================================================================\n\n /**\n * @zh 加入房间\n * @en Join a room\n */\n async joinRoom(roomType: string, options?: Record<string, unknown>): Promise<JoinRoomResult> {\n const result = await this.call<JoinRoomResult>('JoinRoom', { roomType, options })\n this._currentRoomId = result.roomId\n this._currentPlayerId = result.playerId\n return result\n }\n\n /**\n * @zh 通过 ID 加入房间\n * @en Join a room by ID\n */\n async joinRoomById(roomId: string): Promise<JoinRoomResult> {\n const result = await this.call<JoinRoomResult>('JoinRoom', { roomId })\n this._currentRoomId = result.roomId\n this._currentPlayerId = result.playerId\n return result\n }\n\n /**\n * @zh 离开房间\n * @en Leave room\n */\n async leaveRoom(): Promise<void> {\n await this.call('LeaveRoom', {})\n this._currentRoomId = null\n this._currentPlayerId = null\n }\n\n /**\n * @zh 发送消息到房间\n * @en Send message to room\n */\n sendToRoom(type: string, data: unknown): void {\n this.send('RoomMessage', { type, data })\n }\n\n // ========================================================================\n // API Calls | API 调用\n // ========================================================================\n\n /**\n * @zh 调用 API\n * @en Call API\n */\n call<T = unknown>(name: string, input: unknown): Promise<T> {\n return new Promise((resolve, reject) => {\n if (!this._connected || !this._ws) {\n reject(new Error('Not connected'))\n return\n }\n\n const id = ++this._callIdCounter\n const timer = setTimeout(() => {\n this._pendingCalls.delete(id)\n reject(new Error(`API call '${name}' timeout after ${this._timeout}ms`))\n }, this._timeout)\n\n this._pendingCalls.set(id, {\n resolve: resolve as (v: unknown) => void,\n reject,\n timer,\n })\n\n const packet = [PacketType.ApiRequest, id, name, input]\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n this._ws.send(this._codec.encode(packet as any) as Buffer)\n })\n }\n\n /**\n * @zh 发送消息\n * @en Send message\n */\n send(name: string, data: unknown): void {\n if (!this._connected || !this._ws) return\n const packet = [PacketType.Message, name, data]\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n this._ws.send(this._codec.encode(packet as any) as Buffer)\n }\n\n // ========================================================================\n // Message Handling | 消息处理\n // ========================================================================\n\n /**\n * @zh 监听消息\n * @en Listen for message\n */\n on(name: string, handler: (data: unknown) => void): this {\n let handlers = this._msgHandlers.get(name)\n if (!handlers) {\n handlers = new Set()\n this._msgHandlers.set(name, handlers)\n }\n handlers.add(handler)\n return this\n }\n\n /**\n * @zh 取消监听消息\n * @en Remove message listener\n */\n off(name: string, handler?: (data: unknown) => void): this {\n if (handler) {\n this._msgHandlers.get(name)?.delete(handler)\n } else {\n this._msgHandlers.delete(name)\n }\n return this\n }\n\n /**\n * @zh 等待收到指定消息\n * @en Wait for a specific message\n */\n waitForMessage<T = unknown>(type: string, timeout?: number): Promise<T> {\n return new Promise((resolve, reject) => {\n const timeoutMs = timeout ?? this._timeout\n\n const timer = setTimeout(() => {\n this.off(type, handler)\n reject(new Error(`Timeout waiting for message '${type}' after ${timeoutMs}ms`))\n }, timeoutMs)\n\n const handler = (data: unknown) => {\n clearTimeout(timer)\n this.off(type, handler)\n resolve(data as T)\n }\n\n this.on(type, handler)\n })\n }\n\n /**\n * @zh 等待收到指定房间消息\n * @en Wait for a specific room message\n */\n waitForRoomMessage<T = unknown>(type: string, timeout?: number): Promise<T> {\n return new Promise((resolve, reject) => {\n const timeoutMs = timeout ?? this._timeout\n\n const timer = setTimeout(() => {\n this.off('RoomMessage', handler)\n reject(new Error(`Timeout waiting for room message '${type}' after ${timeoutMs}ms`))\n }, timeoutMs)\n\n const handler = (data: unknown) => {\n const msg = data as { type: string; data: unknown }\n if (msg.type === type) {\n clearTimeout(timer)\n this.off('RoomMessage', handler)\n resolve(msg.data as T)\n }\n }\n\n this.on('RoomMessage', handler)\n })\n }\n\n // ========================================================================\n // Assertions | 断言辅助\n // ========================================================================\n\n /**\n * @zh 是否收到过指定消息\n * @en Whether received a specific message\n */\n hasReceivedMessage(type: string): boolean {\n return this._receivedMessages.some((m) => m.type === type)\n }\n\n /**\n * @zh 获取指定类型的所有消息\n * @en Get all messages of a specific type\n */\n getMessagesOfType<T = unknown>(type: string): T[] {\n return this._receivedMessages\n .filter((m) => m.type === type)\n .map((m) => m.data as T)\n }\n\n /**\n * @zh 获取最后收到的指定类型消息\n * @en Get the last received message of a specific type\n */\n getLastMessage<T = unknown>(type: string): T | undefined {\n for (let i = this._receivedMessages.length - 1; i >= 0; i--) {\n if (this._receivedMessages[i].type === type) {\n return this._receivedMessages[i].data as T\n }\n }\n return undefined\n }\n\n /**\n * @zh 清空消息记录\n * @en Clear message records\n */\n clearMessages(): void {\n this._receivedMessages.length = 0\n }\n\n /**\n * @zh 获取收到的消息数量\n * @en Get received message count\n */\n getMessageCount(type?: string): number {\n if (type) {\n return this._receivedMessages.filter((m) => m.type === type).length\n }\n return this._receivedMessages.length\n }\n\n // ========================================================================\n // Private Methods | 私有方法\n // ========================================================================\n\n private _handleMessage(raw: Buffer): void {\n try {\n const packet = this._codec.decode(raw) as unknown[]\n const type = packet[0] as number\n\n switch (type) {\n case PacketType.ApiResponse:\n this._handleApiResponse([packet[0], packet[1], packet[2]] as [number, number, unknown])\n break\n case PacketType.ApiError:\n this._handleApiError([packet[0], packet[1], packet[2], packet[3]] as [number, number, string, string])\n break\n case PacketType.Message:\n this._handleMsg([packet[0], packet[1], packet[2]] as [number, string, unknown])\n break\n }\n } catch (err) {\n console.error('[TestClient] Failed to handle message:', err)\n }\n }\n\n private _handleApiResponse([, id, result]: [number, number, unknown]): void {\n const pending = this._pendingCalls.get(id)\n if (pending) {\n clearTimeout(pending.timer)\n this._pendingCalls.delete(id)\n pending.resolve(result)\n }\n }\n\n private _handleApiError([, id, code, message]: [number, number, string, string]): void {\n const pending = this._pendingCalls.get(id)\n if (pending) {\n clearTimeout(pending.timer)\n this._pendingCalls.delete(id)\n pending.reject(new Error(`[${code}] ${message}`))\n }\n }\n\n private _handleMsg([, name, data]: [number, string, unknown]): void {\n // 记录消息\n this._receivedMessages.push({\n type: name,\n data,\n timestamp: Date.now(),\n })\n\n // 触发处理器\n const handlers = this._msgHandlers.get(name)\n if (handlers) {\n for (const handler of handlers) {\n try {\n handler(data)\n } catch (err) {\n console.error('[TestClient] Handler error:', err)\n }\n }\n }\n }\n\n private _rejectAllPending(reason: string): void {\n for (const [, pending] of this._pendingCalls) {\n clearTimeout(pending.timer)\n pending.reject(new Error(reason))\n }\n this._pendingCalls.clear()\n }\n}\n","/**\n * @zh 测试服务器工具\n * @en Test server utilities\n */\n\nimport { createServer } from '../core/server.js'\nimport type { GameServer } from '../types/index.js'\nimport { TestClient, type TestClientOptions } from './TestClient.js'\n\n// ============================================================================\n// Types | 类型定义\n// ============================================================================\n\n/**\n * @zh 测试服务器配置\n * @en Test server options\n */\nexport interface TestServerOptions {\n /**\n * @zh 端口号,0 表示随机端口\n * @en Port number, 0 for random port\n * @defaultValue 0\n */\n port?: number\n\n /**\n * @zh Tick 速率\n * @en Tick rate\n * @defaultValue 0\n */\n tickRate?: number\n\n /**\n * @zh 是否禁用控制台日志\n * @en Whether to suppress console logs\n * @defaultValue true\n */\n silent?: boolean\n}\n\n/**\n * @zh 测试环境\n * @en Test environment\n */\nexport interface TestEnvironment {\n /**\n * @zh 服务器实例\n * @en Server instance\n */\n server: GameServer\n\n /**\n * @zh 服务器端口\n * @en Server port\n */\n port: number\n\n /**\n * @zh 创建测试客户端\n * @en Create test client\n */\n createClient(options?: TestClientOptions): Promise<TestClient>\n\n /**\n * @zh 创建多个测试客户端\n * @en Create multiple test clients\n */\n createClients(count: number, options?: TestClientOptions): Promise<TestClient[]>\n\n /**\n * @zh 清理测试环境\n * @en Cleanup test environment\n */\n cleanup(): Promise<void>\n\n /**\n * @zh 所有已创建的客户端\n * @en All created clients\n */\n readonly clients: ReadonlyArray<TestClient>\n}\n\n// ============================================================================\n// Helper Functions | 辅助函数\n// ============================================================================\n\n/**\n * @zh 获取随机可用端口\n * @en Get a random available port\n */\nasync function getRandomPort(): Promise<number> {\n const net = await import('node:net')\n return new Promise((resolve, reject) => {\n const server = net.createServer()\n server.listen(0, () => {\n const address = server.address()\n if (address && typeof address === 'object') {\n const port = address.port\n server.close(() => resolve(port))\n } else {\n server.close(() => reject(new Error('Failed to get port')))\n }\n })\n server.on('error', reject)\n })\n}\n\n/**\n * @zh 等待指定毫秒\n * @en Wait for specified milliseconds\n */\nexport function wait(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n// ============================================================================\n// Factory Functions | 工厂函数\n// ============================================================================\n\n/**\n * @zh 创建测试服务器\n * @en Create test server\n *\n * @example\n * ```typescript\n * const { server, port, cleanup } = await createTestServer()\n * server.define('game', GameRoom)\n *\n * const client = new TestClient(port)\n * await client.connect()\n *\n * // ... run tests ...\n *\n * await cleanup()\n * ```\n */\nexport async function createTestServer(\n options: TestServerOptions = {}\n): Promise<{ server: GameServer; port: number; cleanup: () => Promise<void> }> {\n const port = options.port || (await getRandomPort())\n const silent = options.silent ?? true\n\n // 临时禁用 console.log\n const originalLog = console.log\n if (silent) {\n console.log = () => {}\n }\n\n const server = await createServer({\n port,\n tickRate: options.tickRate ?? 0,\n apiDir: '__non_existent_api__',\n msgDir: '__non_existent_msg__',\n })\n\n await server.start()\n\n // 恢复 console.log\n if (silent) {\n console.log = originalLog\n }\n\n return {\n server,\n port,\n cleanup: async () => {\n await server.stop()\n },\n }\n}\n\n/**\n * @zh 创建完整测试环境\n * @en Create complete test environment\n *\n * @zh 包含服务器、客户端创建和清理功能的完整测试环境\n * @en Complete test environment with server, client creation and cleanup\n *\n * @example\n * ```typescript\n * describe('GameRoom', () => {\n * let env: TestEnvironment\n *\n * beforeEach(async () => {\n * env = await createTestEnv()\n * env.server.define('game', GameRoom)\n * })\n *\n * afterEach(async () => {\n * await env.cleanup()\n * })\n *\n * it('should handle player join', async () => {\n * const client = await env.createClient()\n * const result = await client.joinRoom('game')\n * expect(result.roomId).toBeDefined()\n * })\n *\n * it('should broadcast to all players', async () => {\n * const [client1, client2] = await env.createClients(2)\n *\n * await client1.joinRoom('game')\n * const joinPromise = client1.waitForRoomMessage('PlayerJoined')\n *\n * await client2.joinRoom('game')\n * const msg = await joinPromise\n *\n * expect(msg).toBeDefined()\n * })\n * })\n * ```\n */\nexport async function createTestEnv(options: TestServerOptions = {}): Promise<TestEnvironment> {\n const { server, port, cleanup: serverCleanup } = await createTestServer(options)\n const clients: TestClient[] = []\n\n return {\n server,\n port,\n clients,\n\n async createClient(clientOptions?: TestClientOptions): Promise<TestClient> {\n const client = new TestClient(port, clientOptions)\n await client.connect()\n clients.push(client)\n return client\n },\n\n async createClients(count: number, clientOptions?: TestClientOptions): Promise<TestClient[]> {\n const newClients: TestClient[] = []\n for (let i = 0; i < count; i++) {\n const client = new TestClient(port, clientOptions)\n await client.connect()\n clients.push(client)\n newClients.push(client)\n }\n return newClients\n },\n\n async cleanup(): Promise<void> {\n // 断开所有客户端\n await Promise.all(clients.map((c) => c.disconnect().catch(() => {})))\n clients.length = 0\n\n // 停止服务器\n await serverCleanup()\n },\n }\n}\n","/**\n * @zh 模拟房间\n * @en Mock room for testing\n */\n\nimport { Room, onMessage, type Player } from '../room/index.js'\n\n/**\n * @zh 模拟房间状态\n * @en Mock room state\n */\nexport interface MockRoomState {\n messages: Array<{ type: string; data: unknown; playerId: string }>\n joinCount: number\n leaveCount: number\n}\n\n/**\n * @zh 模拟房间\n * @en Mock room for testing\n *\n * @zh 记录所有事件和消息,用于测试断言\n * @en Records all events and messages for test assertions\n *\n * @example\n * ```typescript\n * const env = await createTestEnv()\n * env.server.define('mock', MockRoom)\n *\n * const client = await env.createClient()\n * await client.joinRoom('mock')\n *\n * client.sendToRoom('Test', { value: 123 })\n * await wait(50)\n *\n * // MockRoom 会广播收到的消息\n * const msg = client.getLastMessage('RoomMessage')\n * ```\n */\nexport class MockRoom extends Room<MockRoomState> {\n state: MockRoomState = {\n messages: [],\n joinCount: 0,\n leaveCount: 0,\n }\n\n onCreate(): void {\n // 房间创建\n }\n\n onJoin(player: Player): void {\n this.state.joinCount++\n this.broadcast('PlayerJoined', {\n playerId: player.id,\n joinCount: this.state.joinCount,\n })\n }\n\n onLeave(player: Player): void {\n this.state.leaveCount++\n this.broadcast('PlayerLeft', {\n playerId: player.id,\n leaveCount: this.state.leaveCount,\n })\n }\n\n @onMessage('*')\n handleAnyMessage(data: unknown, player: Player, type: string): void {\n this.state.messages.push({\n type,\n data,\n playerId: player.id,\n })\n\n // 回显消息给所有玩家\n this.broadcast('MessageReceived', {\n type,\n data,\n from: player.id,\n })\n }\n\n @onMessage('Echo')\n handleEcho(data: unknown, player: Player): void {\n // 只回复给发送者\n player.send('EchoReply', data)\n }\n\n @onMessage('Broadcast')\n handleBroadcast(data: unknown, _player: Player): void {\n this.broadcast('BroadcastMessage', data)\n }\n\n @onMessage('Ping')\n handlePing(_data: unknown, player: Player): void {\n player.send('Pong', { timestamp: Date.now() })\n }\n}\n\n/**\n * @zh 简单回显房间\n * @en Simple echo room\n *\n * @zh 将收到的任何消息回显给发送者\n * @en Echoes any received message back to sender\n */\nexport class EchoRoom extends Room {\n @onMessage('*')\n handleAnyMessage(data: unknown, player: Player, type: string): void {\n player.send(type, data)\n }\n}\n\n/**\n * @zh 广播房间\n * @en Broadcast room\n *\n * @zh 将收到的任何消息广播给所有玩家\n * @en Broadcasts any received message to all players\n */\nexport class BroadcastRoom extends Room {\n onJoin(player: Player): void {\n this.broadcast('PlayerJoined', { id: player.id })\n }\n\n onLeave(player: Player): void {\n this.broadcast('PlayerLeft', { id: player.id })\n }\n\n @onMessage('*')\n handleAnyMessage(data: unknown, player: Player, type: string): void {\n this.broadcast(type, { from: player.id, data })\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/testing/TestClient.ts","../../src/testing/TestServer.ts","../../src/testing/MockRoom.ts"],"names":["PacketType","ApiRequest","ApiResponse","ApiError","Message","TestClient","port","options","_port","_codec","_timeout","_connectTimeout","_ws","_callIdCounter","_connected","_currentRoomId","_currentPlayerId","_pendingCalls","Map","_msgHandlers","_receivedMessages","codec","json","timeout","connectTimeout","isConnected","roomId","playerId","receivedMessages","connect","Promise","resolve","reject","url","WebSocket","setTimeout","close","Error","on","clearTimeout","_rejectAllPending","err","data","_handleMessage","disconnect","readyState","CLOSED","once","joinRoom","roomType","result","call","joinRoomById","leaveRoom","sendToRoom","type","send","name","input","id","timer","delete","set","packet","encode","handler","handlers","get","Set","add","off","waitForMessage","timeoutMs","waitForRoomMessage","msg","hasReceivedMessage","some","m","getMessagesOfType","filter","map","getLastMessage","i","length","undefined","clearMessages","getMessageCount","raw","decode","_handleApiResponse","_handleApiError","_handleMsg","console","error","pending","code","message","push","timestamp","Date","now","reason","clear","getRandomPort","net","server","createServer","listen","address","createTestServer","silent","originalLog","log","tickRate","apiDir","msgDir","start","cleanup","stop","createTestEnv","serverCleanup","clients","createClient","clientOptions","client","createClients","count","newClients","all","c","catch","MockRoom","Room","state","messages","joinCount","leaveCount","onCreate","onJoin","player","broadcast","onLeave","handleAnyMessage","from","handleEcho","handleBroadcast","_player","handlePing","_data","EchoRoom","BroadcastRoom"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @zh 测试客户端\n * @en Test client for server testing\n */\n\nimport WebSocket from 'ws'\nimport { json } from '@esengine/rpc/codec'\nimport type { Codec } from '@esengine/rpc/codec'\n\n// ============================================================================\n// Types | 类型定义\n// ============================================================================\n\n/**\n * @zh 测试客户端配置\n * @en Test client options\n */\nexport interface TestClientOptions {\n /**\n * @zh 编解码器\n * @en Codec\n * @defaultValue json()\n */\n codec?: Codec\n\n /**\n * @zh API 调用超时(毫秒)\n * @en API call timeout in milliseconds\n * @defaultValue 5000\n */\n timeout?: number\n\n /**\n * @zh 连接超时(毫秒)\n * @en Connection timeout in milliseconds\n * @defaultValue 5000\n */\n connectTimeout?: number\n}\n\n/**\n * @zh 房间加入结果\n * @en Room join result\n */\nexport interface JoinRoomResult {\n roomId: string\n playerId: string\n}\n\n/**\n * @zh 收到的消息记录\n * @en Received message record\n */\nexport interface ReceivedMessage {\n type: string\n data: unknown\n timestamp: number\n}\n\n// ============================================================================\n// Constants | 常量\n// ============================================================================\n\nconst PacketType = {\n ApiRequest: 0,\n ApiResponse: 1,\n ApiError: 2,\n Message: 3,\n} as const\n\n// ============================================================================\n// TestClient Class | 测试客户端类\n// ============================================================================\n\ninterface PendingCall {\n resolve: (value: unknown) => void\n reject: (error: Error) => void\n timer: ReturnType<typeof setTimeout>\n}\n\n/**\n * @zh 测试客户端\n * @en Test client for server integration testing\n *\n * @zh 专为测试设计的客户端,提供便捷的断言方法和消息记录功能\n * @en Client designed for testing, with convenient assertion methods and message recording\n *\n * @example\n * ```typescript\n * const client = new TestClient(3000)\n * await client.connect()\n *\n * // 加入房间\n * const { roomId } = await client.joinRoom('game')\n *\n * // 发送消息\n * client.sendToRoom('Move', { x: 10, y: 20 })\n *\n * // 等待收到特定消息\n * const msg = await client.waitForMessage('PlayerMoved')\n *\n * // 断言收到消息\n * expect(client.hasReceivedMessage('PlayerMoved')).toBe(true)\n *\n * await client.disconnect()\n * ```\n */\nexport class TestClient {\n private readonly _port: number\n private readonly _codec: Codec\n private readonly _timeout: number\n private readonly _connectTimeout: number\n\n private _ws: WebSocket | null = null\n private _callIdCounter = 0\n private _connected = false\n private _currentRoomId: string | null = null\n private _currentPlayerId: string | null = null\n\n private readonly _pendingCalls = new Map<number, PendingCall>()\n private readonly _msgHandlers = new Map<string, Set<(data: unknown) => void>>()\n private readonly _receivedMessages: ReceivedMessage[] = []\n\n constructor(port: number, options: TestClientOptions = {}) {\n this._port = port\n this._codec = options.codec ?? json()\n this._timeout = options.timeout ?? 5000\n this._connectTimeout = options.connectTimeout ?? 5000\n }\n\n // ========================================================================\n // Properties | 属性\n // ========================================================================\n\n /**\n * @zh 是否已连接\n * @en Whether connected\n */\n get isConnected(): boolean {\n return this._connected\n }\n\n /**\n * @zh 当前房间 ID\n * @en Current room ID\n */\n get roomId(): string | null {\n return this._currentRoomId\n }\n\n /**\n * @zh 当前玩家 ID\n * @en Current player ID\n */\n get playerId(): string | null {\n return this._currentPlayerId\n }\n\n /**\n * @zh 收到的所有消息\n * @en All received messages\n */\n get receivedMessages(): ReadonlyArray<ReceivedMessage> {\n return this._receivedMessages\n }\n\n // ========================================================================\n // Connection | 连接管理\n // ========================================================================\n\n /**\n * @zh 连接到服务器\n * @en Connect to server\n */\n connect(): Promise<this> {\n return new Promise((resolve, reject) => {\n const url = `ws://localhost:${this._port}`\n this._ws = new WebSocket(url)\n\n const timeout = setTimeout(() => {\n this._ws?.close()\n reject(new Error(`Connection timeout after ${this._connectTimeout}ms`))\n }, this._connectTimeout)\n\n this._ws.on('open', () => {\n clearTimeout(timeout)\n this._connected = true\n resolve(this)\n })\n\n this._ws.on('close', () => {\n this._connected = false\n this._rejectAllPending('Connection closed')\n })\n\n this._ws.on('error', (err) => {\n clearTimeout(timeout)\n if (!this._connected) {\n reject(err)\n }\n })\n\n this._ws.on('message', (data: Buffer) => {\n this._handleMessage(data)\n })\n })\n }\n\n /**\n * @zh 断开连接\n * @en Disconnect from server\n */\n async disconnect(): Promise<void> {\n return new Promise((resolve) => {\n if (!this._ws || this._ws.readyState === WebSocket.CLOSED) {\n resolve()\n return\n }\n\n this._ws.once('close', () => {\n this._connected = false\n this._ws = null\n resolve()\n })\n\n this._ws.close()\n })\n }\n\n // ========================================================================\n // Room Operations | 房间操作\n // ========================================================================\n\n /**\n * @zh 加入房间\n * @en Join a room\n */\n async joinRoom(roomType: string, options?: Record<string, unknown>): Promise<JoinRoomResult> {\n const result = await this.call<JoinRoomResult>('JoinRoom', { roomType, options })\n this._currentRoomId = result.roomId\n this._currentPlayerId = result.playerId\n return result\n }\n\n /**\n * @zh 通过 ID 加入房间\n * @en Join a room by ID\n */\n async joinRoomById(roomId: string): Promise<JoinRoomResult> {\n const result = await this.call<JoinRoomResult>('JoinRoom', { roomId })\n this._currentRoomId = result.roomId\n this._currentPlayerId = result.playerId\n return result\n }\n\n /**\n * @zh 离开房间\n * @en Leave room\n */\n async leaveRoom(): Promise<void> {\n await this.call('LeaveRoom', {})\n this._currentRoomId = null\n this._currentPlayerId = null\n }\n\n /**\n * @zh 发送消息到房间\n * @en Send message to room\n */\n sendToRoom(type: string, data: unknown): void {\n this.send('RoomMessage', { type, data })\n }\n\n // ========================================================================\n // API Calls | API 调用\n // ========================================================================\n\n /**\n * @zh 调用 API\n * @en Call API\n */\n call<T = unknown>(name: string, input: unknown): Promise<T> {\n return new Promise((resolve, reject) => {\n if (!this._connected || !this._ws) {\n reject(new Error('Not connected'))\n return\n }\n\n const id = ++this._callIdCounter\n const timer = setTimeout(() => {\n this._pendingCalls.delete(id)\n reject(new Error(`API call '${name}' timeout after ${this._timeout}ms`))\n }, this._timeout)\n\n this._pendingCalls.set(id, {\n resolve: resolve as (v: unknown) => void,\n reject,\n timer,\n })\n\n const packet = [PacketType.ApiRequest, id, name, input]\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n this._ws.send(this._codec.encode(packet as any) as Buffer)\n })\n }\n\n /**\n * @zh 发送消息\n * @en Send message\n */\n send(name: string, data: unknown): void {\n if (!this._connected || !this._ws) return\n const packet = [PacketType.Message, name, data]\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n this._ws.send(this._codec.encode(packet as any) as Buffer)\n }\n\n // ========================================================================\n // Message Handling | 消息处理\n // ========================================================================\n\n /**\n * @zh 监听消息\n * @en Listen for message\n */\n on(name: string, handler: (data: unknown) => void): this {\n let handlers = this._msgHandlers.get(name)\n if (!handlers) {\n handlers = new Set()\n this._msgHandlers.set(name, handlers)\n }\n handlers.add(handler)\n return this\n }\n\n /**\n * @zh 取消监听消息\n * @en Remove message listener\n */\n off(name: string, handler?: (data: unknown) => void): this {\n if (handler) {\n this._msgHandlers.get(name)?.delete(handler)\n } else {\n this._msgHandlers.delete(name)\n }\n return this\n }\n\n /**\n * @zh 等待收到指定消息\n * @en Wait for a specific message\n */\n waitForMessage<T = unknown>(type: string, timeout?: number): Promise<T> {\n return new Promise((resolve, reject) => {\n const timeoutMs = timeout ?? this._timeout\n\n const timer = setTimeout(() => {\n this.off(type, handler)\n reject(new Error(`Timeout waiting for message '${type}' after ${timeoutMs}ms`))\n }, timeoutMs)\n\n const handler = (data: unknown) => {\n clearTimeout(timer)\n this.off(type, handler)\n resolve(data as T)\n }\n\n this.on(type, handler)\n })\n }\n\n /**\n * @zh 等待收到指定房间消息\n * @en Wait for a specific room message\n */\n waitForRoomMessage<T = unknown>(type: string, timeout?: number): Promise<T> {\n return new Promise((resolve, reject) => {\n const timeoutMs = timeout ?? this._timeout\n\n const timer = setTimeout(() => {\n this.off('RoomMessage', handler)\n reject(new Error(`Timeout waiting for room message '${type}' after ${timeoutMs}ms`))\n }, timeoutMs)\n\n const handler = (data: unknown) => {\n const msg = data as { type: string; data: unknown }\n if (msg.type === type) {\n clearTimeout(timer)\n this.off('RoomMessage', handler)\n resolve(msg.data as T)\n }\n }\n\n this.on('RoomMessage', handler)\n })\n }\n\n // ========================================================================\n // Assertions | 断言辅助\n // ========================================================================\n\n /**\n * @zh 是否收到过指定消息\n * @en Whether received a specific message\n */\n hasReceivedMessage(type: string): boolean {\n return this._receivedMessages.some((m) => m.type === type)\n }\n\n /**\n * @zh 获取指定类型的所有消息\n * @en Get all messages of a specific type\n */\n getMessagesOfType<T = unknown>(type: string): T[] {\n return this._receivedMessages\n .filter((m) => m.type === type)\n .map((m) => m.data as T)\n }\n\n /**\n * @zh 获取最后收到的指定类型消息\n * @en Get the last received message of a specific type\n */\n getLastMessage<T = unknown>(type: string): T | undefined {\n for (let i = this._receivedMessages.length - 1; i >= 0; i--) {\n if (this._receivedMessages[i].type === type) {\n return this._receivedMessages[i].data as T\n }\n }\n return undefined\n }\n\n /**\n * @zh 清空消息记录\n * @en Clear message records\n */\n clearMessages(): void {\n this._receivedMessages.length = 0\n }\n\n /**\n * @zh 获取收到的消息数量\n * @en Get received message count\n */\n getMessageCount(type?: string): number {\n if (type) {\n return this._receivedMessages.filter((m) => m.type === type).length\n }\n return this._receivedMessages.length\n }\n\n // ========================================================================\n // Private Methods | 私有方法\n // ========================================================================\n\n private _handleMessage(raw: Buffer): void {\n try {\n const packet = this._codec.decode(raw) as unknown[]\n const type = packet[0] as number\n\n switch (type) {\n case PacketType.ApiResponse:\n this._handleApiResponse([packet[0], packet[1], packet[2]] as [number, number, unknown])\n break\n case PacketType.ApiError:\n this._handleApiError([packet[0], packet[1], packet[2], packet[3]] as [number, number, string, string])\n break\n case PacketType.Message:\n this._handleMsg([packet[0], packet[1], packet[2]] as [number, string, unknown])\n break\n }\n } catch (err) {\n console.error('[TestClient] Failed to handle message:', err)\n }\n }\n\n private _handleApiResponse([, id, result]: [number, number, unknown]): void {\n const pending = this._pendingCalls.get(id)\n if (pending) {\n clearTimeout(pending.timer)\n this._pendingCalls.delete(id)\n pending.resolve(result)\n }\n }\n\n private _handleApiError([, id, code, message]: [number, number, string, string]): void {\n const pending = this._pendingCalls.get(id)\n if (pending) {\n clearTimeout(pending.timer)\n this._pendingCalls.delete(id)\n pending.reject(new Error(`[${code}] ${message}`))\n }\n }\n\n private _handleMsg([, name, data]: [number, string, unknown]): void {\n // 记录消息\n this._receivedMessages.push({\n type: name,\n data,\n timestamp: Date.now(),\n })\n\n // 触发处理器\n const handlers = this._msgHandlers.get(name)\n if (handlers) {\n for (const handler of handlers) {\n try {\n handler(data)\n } catch (err) {\n console.error('[TestClient] Handler error:', err)\n }\n }\n }\n }\n\n private _rejectAllPending(reason: string): void {\n for (const [, pending] of this._pendingCalls) {\n clearTimeout(pending.timer)\n pending.reject(new Error(reason))\n }\n this._pendingCalls.clear()\n }\n}\n","/**\n * @zh 测试服务器工具\n * @en Test server utilities\n */\n\nimport { createServer } from '../core/server.js'\nimport type { GameServer } from '../types/index.js'\nimport { TestClient, type TestClientOptions } from './TestClient.js'\n\n// ============================================================================\n// Types | 类型定义\n// ============================================================================\n\n/**\n * @zh 测试服务器配置\n * @en Test server options\n */\nexport interface TestServerOptions {\n /**\n * @zh 端口号,0 表示随机端口\n * @en Port number, 0 for random port\n * @defaultValue 0\n */\n port?: number\n\n /**\n * @zh Tick 速率\n * @en Tick rate\n * @defaultValue 0\n */\n tickRate?: number\n\n /**\n * @zh 是否禁用控制台日志\n * @en Whether to suppress console logs\n * @defaultValue true\n */\n silent?: boolean\n}\n\n/**\n * @zh 测试环境\n * @en Test environment\n */\nexport interface TestEnvironment {\n /**\n * @zh 服务器实例\n * @en Server instance\n */\n server: GameServer\n\n /**\n * @zh 服务器端口\n * @en Server port\n */\n port: number\n\n /**\n * @zh 创建测试客户端\n * @en Create test client\n */\n createClient(options?: TestClientOptions): Promise<TestClient>\n\n /**\n * @zh 创建多个测试客户端\n * @en Create multiple test clients\n */\n createClients(count: number, options?: TestClientOptions): Promise<TestClient[]>\n\n /**\n * @zh 清理测试环境\n * @en Cleanup test environment\n */\n cleanup(): Promise<void>\n\n /**\n * @zh 所有已创建的客户端\n * @en All created clients\n */\n readonly clients: ReadonlyArray<TestClient>\n}\n\n// ============================================================================\n// Helper Functions | 辅助函数\n// ============================================================================\n\n/**\n * @zh 获取随机可用端口\n * @en Get a random available port\n */\nasync function getRandomPort(): Promise<number> {\n const net = await import('node:net')\n return new Promise((resolve, reject) => {\n const server = net.createServer()\n server.listen(0, () => {\n const address = server.address()\n if (address && typeof address === 'object') {\n const port = address.port\n server.close(() => resolve(port))\n } else {\n server.close(() => reject(new Error('Failed to get port')))\n }\n })\n server.on('error', reject)\n })\n}\n\n/**\n * @zh 等待指定毫秒\n * @en Wait for specified milliseconds\n */\nexport function wait(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n// ============================================================================\n// Factory Functions | 工厂函数\n// ============================================================================\n\n/**\n * @zh 创建测试服务器\n * @en Create test server\n *\n * @example\n * ```typescript\n * const { server, port, cleanup } = await createTestServer()\n * server.define('game', GameRoom)\n *\n * const client = new TestClient(port)\n * await client.connect()\n *\n * // ... run tests ...\n *\n * await cleanup()\n * ```\n */\nexport async function createTestServer(\n options: TestServerOptions = {}\n): Promise<{ server: GameServer; port: number; cleanup: () => Promise<void> }> {\n const port = options.port || (await getRandomPort())\n const silent = options.silent ?? true\n\n // 临时禁用 console.log\n const originalLog = console.log\n if (silent) {\n console.log = () => {}\n }\n\n const server = await createServer({\n port,\n tickRate: options.tickRate ?? 0,\n apiDir: '__non_existent_api__',\n msgDir: '__non_existent_msg__',\n })\n\n await server.start()\n\n // 恢复 console.log\n if (silent) {\n console.log = originalLog\n }\n\n return {\n server,\n port,\n cleanup: async () => {\n await server.stop()\n },\n }\n}\n\n/**\n * @zh 创建完整测试环境\n * @en Create complete test environment\n *\n * @zh 包含服务器、客户端创建和清理功能的完整测试环境\n * @en Complete test environment with server, client creation and cleanup\n *\n * @example\n * ```typescript\n * describe('GameRoom', () => {\n * let env: TestEnvironment\n *\n * beforeEach(async () => {\n * env = await createTestEnv()\n * env.server.define('game', GameRoom)\n * })\n *\n * afterEach(async () => {\n * await env.cleanup()\n * })\n *\n * it('should handle player join', async () => {\n * const client = await env.createClient()\n * const result = await client.joinRoom('game')\n * expect(result.roomId).toBeDefined()\n * })\n *\n * it('should broadcast to all players', async () => {\n * const [client1, client2] = await env.createClients(2)\n *\n * await client1.joinRoom('game')\n * const joinPromise = client1.waitForRoomMessage('PlayerJoined')\n *\n * await client2.joinRoom('game')\n * const msg = await joinPromise\n *\n * expect(msg).toBeDefined()\n * })\n * })\n * ```\n */\nexport async function createTestEnv(options: TestServerOptions = {}): Promise<TestEnvironment> {\n const { server, port, cleanup: serverCleanup } = await createTestServer(options)\n const clients: TestClient[] = []\n\n return {\n server,\n port,\n clients,\n\n async createClient(clientOptions?: TestClientOptions): Promise<TestClient> {\n const client = new TestClient(port, clientOptions)\n await client.connect()\n clients.push(client)\n return client\n },\n\n async createClients(count: number, clientOptions?: TestClientOptions): Promise<TestClient[]> {\n const newClients: TestClient[] = []\n for (let i = 0; i < count; i++) {\n const client = new TestClient(port, clientOptions)\n await client.connect()\n clients.push(client)\n newClients.push(client)\n }\n return newClients\n },\n\n async cleanup(): Promise<void> {\n // 断开所有客户端\n await Promise.all(clients.map((c) => c.disconnect().catch(() => {})))\n clients.length = 0\n\n // 停止服务器\n await serverCleanup()\n },\n }\n}\n","/**\n * @zh 模拟房间\n * @en Mock room for testing\n */\n\nimport { Room, onMessage, type Player } from '../room/index.js'\n\n/**\n * @zh 模拟房间状态\n * @en Mock room state\n */\nexport interface MockRoomState {\n messages: Array<{ type: string; data: unknown; playerId: string }>\n joinCount: number\n leaveCount: number\n}\n\n/**\n * @zh 模拟房间\n * @en Mock room for testing\n *\n * @zh 记录所有事件和消息,用于测试断言\n * @en Records all events and messages for test assertions\n *\n * @example\n * ```typescript\n * const env = await createTestEnv()\n * env.server.define('mock', MockRoom)\n *\n * const client = await env.createClient()\n * await client.joinRoom('mock')\n *\n * client.sendToRoom('Test', { value: 123 })\n * await wait(50)\n *\n * // MockRoom 会广播收到的消息\n * const msg = client.getLastMessage('RoomMessage')\n * ```\n */\nexport class MockRoom extends Room<MockRoomState> {\n state: MockRoomState = {\n messages: [],\n joinCount: 0,\n leaveCount: 0,\n }\n\n onCreate(): void {\n // 房间创建\n }\n\n onJoin(player: Player): void {\n this.state.joinCount++\n this.broadcast('PlayerJoined', {\n playerId: player.id,\n joinCount: this.state.joinCount,\n })\n }\n\n onLeave(player: Player): void {\n this.state.leaveCount++\n this.broadcast('PlayerLeft', {\n playerId: player.id,\n leaveCount: this.state.leaveCount,\n })\n }\n\n @onMessage('*')\n handleAnyMessage(data: unknown, player: Player, type: string): void {\n this.state.messages.push({\n type,\n data,\n playerId: player.id,\n })\n\n // 回显消息给所有玩家\n this.broadcast('MessageReceived', {\n type,\n data,\n from: player.id,\n })\n }\n\n @onMessage('Echo')\n handleEcho(data: unknown, player: Player): void {\n // 只回复给发送者\n player.send('EchoReply', data)\n }\n\n @onMessage('Broadcast')\n handleBroadcast(data: unknown, _player: Player): void {\n this.broadcast('BroadcastMessage', data)\n }\n\n @onMessage('Ping')\n handlePing(_data: unknown, player: Player): void {\n player.send('Pong', { timestamp: Date.now() })\n }\n}\n\n/**\n * @zh 简单回显房间\n * @en Simple echo room\n *\n * @zh 将收到的任何消息回显给发送者\n * @en Echoes any received message back to sender\n */\nexport class EchoRoom extends Room {\n @onMessage('*')\n handleAnyMessage(data: unknown, player: Player, type: string): void {\n player.send(type, data)\n }\n}\n\n/**\n * @zh 广播房间\n * @en Broadcast room\n *\n * @zh 将收到的任何消息广播给所有玩家\n * @en Broadcasts any received message to all players\n */\nexport class BroadcastRoom extends Room {\n onJoin(player: Player): void {\n this.broadcast('PlayerJoined', { id: player.id })\n }\n\n onLeave(player: Player): void {\n this.broadcast('PlayerLeft', { id: player.id })\n }\n\n @onMessage('*')\n handleAnyMessage(data: unknown, player: Player, type: string): void {\n this.broadcast(type, { from: player.id, data })\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@esengine/server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Game server framework for ESEngine with file-based routing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
"./testing": {
|
|
27
27
|
"import": "./dist/testing/index.js",
|
|
28
28
|
"types": "./dist/testing/index.d.ts"
|
|
29
|
+
},
|
|
30
|
+
"./ecs": {
|
|
31
|
+
"import": "./dist/ecs/index.js",
|
|
32
|
+
"types": "./dist/ecs/index.d.ts"
|
|
29
33
|
}
|
|
30
34
|
},
|
|
31
35
|
"files": [
|
|
@@ -37,11 +41,15 @@
|
|
|
37
41
|
},
|
|
38
42
|
"peerDependencies": {
|
|
39
43
|
"ws": ">=8.0.0",
|
|
40
|
-
"jsonwebtoken": ">=9.0.0"
|
|
44
|
+
"jsonwebtoken": ">=9.0.0",
|
|
45
|
+
"@esengine/ecs-framework": ">=2.5.0"
|
|
41
46
|
},
|
|
42
47
|
"peerDependenciesMeta": {
|
|
43
48
|
"jsonwebtoken": {
|
|
44
49
|
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"@esengine/ecs-framework": {
|
|
52
|
+
"optional": true
|
|
45
53
|
}
|
|
46
54
|
},
|
|
47
55
|
"devDependencies": {
|
|
@@ -53,7 +61,8 @@
|
|
|
53
61
|
"tsup": "^8.0.0",
|
|
54
62
|
"typescript": "^5.7.0",
|
|
55
63
|
"vitest": "^2.0.0",
|
|
56
|
-
"ws": "^8.18.0"
|
|
64
|
+
"ws": "^8.18.0",
|
|
65
|
+
"@esengine/ecs-framework": "2.5.0"
|
|
57
66
|
},
|
|
58
67
|
"publishConfig": {
|
|
59
68
|
"access": "public"
|