@esengine/server 1.1.4 → 1.3.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/Room-BnKpl5Sj.d.ts +237 -0
- package/dist/auth/index.d.ts +711 -0
- package/dist/auth/index.js +707 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/testing/index.d.ts +153 -0
- package/dist/auth/testing/index.js +165 -0
- package/dist/auth/testing/index.js.map +1 -0
- package/dist/chunk-7C6JZO4O.js +744 -0
- package/dist/chunk-7C6JZO4O.js.map +1 -0
- package/dist/chunk-T626JPC7.js +8 -0
- package/dist/chunk-T626JPC7.js.map +1 -0
- package/dist/index-DgaJIm6-.d.ts +170 -0
- package/dist/index.d.ts +4 -403
- package/dist/index.js +3 -745
- package/dist/index.js.map +1 -1
- package/dist/ratelimit/index.d.ts +787 -0
- package/dist/ratelimit/index.js +818 -0
- package/dist/ratelimit/index.js.map +1 -0
- package/dist/testing/index.d.ts +386 -0
- package/dist/testing/index.js +629 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/types-BmO5ykKp.d.ts +311 -0
- package/package.json +30 -3
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
import { __name, __publicField } from './chunk-T626JPC7.js';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { serve } from '@esengine/rpc/server';
|
|
4
|
+
import { rpc } from '@esengine/rpc';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import { pathToFileURL } from 'url';
|
|
7
|
+
|
|
8
|
+
// src/room/Player.ts
|
|
9
|
+
var _Player = class _Player {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
__publicField(this, "id");
|
|
12
|
+
__publicField(this, "roomId");
|
|
13
|
+
__publicField(this, "data");
|
|
14
|
+
__publicField(this, "_conn");
|
|
15
|
+
__publicField(this, "_sendFn");
|
|
16
|
+
__publicField(this, "_leaveFn");
|
|
17
|
+
this.id = options.id;
|
|
18
|
+
this.roomId = options.roomId;
|
|
19
|
+
this._conn = options.conn;
|
|
20
|
+
this._sendFn = options.sendFn;
|
|
21
|
+
this._leaveFn = options.leaveFn;
|
|
22
|
+
this.data = options.initialData ?? {};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* @zh 获取底层连接
|
|
26
|
+
* @en Get underlying connection
|
|
27
|
+
*/
|
|
28
|
+
get connection() {
|
|
29
|
+
return this._conn;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* @zh 发送消息给玩家
|
|
33
|
+
* @en Send message to player
|
|
34
|
+
*/
|
|
35
|
+
send(type, data) {
|
|
36
|
+
this._sendFn(this._conn, type, data);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* @zh 让玩家离开房间
|
|
40
|
+
* @en Make player leave the room
|
|
41
|
+
*/
|
|
42
|
+
leave(reason) {
|
|
43
|
+
this._leaveFn(this, reason);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
__name(_Player, "Player");
|
|
47
|
+
var Player = _Player;
|
|
48
|
+
|
|
49
|
+
// src/room/Room.ts
|
|
50
|
+
var MESSAGE_HANDLERS = /* @__PURE__ */ Symbol("messageHandlers");
|
|
51
|
+
var _Room = class _Room {
|
|
52
|
+
constructor() {
|
|
53
|
+
// ========================================================================
|
|
54
|
+
// 配置 | Configuration
|
|
55
|
+
// ========================================================================
|
|
56
|
+
/**
|
|
57
|
+
* @zh 最大玩家数
|
|
58
|
+
* @en Maximum players
|
|
59
|
+
*/
|
|
60
|
+
__publicField(this, "maxPlayers", 16);
|
|
61
|
+
/**
|
|
62
|
+
* @zh Tick 速率(每秒),0 = 不自动 tick
|
|
63
|
+
* @en Tick rate (per second), 0 = no auto tick
|
|
64
|
+
*/
|
|
65
|
+
__publicField(this, "tickRate", 0);
|
|
66
|
+
/**
|
|
67
|
+
* @zh 空房间自动销毁
|
|
68
|
+
* @en Auto dispose when empty
|
|
69
|
+
*/
|
|
70
|
+
__publicField(this, "autoDispose", true);
|
|
71
|
+
// ========================================================================
|
|
72
|
+
// 状态 | State
|
|
73
|
+
// ========================================================================
|
|
74
|
+
/**
|
|
75
|
+
* @zh 房间状态
|
|
76
|
+
* @en Room state
|
|
77
|
+
*/
|
|
78
|
+
__publicField(this, "state", {});
|
|
79
|
+
// ========================================================================
|
|
80
|
+
// 内部属性 | Internal properties
|
|
81
|
+
// ========================================================================
|
|
82
|
+
__publicField(this, "_id", "");
|
|
83
|
+
__publicField(this, "_players", /* @__PURE__ */ new Map());
|
|
84
|
+
__publicField(this, "_locked", false);
|
|
85
|
+
__publicField(this, "_disposed", false);
|
|
86
|
+
__publicField(this, "_tickInterval", null);
|
|
87
|
+
__publicField(this, "_lastTickTime", 0);
|
|
88
|
+
__publicField(this, "_broadcastFn", null);
|
|
89
|
+
__publicField(this, "_sendFn", null);
|
|
90
|
+
__publicField(this, "_disposeFn", null);
|
|
91
|
+
}
|
|
92
|
+
// ========================================================================
|
|
93
|
+
// 只读属性 | Readonly properties
|
|
94
|
+
// ========================================================================
|
|
95
|
+
/**
|
|
96
|
+
* @zh 房间 ID
|
|
97
|
+
* @en Room ID
|
|
98
|
+
*/
|
|
99
|
+
get id() {
|
|
100
|
+
return this._id;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* @zh 所有玩家
|
|
104
|
+
* @en All players
|
|
105
|
+
*/
|
|
106
|
+
get players() {
|
|
107
|
+
return Array.from(this._players.values());
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* @zh 玩家数量
|
|
111
|
+
* @en Player count
|
|
112
|
+
*/
|
|
113
|
+
get playerCount() {
|
|
114
|
+
return this._players.size;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* @zh 是否已满
|
|
118
|
+
* @en Is full
|
|
119
|
+
*/
|
|
120
|
+
get isFull() {
|
|
121
|
+
return this._players.size >= this.maxPlayers;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* @zh 是否已锁定
|
|
125
|
+
* @en Is locked
|
|
126
|
+
*/
|
|
127
|
+
get isLocked() {
|
|
128
|
+
return this._locked;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* @zh 是否已销毁
|
|
132
|
+
* @en Is disposed
|
|
133
|
+
*/
|
|
134
|
+
get isDisposed() {
|
|
135
|
+
return this._disposed;
|
|
136
|
+
}
|
|
137
|
+
// ========================================================================
|
|
138
|
+
// 生命周期 | Lifecycle
|
|
139
|
+
// ========================================================================
|
|
140
|
+
/**
|
|
141
|
+
* @zh 房间创建时调用
|
|
142
|
+
* @en Called when room is created
|
|
143
|
+
*/
|
|
144
|
+
onCreate(options) {
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* @zh 玩家加入时调用
|
|
148
|
+
* @en Called when player joins
|
|
149
|
+
*/
|
|
150
|
+
onJoin(player) {
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* @zh 玩家离开时调用
|
|
154
|
+
* @en Called when player leaves
|
|
155
|
+
*/
|
|
156
|
+
onLeave(player, reason) {
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* @zh 游戏循环
|
|
160
|
+
* @en Game tick
|
|
161
|
+
*/
|
|
162
|
+
onTick(dt) {
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* @zh 房间销毁时调用
|
|
166
|
+
* @en Called when room is disposed
|
|
167
|
+
*/
|
|
168
|
+
onDispose() {
|
|
169
|
+
}
|
|
170
|
+
// ========================================================================
|
|
171
|
+
// 公共方法 | Public methods
|
|
172
|
+
// ========================================================================
|
|
173
|
+
/**
|
|
174
|
+
* @zh 广播消息给所有玩家
|
|
175
|
+
* @en Broadcast message to all players
|
|
176
|
+
*/
|
|
177
|
+
broadcast(type, data) {
|
|
178
|
+
for (const player of this._players.values()) {
|
|
179
|
+
player.send(type, data);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* @zh 广播消息给除指定玩家外的所有玩家
|
|
184
|
+
* @en Broadcast message to all players except one
|
|
185
|
+
*/
|
|
186
|
+
broadcastExcept(except, type, data) {
|
|
187
|
+
for (const player of this._players.values()) {
|
|
188
|
+
if (player.id !== except.id) {
|
|
189
|
+
player.send(type, data);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* @zh 获取玩家
|
|
195
|
+
* @en Get player by id
|
|
196
|
+
*/
|
|
197
|
+
getPlayer(id) {
|
|
198
|
+
return this._players.get(id);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* @zh 踢出玩家
|
|
202
|
+
* @en Kick player
|
|
203
|
+
*/
|
|
204
|
+
kick(player, reason) {
|
|
205
|
+
player.leave(reason ?? "kicked");
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* @zh 锁定房间
|
|
209
|
+
* @en Lock room
|
|
210
|
+
*/
|
|
211
|
+
lock() {
|
|
212
|
+
this._locked = true;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* @zh 解锁房间
|
|
216
|
+
* @en Unlock room
|
|
217
|
+
*/
|
|
218
|
+
unlock() {
|
|
219
|
+
this._locked = false;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* @zh 手动销毁房间
|
|
223
|
+
* @en Manually dispose room
|
|
224
|
+
*/
|
|
225
|
+
dispose() {
|
|
226
|
+
if (this._disposed) return;
|
|
227
|
+
this._disposed = true;
|
|
228
|
+
this._stopTick();
|
|
229
|
+
for (const player of this._players.values()) {
|
|
230
|
+
player.leave("room_disposed");
|
|
231
|
+
}
|
|
232
|
+
this._players.clear();
|
|
233
|
+
this.onDispose();
|
|
234
|
+
this._disposeFn?.();
|
|
235
|
+
}
|
|
236
|
+
// ========================================================================
|
|
237
|
+
// 内部方法 | Internal methods
|
|
238
|
+
// ========================================================================
|
|
239
|
+
/**
|
|
240
|
+
* @internal
|
|
241
|
+
*/
|
|
242
|
+
_init(options) {
|
|
243
|
+
this._id = options.id;
|
|
244
|
+
this._sendFn = options.sendFn;
|
|
245
|
+
this._broadcastFn = options.broadcastFn;
|
|
246
|
+
this._disposeFn = options.disposeFn;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* @internal
|
|
250
|
+
*/
|
|
251
|
+
async _create(options) {
|
|
252
|
+
await this.onCreate(options);
|
|
253
|
+
this._startTick();
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* @internal
|
|
257
|
+
*/
|
|
258
|
+
async _addPlayer(id, conn) {
|
|
259
|
+
if (this._locked || this.isFull || this._disposed) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
const player = new Player({
|
|
263
|
+
id,
|
|
264
|
+
roomId: this._id,
|
|
265
|
+
conn,
|
|
266
|
+
sendFn: this._sendFn,
|
|
267
|
+
leaveFn: /* @__PURE__ */ __name((p, reason) => this._removePlayer(p.id, reason), "leaveFn")
|
|
268
|
+
});
|
|
269
|
+
this._players.set(id, player);
|
|
270
|
+
await this.onJoin(player);
|
|
271
|
+
return player;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* @internal
|
|
275
|
+
*/
|
|
276
|
+
async _removePlayer(id, reason) {
|
|
277
|
+
const player = this._players.get(id);
|
|
278
|
+
if (!player) return;
|
|
279
|
+
this._players.delete(id);
|
|
280
|
+
await this.onLeave(player, reason);
|
|
281
|
+
if (this.autoDispose && this._players.size === 0) {
|
|
282
|
+
this.dispose();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* @internal
|
|
287
|
+
*/
|
|
288
|
+
_handleMessage(type, data, playerId) {
|
|
289
|
+
const player = this._players.get(playerId);
|
|
290
|
+
if (!player) return;
|
|
291
|
+
const handlers = this.constructor[MESSAGE_HANDLERS];
|
|
292
|
+
if (handlers) {
|
|
293
|
+
for (const handler of handlers) {
|
|
294
|
+
if (handler.type === type) {
|
|
295
|
+
const method = this[handler.method];
|
|
296
|
+
if (typeof method === "function") {
|
|
297
|
+
method.call(this, data, player);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
_startTick() {
|
|
304
|
+
if (this.tickRate <= 0) return;
|
|
305
|
+
this._lastTickTime = performance.now();
|
|
306
|
+
this._tickInterval = setInterval(() => {
|
|
307
|
+
const now = performance.now();
|
|
308
|
+
const dt = (now - this._lastTickTime) / 1e3;
|
|
309
|
+
this._lastTickTime = now;
|
|
310
|
+
this.onTick(dt);
|
|
311
|
+
}, 1e3 / this.tickRate);
|
|
312
|
+
}
|
|
313
|
+
_stopTick() {
|
|
314
|
+
if (this._tickInterval) {
|
|
315
|
+
clearInterval(this._tickInterval);
|
|
316
|
+
this._tickInterval = null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
__name(_Room, "Room");
|
|
321
|
+
var Room = _Room;
|
|
322
|
+
function registerMessageHandler(target, type, method) {
|
|
323
|
+
if (!target[MESSAGE_HANDLERS]) {
|
|
324
|
+
target[MESSAGE_HANDLERS] = [];
|
|
325
|
+
}
|
|
326
|
+
target[MESSAGE_HANDLERS].push({
|
|
327
|
+
type,
|
|
328
|
+
method
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
__name(registerMessageHandler, "registerMessageHandler");
|
|
332
|
+
|
|
333
|
+
// src/room/decorators.ts
|
|
334
|
+
function onMessage(type) {
|
|
335
|
+
return function(target, propertyKey, _descriptor) {
|
|
336
|
+
registerMessageHandler(target.constructor, type, propertyKey);
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
__name(onMessage, "onMessage");
|
|
340
|
+
function fileNameToHandlerName(fileName) {
|
|
341
|
+
const baseName = fileName.replace(/\.(ts|js|mts|mjs)$/, "");
|
|
342
|
+
return baseName.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
343
|
+
}
|
|
344
|
+
__name(fileNameToHandlerName, "fileNameToHandlerName");
|
|
345
|
+
function scanDirectory(dir) {
|
|
346
|
+
if (!fs.existsSync(dir)) {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
const files = [];
|
|
350
|
+
const entries = fs.readdirSync(dir, {
|
|
351
|
+
withFileTypes: true
|
|
352
|
+
});
|
|
353
|
+
for (const entry of entries) {
|
|
354
|
+
if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name)) {
|
|
355
|
+
if (entry.name.startsWith("_") || entry.name.startsWith("index.")) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
files.push(path.join(dir, entry.name));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return files;
|
|
362
|
+
}
|
|
363
|
+
__name(scanDirectory, "scanDirectory");
|
|
364
|
+
async function loadApiHandlers(apiDir) {
|
|
365
|
+
const files = scanDirectory(apiDir);
|
|
366
|
+
const handlers = [];
|
|
367
|
+
for (const filePath of files) {
|
|
368
|
+
try {
|
|
369
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
370
|
+
const module = await import(fileUrl);
|
|
371
|
+
const definition = module.default;
|
|
372
|
+
if (definition && typeof definition.handler === "function") {
|
|
373
|
+
const name = fileNameToHandlerName(path.basename(filePath));
|
|
374
|
+
handlers.push({
|
|
375
|
+
name,
|
|
376
|
+
path: filePath,
|
|
377
|
+
definition
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
} catch (err) {
|
|
381
|
+
console.warn(`[Server] Failed to load API handler: ${filePath}`, err);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return handlers;
|
|
385
|
+
}
|
|
386
|
+
__name(loadApiHandlers, "loadApiHandlers");
|
|
387
|
+
async function loadMsgHandlers(msgDir) {
|
|
388
|
+
const files = scanDirectory(msgDir);
|
|
389
|
+
const handlers = [];
|
|
390
|
+
for (const filePath of files) {
|
|
391
|
+
try {
|
|
392
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
393
|
+
const module = await import(fileUrl);
|
|
394
|
+
const definition = module.default;
|
|
395
|
+
if (definition && typeof definition.handler === "function") {
|
|
396
|
+
const name = fileNameToHandlerName(path.basename(filePath));
|
|
397
|
+
handlers.push({
|
|
398
|
+
name,
|
|
399
|
+
path: filePath,
|
|
400
|
+
definition
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
} catch (err) {
|
|
404
|
+
console.warn(`[Server] Failed to load msg handler: ${filePath}`, err);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return handlers;
|
|
408
|
+
}
|
|
409
|
+
__name(loadMsgHandlers, "loadMsgHandlers");
|
|
410
|
+
|
|
411
|
+
// src/room/RoomManager.ts
|
|
412
|
+
var _RoomManager = class _RoomManager {
|
|
413
|
+
constructor(sendFn) {
|
|
414
|
+
__publicField(this, "_definitions", /* @__PURE__ */ new Map());
|
|
415
|
+
__publicField(this, "_rooms", /* @__PURE__ */ new Map());
|
|
416
|
+
__publicField(this, "_playerToRoom", /* @__PURE__ */ new Map());
|
|
417
|
+
__publicField(this, "_nextRoomId", 1);
|
|
418
|
+
__publicField(this, "_sendFn");
|
|
419
|
+
this._sendFn = sendFn;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* @zh 注册房间类型
|
|
423
|
+
* @en Define room type
|
|
424
|
+
*/
|
|
425
|
+
define(name, roomClass) {
|
|
426
|
+
this._definitions.set(name, {
|
|
427
|
+
roomClass
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* @zh 创建房间
|
|
432
|
+
* @en Create room
|
|
433
|
+
*/
|
|
434
|
+
async create(name, options) {
|
|
435
|
+
const def = this._definitions.get(name);
|
|
436
|
+
if (!def) {
|
|
437
|
+
console.warn(`[RoomManager] Room type not found: ${name}`);
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
const roomId = this._generateRoomId();
|
|
441
|
+
const room = new def.roomClass();
|
|
442
|
+
room._init({
|
|
443
|
+
id: roomId,
|
|
444
|
+
sendFn: this._sendFn,
|
|
445
|
+
broadcastFn: /* @__PURE__ */ __name((type, data) => {
|
|
446
|
+
for (const player of room.players) {
|
|
447
|
+
player.send(type, data);
|
|
448
|
+
}
|
|
449
|
+
}, "broadcastFn"),
|
|
450
|
+
disposeFn: /* @__PURE__ */ __name(() => {
|
|
451
|
+
this._rooms.delete(roomId);
|
|
452
|
+
}, "disposeFn")
|
|
453
|
+
});
|
|
454
|
+
this._rooms.set(roomId, room);
|
|
455
|
+
await room._create(options);
|
|
456
|
+
console.log(`[Room] Created: ${name} (${roomId})`);
|
|
457
|
+
return room;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* @zh 加入或创建房间
|
|
461
|
+
* @en Join or create room
|
|
462
|
+
*/
|
|
463
|
+
async joinOrCreate(name, playerId, conn, options) {
|
|
464
|
+
let room = this._findAvailableRoom(name);
|
|
465
|
+
if (!room) {
|
|
466
|
+
room = await this.create(name, options);
|
|
467
|
+
if (!room) return null;
|
|
468
|
+
}
|
|
469
|
+
const player = await room._addPlayer(playerId, conn);
|
|
470
|
+
if (!player) return null;
|
|
471
|
+
this._playerToRoom.set(playerId, room.id);
|
|
472
|
+
console.log(`[Room] Player ${playerId} joined ${room.id}`);
|
|
473
|
+
return {
|
|
474
|
+
room,
|
|
475
|
+
player
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* @zh 加入指定房间
|
|
480
|
+
* @en Join specific room
|
|
481
|
+
*/
|
|
482
|
+
async joinById(roomId, playerId, conn) {
|
|
483
|
+
const room = this._rooms.get(roomId);
|
|
484
|
+
if (!room) return null;
|
|
485
|
+
const player = await room._addPlayer(playerId, conn);
|
|
486
|
+
if (!player) return null;
|
|
487
|
+
this._playerToRoom.set(playerId, room.id);
|
|
488
|
+
console.log(`[Room] Player ${playerId} joined ${room.id}`);
|
|
489
|
+
return {
|
|
490
|
+
room,
|
|
491
|
+
player
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* @zh 玩家离开
|
|
496
|
+
* @en Player leave
|
|
497
|
+
*/
|
|
498
|
+
async leave(playerId, reason) {
|
|
499
|
+
const roomId = this._playerToRoom.get(playerId);
|
|
500
|
+
if (!roomId) return;
|
|
501
|
+
const room = this._rooms.get(roomId);
|
|
502
|
+
if (room) {
|
|
503
|
+
await room._removePlayer(playerId, reason);
|
|
504
|
+
}
|
|
505
|
+
this._playerToRoom.delete(playerId);
|
|
506
|
+
console.log(`[Room] Player ${playerId} left ${roomId}`);
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* @zh 处理消息
|
|
510
|
+
* @en Handle message
|
|
511
|
+
*/
|
|
512
|
+
handleMessage(playerId, type, data) {
|
|
513
|
+
const roomId = this._playerToRoom.get(playerId);
|
|
514
|
+
if (!roomId) return;
|
|
515
|
+
const room = this._rooms.get(roomId);
|
|
516
|
+
if (room) {
|
|
517
|
+
room._handleMessage(type, data, playerId);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* @zh 获取房间
|
|
522
|
+
* @en Get room
|
|
523
|
+
*/
|
|
524
|
+
getRoom(roomId) {
|
|
525
|
+
return this._rooms.get(roomId);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* @zh 获取玩家所在房间
|
|
529
|
+
* @en Get player's room
|
|
530
|
+
*/
|
|
531
|
+
getPlayerRoom(playerId) {
|
|
532
|
+
const roomId = this._playerToRoom.get(playerId);
|
|
533
|
+
return roomId ? this._rooms.get(roomId) : void 0;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* @zh 获取所有房间
|
|
537
|
+
* @en Get all rooms
|
|
538
|
+
*/
|
|
539
|
+
getRooms() {
|
|
540
|
+
return Array.from(this._rooms.values());
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* @zh 获取指定类型的所有房间
|
|
544
|
+
* @en Get all rooms of a type
|
|
545
|
+
*/
|
|
546
|
+
getRoomsByType(name) {
|
|
547
|
+
const def = this._definitions.get(name);
|
|
548
|
+
if (!def) return [];
|
|
549
|
+
return Array.from(this._rooms.values()).filter((room) => room instanceof def.roomClass);
|
|
550
|
+
}
|
|
551
|
+
_findAvailableRoom(name) {
|
|
552
|
+
const def = this._definitions.get(name);
|
|
553
|
+
if (!def) return null;
|
|
554
|
+
for (const room of this._rooms.values()) {
|
|
555
|
+
if (room instanceof def.roomClass && !room.isFull && !room.isLocked && !room.isDisposed) {
|
|
556
|
+
return room;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
_generateRoomId() {
|
|
562
|
+
return `room_${this._nextRoomId++}`;
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
__name(_RoomManager, "RoomManager");
|
|
566
|
+
var RoomManager = _RoomManager;
|
|
567
|
+
|
|
568
|
+
// src/core/server.ts
|
|
569
|
+
var DEFAULT_CONFIG = {
|
|
570
|
+
port: 3e3,
|
|
571
|
+
apiDir: "src/api",
|
|
572
|
+
msgDir: "src/msg",
|
|
573
|
+
tickRate: 20
|
|
574
|
+
};
|
|
575
|
+
async function createServer(config = {}) {
|
|
576
|
+
const opts = {
|
|
577
|
+
...DEFAULT_CONFIG,
|
|
578
|
+
...config
|
|
579
|
+
};
|
|
580
|
+
const cwd = process.cwd();
|
|
581
|
+
const apiHandlers = await loadApiHandlers(path.resolve(cwd, opts.apiDir));
|
|
582
|
+
const msgHandlers = await loadMsgHandlers(path.resolve(cwd, opts.msgDir));
|
|
583
|
+
if (apiHandlers.length > 0) {
|
|
584
|
+
console.log(`[Server] Loaded ${apiHandlers.length} API handlers`);
|
|
585
|
+
}
|
|
586
|
+
if (msgHandlers.length > 0) {
|
|
587
|
+
console.log(`[Server] Loaded ${msgHandlers.length} message handlers`);
|
|
588
|
+
}
|
|
589
|
+
const apiDefs = {
|
|
590
|
+
// 内置 API
|
|
591
|
+
JoinRoom: rpc.api(),
|
|
592
|
+
LeaveRoom: rpc.api()
|
|
593
|
+
};
|
|
594
|
+
const msgDefs = {
|
|
595
|
+
// 内置消息(房间消息透传)
|
|
596
|
+
RoomMessage: rpc.msg()
|
|
597
|
+
};
|
|
598
|
+
for (const handler of apiHandlers) {
|
|
599
|
+
apiDefs[handler.name] = rpc.api();
|
|
600
|
+
}
|
|
601
|
+
for (const handler of msgHandlers) {
|
|
602
|
+
msgDefs[handler.name] = rpc.msg();
|
|
603
|
+
}
|
|
604
|
+
const protocol = rpc.define({
|
|
605
|
+
api: apiDefs,
|
|
606
|
+
msg: msgDefs
|
|
607
|
+
});
|
|
608
|
+
let currentTick = 0;
|
|
609
|
+
let tickInterval = null;
|
|
610
|
+
let rpcServer = null;
|
|
611
|
+
const roomManager = new RoomManager((conn, type, data) => {
|
|
612
|
+
rpcServer?.send(conn, "RoomMessage", {
|
|
613
|
+
type,
|
|
614
|
+
data
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
const apiMap = {};
|
|
618
|
+
for (const handler of apiHandlers) {
|
|
619
|
+
apiMap[handler.name] = handler;
|
|
620
|
+
}
|
|
621
|
+
const msgMap = {};
|
|
622
|
+
for (const handler of msgHandlers) {
|
|
623
|
+
msgMap[handler.name] = handler;
|
|
624
|
+
}
|
|
625
|
+
const gameServer = {
|
|
626
|
+
get connections() {
|
|
627
|
+
return rpcServer?.connections ?? [];
|
|
628
|
+
},
|
|
629
|
+
get tick() {
|
|
630
|
+
return currentTick;
|
|
631
|
+
},
|
|
632
|
+
get rooms() {
|
|
633
|
+
return roomManager;
|
|
634
|
+
},
|
|
635
|
+
/**
|
|
636
|
+
* @zh 注册房间类型
|
|
637
|
+
* @en Define room type
|
|
638
|
+
*/
|
|
639
|
+
define(name, roomClass) {
|
|
640
|
+
roomManager.define(name, roomClass);
|
|
641
|
+
},
|
|
642
|
+
async start() {
|
|
643
|
+
const apiHandlersObj = {};
|
|
644
|
+
apiHandlersObj["JoinRoom"] = async (input, conn) => {
|
|
645
|
+
const { roomType, roomId, options } = input;
|
|
646
|
+
if (roomId) {
|
|
647
|
+
const result = await roomManager.joinById(roomId, conn.id, conn);
|
|
648
|
+
if (!result) {
|
|
649
|
+
throw new Error("Failed to join room");
|
|
650
|
+
}
|
|
651
|
+
return {
|
|
652
|
+
roomId: result.room.id,
|
|
653
|
+
playerId: result.player.id
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
if (roomType) {
|
|
657
|
+
const result = await roomManager.joinOrCreate(roomType, conn.id, conn, options);
|
|
658
|
+
if (!result) {
|
|
659
|
+
throw new Error("Failed to join or create room");
|
|
660
|
+
}
|
|
661
|
+
return {
|
|
662
|
+
roomId: result.room.id,
|
|
663
|
+
playerId: result.player.id
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
throw new Error("roomType or roomId required");
|
|
667
|
+
};
|
|
668
|
+
apiHandlersObj["LeaveRoom"] = async (_input, conn) => {
|
|
669
|
+
await roomManager.leave(conn.id);
|
|
670
|
+
return {
|
|
671
|
+
success: true
|
|
672
|
+
};
|
|
673
|
+
};
|
|
674
|
+
for (const [name, handler] of Object.entries(apiMap)) {
|
|
675
|
+
apiHandlersObj[name] = async (input, conn) => {
|
|
676
|
+
const ctx = {
|
|
677
|
+
conn,
|
|
678
|
+
server: gameServer
|
|
679
|
+
};
|
|
680
|
+
return handler.definition.handler(input, ctx);
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
const msgHandlersObj = {};
|
|
684
|
+
msgHandlersObj["RoomMessage"] = async (data, conn) => {
|
|
685
|
+
const { type, data: payload } = data;
|
|
686
|
+
roomManager.handleMessage(conn.id, type, payload);
|
|
687
|
+
};
|
|
688
|
+
for (const [name, handler] of Object.entries(msgMap)) {
|
|
689
|
+
msgHandlersObj[name] = async (data, conn) => {
|
|
690
|
+
const ctx = {
|
|
691
|
+
conn,
|
|
692
|
+
server: gameServer
|
|
693
|
+
};
|
|
694
|
+
await handler.definition.handler(data, ctx);
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
rpcServer = serve(protocol, {
|
|
698
|
+
port: opts.port,
|
|
699
|
+
createConnData: /* @__PURE__ */ __name(() => ({}), "createConnData"),
|
|
700
|
+
onStart: /* @__PURE__ */ __name((p) => {
|
|
701
|
+
console.log(`[Server] Started on ws://localhost:${p}`);
|
|
702
|
+
opts.onStart?.(p);
|
|
703
|
+
}, "onStart"),
|
|
704
|
+
onConnect: /* @__PURE__ */ __name(async (conn) => {
|
|
705
|
+
await config.onConnect?.(conn);
|
|
706
|
+
}, "onConnect"),
|
|
707
|
+
onDisconnect: /* @__PURE__ */ __name(async (conn) => {
|
|
708
|
+
await roomManager?.leave(conn.id, "disconnected");
|
|
709
|
+
await config.onDisconnect?.(conn);
|
|
710
|
+
}, "onDisconnect"),
|
|
711
|
+
api: apiHandlersObj,
|
|
712
|
+
msg: msgHandlersObj
|
|
713
|
+
});
|
|
714
|
+
await rpcServer.start();
|
|
715
|
+
if (opts.tickRate > 0) {
|
|
716
|
+
tickInterval = setInterval(() => {
|
|
717
|
+
currentTick++;
|
|
718
|
+
}, 1e3 / opts.tickRate);
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
async stop() {
|
|
722
|
+
if (tickInterval) {
|
|
723
|
+
clearInterval(tickInterval);
|
|
724
|
+
tickInterval = null;
|
|
725
|
+
}
|
|
726
|
+
if (rpcServer) {
|
|
727
|
+
await rpcServer.stop();
|
|
728
|
+
rpcServer = null;
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
broadcast(name, data) {
|
|
732
|
+
rpcServer?.broadcast(name, data);
|
|
733
|
+
},
|
|
734
|
+
send(conn, name, data) {
|
|
735
|
+
rpcServer?.send(conn, name, data);
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
return gameServer;
|
|
739
|
+
}
|
|
740
|
+
__name(createServer, "createServer");
|
|
741
|
+
|
|
742
|
+
export { Player, Room, createServer, onMessage };
|
|
743
|
+
//# sourceMappingURL=chunk-7C6JZO4O.js.map
|
|
744
|
+
//# sourceMappingURL=chunk-7C6JZO4O.js.map
|