@esengine/server 4.3.0 → 4.5.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 → Room-5owFVIFR.d.ts} +18 -0
- package/dist/auth/index.d.ts +4 -4
- package/dist/auth/testing/index.d.ts +2 -2
- package/dist/chunk-NWZLKNGV.js +2045 -0
- package/dist/chunk-NWZLKNGV.js.map +1 -0
- package/dist/{chunk-FACTBKJ3.js → chunk-T3QJOPNG.js} +37 -2
- package/dist/chunk-T3QJOPNG.js.map +1 -0
- package/dist/chunk-ZUTL4RI7.js +285 -0
- package/dist/chunk-ZUTL4RI7.js.map +1 -0
- package/dist/ecs/index.d.ts +31 -4
- package/dist/ecs/index.js +3 -280
- package/dist/ecs/index.js.map +1 -1
- package/dist/index-B1sr5YAl.d.ts +1076 -0
- package/dist/index.d.ts +1453 -7
- package/dist/index.js +1502 -4
- package/dist/index.js.map +1 -1
- package/dist/ratelimit/index.d.ts +1 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/{types-C7sS8Sfi.d.ts → types-BCTRacMF.d.ts} +2 -2
- package/package.json +1 -1
- package/dist/chunk-FACTBKJ3.js.map +0 -1
- package/dist/chunk-M7VONMZJ.js +0 -938
- package/dist/chunk-M7VONMZJ.js.map +0 -1
- package/dist/decorators-DY8nZ8Nh.d.ts +0 -26
- package/dist/index-lcuKuQsL.d.ts +0 -470
|
@@ -0,0 +1,2045 @@
|
|
|
1
|
+
import { createLogger } from './chunk-I4QQSQ72.js';
|
|
2
|
+
import { __name, __publicField } from './chunk-T626JPC7.js';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { createServer as createServer$1 } from 'http';
|
|
5
|
+
import { serve } from '@esengine/rpc/server';
|
|
6
|
+
import { rpc } from '@esengine/rpc';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import { pathToFileURL } from 'url';
|
|
9
|
+
|
|
10
|
+
// src/room/RoomManager.ts
|
|
11
|
+
var logger = createLogger("Room");
|
|
12
|
+
var _RoomManager = class _RoomManager {
|
|
13
|
+
constructor(sendFn, sendBinaryFn) {
|
|
14
|
+
/**
|
|
15
|
+
* @zh 房间类型定义映射
|
|
16
|
+
* @en Room type definitions map
|
|
17
|
+
*/
|
|
18
|
+
__publicField(this, "_definitions", /* @__PURE__ */ new Map());
|
|
19
|
+
/**
|
|
20
|
+
* @zh 房间实例映射
|
|
21
|
+
* @en Room instances map
|
|
22
|
+
*/
|
|
23
|
+
__publicField(this, "_rooms", /* @__PURE__ */ new Map());
|
|
24
|
+
/**
|
|
25
|
+
* @zh 玩家到房间的映射
|
|
26
|
+
* @en Player to room mapping
|
|
27
|
+
*/
|
|
28
|
+
__publicField(this, "_playerToRoom", /* @__PURE__ */ new Map());
|
|
29
|
+
/**
|
|
30
|
+
* @zh 下一个房间 ID 计数器
|
|
31
|
+
* @en Next room ID counter
|
|
32
|
+
*/
|
|
33
|
+
__publicField(this, "_nextRoomId", 1);
|
|
34
|
+
/**
|
|
35
|
+
* @zh 消息发送函数
|
|
36
|
+
* @en Message send function
|
|
37
|
+
*/
|
|
38
|
+
__publicField(this, "_sendFn");
|
|
39
|
+
/**
|
|
40
|
+
* @zh 二进制发送函数
|
|
41
|
+
* @en Binary send function
|
|
42
|
+
*/
|
|
43
|
+
__publicField(this, "_sendBinaryFn");
|
|
44
|
+
this._sendFn = sendFn;
|
|
45
|
+
this._sendBinaryFn = sendBinaryFn;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* @zh 注册房间类型
|
|
49
|
+
* @en Define room type
|
|
50
|
+
*/
|
|
51
|
+
define(name, roomClass) {
|
|
52
|
+
this._definitions.set(name, {
|
|
53
|
+
roomClass
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* @zh 创建房间
|
|
58
|
+
* @en Create room
|
|
59
|
+
*
|
|
60
|
+
* @param name - 房间类型名称 | Room type name
|
|
61
|
+
* @param options - 房间配置 | Room options
|
|
62
|
+
* @returns 房间实例或 null | Room instance or null
|
|
63
|
+
*/
|
|
64
|
+
async create(name, options) {
|
|
65
|
+
const room = await this._createRoomInstance(name, options);
|
|
66
|
+
if (room) {
|
|
67
|
+
await this._onRoomCreated(name, room);
|
|
68
|
+
logger.info(`Created: ${name} (${room.id})`);
|
|
69
|
+
}
|
|
70
|
+
return room;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* @zh 房间创建后的回调
|
|
74
|
+
* @en Callback after room is created
|
|
75
|
+
*
|
|
76
|
+
* @param _name - 房间类型名称 | Room type name
|
|
77
|
+
* @param _room - 房间实例 | Room instance
|
|
78
|
+
*/
|
|
79
|
+
async _onRoomCreated(_name, _room) {
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* @zh 加入或创建房间
|
|
83
|
+
* @en Join or create room
|
|
84
|
+
*
|
|
85
|
+
* @param name - 房间类型名称 | Room type name
|
|
86
|
+
* @param playerId - 玩家 ID | Player ID
|
|
87
|
+
* @param conn - 玩家连接 | Player connection
|
|
88
|
+
* @param options - 房间配置 | Room options
|
|
89
|
+
* @returns 房间和玩家实例或 null | Room and player instance or null
|
|
90
|
+
*/
|
|
91
|
+
async joinOrCreate(name, playerId, conn, options) {
|
|
92
|
+
let room = this._findAvailableRoom(name);
|
|
93
|
+
if (!room) {
|
|
94
|
+
room = await this.create(name, options);
|
|
95
|
+
if (!room) return null;
|
|
96
|
+
}
|
|
97
|
+
const player = await room._addPlayer(playerId, conn);
|
|
98
|
+
if (!player) return null;
|
|
99
|
+
this._onPlayerJoined(playerId, room.id, player);
|
|
100
|
+
logger.info(`Player ${playerId} joined ${room.id}`);
|
|
101
|
+
return {
|
|
102
|
+
room,
|
|
103
|
+
player
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* @zh 加入指定房间
|
|
108
|
+
* @en Join specific room
|
|
109
|
+
*
|
|
110
|
+
* @param roomId - 房间 ID | Room ID
|
|
111
|
+
* @param playerId - 玩家 ID | Player ID
|
|
112
|
+
* @param conn - 玩家连接 | Player connection
|
|
113
|
+
* @returns 房间和玩家实例或 null | Room and player instance or null
|
|
114
|
+
*/
|
|
115
|
+
async joinById(roomId, playerId, conn) {
|
|
116
|
+
const room = this._rooms.get(roomId);
|
|
117
|
+
if (!room) return null;
|
|
118
|
+
const player = await room._addPlayer(playerId, conn);
|
|
119
|
+
if (!player) return null;
|
|
120
|
+
this._onPlayerJoined(playerId, room.id, player);
|
|
121
|
+
logger.info(`Player ${playerId} joined ${room.id}`);
|
|
122
|
+
return {
|
|
123
|
+
room,
|
|
124
|
+
player
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* @zh 玩家离开
|
|
129
|
+
* @en Player leave
|
|
130
|
+
*
|
|
131
|
+
* @param playerId - 玩家 ID | Player ID
|
|
132
|
+
* @param reason - 离开原因 | Leave reason
|
|
133
|
+
*/
|
|
134
|
+
async leave(playerId, reason) {
|
|
135
|
+
const roomId = this._playerToRoom.get(playerId);
|
|
136
|
+
if (!roomId) return;
|
|
137
|
+
const room = this._rooms.get(roomId);
|
|
138
|
+
if (room) {
|
|
139
|
+
await room._removePlayer(playerId, reason);
|
|
140
|
+
}
|
|
141
|
+
this._onPlayerLeft(playerId, roomId);
|
|
142
|
+
logger.info(`Player ${playerId} left ${roomId}`);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* @zh 处理消息
|
|
146
|
+
* @en Handle message
|
|
147
|
+
*/
|
|
148
|
+
handleMessage(playerId, type, data) {
|
|
149
|
+
const roomId = this._playerToRoom.get(playerId);
|
|
150
|
+
if (!roomId) return;
|
|
151
|
+
const room = this._rooms.get(roomId);
|
|
152
|
+
if (room) {
|
|
153
|
+
room._handleMessage(type, data, playerId);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* @zh 获取房间
|
|
158
|
+
* @en Get room
|
|
159
|
+
*/
|
|
160
|
+
getRoom(roomId) {
|
|
161
|
+
return this._rooms.get(roomId);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* @zh 获取玩家所在房间
|
|
165
|
+
* @en Get player's room
|
|
166
|
+
*/
|
|
167
|
+
getPlayerRoom(playerId) {
|
|
168
|
+
const roomId = this._playerToRoom.get(playerId);
|
|
169
|
+
return roomId ? this._rooms.get(roomId) : void 0;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* @zh 获取所有房间
|
|
173
|
+
* @en Get all rooms
|
|
174
|
+
*/
|
|
175
|
+
getRooms() {
|
|
176
|
+
return Array.from(this._rooms.values());
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* @zh 获取指定类型的所有房间
|
|
180
|
+
* @en Get all rooms of a type
|
|
181
|
+
*/
|
|
182
|
+
getRoomsByType(name) {
|
|
183
|
+
const def = this._definitions.get(name);
|
|
184
|
+
if (!def) return [];
|
|
185
|
+
return Array.from(this._rooms.values()).filter((room) => room instanceof def.roomClass);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* @zh 查找可用房间
|
|
189
|
+
* @en Find available room
|
|
190
|
+
*
|
|
191
|
+
* @param name - 房间类型名称 | Room type name
|
|
192
|
+
* @returns 可用房间或 null | Available room or null
|
|
193
|
+
*/
|
|
194
|
+
_findAvailableRoom(name) {
|
|
195
|
+
const def = this._definitions.get(name);
|
|
196
|
+
if (!def) return null;
|
|
197
|
+
for (const room of this._rooms.values()) {
|
|
198
|
+
if (room instanceof def.roomClass && !room.isFull && !room.isLocked && !room.isDisposed) {
|
|
199
|
+
return room;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* @zh 生成房间 ID
|
|
206
|
+
* @en Generate room ID
|
|
207
|
+
*
|
|
208
|
+
* @returns 新的房间 ID | New room ID
|
|
209
|
+
*/
|
|
210
|
+
_generateRoomId() {
|
|
211
|
+
return `room_${this._nextRoomId++}`;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* @zh 获取房间定义
|
|
215
|
+
* @en Get room definition
|
|
216
|
+
*
|
|
217
|
+
* @param name - 房间类型名称 | Room type name
|
|
218
|
+
* @returns 房间定义或 undefined | Room definition or undefined
|
|
219
|
+
*/
|
|
220
|
+
_getDefinition(name) {
|
|
221
|
+
return this._definitions.get(name);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* @zh 内部创建房间实例
|
|
225
|
+
* @en Internal create room instance
|
|
226
|
+
*
|
|
227
|
+
* @param name - 房间类型名称 | Room type name
|
|
228
|
+
* @param options - 房间配置 | Room options
|
|
229
|
+
* @param roomId - 可选的房间 ID(用于分布式恢复) | Optional room ID (for distributed recovery)
|
|
230
|
+
* @returns 房间实例或 null | Room instance or null
|
|
231
|
+
*/
|
|
232
|
+
async _createRoomInstance(name, options, roomId) {
|
|
233
|
+
const def = this._definitions.get(name);
|
|
234
|
+
if (!def) {
|
|
235
|
+
logger.warn(`Room type not found: ${name}`);
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const finalRoomId = roomId ?? this._generateRoomId();
|
|
239
|
+
const room = new def.roomClass();
|
|
240
|
+
room._init({
|
|
241
|
+
id: finalRoomId,
|
|
242
|
+
sendFn: this._sendFn,
|
|
243
|
+
sendBinaryFn: this._sendBinaryFn,
|
|
244
|
+
broadcastFn: /* @__PURE__ */ __name((type, data) => {
|
|
245
|
+
for (const player of room.players) {
|
|
246
|
+
player.send(type, data);
|
|
247
|
+
}
|
|
248
|
+
}, "broadcastFn"),
|
|
249
|
+
disposeFn: /* @__PURE__ */ __name(() => {
|
|
250
|
+
this._onRoomDisposed(finalRoomId);
|
|
251
|
+
}, "disposeFn")
|
|
252
|
+
});
|
|
253
|
+
this._rooms.set(finalRoomId, room);
|
|
254
|
+
await room._create(options);
|
|
255
|
+
return room;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* @zh 房间销毁回调
|
|
259
|
+
* @en Room disposed callback
|
|
260
|
+
*
|
|
261
|
+
* @param roomId - 房间 ID | Room ID
|
|
262
|
+
*/
|
|
263
|
+
_onRoomDisposed(roomId) {
|
|
264
|
+
this._rooms.delete(roomId);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* @zh 玩家加入房间后的回调
|
|
268
|
+
* @en Callback after player joins room
|
|
269
|
+
*
|
|
270
|
+
* @param playerId - 玩家 ID | Player ID
|
|
271
|
+
* @param roomId - 房间 ID | Room ID
|
|
272
|
+
* @param player - 玩家实例 | Player instance
|
|
273
|
+
*/
|
|
274
|
+
_onPlayerJoined(playerId, roomId, _player) {
|
|
275
|
+
this._playerToRoom.set(playerId, roomId);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* @zh 玩家离开房间后的回调
|
|
279
|
+
* @en Callback after player leaves room
|
|
280
|
+
*
|
|
281
|
+
* @param playerId - 玩家 ID | Player ID
|
|
282
|
+
* @param _roomId - 房间 ID | Room ID
|
|
283
|
+
*/
|
|
284
|
+
_onPlayerLeft(playerId, _roomId) {
|
|
285
|
+
this._playerToRoom.delete(playerId);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
__name(_RoomManager, "RoomManager");
|
|
289
|
+
var RoomManager = _RoomManager;
|
|
290
|
+
|
|
291
|
+
// src/http/router.ts
|
|
292
|
+
var logger2 = createLogger("HTTP");
|
|
293
|
+
function parseRoutePath(path3) {
|
|
294
|
+
const paramNames = [];
|
|
295
|
+
const isStatic = !path3.includes(":");
|
|
296
|
+
if (isStatic) {
|
|
297
|
+
return {
|
|
298
|
+
pattern: new RegExp(`^${escapeRegex(path3)}$`),
|
|
299
|
+
paramNames,
|
|
300
|
+
isStatic: true
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
const segments = path3.split("/").map((segment) => {
|
|
304
|
+
if (segment.startsWith(":")) {
|
|
305
|
+
const paramName = segment.slice(1);
|
|
306
|
+
paramNames.push(paramName);
|
|
307
|
+
return "([^/]+)";
|
|
308
|
+
}
|
|
309
|
+
return escapeRegex(segment);
|
|
310
|
+
});
|
|
311
|
+
return {
|
|
312
|
+
pattern: new RegExp(`^${segments.join("/")}$`),
|
|
313
|
+
paramNames,
|
|
314
|
+
isStatic: false
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
__name(parseRoutePath, "parseRoutePath");
|
|
318
|
+
function escapeRegex(str) {
|
|
319
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
320
|
+
}
|
|
321
|
+
__name(escapeRegex, "escapeRegex");
|
|
322
|
+
function matchRoute(routes, path3, method) {
|
|
323
|
+
for (const route of routes) {
|
|
324
|
+
if (!route.isStatic) continue;
|
|
325
|
+
if (route.method !== "*" && route.method !== method) continue;
|
|
326
|
+
if (route.pattern.test(path3)) {
|
|
327
|
+
return {
|
|
328
|
+
route,
|
|
329
|
+
params: {}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
for (const route of routes) {
|
|
334
|
+
if (route.isStatic) continue;
|
|
335
|
+
if (route.method !== "*" && route.method !== method) continue;
|
|
336
|
+
const match = path3.match(route.pattern);
|
|
337
|
+
if (match) {
|
|
338
|
+
const params = {};
|
|
339
|
+
route.paramNames.forEach((name, index) => {
|
|
340
|
+
params[name] = decodeURIComponent(match[index + 1]);
|
|
341
|
+
});
|
|
342
|
+
return {
|
|
343
|
+
route,
|
|
344
|
+
params
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
__name(matchRoute, "matchRoute");
|
|
351
|
+
async function createRequest(req, params = {}) {
|
|
352
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
353
|
+
const query = {};
|
|
354
|
+
url.searchParams.forEach((value, key) => {
|
|
355
|
+
query[key] = value;
|
|
356
|
+
});
|
|
357
|
+
let body = null;
|
|
358
|
+
if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") {
|
|
359
|
+
body = await parseBody(req);
|
|
360
|
+
}
|
|
361
|
+
const ip = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket?.remoteAddress || "unknown";
|
|
362
|
+
return {
|
|
363
|
+
raw: req,
|
|
364
|
+
method: req.method ?? "GET",
|
|
365
|
+
path: url.pathname,
|
|
366
|
+
params,
|
|
367
|
+
query,
|
|
368
|
+
headers: req.headers,
|
|
369
|
+
body,
|
|
370
|
+
ip
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
__name(createRequest, "createRequest");
|
|
374
|
+
function parseBody(req) {
|
|
375
|
+
return new Promise((resolve2) => {
|
|
376
|
+
const chunks = [];
|
|
377
|
+
req.on("data", (chunk) => {
|
|
378
|
+
chunks.push(chunk);
|
|
379
|
+
});
|
|
380
|
+
req.on("end", () => {
|
|
381
|
+
const rawBody = Buffer.concat(chunks).toString("utf-8");
|
|
382
|
+
if (!rawBody) {
|
|
383
|
+
resolve2(null);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const contentType = req.headers["content-type"] ?? "";
|
|
387
|
+
if (contentType.includes("application/json")) {
|
|
388
|
+
try {
|
|
389
|
+
resolve2(JSON.parse(rawBody));
|
|
390
|
+
} catch {
|
|
391
|
+
resolve2(rawBody);
|
|
392
|
+
}
|
|
393
|
+
} else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
394
|
+
const params = new URLSearchParams(rawBody);
|
|
395
|
+
const result = {};
|
|
396
|
+
params.forEach((value, key) => {
|
|
397
|
+
result[key] = value;
|
|
398
|
+
});
|
|
399
|
+
resolve2(result);
|
|
400
|
+
} else {
|
|
401
|
+
resolve2(rawBody);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
req.on("error", () => {
|
|
405
|
+
resolve2(null);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
__name(parseBody, "parseBody");
|
|
410
|
+
function createResponse(res) {
|
|
411
|
+
let statusCode = 200;
|
|
412
|
+
let ended = false;
|
|
413
|
+
const response = {
|
|
414
|
+
raw: res,
|
|
415
|
+
status(code) {
|
|
416
|
+
statusCode = code;
|
|
417
|
+
return response;
|
|
418
|
+
},
|
|
419
|
+
header(name, value) {
|
|
420
|
+
if (!ended) {
|
|
421
|
+
res.setHeader(name, value);
|
|
422
|
+
}
|
|
423
|
+
return response;
|
|
424
|
+
},
|
|
425
|
+
json(data) {
|
|
426
|
+
if (ended) return;
|
|
427
|
+
ended = true;
|
|
428
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
429
|
+
res.statusCode = statusCode;
|
|
430
|
+
res.end(JSON.stringify(data));
|
|
431
|
+
},
|
|
432
|
+
text(data) {
|
|
433
|
+
if (ended) return;
|
|
434
|
+
ended = true;
|
|
435
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
436
|
+
res.statusCode = statusCode;
|
|
437
|
+
res.end(data);
|
|
438
|
+
},
|
|
439
|
+
error(code, message) {
|
|
440
|
+
if (ended) return;
|
|
441
|
+
ended = true;
|
|
442
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
443
|
+
res.statusCode = code;
|
|
444
|
+
res.end(JSON.stringify({
|
|
445
|
+
error: message
|
|
446
|
+
}));
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
return response;
|
|
450
|
+
}
|
|
451
|
+
__name(createResponse, "createResponse");
|
|
452
|
+
function createOriginWhitelist(origins) {
|
|
453
|
+
const whitelist = {};
|
|
454
|
+
for (const origin of origins) {
|
|
455
|
+
whitelist[origin] = true;
|
|
456
|
+
}
|
|
457
|
+
return whitelist;
|
|
458
|
+
}
|
|
459
|
+
__name(createOriginWhitelist, "createOriginWhitelist");
|
|
460
|
+
function applyCors(res, req, cors) {
|
|
461
|
+
const credentials = cors.credentials ?? false;
|
|
462
|
+
if (typeof cors.origin === "string" && cors.origin !== "*") {
|
|
463
|
+
res.setHeader("Access-Control-Allow-Origin", cors.origin);
|
|
464
|
+
if (credentials) {
|
|
465
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
466
|
+
}
|
|
467
|
+
} else if (Array.isArray(cors.origin)) {
|
|
468
|
+
const requestOrigin = req.headers.origin;
|
|
469
|
+
if (typeof requestOrigin === "string") {
|
|
470
|
+
const whitelist = createOriginWhitelist(cors.origin);
|
|
471
|
+
if (requestOrigin in whitelist) {
|
|
472
|
+
res.setHeader("Access-Control-Allow-Origin", requestOrigin);
|
|
473
|
+
if (credentials) {
|
|
474
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
} else if (!credentials) {
|
|
479
|
+
if (cors.origin === "*" || cors.origin === true) {
|
|
480
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
res.setHeader("Access-Control-Allow-Methods", cors.methods?.join(", ") ?? "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
|
484
|
+
res.setHeader("Access-Control-Allow-Headers", cors.allowedHeaders?.join(", ") ?? "Content-Type, Authorization");
|
|
485
|
+
if (cors.maxAge) {
|
|
486
|
+
res.setHeader("Access-Control-Max-Age", String(cors.maxAge));
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
__name(applyCors, "applyCors");
|
|
490
|
+
async function executeMiddlewares(middlewares, req, res, finalHandler) {
|
|
491
|
+
let index = 0;
|
|
492
|
+
const next = /* @__PURE__ */ __name(async () => {
|
|
493
|
+
if (index < middlewares.length) {
|
|
494
|
+
const middleware = middlewares[index++];
|
|
495
|
+
await middleware(req, res, next);
|
|
496
|
+
} else {
|
|
497
|
+
await finalHandler();
|
|
498
|
+
}
|
|
499
|
+
}, "next");
|
|
500
|
+
await next();
|
|
501
|
+
}
|
|
502
|
+
__name(executeMiddlewares, "executeMiddlewares");
|
|
503
|
+
async function executeWithTimeout(handler, timeoutMs, res) {
|
|
504
|
+
let resolved = false;
|
|
505
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
506
|
+
setTimeout(() => {
|
|
507
|
+
if (!resolved) {
|
|
508
|
+
reject(new Error("Request timeout"));
|
|
509
|
+
}
|
|
510
|
+
}, timeoutMs);
|
|
511
|
+
});
|
|
512
|
+
try {
|
|
513
|
+
await Promise.race([
|
|
514
|
+
handler().then(() => {
|
|
515
|
+
resolved = true;
|
|
516
|
+
}),
|
|
517
|
+
timeoutPromise
|
|
518
|
+
]);
|
|
519
|
+
} catch (error) {
|
|
520
|
+
if (error instanceof Error && error.message === "Request timeout") {
|
|
521
|
+
if (!res.writableEnded) {
|
|
522
|
+
res.statusCode = 408;
|
|
523
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
524
|
+
res.end(JSON.stringify({
|
|
525
|
+
error: "Request Timeout"
|
|
526
|
+
}));
|
|
527
|
+
}
|
|
528
|
+
} else {
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
__name(executeWithTimeout, "executeWithTimeout");
|
|
534
|
+
function isHandlerDefinition(value) {
|
|
535
|
+
return typeof value === "object" && value !== null && "handler" in value && typeof value.handler === "function";
|
|
536
|
+
}
|
|
537
|
+
__name(isHandlerDefinition, "isHandlerDefinition");
|
|
538
|
+
function isRouteMethods(value) {
|
|
539
|
+
if (typeof value !== "object" || value === null) return false;
|
|
540
|
+
const methods = [
|
|
541
|
+
"GET",
|
|
542
|
+
"POST",
|
|
543
|
+
"PUT",
|
|
544
|
+
"DELETE",
|
|
545
|
+
"PATCH",
|
|
546
|
+
"OPTIONS"
|
|
547
|
+
];
|
|
548
|
+
return Object.keys(value).some((key) => methods.includes(key));
|
|
549
|
+
}
|
|
550
|
+
__name(isRouteMethods, "isRouteMethods");
|
|
551
|
+
function extractHandler(methodHandler) {
|
|
552
|
+
if (isHandlerDefinition(methodHandler)) {
|
|
553
|
+
return {
|
|
554
|
+
handler: methodHandler.handler,
|
|
555
|
+
middlewares: methodHandler.middlewares ?? [],
|
|
556
|
+
timeout: methodHandler.timeout
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
handler: methodHandler,
|
|
561
|
+
middlewares: [],
|
|
562
|
+
timeout: void 0
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
__name(extractHandler, "extractHandler");
|
|
566
|
+
function createHttpRouter(routes, options = {}) {
|
|
567
|
+
const globalMiddlewares = options.middlewares ?? [];
|
|
568
|
+
const globalTimeout = options.timeout;
|
|
569
|
+
const parsedRoutes = [];
|
|
570
|
+
for (const [path3, handlerOrMethods] of Object.entries(routes)) {
|
|
571
|
+
const { pattern, paramNames, isStatic } = parseRoutePath(path3);
|
|
572
|
+
if (typeof handlerOrMethods === "function") {
|
|
573
|
+
parsedRoutes.push({
|
|
574
|
+
method: "*",
|
|
575
|
+
path: path3,
|
|
576
|
+
handler: handlerOrMethods,
|
|
577
|
+
pattern,
|
|
578
|
+
paramNames,
|
|
579
|
+
middlewares: [],
|
|
580
|
+
timeout: void 0,
|
|
581
|
+
isStatic
|
|
582
|
+
});
|
|
583
|
+
} else if (isRouteMethods(handlerOrMethods)) {
|
|
584
|
+
for (const [method, methodHandler] of Object.entries(handlerOrMethods)) {
|
|
585
|
+
if (methodHandler !== void 0) {
|
|
586
|
+
const { handler, middlewares, timeout } = extractHandler(methodHandler);
|
|
587
|
+
parsedRoutes.push({
|
|
588
|
+
method,
|
|
589
|
+
path: path3,
|
|
590
|
+
handler,
|
|
591
|
+
pattern,
|
|
592
|
+
paramNames,
|
|
593
|
+
middlewares,
|
|
594
|
+
timeout,
|
|
595
|
+
isStatic
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
} else if (isHandlerDefinition(handlerOrMethods)) {
|
|
600
|
+
const { handler, middlewares, timeout } = extractHandler(handlerOrMethods);
|
|
601
|
+
parsedRoutes.push({
|
|
602
|
+
method: "*",
|
|
603
|
+
path: path3,
|
|
604
|
+
handler,
|
|
605
|
+
pattern,
|
|
606
|
+
paramNames,
|
|
607
|
+
middlewares,
|
|
608
|
+
timeout,
|
|
609
|
+
isStatic
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
const corsOptions = options.cors === true ? {
|
|
614
|
+
origin: "*"
|
|
615
|
+
} : options.cors === false ? null : options.cors ?? null;
|
|
616
|
+
return /* @__PURE__ */ __name(async function handleRequest(req, res) {
|
|
617
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
618
|
+
const path3 = url.pathname;
|
|
619
|
+
const method = req.method ?? "GET";
|
|
620
|
+
if (corsOptions) {
|
|
621
|
+
applyCors(res, req, corsOptions);
|
|
622
|
+
if (method === "OPTIONS") {
|
|
623
|
+
res.statusCode = 204;
|
|
624
|
+
res.end();
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const match = matchRoute(parsedRoutes, path3, method);
|
|
629
|
+
if (!match) {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
const { route, params } = match;
|
|
633
|
+
try {
|
|
634
|
+
const httpReq = await createRequest(req, params);
|
|
635
|
+
const httpRes = createResponse(res);
|
|
636
|
+
const allMiddlewares = [
|
|
637
|
+
...globalMiddlewares,
|
|
638
|
+
...route.middlewares
|
|
639
|
+
];
|
|
640
|
+
const timeout = route.timeout ?? globalTimeout;
|
|
641
|
+
const finalHandler = /* @__PURE__ */ __name(async () => {
|
|
642
|
+
await route.handler(httpReq, httpRes);
|
|
643
|
+
}, "finalHandler");
|
|
644
|
+
const executeHandler = /* @__PURE__ */ __name(async () => {
|
|
645
|
+
if (allMiddlewares.length > 0) {
|
|
646
|
+
await executeMiddlewares(allMiddlewares, httpReq, httpRes, finalHandler);
|
|
647
|
+
} else {
|
|
648
|
+
await finalHandler();
|
|
649
|
+
}
|
|
650
|
+
}, "executeHandler");
|
|
651
|
+
if (timeout && timeout > 0) {
|
|
652
|
+
await executeWithTimeout(executeHandler, timeout, res);
|
|
653
|
+
} else {
|
|
654
|
+
await executeHandler();
|
|
655
|
+
}
|
|
656
|
+
return true;
|
|
657
|
+
} catch (error) {
|
|
658
|
+
logger2.error("Route handler error:", error);
|
|
659
|
+
if (!res.writableEnded) {
|
|
660
|
+
res.statusCode = 500;
|
|
661
|
+
res.setHeader("Content-Type", "application/json");
|
|
662
|
+
res.end(JSON.stringify({
|
|
663
|
+
error: "Internal Server Error"
|
|
664
|
+
}));
|
|
665
|
+
}
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
}, "handleRequest");
|
|
669
|
+
}
|
|
670
|
+
__name(createHttpRouter, "createHttpRouter");
|
|
671
|
+
|
|
672
|
+
// src/distributed/DistributedRoomManager.ts
|
|
673
|
+
var logger3 = createLogger("DistributedRoom");
|
|
674
|
+
var _DistributedRoomManager = class _DistributedRoomManager extends RoomManager {
|
|
675
|
+
/**
|
|
676
|
+
* @zh 创建分布式房间管理器
|
|
677
|
+
* @en Create distributed room manager
|
|
678
|
+
*
|
|
679
|
+
* @param adapter - 分布式适配器 | Distributed adapter
|
|
680
|
+
* @param config - 配置 | Configuration
|
|
681
|
+
* @param sendFn - 消息发送函数 | Message send function
|
|
682
|
+
* @param sendBinaryFn - 二进制发送函数 | Binary send function
|
|
683
|
+
*/
|
|
684
|
+
constructor(adapter, config, sendFn, sendBinaryFn) {
|
|
685
|
+
super(sendFn, sendBinaryFn);
|
|
686
|
+
__publicField(this, "_adapter");
|
|
687
|
+
__publicField(this, "_config");
|
|
688
|
+
__publicField(this, "_serverId");
|
|
689
|
+
__publicField(this, "_heartbeatTimer", null);
|
|
690
|
+
__publicField(this, "_snapshotTimer", null);
|
|
691
|
+
__publicField(this, "_subscriptions", []);
|
|
692
|
+
__publicField(this, "_isShuttingDown", false);
|
|
693
|
+
this._adapter = adapter;
|
|
694
|
+
this._serverId = config.serverId;
|
|
695
|
+
this._config = {
|
|
696
|
+
serverId: config.serverId,
|
|
697
|
+
serverAddress: config.serverAddress,
|
|
698
|
+
serverPort: config.serverPort,
|
|
699
|
+
heartbeatInterval: config.heartbeatInterval ?? 5e3,
|
|
700
|
+
snapshotInterval: config.snapshotInterval ?? 3e4,
|
|
701
|
+
migrationTimeout: config.migrationTimeout ?? 1e4,
|
|
702
|
+
enableFailover: config.enableFailover ?? true,
|
|
703
|
+
capacity: config.capacity ?? 100,
|
|
704
|
+
metadata: config.metadata ?? {}
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* @zh 获取服务器 ID
|
|
709
|
+
* @en Get server ID
|
|
710
|
+
*/
|
|
711
|
+
get serverId() {
|
|
712
|
+
return this._serverId;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* @zh 获取分布式适配器
|
|
716
|
+
* @en Get distributed adapter
|
|
717
|
+
*/
|
|
718
|
+
get adapter() {
|
|
719
|
+
return this._adapter;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* @zh 获取配置
|
|
723
|
+
* @en Get configuration
|
|
724
|
+
*/
|
|
725
|
+
get config() {
|
|
726
|
+
return this._config;
|
|
727
|
+
}
|
|
728
|
+
// =========================================================================
|
|
729
|
+
// 生命周期 | Lifecycle
|
|
730
|
+
// =========================================================================
|
|
731
|
+
/**
|
|
732
|
+
* @zh 启动分布式房间管理器
|
|
733
|
+
* @en Start distributed room manager
|
|
734
|
+
*/
|
|
735
|
+
async start() {
|
|
736
|
+
if (!this._adapter.isConnected()) {
|
|
737
|
+
await this._adapter.connect();
|
|
738
|
+
}
|
|
739
|
+
await this._registerServer();
|
|
740
|
+
await this._subscribeToEvents();
|
|
741
|
+
this._startHeartbeat();
|
|
742
|
+
if (this._config.snapshotInterval > 0) {
|
|
743
|
+
this._startSnapshotTimer();
|
|
744
|
+
}
|
|
745
|
+
logger3.info(`Distributed room manager started: ${this._serverId}`);
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* @zh 停止分布式房间管理器
|
|
749
|
+
* @en Stop distributed room manager
|
|
750
|
+
*
|
|
751
|
+
* @param graceful - 是否优雅关闭(等待玩家退出)| Whether to gracefully shutdown (wait for players)
|
|
752
|
+
*/
|
|
753
|
+
async stop(graceful = true) {
|
|
754
|
+
this._isShuttingDown = true;
|
|
755
|
+
if (this._heartbeatTimer) {
|
|
756
|
+
clearInterval(this._heartbeatTimer);
|
|
757
|
+
this._heartbeatTimer = null;
|
|
758
|
+
}
|
|
759
|
+
if (this._snapshotTimer) {
|
|
760
|
+
clearInterval(this._snapshotTimer);
|
|
761
|
+
this._snapshotTimer = null;
|
|
762
|
+
}
|
|
763
|
+
for (const unsub of this._subscriptions) {
|
|
764
|
+
unsub();
|
|
765
|
+
}
|
|
766
|
+
this._subscriptions = [];
|
|
767
|
+
if (graceful) {
|
|
768
|
+
await this._adapter.updateServer(this._serverId, {
|
|
769
|
+
status: "draining"
|
|
770
|
+
});
|
|
771
|
+
await this._saveAllSnapshots();
|
|
772
|
+
}
|
|
773
|
+
await this._adapter.unregisterServer(this._serverId);
|
|
774
|
+
logger3.info(`Distributed room manager stopped: ${this._serverId}`);
|
|
775
|
+
}
|
|
776
|
+
// =========================================================================
|
|
777
|
+
// 房间操作覆盖 | Room Operation Overrides
|
|
778
|
+
// =========================================================================
|
|
779
|
+
/**
|
|
780
|
+
* @zh 房间创建后注册到分布式系统
|
|
781
|
+
* @en Register room to distributed system after creation
|
|
782
|
+
*/
|
|
783
|
+
async _onRoomCreated(name, room) {
|
|
784
|
+
const registration = {
|
|
785
|
+
roomId: room.id,
|
|
786
|
+
roomType: name,
|
|
787
|
+
serverId: this._serverId,
|
|
788
|
+
serverAddress: `${this._config.serverAddress}:${this._config.serverPort}`,
|
|
789
|
+
playerCount: room.players.length,
|
|
790
|
+
maxPlayers: room.maxPlayers,
|
|
791
|
+
isLocked: room.isLocked,
|
|
792
|
+
metadata: {},
|
|
793
|
+
createdAt: Date.now(),
|
|
794
|
+
updatedAt: Date.now()
|
|
795
|
+
};
|
|
796
|
+
await this._adapter.registerRoom(registration);
|
|
797
|
+
logger3.debug(`Registered room: ${room.id}`);
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* @zh 房间销毁时从分布式系统注销
|
|
801
|
+
* @en Unregister room from distributed system when disposed
|
|
802
|
+
*/
|
|
803
|
+
_onRoomDisposed(roomId) {
|
|
804
|
+
super._onRoomDisposed(roomId);
|
|
805
|
+
this._adapter.unregisterRoom(roomId).catch((err) => {
|
|
806
|
+
logger3.error(`Failed to unregister room ${roomId}:`, err);
|
|
807
|
+
});
|
|
808
|
+
this._adapter.deleteSnapshot(roomId).catch((err) => {
|
|
809
|
+
logger3.error(`Failed to delete snapshot for ${roomId}:`, err);
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* @zh 玩家加入后更新分布式房间信息
|
|
814
|
+
* @en Update distributed room info after player joins
|
|
815
|
+
*/
|
|
816
|
+
_onPlayerJoined(playerId, roomId, player) {
|
|
817
|
+
super._onPlayerJoined(playerId, roomId, player);
|
|
818
|
+
const room = this._rooms.get(roomId);
|
|
819
|
+
if (room) {
|
|
820
|
+
this._adapter.updateRoom(roomId, {
|
|
821
|
+
playerCount: room.players.length,
|
|
822
|
+
updatedAt: Date.now()
|
|
823
|
+
}).catch((err) => {
|
|
824
|
+
logger3.error(`Failed to update room ${roomId}:`, err);
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* @zh 玩家离开后更新分布式房间信息
|
|
830
|
+
* @en Update distributed room info after player leaves
|
|
831
|
+
*/
|
|
832
|
+
_onPlayerLeft(playerId, roomId) {
|
|
833
|
+
super._onPlayerLeft(playerId, roomId);
|
|
834
|
+
const room = this._rooms.get(roomId);
|
|
835
|
+
if (room) {
|
|
836
|
+
this._adapter.updateRoom(roomId, {
|
|
837
|
+
playerCount: room.players.length,
|
|
838
|
+
updatedAt: Date.now()
|
|
839
|
+
}).catch((err) => {
|
|
840
|
+
logger3.error(`Failed to update room ${roomId}:`, err);
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
// =========================================================================
|
|
845
|
+
// 分布式路由 | Distributed Routing
|
|
846
|
+
// =========================================================================
|
|
847
|
+
/**
|
|
848
|
+
* @zh 路由玩家到合适的房间/服务器
|
|
849
|
+
* @en Route player to appropriate room/server
|
|
850
|
+
*
|
|
851
|
+
* @param request - 路由请求 | Routing request
|
|
852
|
+
* @returns 路由结果 | Routing result
|
|
853
|
+
*/
|
|
854
|
+
async route(request) {
|
|
855
|
+
if (request.roomId) {
|
|
856
|
+
return this._routeToRoom(request.roomId);
|
|
857
|
+
}
|
|
858
|
+
if (request.roomType) {
|
|
859
|
+
return this._routeByType(request.roomType, request.query);
|
|
860
|
+
}
|
|
861
|
+
return {
|
|
862
|
+
type: "unavailable",
|
|
863
|
+
reason: "No room type or room ID specified"
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* @zh 加入或创建房间(分布式版本)
|
|
868
|
+
* @en Join or create room (distributed version)
|
|
869
|
+
*
|
|
870
|
+
* @zh 此方法会:
|
|
871
|
+
* 1. 先在分布式注册表中查找可用房间
|
|
872
|
+
* 2. 如果找到其他服务器的房间,返回重定向
|
|
873
|
+
* 3. 如果找到本地房间或需要创建,在本地处理
|
|
874
|
+
* @en This method will:
|
|
875
|
+
* 1. First search for available room in distributed registry
|
|
876
|
+
* 2. If room found on another server, return redirect
|
|
877
|
+
* 3. If local room found or creation needed, handle locally
|
|
878
|
+
*/
|
|
879
|
+
async joinOrCreateDistributed(name, playerId, conn, options) {
|
|
880
|
+
const lockKey = `joinOrCreate:${name}`;
|
|
881
|
+
const locked = await this._adapter.acquireLock(lockKey, 5e3);
|
|
882
|
+
if (!locked) {
|
|
883
|
+
await this._sleep(100);
|
|
884
|
+
return this.joinOrCreateDistributed(name, playerId, conn, options);
|
|
885
|
+
}
|
|
886
|
+
try {
|
|
887
|
+
const availableRoom = await this._adapter.findAvailableRoom(name);
|
|
888
|
+
if (availableRoom) {
|
|
889
|
+
if (availableRoom.serverId === this._serverId) {
|
|
890
|
+
return super.joinOrCreate(name, playerId, conn, options);
|
|
891
|
+
} else {
|
|
892
|
+
return {
|
|
893
|
+
redirect: availableRoom.serverAddress
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return super.joinOrCreate(name, playerId, conn, options);
|
|
898
|
+
} finally {
|
|
899
|
+
await this._adapter.releaseLock(lockKey);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
// =========================================================================
|
|
903
|
+
// 状态管理 | State Management
|
|
904
|
+
// =========================================================================
|
|
905
|
+
/**
|
|
906
|
+
* @zh 保存房间状态快照
|
|
907
|
+
* @en Save room state snapshot
|
|
908
|
+
*
|
|
909
|
+
* @param roomId - 房间 ID | Room ID
|
|
910
|
+
*/
|
|
911
|
+
async saveSnapshot(roomId) {
|
|
912
|
+
const room = this._rooms.get(roomId);
|
|
913
|
+
if (!room) return;
|
|
914
|
+
const def = this._getDefinitionByRoom(room);
|
|
915
|
+
if (!def) return;
|
|
916
|
+
const snapshot = {
|
|
917
|
+
roomId: room.id,
|
|
918
|
+
roomType: def.name,
|
|
919
|
+
state: room.state ?? {},
|
|
920
|
+
players: room.players.map((p) => ({
|
|
921
|
+
id: p.id,
|
|
922
|
+
data: p.data ?? {}
|
|
923
|
+
})),
|
|
924
|
+
version: Date.now(),
|
|
925
|
+
timestamp: Date.now()
|
|
926
|
+
};
|
|
927
|
+
await this._adapter.saveSnapshot(snapshot);
|
|
928
|
+
logger3.debug(`Saved snapshot for room: ${roomId}`);
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* @zh 从快照恢复房间
|
|
932
|
+
* @en Restore room from snapshot
|
|
933
|
+
*
|
|
934
|
+
* @param roomId - 房间 ID | Room ID
|
|
935
|
+
* @returns 是否成功恢复 | Whether restore was successful
|
|
936
|
+
*/
|
|
937
|
+
async restoreFromSnapshot(roomId) {
|
|
938
|
+
const snapshot = await this._adapter.loadSnapshot(roomId);
|
|
939
|
+
if (!snapshot) return false;
|
|
940
|
+
const room = await this._createRoomInstance(snapshot.roomType, {
|
|
941
|
+
state: snapshot.state
|
|
942
|
+
}, snapshot.roomId);
|
|
943
|
+
if (!room) return false;
|
|
944
|
+
await this._onRoomCreated(snapshot.roomType, room);
|
|
945
|
+
logger3.info(`Restored room from snapshot: ${roomId}`);
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
// =========================================================================
|
|
949
|
+
// 私有方法 | Private Methods
|
|
950
|
+
// =========================================================================
|
|
951
|
+
/**
|
|
952
|
+
* @zh 注册服务器到分布式系统
|
|
953
|
+
* @en Register server to distributed system
|
|
954
|
+
*/
|
|
955
|
+
async _registerServer() {
|
|
956
|
+
const registration = {
|
|
957
|
+
serverId: this._serverId,
|
|
958
|
+
address: this._config.serverAddress,
|
|
959
|
+
port: this._config.serverPort,
|
|
960
|
+
roomCount: this._rooms.size,
|
|
961
|
+
playerCount: this._countTotalPlayers(),
|
|
962
|
+
capacity: this._config.capacity,
|
|
963
|
+
status: "online",
|
|
964
|
+
lastHeartbeat: Date.now(),
|
|
965
|
+
metadata: this._config.metadata
|
|
966
|
+
};
|
|
967
|
+
await this._adapter.registerServer(registration);
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* @zh 订阅分布式事件
|
|
971
|
+
* @en Subscribe to distributed events
|
|
972
|
+
*/
|
|
973
|
+
async _subscribeToEvents() {
|
|
974
|
+
if (this._config.enableFailover) {
|
|
975
|
+
const unsub = await this._adapter.subscribe("server:offline", (event) => {
|
|
976
|
+
this._handleServerOffline(event);
|
|
977
|
+
});
|
|
978
|
+
this._subscriptions.push(unsub);
|
|
979
|
+
}
|
|
980
|
+
const roomMsgUnsub = await this._adapter.subscribe("room:message", (event) => {
|
|
981
|
+
this._handleRoomMessage(event);
|
|
982
|
+
});
|
|
983
|
+
this._subscriptions.push(roomMsgUnsub);
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* @zh 启动心跳定时器
|
|
987
|
+
* @en Start heartbeat timer
|
|
988
|
+
*/
|
|
989
|
+
_startHeartbeat() {
|
|
990
|
+
this._heartbeatTimer = setInterval(async () => {
|
|
991
|
+
try {
|
|
992
|
+
await this._adapter.heartbeat(this._serverId);
|
|
993
|
+
await this._adapter.updateServer(this._serverId, {
|
|
994
|
+
roomCount: this._rooms.size,
|
|
995
|
+
playerCount: this._countTotalPlayers()
|
|
996
|
+
});
|
|
997
|
+
} catch (err) {
|
|
998
|
+
logger3.error("Heartbeat failed:", err);
|
|
999
|
+
}
|
|
1000
|
+
}, this._config.heartbeatInterval);
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* @zh 启动快照定时器
|
|
1004
|
+
* @en Start snapshot timer
|
|
1005
|
+
*/
|
|
1006
|
+
_startSnapshotTimer() {
|
|
1007
|
+
this._snapshotTimer = setInterval(async () => {
|
|
1008
|
+
await this._saveAllSnapshots();
|
|
1009
|
+
}, this._config.snapshotInterval);
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* @zh 保存所有房间快照
|
|
1013
|
+
* @en Save all room snapshots
|
|
1014
|
+
*/
|
|
1015
|
+
async _saveAllSnapshots() {
|
|
1016
|
+
const promises = [];
|
|
1017
|
+
for (const roomId of this._rooms.keys()) {
|
|
1018
|
+
promises.push(this.saveSnapshot(roomId));
|
|
1019
|
+
}
|
|
1020
|
+
await Promise.allSettled(promises);
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* @zh 路由到指定房间
|
|
1024
|
+
* @en Route to specific room
|
|
1025
|
+
*/
|
|
1026
|
+
async _routeToRoom(roomId) {
|
|
1027
|
+
if (this._rooms.has(roomId)) {
|
|
1028
|
+
return {
|
|
1029
|
+
type: "local",
|
|
1030
|
+
roomId
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
const registration = await this._adapter.getRoom(roomId);
|
|
1034
|
+
if (!registration) {
|
|
1035
|
+
return {
|
|
1036
|
+
type: "unavailable",
|
|
1037
|
+
reason: "Room not found"
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
if (registration.serverId === this._serverId) {
|
|
1041
|
+
return {
|
|
1042
|
+
type: "local",
|
|
1043
|
+
roomId
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
return {
|
|
1047
|
+
type: "redirect",
|
|
1048
|
+
serverAddress: registration.serverAddress,
|
|
1049
|
+
roomId
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* @zh 按类型路由
|
|
1054
|
+
* @en Route by type
|
|
1055
|
+
*/
|
|
1056
|
+
async _routeByType(roomType, _query) {
|
|
1057
|
+
const availableRoom = await this._adapter.findAvailableRoom(roomType);
|
|
1058
|
+
if (!availableRoom) {
|
|
1059
|
+
return {
|
|
1060
|
+
type: "create",
|
|
1061
|
+
roomId: void 0
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
if (availableRoom.serverId === this._serverId) {
|
|
1065
|
+
return {
|
|
1066
|
+
type: "local",
|
|
1067
|
+
roomId: availableRoom.roomId
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
type: "redirect",
|
|
1072
|
+
serverAddress: availableRoom.serverAddress,
|
|
1073
|
+
roomId: availableRoom.roomId
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* @zh 处理服务器离线事件
|
|
1078
|
+
* @en Handle server offline event
|
|
1079
|
+
*/
|
|
1080
|
+
_handleServerOffline(event) {
|
|
1081
|
+
if (this._isShuttingDown) return;
|
|
1082
|
+
if (!this._config.enableFailover) return;
|
|
1083
|
+
const offlineServerId = event.serverId;
|
|
1084
|
+
if (offlineServerId === this._serverId) return;
|
|
1085
|
+
logger3.info(`Server offline detected: ${offlineServerId}`);
|
|
1086
|
+
this._tryRecoverRoomsFromServer(offlineServerId).catch((err) => {
|
|
1087
|
+
logger3.error(`Failed to recover rooms from ${offlineServerId}:`, err);
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* @zh 尝试从离线服务器恢复房间
|
|
1092
|
+
* @en Try to recover rooms from offline server
|
|
1093
|
+
*/
|
|
1094
|
+
async _tryRecoverRoomsFromServer(offlineServerId) {
|
|
1095
|
+
if (this._rooms.size >= this._config.capacity) {
|
|
1096
|
+
logger3.warn(`Cannot recover rooms: server at capacity (${this._rooms.size}/${this._config.capacity})`);
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
const rooms = await this._adapter.queryRooms({
|
|
1100
|
+
serverId: offlineServerId
|
|
1101
|
+
});
|
|
1102
|
+
if (rooms.length === 0) {
|
|
1103
|
+
logger3.info(`No rooms to recover from ${offlineServerId}`);
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
logger3.info(`Attempting to recover ${rooms.length} rooms from ${offlineServerId}`);
|
|
1107
|
+
for (const roomReg of rooms) {
|
|
1108
|
+
if (this._rooms.size >= this._config.capacity) {
|
|
1109
|
+
logger3.warn("Reached capacity during recovery, stopping");
|
|
1110
|
+
break;
|
|
1111
|
+
}
|
|
1112
|
+
const lockKey = `failover:${roomReg.roomId}`;
|
|
1113
|
+
const acquired = await this._adapter.acquireLock(lockKey, this._config.migrationTimeout);
|
|
1114
|
+
if (!acquired) {
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
try {
|
|
1118
|
+
const success = await this.restoreFromSnapshot(roomReg.roomId);
|
|
1119
|
+
if (success) {
|
|
1120
|
+
logger3.info(`Successfully recovered room ${roomReg.roomId}`);
|
|
1121
|
+
await this._adapter.publish({
|
|
1122
|
+
type: "room:migrated",
|
|
1123
|
+
serverId: this._serverId,
|
|
1124
|
+
roomId: roomReg.roomId,
|
|
1125
|
+
payload: {
|
|
1126
|
+
fromServer: offlineServerId,
|
|
1127
|
+
toServer: this._serverId
|
|
1128
|
+
},
|
|
1129
|
+
timestamp: Date.now()
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
} finally {
|
|
1133
|
+
await this._adapter.releaseLock(lockKey);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* @zh 处理跨服务器房间消息
|
|
1139
|
+
* @en Handle cross-server room message
|
|
1140
|
+
*/
|
|
1141
|
+
_handleRoomMessage(event) {
|
|
1142
|
+
if (!event.roomId) return;
|
|
1143
|
+
const room = this._rooms.get(event.roomId);
|
|
1144
|
+
if (!room) return;
|
|
1145
|
+
const payload = event.payload;
|
|
1146
|
+
if (payload.playerId) {
|
|
1147
|
+
room._handleMessage(payload.messageType, payload.data, payload.playerId);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* @zh 统计总玩家数
|
|
1152
|
+
* @en Count total players
|
|
1153
|
+
*/
|
|
1154
|
+
_countTotalPlayers() {
|
|
1155
|
+
let count = 0;
|
|
1156
|
+
for (const room of this._rooms.values()) {
|
|
1157
|
+
count += room.players.length;
|
|
1158
|
+
}
|
|
1159
|
+
return count;
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* @zh 根据房间实例获取定义
|
|
1163
|
+
* @en Get definition by room instance
|
|
1164
|
+
*/
|
|
1165
|
+
_getDefinitionByRoom(room) {
|
|
1166
|
+
for (const [name, def] of this._definitions) {
|
|
1167
|
+
if (room instanceof def.roomClass) {
|
|
1168
|
+
return {
|
|
1169
|
+
name
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* @zh 休眠指定时间
|
|
1177
|
+
* @en Sleep for specified time
|
|
1178
|
+
*/
|
|
1179
|
+
_sleep(ms) {
|
|
1180
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* @zh 向其他服务器的房间发送消息
|
|
1184
|
+
* @en Send message to room on another server
|
|
1185
|
+
*
|
|
1186
|
+
* @param roomId - 房间 ID | Room ID
|
|
1187
|
+
* @param messageType - 消息类型 | Message type
|
|
1188
|
+
* @param data - 消息数据 | Message data
|
|
1189
|
+
* @param playerId - 发送者玩家 ID(可选)| Sender player ID (optional)
|
|
1190
|
+
*/
|
|
1191
|
+
async sendToRemoteRoom(roomId, messageType, data, playerId) {
|
|
1192
|
+
await this._adapter.sendToRoom(roomId, messageType, data, playerId);
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* @zh 获取所有在线服务器
|
|
1196
|
+
* @en Get all online servers
|
|
1197
|
+
*/
|
|
1198
|
+
async getServers() {
|
|
1199
|
+
return this._adapter.getServers();
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* @zh 查询分布式房间
|
|
1203
|
+
* @en Query distributed rooms
|
|
1204
|
+
*/
|
|
1205
|
+
async queryDistributedRooms(query) {
|
|
1206
|
+
return this._adapter.queryRooms(query);
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
__name(_DistributedRoomManager, "DistributedRoomManager");
|
|
1210
|
+
var DistributedRoomManager = _DistributedRoomManager;
|
|
1211
|
+
|
|
1212
|
+
// src/distributed/adapters/MemoryAdapter.ts
|
|
1213
|
+
var _MemoryAdapter = class _MemoryAdapter {
|
|
1214
|
+
constructor(config = {}) {
|
|
1215
|
+
__publicField(this, "_config");
|
|
1216
|
+
__publicField(this, "_connected", false);
|
|
1217
|
+
// 存储
|
|
1218
|
+
__publicField(this, "_servers", /* @__PURE__ */ new Map());
|
|
1219
|
+
__publicField(this, "_rooms", /* @__PURE__ */ new Map());
|
|
1220
|
+
__publicField(this, "_snapshots", /* @__PURE__ */ new Map());
|
|
1221
|
+
__publicField(this, "_locks", /* @__PURE__ */ new Map());
|
|
1222
|
+
// 事件订阅
|
|
1223
|
+
__publicField(this, "_subscribers", /* @__PURE__ */ new Map());
|
|
1224
|
+
__publicField(this, "_subscriberId", 0);
|
|
1225
|
+
// TTL 检查定时器
|
|
1226
|
+
__publicField(this, "_ttlCheckTimer", null);
|
|
1227
|
+
this._config = {
|
|
1228
|
+
serverTtl: 15e3,
|
|
1229
|
+
enableTtlCheck: true,
|
|
1230
|
+
ttlCheckInterval: 5e3,
|
|
1231
|
+
...config
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
// =========================================================================
|
|
1235
|
+
// 生命周期 | Lifecycle
|
|
1236
|
+
// =========================================================================
|
|
1237
|
+
async connect() {
|
|
1238
|
+
if (this._connected) return;
|
|
1239
|
+
this._connected = true;
|
|
1240
|
+
if (this._config.enableTtlCheck) {
|
|
1241
|
+
this._ttlCheckTimer = setInterval(() => this._checkServerTtl(), this._config.ttlCheckInterval);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
async disconnect() {
|
|
1245
|
+
if (!this._connected) return;
|
|
1246
|
+
if (this._ttlCheckTimer) {
|
|
1247
|
+
clearInterval(this._ttlCheckTimer);
|
|
1248
|
+
this._ttlCheckTimer = null;
|
|
1249
|
+
}
|
|
1250
|
+
this._connected = false;
|
|
1251
|
+
this._servers.clear();
|
|
1252
|
+
this._rooms.clear();
|
|
1253
|
+
this._snapshots.clear();
|
|
1254
|
+
this._locks.clear();
|
|
1255
|
+
this._subscribers.clear();
|
|
1256
|
+
}
|
|
1257
|
+
isConnected() {
|
|
1258
|
+
return this._connected;
|
|
1259
|
+
}
|
|
1260
|
+
// =========================================================================
|
|
1261
|
+
// 服务器注册 | Server Registry
|
|
1262
|
+
// =========================================================================
|
|
1263
|
+
async registerServer(server) {
|
|
1264
|
+
this._ensureConnected();
|
|
1265
|
+
this._servers.set(server.serverId, {
|
|
1266
|
+
...server,
|
|
1267
|
+
lastHeartbeat: Date.now()
|
|
1268
|
+
});
|
|
1269
|
+
await this.publish({
|
|
1270
|
+
type: "server:online",
|
|
1271
|
+
serverId: server.serverId,
|
|
1272
|
+
payload: server,
|
|
1273
|
+
timestamp: Date.now()
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
async unregisterServer(serverId) {
|
|
1277
|
+
this._ensureConnected();
|
|
1278
|
+
const server = this._servers.get(serverId);
|
|
1279
|
+
if (!server) return;
|
|
1280
|
+
this._servers.delete(serverId);
|
|
1281
|
+
for (const [roomId, room] of this._rooms) {
|
|
1282
|
+
if (room.serverId === serverId) {
|
|
1283
|
+
this._rooms.delete(roomId);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
await this.publish({
|
|
1287
|
+
type: "server:offline",
|
|
1288
|
+
serverId,
|
|
1289
|
+
payload: {
|
|
1290
|
+
serverId
|
|
1291
|
+
},
|
|
1292
|
+
timestamp: Date.now()
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
async heartbeat(serverId) {
|
|
1296
|
+
this._ensureConnected();
|
|
1297
|
+
const server = this._servers.get(serverId);
|
|
1298
|
+
if (server) {
|
|
1299
|
+
server.lastHeartbeat = Date.now();
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
async getServers() {
|
|
1303
|
+
this._ensureConnected();
|
|
1304
|
+
return Array.from(this._servers.values()).filter((s) => s.status === "online");
|
|
1305
|
+
}
|
|
1306
|
+
async getServer(serverId) {
|
|
1307
|
+
this._ensureConnected();
|
|
1308
|
+
return this._servers.get(serverId) ?? null;
|
|
1309
|
+
}
|
|
1310
|
+
async updateServer(serverId, updates) {
|
|
1311
|
+
this._ensureConnected();
|
|
1312
|
+
const server = this._servers.get(serverId);
|
|
1313
|
+
if (server) {
|
|
1314
|
+
Object.assign(server, updates);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
// =========================================================================
|
|
1318
|
+
// 房间注册 | Room Registry
|
|
1319
|
+
// =========================================================================
|
|
1320
|
+
async registerRoom(room) {
|
|
1321
|
+
this._ensureConnected();
|
|
1322
|
+
this._rooms.set(room.roomId, {
|
|
1323
|
+
...room
|
|
1324
|
+
});
|
|
1325
|
+
const server = this._servers.get(room.serverId);
|
|
1326
|
+
if (server) {
|
|
1327
|
+
server.roomCount = this._countRoomsByServer(room.serverId);
|
|
1328
|
+
}
|
|
1329
|
+
await this.publish({
|
|
1330
|
+
type: "room:created",
|
|
1331
|
+
serverId: room.serverId,
|
|
1332
|
+
roomId: room.roomId,
|
|
1333
|
+
payload: {
|
|
1334
|
+
roomType: room.roomType
|
|
1335
|
+
},
|
|
1336
|
+
timestamp: Date.now()
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
async unregisterRoom(roomId) {
|
|
1340
|
+
this._ensureConnected();
|
|
1341
|
+
const room = this._rooms.get(roomId);
|
|
1342
|
+
if (!room) return;
|
|
1343
|
+
this._rooms.delete(roomId);
|
|
1344
|
+
this._snapshots.delete(roomId);
|
|
1345
|
+
const server = this._servers.get(room.serverId);
|
|
1346
|
+
if (server) {
|
|
1347
|
+
server.roomCount = this._countRoomsByServer(room.serverId);
|
|
1348
|
+
}
|
|
1349
|
+
await this.publish({
|
|
1350
|
+
type: "room:disposed",
|
|
1351
|
+
serverId: room.serverId,
|
|
1352
|
+
roomId,
|
|
1353
|
+
payload: {},
|
|
1354
|
+
timestamp: Date.now()
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
async updateRoom(roomId, updates) {
|
|
1358
|
+
this._ensureConnected();
|
|
1359
|
+
const room = this._rooms.get(roomId);
|
|
1360
|
+
if (!room) return;
|
|
1361
|
+
Object.assign(room, updates, {
|
|
1362
|
+
updatedAt: Date.now()
|
|
1363
|
+
});
|
|
1364
|
+
await this.publish({
|
|
1365
|
+
type: "room:updated",
|
|
1366
|
+
serverId: room.serverId,
|
|
1367
|
+
roomId,
|
|
1368
|
+
payload: updates,
|
|
1369
|
+
timestamp: Date.now()
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
async getRoom(roomId) {
|
|
1373
|
+
this._ensureConnected();
|
|
1374
|
+
return this._rooms.get(roomId) ?? null;
|
|
1375
|
+
}
|
|
1376
|
+
async queryRooms(query) {
|
|
1377
|
+
this._ensureConnected();
|
|
1378
|
+
let results = Array.from(this._rooms.values());
|
|
1379
|
+
if (query.roomType) {
|
|
1380
|
+
results = results.filter((r) => r.roomType === query.roomType);
|
|
1381
|
+
}
|
|
1382
|
+
if (query.hasSpace) {
|
|
1383
|
+
results = results.filter((r) => r.playerCount < r.maxPlayers);
|
|
1384
|
+
}
|
|
1385
|
+
if (query.notLocked) {
|
|
1386
|
+
results = results.filter((r) => !r.isLocked);
|
|
1387
|
+
}
|
|
1388
|
+
if (query.metadata) {
|
|
1389
|
+
results = results.filter((r) => {
|
|
1390
|
+
for (const [key, value] of Object.entries(query.metadata)) {
|
|
1391
|
+
if (r.metadata[key] !== value) {
|
|
1392
|
+
return false;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
return true;
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
if (query.offset) {
|
|
1399
|
+
results = results.slice(query.offset);
|
|
1400
|
+
}
|
|
1401
|
+
if (query.limit) {
|
|
1402
|
+
results = results.slice(0, query.limit);
|
|
1403
|
+
}
|
|
1404
|
+
return results;
|
|
1405
|
+
}
|
|
1406
|
+
async findAvailableRoom(roomType) {
|
|
1407
|
+
const rooms = await this.queryRooms({
|
|
1408
|
+
roomType,
|
|
1409
|
+
hasSpace: true,
|
|
1410
|
+
notLocked: true,
|
|
1411
|
+
limit: 1
|
|
1412
|
+
});
|
|
1413
|
+
return rooms[0] ?? null;
|
|
1414
|
+
}
|
|
1415
|
+
async getRoomsByServer(serverId) {
|
|
1416
|
+
this._ensureConnected();
|
|
1417
|
+
return Array.from(this._rooms.values()).filter((r) => r.serverId === serverId);
|
|
1418
|
+
}
|
|
1419
|
+
// =========================================================================
|
|
1420
|
+
// 房间状态 | Room State
|
|
1421
|
+
// =========================================================================
|
|
1422
|
+
async saveSnapshot(snapshot) {
|
|
1423
|
+
this._ensureConnected();
|
|
1424
|
+
this._snapshots.set(snapshot.roomId, {
|
|
1425
|
+
...snapshot
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
async loadSnapshot(roomId) {
|
|
1429
|
+
this._ensureConnected();
|
|
1430
|
+
return this._snapshots.get(roomId) ?? null;
|
|
1431
|
+
}
|
|
1432
|
+
async deleteSnapshot(roomId) {
|
|
1433
|
+
this._ensureConnected();
|
|
1434
|
+
this._snapshots.delete(roomId);
|
|
1435
|
+
}
|
|
1436
|
+
// =========================================================================
|
|
1437
|
+
// 发布/订阅 | Pub/Sub
|
|
1438
|
+
// =========================================================================
|
|
1439
|
+
async publish(event) {
|
|
1440
|
+
this._ensureConnected();
|
|
1441
|
+
const wildcardHandlers = this._subscribers.get("*") ?? /* @__PURE__ */ new Set();
|
|
1442
|
+
const typeHandlers = this._subscribers.get(event.type) ?? /* @__PURE__ */ new Set();
|
|
1443
|
+
for (const handler of wildcardHandlers) {
|
|
1444
|
+
try {
|
|
1445
|
+
handler(event);
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
console.error("Event handler error:", error);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
for (const handler of typeHandlers) {
|
|
1451
|
+
try {
|
|
1452
|
+
handler(event);
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
console.error("Event handler error:", error);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
async subscribe(pattern, handler) {
|
|
1459
|
+
this._ensureConnected();
|
|
1460
|
+
if (!this._subscribers.has(pattern)) {
|
|
1461
|
+
this._subscribers.set(pattern, /* @__PURE__ */ new Set());
|
|
1462
|
+
}
|
|
1463
|
+
this._subscribers.get(pattern).add(handler);
|
|
1464
|
+
return () => {
|
|
1465
|
+
const handlers = this._subscribers.get(pattern);
|
|
1466
|
+
if (handlers) {
|
|
1467
|
+
handlers.delete(handler);
|
|
1468
|
+
if (handlers.size === 0) {
|
|
1469
|
+
this._subscribers.delete(pattern);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
async sendToRoom(roomId, messageType, data, playerId) {
|
|
1475
|
+
this._ensureConnected();
|
|
1476
|
+
const room = this._rooms.get(roomId);
|
|
1477
|
+
if (!room) return;
|
|
1478
|
+
await this.publish({
|
|
1479
|
+
type: "room:message",
|
|
1480
|
+
serverId: room.serverId,
|
|
1481
|
+
roomId,
|
|
1482
|
+
payload: {
|
|
1483
|
+
messageType,
|
|
1484
|
+
data,
|
|
1485
|
+
playerId
|
|
1486
|
+
},
|
|
1487
|
+
timestamp: Date.now()
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
// =========================================================================
|
|
1491
|
+
// 分布式锁 | Distributed Lock
|
|
1492
|
+
// =========================================================================
|
|
1493
|
+
async acquireLock(key, ttlMs) {
|
|
1494
|
+
this._ensureConnected();
|
|
1495
|
+
const now = Date.now();
|
|
1496
|
+
const existing = this._locks.get(key);
|
|
1497
|
+
if (existing && existing.expireAt > now) {
|
|
1498
|
+
return false;
|
|
1499
|
+
}
|
|
1500
|
+
const owner = `lock_${++this._subscriberId}`;
|
|
1501
|
+
this._locks.set(key, {
|
|
1502
|
+
owner,
|
|
1503
|
+
expireAt: now + ttlMs
|
|
1504
|
+
});
|
|
1505
|
+
return true;
|
|
1506
|
+
}
|
|
1507
|
+
async releaseLock(key) {
|
|
1508
|
+
this._ensureConnected();
|
|
1509
|
+
this._locks.delete(key);
|
|
1510
|
+
}
|
|
1511
|
+
async extendLock(key, ttlMs) {
|
|
1512
|
+
this._ensureConnected();
|
|
1513
|
+
const lock = this._locks.get(key);
|
|
1514
|
+
if (!lock) return false;
|
|
1515
|
+
lock.expireAt = Date.now() + ttlMs;
|
|
1516
|
+
return true;
|
|
1517
|
+
}
|
|
1518
|
+
// =========================================================================
|
|
1519
|
+
// 私有方法 | Private Methods
|
|
1520
|
+
// =========================================================================
|
|
1521
|
+
_ensureConnected() {
|
|
1522
|
+
if (!this._connected) {
|
|
1523
|
+
throw new Error("MemoryAdapter is not connected");
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
_countRoomsByServer(serverId) {
|
|
1527
|
+
let count = 0;
|
|
1528
|
+
for (const room of this._rooms.values()) {
|
|
1529
|
+
if (room.serverId === serverId) {
|
|
1530
|
+
count++;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return count;
|
|
1534
|
+
}
|
|
1535
|
+
async _checkServerTtl() {
|
|
1536
|
+
const now = Date.now();
|
|
1537
|
+
const expiredServers = [];
|
|
1538
|
+
for (const [serverId, server] of this._servers) {
|
|
1539
|
+
if (server.status === "online" && now - server.lastHeartbeat > this._config.serverTtl) {
|
|
1540
|
+
server.status = "offline";
|
|
1541
|
+
expiredServers.push(serverId);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
for (const serverId of expiredServers) {
|
|
1545
|
+
await this.publish({
|
|
1546
|
+
type: "server:offline",
|
|
1547
|
+
serverId,
|
|
1548
|
+
payload: {
|
|
1549
|
+
serverId,
|
|
1550
|
+
reason: "heartbeat_timeout"
|
|
1551
|
+
},
|
|
1552
|
+
timestamp: now
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
// =========================================================================
|
|
1557
|
+
// 测试辅助方法 | Test Helper Methods
|
|
1558
|
+
// =========================================================================
|
|
1559
|
+
/**
|
|
1560
|
+
* @zh 清除所有数据(仅用于测试)
|
|
1561
|
+
* @en Clear all data (for testing only)
|
|
1562
|
+
*/
|
|
1563
|
+
_clear() {
|
|
1564
|
+
this._servers.clear();
|
|
1565
|
+
this._rooms.clear();
|
|
1566
|
+
this._snapshots.clear();
|
|
1567
|
+
this._locks.clear();
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* @zh 获取内部状态(仅用于测试)
|
|
1571
|
+
* @en Get internal state (for testing only)
|
|
1572
|
+
*/
|
|
1573
|
+
_getState() {
|
|
1574
|
+
return {
|
|
1575
|
+
servers: this._servers,
|
|
1576
|
+
rooms: this._rooms,
|
|
1577
|
+
snapshots: this._snapshots
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
__name(_MemoryAdapter, "MemoryAdapter");
|
|
1582
|
+
var MemoryAdapter = _MemoryAdapter;
|
|
1583
|
+
var logger4 = createLogger("Server");
|
|
1584
|
+
function fileNameToHandlerName(fileName) {
|
|
1585
|
+
const baseName = fileName.replace(/\.(ts|js|mts|mjs)$/, "");
|
|
1586
|
+
return baseName.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1587
|
+
}
|
|
1588
|
+
__name(fileNameToHandlerName, "fileNameToHandlerName");
|
|
1589
|
+
function scanDirectory(dir) {
|
|
1590
|
+
if (!fs.existsSync(dir)) {
|
|
1591
|
+
return [];
|
|
1592
|
+
}
|
|
1593
|
+
const files = [];
|
|
1594
|
+
const entries = fs.readdirSync(dir, {
|
|
1595
|
+
withFileTypes: true
|
|
1596
|
+
});
|
|
1597
|
+
for (const entry of entries) {
|
|
1598
|
+
if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name)) {
|
|
1599
|
+
if (entry.name.startsWith("_") || entry.name.startsWith("index.")) {
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
files.push(path.join(dir, entry.name));
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
return files;
|
|
1606
|
+
}
|
|
1607
|
+
__name(scanDirectory, "scanDirectory");
|
|
1608
|
+
async function loadApiHandlers(apiDir) {
|
|
1609
|
+
const files = scanDirectory(apiDir);
|
|
1610
|
+
const handlers = [];
|
|
1611
|
+
for (const filePath of files) {
|
|
1612
|
+
try {
|
|
1613
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
1614
|
+
const module = await import(fileUrl);
|
|
1615
|
+
const definition = module.default;
|
|
1616
|
+
if (definition && typeof definition.handler === "function") {
|
|
1617
|
+
const name = fileNameToHandlerName(path.basename(filePath));
|
|
1618
|
+
handlers.push({
|
|
1619
|
+
name,
|
|
1620
|
+
path: filePath,
|
|
1621
|
+
definition
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
} catch (err) {
|
|
1625
|
+
logger4.warn(`Failed to load API handler: ${filePath}`, err);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
return handlers;
|
|
1629
|
+
}
|
|
1630
|
+
__name(loadApiHandlers, "loadApiHandlers");
|
|
1631
|
+
async function loadMsgHandlers(msgDir) {
|
|
1632
|
+
const files = scanDirectory(msgDir);
|
|
1633
|
+
const handlers = [];
|
|
1634
|
+
for (const filePath of files) {
|
|
1635
|
+
try {
|
|
1636
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
1637
|
+
const module = await import(fileUrl);
|
|
1638
|
+
const definition = module.default;
|
|
1639
|
+
if (definition && typeof definition.handler === "function") {
|
|
1640
|
+
const name = fileNameToHandlerName(path.basename(filePath));
|
|
1641
|
+
handlers.push({
|
|
1642
|
+
name,
|
|
1643
|
+
path: filePath,
|
|
1644
|
+
definition
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
} catch (err) {
|
|
1648
|
+
logger4.warn(`Failed to load msg handler: ${filePath}`, err);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
return handlers;
|
|
1652
|
+
}
|
|
1653
|
+
__name(loadMsgHandlers, "loadMsgHandlers");
|
|
1654
|
+
function scanDirectoryRecursive(dir, baseDir = dir) {
|
|
1655
|
+
if (!fs.existsSync(dir)) {
|
|
1656
|
+
return [];
|
|
1657
|
+
}
|
|
1658
|
+
const files = [];
|
|
1659
|
+
const entries = fs.readdirSync(dir, {
|
|
1660
|
+
withFileTypes: true
|
|
1661
|
+
});
|
|
1662
|
+
for (const entry of entries) {
|
|
1663
|
+
const fullPath = path.join(dir, entry.name);
|
|
1664
|
+
if (entry.isDirectory()) {
|
|
1665
|
+
files.push(...scanDirectoryRecursive(fullPath, baseDir));
|
|
1666
|
+
} else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name)) {
|
|
1667
|
+
if (entry.name.startsWith("_") || entry.name.startsWith("index.")) {
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
1671
|
+
files.push({
|
|
1672
|
+
filePath: fullPath,
|
|
1673
|
+
relativePath
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return files;
|
|
1678
|
+
}
|
|
1679
|
+
__name(scanDirectoryRecursive, "scanDirectoryRecursive");
|
|
1680
|
+
function filePathToRoute(relativePath, prefix) {
|
|
1681
|
+
let route = relativePath.replace(/\.(ts|js|mts|mjs)$/, "").replace(/\\/g, "/").replace(/\[(\w+)\]/g, ":$1");
|
|
1682
|
+
if (!route.startsWith("/")) {
|
|
1683
|
+
route = "/" + route;
|
|
1684
|
+
}
|
|
1685
|
+
const fullRoute = prefix.endsWith("/") ? prefix.slice(0, -1) + route : prefix + route;
|
|
1686
|
+
return fullRoute;
|
|
1687
|
+
}
|
|
1688
|
+
__name(filePathToRoute, "filePathToRoute");
|
|
1689
|
+
async function loadHttpHandlers(httpDir, prefix = "/api") {
|
|
1690
|
+
const files = scanDirectoryRecursive(httpDir);
|
|
1691
|
+
const handlers = [];
|
|
1692
|
+
for (const { filePath, relativePath } of files) {
|
|
1693
|
+
try {
|
|
1694
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
1695
|
+
const module = await import(fileUrl);
|
|
1696
|
+
const definition = module.default;
|
|
1697
|
+
if (definition && typeof definition.handler === "function") {
|
|
1698
|
+
const route = filePathToRoute(relativePath, prefix);
|
|
1699
|
+
const method = definition.method ?? "POST";
|
|
1700
|
+
handlers.push({
|
|
1701
|
+
route,
|
|
1702
|
+
method,
|
|
1703
|
+
path: filePath,
|
|
1704
|
+
definition
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
} catch (err) {
|
|
1708
|
+
logger4.warn(`Failed to load HTTP handler: ${filePath}`, err);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
return handlers;
|
|
1712
|
+
}
|
|
1713
|
+
__name(loadHttpHandlers, "loadHttpHandlers");
|
|
1714
|
+
|
|
1715
|
+
// src/core/server.ts
|
|
1716
|
+
var DEFAULT_CONFIG = {
|
|
1717
|
+
port: 3e3,
|
|
1718
|
+
apiDir: "src/api",
|
|
1719
|
+
msgDir: "src/msg",
|
|
1720
|
+
httpDir: "src/http",
|
|
1721
|
+
httpPrefix: "/api",
|
|
1722
|
+
tickRate: 20
|
|
1723
|
+
};
|
|
1724
|
+
async function createServer(config = {}) {
|
|
1725
|
+
const opts = {
|
|
1726
|
+
...DEFAULT_CONFIG,
|
|
1727
|
+
...config
|
|
1728
|
+
};
|
|
1729
|
+
const cwd = process.cwd();
|
|
1730
|
+
const logger5 = createLogger("Server");
|
|
1731
|
+
const apiHandlers = await loadApiHandlers(path.resolve(cwd, opts.apiDir));
|
|
1732
|
+
const msgHandlers = await loadMsgHandlers(path.resolve(cwd, opts.msgDir));
|
|
1733
|
+
const httpDir = config.httpDir ?? opts.httpDir;
|
|
1734
|
+
const httpPrefix = config.httpPrefix ?? opts.httpPrefix;
|
|
1735
|
+
const httpHandlers = await loadHttpHandlers(path.resolve(cwd, httpDir), httpPrefix);
|
|
1736
|
+
if (apiHandlers.length > 0) {
|
|
1737
|
+
logger5.info(`Loaded ${apiHandlers.length} API handlers`);
|
|
1738
|
+
}
|
|
1739
|
+
if (msgHandlers.length > 0) {
|
|
1740
|
+
logger5.info(`Loaded ${msgHandlers.length} message handlers`);
|
|
1741
|
+
}
|
|
1742
|
+
if (httpHandlers.length > 0) {
|
|
1743
|
+
logger5.info(`Loaded ${httpHandlers.length} HTTP handlers`);
|
|
1744
|
+
}
|
|
1745
|
+
const mergedHttpRoutes = {};
|
|
1746
|
+
for (const handler of httpHandlers) {
|
|
1747
|
+
const existingRoute = mergedHttpRoutes[handler.route];
|
|
1748
|
+
if (existingRoute && typeof existingRoute !== "function") {
|
|
1749
|
+
existingRoute[handler.method] = handler.definition.handler;
|
|
1750
|
+
} else {
|
|
1751
|
+
mergedHttpRoutes[handler.route] = {
|
|
1752
|
+
[handler.method]: handler.definition.handler
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
if (config.http) {
|
|
1757
|
+
for (const [route, handlerOrMethods] of Object.entries(config.http)) {
|
|
1758
|
+
if (typeof handlerOrMethods === "function") {
|
|
1759
|
+
mergedHttpRoutes[route] = handlerOrMethods;
|
|
1760
|
+
} else {
|
|
1761
|
+
const existing = mergedHttpRoutes[route];
|
|
1762
|
+
if (existing && typeof existing !== "function") {
|
|
1763
|
+
Object.assign(existing, handlerOrMethods);
|
|
1764
|
+
} else {
|
|
1765
|
+
mergedHttpRoutes[route] = handlerOrMethods;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
const hasHttpRoutes = Object.keys(mergedHttpRoutes).length > 0;
|
|
1771
|
+
const distributedConfig = config.distributed;
|
|
1772
|
+
const isDistributed = distributedConfig?.enabled ?? false;
|
|
1773
|
+
const apiDefs = {
|
|
1774
|
+
// 内置 API
|
|
1775
|
+
JoinRoom: rpc.api(),
|
|
1776
|
+
LeaveRoom: rpc.api()
|
|
1777
|
+
};
|
|
1778
|
+
const msgDefs = {
|
|
1779
|
+
// 内置消息(房间消息透传)
|
|
1780
|
+
RoomMessage: rpc.msg(),
|
|
1781
|
+
// 分布式重定向消息
|
|
1782
|
+
$redirect: rpc.msg()
|
|
1783
|
+
};
|
|
1784
|
+
for (const handler of apiHandlers) {
|
|
1785
|
+
apiDefs[handler.name] = rpc.api();
|
|
1786
|
+
}
|
|
1787
|
+
for (const handler of msgHandlers) {
|
|
1788
|
+
msgDefs[handler.name] = rpc.msg();
|
|
1789
|
+
}
|
|
1790
|
+
const protocol = rpc.define({
|
|
1791
|
+
api: apiDefs,
|
|
1792
|
+
msg: msgDefs
|
|
1793
|
+
});
|
|
1794
|
+
let currentTick = 0;
|
|
1795
|
+
let tickInterval = null;
|
|
1796
|
+
let rpcServer = null;
|
|
1797
|
+
let httpServer = null;
|
|
1798
|
+
const sendFn = /* @__PURE__ */ __name((conn, type, data) => {
|
|
1799
|
+
rpcServer?.send(conn, "RoomMessage", {
|
|
1800
|
+
type,
|
|
1801
|
+
data
|
|
1802
|
+
});
|
|
1803
|
+
}, "sendFn");
|
|
1804
|
+
const sendBinaryFn = /* @__PURE__ */ __name((conn, data) => {
|
|
1805
|
+
if (conn && typeof conn.sendBinary === "function") {
|
|
1806
|
+
conn.sendBinary(data);
|
|
1807
|
+
}
|
|
1808
|
+
}, "sendBinaryFn");
|
|
1809
|
+
let roomManager;
|
|
1810
|
+
let distributedManager = null;
|
|
1811
|
+
if (isDistributed && distributedConfig) {
|
|
1812
|
+
const adapter = distributedConfig.adapter ?? new MemoryAdapter();
|
|
1813
|
+
distributedManager = new DistributedRoomManager(adapter, {
|
|
1814
|
+
serverId: distributedConfig.serverId,
|
|
1815
|
+
serverAddress: distributedConfig.serverAddress,
|
|
1816
|
+
serverPort: distributedConfig.serverPort ?? opts.port,
|
|
1817
|
+
heartbeatInterval: distributedConfig.heartbeatInterval,
|
|
1818
|
+
snapshotInterval: distributedConfig.snapshotInterval,
|
|
1819
|
+
enableFailover: distributedConfig.enableFailover,
|
|
1820
|
+
capacity: distributedConfig.capacity
|
|
1821
|
+
}, sendFn, sendBinaryFn);
|
|
1822
|
+
roomManager = distributedManager;
|
|
1823
|
+
logger5.info(`Distributed mode enabled (serverId: ${distributedConfig.serverId})`);
|
|
1824
|
+
} else {
|
|
1825
|
+
roomManager = new RoomManager(sendFn, sendBinaryFn);
|
|
1826
|
+
}
|
|
1827
|
+
const apiMap = {};
|
|
1828
|
+
for (const handler of apiHandlers) {
|
|
1829
|
+
apiMap[handler.name] = handler;
|
|
1830
|
+
}
|
|
1831
|
+
const msgMap = {};
|
|
1832
|
+
for (const handler of msgHandlers) {
|
|
1833
|
+
msgMap[handler.name] = handler;
|
|
1834
|
+
}
|
|
1835
|
+
const gameServer = {
|
|
1836
|
+
get connections() {
|
|
1837
|
+
return rpcServer?.connections ?? [];
|
|
1838
|
+
},
|
|
1839
|
+
get tick() {
|
|
1840
|
+
return currentTick;
|
|
1841
|
+
},
|
|
1842
|
+
get rooms() {
|
|
1843
|
+
return roomManager;
|
|
1844
|
+
},
|
|
1845
|
+
/**
|
|
1846
|
+
* @zh 注册房间类型
|
|
1847
|
+
* @en Define room type
|
|
1848
|
+
*/
|
|
1849
|
+
define(name, roomClass) {
|
|
1850
|
+
roomManager.define(name, roomClass);
|
|
1851
|
+
},
|
|
1852
|
+
async start() {
|
|
1853
|
+
const apiHandlersObj = {};
|
|
1854
|
+
apiHandlersObj["JoinRoom"] = async (input, conn) => {
|
|
1855
|
+
const { roomType, roomId, options } = input;
|
|
1856
|
+
if (roomId) {
|
|
1857
|
+
const result = await roomManager.joinById(roomId, conn.id, conn);
|
|
1858
|
+
if (!result) {
|
|
1859
|
+
throw new Error("Failed to join room");
|
|
1860
|
+
}
|
|
1861
|
+
return {
|
|
1862
|
+
roomId: result.room.id,
|
|
1863
|
+
playerId: result.player.id
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
if (roomType) {
|
|
1867
|
+
if (distributedManager) {
|
|
1868
|
+
const result2 = await distributedManager.joinOrCreateDistributed(roomType, conn.id, conn, options);
|
|
1869
|
+
if (!result2) {
|
|
1870
|
+
throw new Error("Failed to join or create room");
|
|
1871
|
+
}
|
|
1872
|
+
if ("redirect" in result2) {
|
|
1873
|
+
rpcServer?.send(conn, "$redirect", {
|
|
1874
|
+
address: result2.redirect,
|
|
1875
|
+
roomType
|
|
1876
|
+
});
|
|
1877
|
+
return {
|
|
1878
|
+
redirect: result2.redirect
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
return {
|
|
1882
|
+
roomId: result2.room.id,
|
|
1883
|
+
playerId: result2.player.id
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
const result = await roomManager.joinOrCreate(roomType, conn.id, conn, options);
|
|
1887
|
+
if (!result) {
|
|
1888
|
+
throw new Error("Failed to join or create room");
|
|
1889
|
+
}
|
|
1890
|
+
return {
|
|
1891
|
+
roomId: result.room.id,
|
|
1892
|
+
playerId: result.player.id
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
throw new Error("roomType or roomId required");
|
|
1896
|
+
};
|
|
1897
|
+
apiHandlersObj["LeaveRoom"] = async (_input, conn) => {
|
|
1898
|
+
await roomManager.leave(conn.id);
|
|
1899
|
+
return {
|
|
1900
|
+
success: true
|
|
1901
|
+
};
|
|
1902
|
+
};
|
|
1903
|
+
for (const [name, handler] of Object.entries(apiMap)) {
|
|
1904
|
+
apiHandlersObj[name] = async (input, conn) => {
|
|
1905
|
+
const ctx = {
|
|
1906
|
+
conn,
|
|
1907
|
+
server: gameServer
|
|
1908
|
+
};
|
|
1909
|
+
const definition = handler.definition;
|
|
1910
|
+
if (definition.schema) {
|
|
1911
|
+
const result = definition.schema.validate(input);
|
|
1912
|
+
if (!result.success) {
|
|
1913
|
+
const pathStr = result.error.path.length > 0 ? ` at "${result.error.path.join(".")}"` : "";
|
|
1914
|
+
throw new Error(`Validation failed${pathStr}: ${result.error.message}`);
|
|
1915
|
+
}
|
|
1916
|
+
return handler.definition.handler(result.data, ctx);
|
|
1917
|
+
}
|
|
1918
|
+
return handler.definition.handler(input, ctx);
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
const msgHandlersObj = {};
|
|
1922
|
+
msgHandlersObj["RoomMessage"] = async (data, conn) => {
|
|
1923
|
+
const { type, data: payload } = data;
|
|
1924
|
+
roomManager.handleMessage(conn.id, type, payload);
|
|
1925
|
+
};
|
|
1926
|
+
for (const [name, handler] of Object.entries(msgMap)) {
|
|
1927
|
+
msgHandlersObj[name] = async (data, conn) => {
|
|
1928
|
+
const ctx = {
|
|
1929
|
+
conn,
|
|
1930
|
+
server: gameServer
|
|
1931
|
+
};
|
|
1932
|
+
const definition = handler.definition;
|
|
1933
|
+
if (definition.schema) {
|
|
1934
|
+
const result = definition.schema.validate(data);
|
|
1935
|
+
if (!result.success) {
|
|
1936
|
+
const pathStr = result.error.path.length > 0 ? ` at "${result.error.path.join(".")}"` : "";
|
|
1937
|
+
logger5.warn(`Message validation failed for ${name}${pathStr}: ${result.error.message}`);
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
await handler.definition.handler(result.data, ctx);
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
await handler.definition.handler(data, ctx);
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
if (hasHttpRoutes) {
|
|
1947
|
+
const httpRouter = createHttpRouter(mergedHttpRoutes, {
|
|
1948
|
+
cors: config.cors ?? true
|
|
1949
|
+
});
|
|
1950
|
+
httpServer = createServer$1(async (req, res) => {
|
|
1951
|
+
const handled = await httpRouter(req, res);
|
|
1952
|
+
if (!handled) {
|
|
1953
|
+
res.statusCode = 404;
|
|
1954
|
+
res.setHeader("Content-Type", "application/json");
|
|
1955
|
+
res.end(JSON.stringify({
|
|
1956
|
+
error: "Not Found"
|
|
1957
|
+
}));
|
|
1958
|
+
}
|
|
1959
|
+
});
|
|
1960
|
+
rpcServer = serve(protocol, {
|
|
1961
|
+
server: httpServer,
|
|
1962
|
+
createConnData: /* @__PURE__ */ __name(() => ({}), "createConnData"),
|
|
1963
|
+
onStart: /* @__PURE__ */ __name(() => {
|
|
1964
|
+
logger5.info(`Started on http://localhost:${opts.port}`);
|
|
1965
|
+
opts.onStart?.(opts.port);
|
|
1966
|
+
}, "onStart"),
|
|
1967
|
+
onConnect: /* @__PURE__ */ __name(async (conn) => {
|
|
1968
|
+
await config.onConnect?.(conn);
|
|
1969
|
+
}, "onConnect"),
|
|
1970
|
+
onDisconnect: /* @__PURE__ */ __name(async (conn) => {
|
|
1971
|
+
await roomManager?.leave(conn.id, "disconnected");
|
|
1972
|
+
await config.onDisconnect?.(conn);
|
|
1973
|
+
}, "onDisconnect"),
|
|
1974
|
+
api: apiHandlersObj,
|
|
1975
|
+
msg: msgHandlersObj
|
|
1976
|
+
});
|
|
1977
|
+
await rpcServer.start();
|
|
1978
|
+
await new Promise((resolve2) => {
|
|
1979
|
+
httpServer.listen(opts.port, () => resolve2());
|
|
1980
|
+
});
|
|
1981
|
+
} else {
|
|
1982
|
+
rpcServer = serve(protocol, {
|
|
1983
|
+
port: opts.port,
|
|
1984
|
+
createConnData: /* @__PURE__ */ __name(() => ({}), "createConnData"),
|
|
1985
|
+
onStart: /* @__PURE__ */ __name((p) => {
|
|
1986
|
+
logger5.info(`Started on ws://localhost:${p}`);
|
|
1987
|
+
opts.onStart?.(p);
|
|
1988
|
+
}, "onStart"),
|
|
1989
|
+
onConnect: /* @__PURE__ */ __name(async (conn) => {
|
|
1990
|
+
await config.onConnect?.(conn);
|
|
1991
|
+
}, "onConnect"),
|
|
1992
|
+
onDisconnect: /* @__PURE__ */ __name(async (conn) => {
|
|
1993
|
+
await roomManager?.leave(conn.id, "disconnected");
|
|
1994
|
+
await config.onDisconnect?.(conn);
|
|
1995
|
+
}, "onDisconnect"),
|
|
1996
|
+
api: apiHandlersObj,
|
|
1997
|
+
msg: msgHandlersObj
|
|
1998
|
+
});
|
|
1999
|
+
await rpcServer.start();
|
|
2000
|
+
}
|
|
2001
|
+
if (distributedManager) {
|
|
2002
|
+
await distributedManager.start();
|
|
2003
|
+
}
|
|
2004
|
+
if (opts.tickRate > 0) {
|
|
2005
|
+
tickInterval = setInterval(() => {
|
|
2006
|
+
currentTick++;
|
|
2007
|
+
}, 1e3 / opts.tickRate);
|
|
2008
|
+
}
|
|
2009
|
+
},
|
|
2010
|
+
async stop() {
|
|
2011
|
+
if (tickInterval) {
|
|
2012
|
+
clearInterval(tickInterval);
|
|
2013
|
+
tickInterval = null;
|
|
2014
|
+
}
|
|
2015
|
+
if (distributedManager) {
|
|
2016
|
+
await distributedManager.stop(true);
|
|
2017
|
+
}
|
|
2018
|
+
if (rpcServer) {
|
|
2019
|
+
await rpcServer.stop();
|
|
2020
|
+
rpcServer = null;
|
|
2021
|
+
}
|
|
2022
|
+
if (httpServer) {
|
|
2023
|
+
await new Promise((resolve2, reject) => {
|
|
2024
|
+
httpServer.close((err) => {
|
|
2025
|
+
if (err) reject(err);
|
|
2026
|
+
else resolve2();
|
|
2027
|
+
});
|
|
2028
|
+
});
|
|
2029
|
+
httpServer = null;
|
|
2030
|
+
}
|
|
2031
|
+
},
|
|
2032
|
+
broadcast(name, data) {
|
|
2033
|
+
rpcServer?.broadcast(name, data);
|
|
2034
|
+
},
|
|
2035
|
+
send(conn, name, data) {
|
|
2036
|
+
rpcServer?.send(conn, name, data);
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
2039
|
+
return gameServer;
|
|
2040
|
+
}
|
|
2041
|
+
__name(createServer, "createServer");
|
|
2042
|
+
|
|
2043
|
+
export { DistributedRoomManager, MemoryAdapter, RoomManager, createHttpRouter, createServer };
|
|
2044
|
+
//# sourceMappingURL=chunk-NWZLKNGV.js.map
|
|
2045
|
+
//# sourceMappingURL=chunk-NWZLKNGV.js.map
|