csterm-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/GameRunner.d.ts +54 -0
- package/dist/GameRunner.js +998 -0
- package/dist/LockstepRunner.d.ts +39 -0
- package/dist/LockstepRunner.js +276 -0
- package/dist/Room.d.ts +43 -0
- package/dist/Room.js +498 -0
- package/dist/RoomManager.d.ts +29 -0
- package/dist/RoomManager.js +277 -0
- package/dist/VoiceRelay.d.ts +62 -0
- package/dist/VoiceRelay.js +167 -0
- package/dist/hub/HubServer.d.ts +37 -0
- package/dist/hub/HubServer.js +266 -0
- package/dist/hub/PoolRegistry.d.ts +35 -0
- package/dist/hub/PoolRegistry.js +185 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +175 -0
- package/dist/pool/GameServer.d.ts +29 -0
- package/dist/pool/GameServer.js +185 -0
- package/dist/pool/PoolClient.d.ts +48 -0
- package/dist/pool/PoolClient.js +204 -0
- package/dist/protocol.d.ts +433 -0
- package/dist/protocol.js +59 -0
- package/dist/test/HeadlessClient.d.ts +44 -0
- package/dist/test/HeadlessClient.js +196 -0
- package/dist/test/testTeamSync.d.ts +2 -0
- package/dist/test/testTeamSync.js +82 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.js +139 -0
- package/package.json +50 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// Room lifecycle management for CS-CLI multiplayer server
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import { WebSocket } from 'ws';
|
|
4
|
+
import { Room } from './Room.js';
|
|
5
|
+
import { serializeServerMessage, } from './protocol.js';
|
|
6
|
+
import { DEFAULT_SERVER_CONFIG } from './types.js';
|
|
7
|
+
export class RoomManager {
|
|
8
|
+
rooms = new Map();
|
|
9
|
+
clients = new Map();
|
|
10
|
+
config;
|
|
11
|
+
// Cleanup interval
|
|
12
|
+
cleanupInterval = null;
|
|
13
|
+
// Event callbacks (for hub notification)
|
|
14
|
+
onRoomCreated;
|
|
15
|
+
onRoomClosed;
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
this.config = { ...DEFAULT_SERVER_CONFIG, ...config };
|
|
18
|
+
// Start cleanup interval (every 30 seconds)
|
|
19
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 30000);
|
|
20
|
+
}
|
|
21
|
+
// ============ Client Management ============
|
|
22
|
+
addClient(socket) {
|
|
23
|
+
const clientId = uuidv4();
|
|
24
|
+
const client = {
|
|
25
|
+
id: clientId,
|
|
26
|
+
socket,
|
|
27
|
+
name: null,
|
|
28
|
+
roomId: null,
|
|
29
|
+
isReady: false,
|
|
30
|
+
lastActivity: Date.now(),
|
|
31
|
+
pendingInputs: [],
|
|
32
|
+
};
|
|
33
|
+
this.clients.set(clientId, client);
|
|
34
|
+
console.log(`Client connected: ${clientId}`);
|
|
35
|
+
return clientId;
|
|
36
|
+
}
|
|
37
|
+
removeClient(clientId) {
|
|
38
|
+
const client = this.clients.get(clientId);
|
|
39
|
+
if (!client)
|
|
40
|
+
return;
|
|
41
|
+
// Leave room if in one
|
|
42
|
+
if (client.roomId) {
|
|
43
|
+
this.leaveRoom(clientId);
|
|
44
|
+
}
|
|
45
|
+
this.clients.delete(clientId);
|
|
46
|
+
console.log(`Client disconnected: ${clientId}`);
|
|
47
|
+
}
|
|
48
|
+
getClient(clientId) {
|
|
49
|
+
return this.clients.get(clientId);
|
|
50
|
+
}
|
|
51
|
+
// ============ Room Management ============
|
|
52
|
+
createRoom(clientId, config) {
|
|
53
|
+
const client = this.clients.get(clientId);
|
|
54
|
+
if (!client)
|
|
55
|
+
return null;
|
|
56
|
+
// Check room limit
|
|
57
|
+
if (this.rooms.size >= this.config.maxRooms) {
|
|
58
|
+
this.sendToClient(clientId, {
|
|
59
|
+
type: 'room_error',
|
|
60
|
+
error: 'Server is full. Cannot create more rooms.',
|
|
61
|
+
});
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
// Validate config
|
|
65
|
+
if (config.maxPlayers > this.config.maxPlayersPerRoom) {
|
|
66
|
+
config.maxPlayers = this.config.maxPlayersPerRoom;
|
|
67
|
+
}
|
|
68
|
+
if (config.maxPlayers < 1)
|
|
69
|
+
config.maxPlayers = 1;
|
|
70
|
+
if (config.botCount < 0)
|
|
71
|
+
config.botCount = 0;
|
|
72
|
+
if (config.botCount > 8)
|
|
73
|
+
config.botCount = 8;
|
|
74
|
+
const roomId = uuidv4().substring(0, 8);
|
|
75
|
+
const room = new Room(roomId, config, clientId, this.config);
|
|
76
|
+
this.rooms.set(roomId, room);
|
|
77
|
+
console.log(`Room created: ${roomId} by ${clientId} (${config.name})`);
|
|
78
|
+
// Notify hub if callback is set
|
|
79
|
+
this.onRoomCreated?.(room.getInfo());
|
|
80
|
+
// Auto-join the creator
|
|
81
|
+
this.joinRoom(clientId, roomId, client.name || 'Host');
|
|
82
|
+
return roomId;
|
|
83
|
+
}
|
|
84
|
+
joinRoom(clientId, roomId, playerName, password) {
|
|
85
|
+
const client = this.clients.get(clientId);
|
|
86
|
+
if (!client)
|
|
87
|
+
return false;
|
|
88
|
+
// Leave current room first
|
|
89
|
+
if (client.roomId) {
|
|
90
|
+
this.leaveRoom(clientId);
|
|
91
|
+
}
|
|
92
|
+
const room = this.rooms.get(roomId);
|
|
93
|
+
if (!room) {
|
|
94
|
+
this.sendToClient(clientId, {
|
|
95
|
+
type: 'room_error',
|
|
96
|
+
error: 'Room not found.',
|
|
97
|
+
});
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
// Check password
|
|
101
|
+
if (room.config.password && room.config.password !== password) {
|
|
102
|
+
this.sendToClient(clientId, {
|
|
103
|
+
type: 'room_error',
|
|
104
|
+
error: 'Incorrect password.',
|
|
105
|
+
});
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
// Check capacity
|
|
109
|
+
if (room.getPlayerCount() >= room.config.maxPlayers) {
|
|
110
|
+
this.sendToClient(clientId, {
|
|
111
|
+
type: 'room_error',
|
|
112
|
+
error: 'Room is full.',
|
|
113
|
+
});
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
// Join the room
|
|
117
|
+
client.name = playerName;
|
|
118
|
+
client.roomId = roomId;
|
|
119
|
+
client.isReady = false;
|
|
120
|
+
// IMPORTANT: Send join confirmation BEFORE addPlayer
|
|
121
|
+
// addPlayer sends existing player info, and client resets lobby on room_joined
|
|
122
|
+
// So room_joined must come first, otherwise existing players get cleared
|
|
123
|
+
this.sendToClient(clientId, {
|
|
124
|
+
type: 'room_joined',
|
|
125
|
+
roomId,
|
|
126
|
+
playerId: clientId,
|
|
127
|
+
room: room.getInfo(),
|
|
128
|
+
});
|
|
129
|
+
// Now add player - this sends existing player info to new joiner
|
|
130
|
+
room.addPlayer(clientId, client);
|
|
131
|
+
// Notify other players (this is also done in room.addPlayer, so remove duplicate)
|
|
132
|
+
// Note: room.addPlayer already broadcasts to other clients
|
|
133
|
+
console.log(`${playerName} (${clientId}) joined room ${roomId}`);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
leaveRoom(clientId) {
|
|
137
|
+
const client = this.clients.get(clientId);
|
|
138
|
+
if (!client || !client.roomId)
|
|
139
|
+
return;
|
|
140
|
+
const room = this.rooms.get(client.roomId);
|
|
141
|
+
if (room) {
|
|
142
|
+
const playerName = client.name || 'Unknown';
|
|
143
|
+
room.removePlayer(clientId);
|
|
144
|
+
// Notify remaining players
|
|
145
|
+
room.broadcast({
|
|
146
|
+
type: 'player_left',
|
|
147
|
+
playerId: clientId,
|
|
148
|
+
playerName,
|
|
149
|
+
});
|
|
150
|
+
console.log(`${playerName} (${clientId}) left room ${room.id}`);
|
|
151
|
+
// Remove room if empty
|
|
152
|
+
if (room.getPlayerCount() === 0) {
|
|
153
|
+
this.removeRoom(room.id);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
client.roomId = null;
|
|
157
|
+
client.isReady = false;
|
|
158
|
+
}
|
|
159
|
+
removeRoom(roomId) {
|
|
160
|
+
const room = this.rooms.get(roomId);
|
|
161
|
+
if (!room)
|
|
162
|
+
return;
|
|
163
|
+
room.stop();
|
|
164
|
+
this.rooms.delete(roomId);
|
|
165
|
+
console.log(`Room removed: ${roomId}`);
|
|
166
|
+
// Notify hub if callback is set
|
|
167
|
+
this.onRoomClosed?.(roomId);
|
|
168
|
+
}
|
|
169
|
+
listRooms() {
|
|
170
|
+
const rooms = [];
|
|
171
|
+
for (const room of this.rooms.values()) {
|
|
172
|
+
// Don't list private rooms
|
|
173
|
+
if (!room.config.isPrivate) {
|
|
174
|
+
rooms.push(room.getInfo());
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return rooms;
|
|
178
|
+
}
|
|
179
|
+
// ============ Message Handling ============
|
|
180
|
+
handleMessage(clientId, message) {
|
|
181
|
+
const client = this.clients.get(clientId);
|
|
182
|
+
if (!client)
|
|
183
|
+
return;
|
|
184
|
+
client.lastActivity = Date.now();
|
|
185
|
+
switch (message.type) {
|
|
186
|
+
case 'list_rooms':
|
|
187
|
+
this.sendToClient(clientId, {
|
|
188
|
+
type: 'room_list',
|
|
189
|
+
rooms: this.listRooms(),
|
|
190
|
+
});
|
|
191
|
+
break;
|
|
192
|
+
case 'create_room':
|
|
193
|
+
this.createRoom(clientId, message.config);
|
|
194
|
+
break;
|
|
195
|
+
case 'join_room':
|
|
196
|
+
this.joinRoom(clientId, message.roomId, message.playerName, message.password);
|
|
197
|
+
break;
|
|
198
|
+
case 'leave_room':
|
|
199
|
+
this.leaveRoom(clientId);
|
|
200
|
+
break;
|
|
201
|
+
default:
|
|
202
|
+
// Forward game messages to the room
|
|
203
|
+
if (client.roomId) {
|
|
204
|
+
const room = this.rooms.get(client.roomId);
|
|
205
|
+
if (room) {
|
|
206
|
+
room.handleMessage(clientId, message);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// ============ Binary Data (Voice) ============
|
|
213
|
+
handleBinaryData(clientId, data) {
|
|
214
|
+
const client = this.clients.get(clientId);
|
|
215
|
+
if (!client || !client.roomId)
|
|
216
|
+
return;
|
|
217
|
+
const room = this.rooms.get(client.roomId);
|
|
218
|
+
if (room) {
|
|
219
|
+
room.handleBinaryData(clientId, data);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// ============ Utility ============
|
|
223
|
+
sendToClient(clientId, message) {
|
|
224
|
+
const client = this.clients.get(clientId);
|
|
225
|
+
if (!client || client.socket.readyState !== WebSocket.OPEN)
|
|
226
|
+
return;
|
|
227
|
+
try {
|
|
228
|
+
client.socket.send(serializeServerMessage(message));
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
console.error(`Failed to send to client ${clientId}:`, e);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
cleanup() {
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
// Remove idle rooms
|
|
237
|
+
for (const [roomId, room] of this.rooms) {
|
|
238
|
+
if (room.getPlayerCount() === 0) {
|
|
239
|
+
const idleTime = now - room.lastActivity;
|
|
240
|
+
if (idleTime > this.config.roomIdleTimeout) {
|
|
241
|
+
this.removeRoom(roomId);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Note: Client cleanup handled by WebSocket close events
|
|
246
|
+
}
|
|
247
|
+
shutdown() {
|
|
248
|
+
// Stop cleanup interval
|
|
249
|
+
if (this.cleanupInterval) {
|
|
250
|
+
clearInterval(this.cleanupInterval);
|
|
251
|
+
this.cleanupInterval = null;
|
|
252
|
+
}
|
|
253
|
+
// Stop all rooms
|
|
254
|
+
for (const room of this.rooms.values()) {
|
|
255
|
+
room.stop();
|
|
256
|
+
}
|
|
257
|
+
this.rooms.clear();
|
|
258
|
+
// Close all client connections
|
|
259
|
+
for (const client of this.clients.values()) {
|
|
260
|
+
try {
|
|
261
|
+
client.socket.close(1001, 'Server shutting down');
|
|
262
|
+
}
|
|
263
|
+
catch (e) {
|
|
264
|
+
// Ignore close errors
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
this.clients.clear();
|
|
268
|
+
console.log('RoomManager shutdown complete');
|
|
269
|
+
}
|
|
270
|
+
// ============ Stats ============
|
|
271
|
+
getStats() {
|
|
272
|
+
return {
|
|
273
|
+
rooms: this.rooms.size,
|
|
274
|
+
clients: this.clients.size,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoiceRelay - Server-side voice frame relay
|
|
3
|
+
*
|
|
4
|
+
* Relays binary voice frames between clients in a room.
|
|
5
|
+
* Handles team filtering and position tracking for spatial audio.
|
|
6
|
+
*/
|
|
7
|
+
import { TeamId } from './protocol.js';
|
|
8
|
+
import { ConnectedClient } from './types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Check if data is a voice frame
|
|
11
|
+
*/
|
|
12
|
+
export declare function isVoiceFrame(data: Buffer | ArrayBuffer | Uint8Array): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Voice relay state for a room
|
|
15
|
+
*/
|
|
16
|
+
export declare class VoiceRelay {
|
|
17
|
+
private positions;
|
|
18
|
+
private senderIdToPlayerId;
|
|
19
|
+
private playerIdToSenderId;
|
|
20
|
+
/**
|
|
21
|
+
* Register a player's sender ID
|
|
22
|
+
*/
|
|
23
|
+
registerPlayer(playerId: string, senderId: number): void;
|
|
24
|
+
/**
|
|
25
|
+
* Unregister a player
|
|
26
|
+
*/
|
|
27
|
+
unregisterPlayer(playerId: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* Update a player's position (for spatial audio)
|
|
30
|
+
*/
|
|
31
|
+
updatePosition(playerId: string, x: number, y: number, z: number): void;
|
|
32
|
+
/**
|
|
33
|
+
* Get player ID from sender ID
|
|
34
|
+
*/
|
|
35
|
+
getPlayerId(senderId: number): string | undefined;
|
|
36
|
+
private debugRelayCount;
|
|
37
|
+
/**
|
|
38
|
+
* Relay a voice frame to room members
|
|
39
|
+
*
|
|
40
|
+
* @param data Binary voice frame
|
|
41
|
+
* @param senderClientId Client ID of sender
|
|
42
|
+
* @param clients Map of all clients in room
|
|
43
|
+
* @param teamAssignments Map of client ID to team
|
|
44
|
+
*/
|
|
45
|
+
relayVoiceFrame(data: Uint8Array, senderClientId: string, clients: Map<string, ConnectedClient>, teamAssignments: Map<string, TeamId>): void;
|
|
46
|
+
/**
|
|
47
|
+
* Cleanup old positions
|
|
48
|
+
*/
|
|
49
|
+
cleanup(maxAgeMs?: number): void;
|
|
50
|
+
/**
|
|
51
|
+
* Clear all state
|
|
52
|
+
*/
|
|
53
|
+
clear(): void;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get or create voice relay for a room
|
|
57
|
+
*/
|
|
58
|
+
export declare function getVoiceRelay(roomId: string): VoiceRelay;
|
|
59
|
+
/**
|
|
60
|
+
* Remove voice relay for a room
|
|
61
|
+
*/
|
|
62
|
+
export declare function removeVoiceRelay(roomId: string): void;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoiceRelay - Server-side voice frame relay
|
|
3
|
+
*
|
|
4
|
+
* Relays binary voice frames between clients in a room.
|
|
5
|
+
* Handles team filtering and position tracking for spatial audio.
|
|
6
|
+
*/
|
|
7
|
+
import { WebSocket } from 'ws';
|
|
8
|
+
// Voice protocol constants (matching client)
|
|
9
|
+
const VOICE_FRAME_TYPE = 0x01;
|
|
10
|
+
const VOICE_FLAG_TEAM_ONLY = 0x02;
|
|
11
|
+
/**
|
|
12
|
+
* Extract sender ID from voice frame (bytes 2-5)
|
|
13
|
+
*/
|
|
14
|
+
function getSenderIdFromFrame(data) {
|
|
15
|
+
if (data.length < 6)
|
|
16
|
+
return 0;
|
|
17
|
+
return data[2] | (data[3] << 8) | (data[4] << 16) | (data[5] << 24);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Get flags from voice frame (byte 1)
|
|
21
|
+
*/
|
|
22
|
+
function getFrameFlags(data) {
|
|
23
|
+
if (data.length < 2)
|
|
24
|
+
return 0;
|
|
25
|
+
return data[1];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check if data is a voice frame
|
|
29
|
+
*/
|
|
30
|
+
export function isVoiceFrame(data) {
|
|
31
|
+
if (data instanceof ArrayBuffer) {
|
|
32
|
+
data = new Uint8Array(data);
|
|
33
|
+
}
|
|
34
|
+
else if (Buffer.isBuffer(data)) {
|
|
35
|
+
data = new Uint8Array(data.buffer, data.byteOffset, data.length);
|
|
36
|
+
}
|
|
37
|
+
return data.length >= 1 && data[0] === VOICE_FRAME_TYPE;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Voice relay state for a room
|
|
41
|
+
*/
|
|
42
|
+
export class VoiceRelay {
|
|
43
|
+
positions = new Map();
|
|
44
|
+
senderIdToPlayerId = new Map();
|
|
45
|
+
playerIdToSenderId = new Map();
|
|
46
|
+
/**
|
|
47
|
+
* Register a player's sender ID
|
|
48
|
+
*/
|
|
49
|
+
registerPlayer(playerId, senderId) {
|
|
50
|
+
this.senderIdToPlayerId.set(senderId, playerId);
|
|
51
|
+
this.playerIdToSenderId.set(playerId, senderId);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Unregister a player
|
|
55
|
+
*/
|
|
56
|
+
unregisterPlayer(playerId) {
|
|
57
|
+
const senderId = this.playerIdToSenderId.get(playerId);
|
|
58
|
+
if (senderId !== undefined) {
|
|
59
|
+
this.senderIdToPlayerId.delete(senderId);
|
|
60
|
+
}
|
|
61
|
+
this.playerIdToSenderId.delete(playerId);
|
|
62
|
+
this.positions.delete(playerId);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Update a player's position (for spatial audio)
|
|
66
|
+
*/
|
|
67
|
+
updatePosition(playerId, x, y, z) {
|
|
68
|
+
this.positions.set(playerId, {
|
|
69
|
+
x, y, z,
|
|
70
|
+
lastUpdate: Date.now(),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get player ID from sender ID
|
|
75
|
+
*/
|
|
76
|
+
getPlayerId(senderId) {
|
|
77
|
+
return this.senderIdToPlayerId.get(senderId);
|
|
78
|
+
}
|
|
79
|
+
// Debug counter
|
|
80
|
+
debugRelayCount = 0;
|
|
81
|
+
/**
|
|
82
|
+
* Relay a voice frame to room members
|
|
83
|
+
*
|
|
84
|
+
* @param data Binary voice frame
|
|
85
|
+
* @param senderClientId Client ID of sender
|
|
86
|
+
* @param clients Map of all clients in room
|
|
87
|
+
* @param teamAssignments Map of client ID to team
|
|
88
|
+
*/
|
|
89
|
+
relayVoiceFrame(data, senderClientId, clients, teamAssignments) {
|
|
90
|
+
const flags = getFrameFlags(data);
|
|
91
|
+
const teamOnly = (flags & VOICE_FLAG_TEAM_ONLY) !== 0;
|
|
92
|
+
const senderTeam = teamAssignments.get(senderClientId);
|
|
93
|
+
const senderId = getSenderIdFromFrame(data);
|
|
94
|
+
this.debugRelayCount++;
|
|
95
|
+
if (this.debugRelayCount % 50 === 1) {
|
|
96
|
+
console.log(`[VoiceRelay] Relaying frame #${this.debugRelayCount} from ${senderClientId.slice(0, 8)} (sender ${senderId.toString(16)}), ${clients.size} clients in room`);
|
|
97
|
+
}
|
|
98
|
+
let relayedTo = 0;
|
|
99
|
+
// Relay to all other clients (optionally filtered by team)
|
|
100
|
+
for (const [clientId, client] of clients) {
|
|
101
|
+
// Don't send back to sender
|
|
102
|
+
if (clientId === senderClientId)
|
|
103
|
+
continue;
|
|
104
|
+
// Check team filter
|
|
105
|
+
if (teamOnly && senderTeam) {
|
|
106
|
+
const clientTeam = teamAssignments.get(clientId);
|
|
107
|
+
if (clientTeam !== senderTeam)
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
// Send binary frame
|
|
111
|
+
if (client.socket.readyState === WebSocket.OPEN) {
|
|
112
|
+
try {
|
|
113
|
+
client.socket.send(data);
|
|
114
|
+
relayedTo++;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Ignore send errors
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (this.debugRelayCount % 50 === 1) {
|
|
122
|
+
console.log(`[VoiceRelay] Relayed to ${relayedTo} clients`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Cleanup old positions
|
|
127
|
+
*/
|
|
128
|
+
cleanup(maxAgeMs = 10000) {
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
for (const [playerId, pos] of this.positions) {
|
|
131
|
+
if (now - pos.lastUpdate > maxAgeMs) {
|
|
132
|
+
this.positions.delete(playerId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Clear all state
|
|
138
|
+
*/
|
|
139
|
+
clear() {
|
|
140
|
+
this.positions.clear();
|
|
141
|
+
this.senderIdToPlayerId.clear();
|
|
142
|
+
this.playerIdToSenderId.clear();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Room voice relays
|
|
146
|
+
const roomRelays = new Map();
|
|
147
|
+
/**
|
|
148
|
+
* Get or create voice relay for a room
|
|
149
|
+
*/
|
|
150
|
+
export function getVoiceRelay(roomId) {
|
|
151
|
+
let relay = roomRelays.get(roomId);
|
|
152
|
+
if (!relay) {
|
|
153
|
+
relay = new VoiceRelay();
|
|
154
|
+
roomRelays.set(roomId, relay);
|
|
155
|
+
}
|
|
156
|
+
return relay;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Remove voice relay for a room
|
|
160
|
+
*/
|
|
161
|
+
export function removeVoiceRelay(roomId) {
|
|
162
|
+
const relay = roomRelays.get(roomId);
|
|
163
|
+
if (relay) {
|
|
164
|
+
relay.clear();
|
|
165
|
+
roomRelays.delete(roomId);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { PoolRegistry } from './PoolRegistry.js';
|
|
2
|
+
export interface HubServerConfig {
|
|
3
|
+
port: number;
|
|
4
|
+
builtInPoolEndpoint?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class HubServer {
|
|
7
|
+
private wss;
|
|
8
|
+
private httpServer;
|
|
9
|
+
private poolRegistry;
|
|
10
|
+
private connections;
|
|
11
|
+
private config;
|
|
12
|
+
private tokenMap;
|
|
13
|
+
constructor(config: HubServerConfig);
|
|
14
|
+
start(): void;
|
|
15
|
+
stop(): void;
|
|
16
|
+
private handleConnection;
|
|
17
|
+
private handleMessage;
|
|
18
|
+
private handlePoolMessage;
|
|
19
|
+
private handlePoolRegister;
|
|
20
|
+
private handleClientMessage;
|
|
21
|
+
private handleListRooms;
|
|
22
|
+
private handleGetEndpoint;
|
|
23
|
+
private handleCreateRoom;
|
|
24
|
+
private handleDisconnect;
|
|
25
|
+
private generateToken;
|
|
26
|
+
private cleanupExpiredTokens;
|
|
27
|
+
validateToken(token: string): {
|
|
28
|
+
roomId: string;
|
|
29
|
+
endpoint: string;
|
|
30
|
+
} | null;
|
|
31
|
+
getPoolRegistry(): PoolRegistry;
|
|
32
|
+
getStats(): {
|
|
33
|
+
pools: number;
|
|
34
|
+
rooms: number;
|
|
35
|
+
players: number;
|
|
36
|
+
};
|
|
37
|
+
}
|