@rpgjs/vite 5.0.0-alpha.41 → 5.0.0-alpha.44
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/index.js +4695 -640
- package/dist/index.js.map +1 -1
- package/dist/mmorpg-build-plugin.d.ts +8 -0
- package/dist/rpgjs-plugin.d.ts +7 -2
- package/dist/server-plugin.d.ts +2 -279
- package/package.json +7 -6
- package/src/mmorpg-build-plugin.ts +123 -0
- package/src/rpgjs-plugin.ts +36 -4
- package/src/server-plugin.ts +15 -918
- package/tsconfig.json +2 -2
- package/src/types/rpgjs__server.d.ts +0 -11
package/src/server-plugin.ts
CHANGED
|
@@ -1,501 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createRpgServerTransport, logNetworkSimulationStatus } from "@rpgjs/server/node";
|
|
2
|
+
import type { RpgTransportServerConstructor, RpgWebSocketServer } from "@rpgjs/server/node";
|
|
2
3
|
import type { ViteDevServer } from "vite";
|
|
3
|
-
import { IncomingMessage } from "http";
|
|
4
|
-
import { Duplex } from "stream";
|
|
5
4
|
|
|
6
|
-
// Types for WebSocket without importing ws directly
|
|
7
|
-
interface WSConnection {
|
|
8
|
-
readyState: number;
|
|
9
|
-
send(data: string): void;
|
|
10
|
-
close(): void;
|
|
11
|
-
on(event: string, callback: (...args: any[]) => void): void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface WSServer {
|
|
15
|
-
handleUpgrade(
|
|
16
|
-
request: IncomingMessage,
|
|
17
|
-
socket: Duplex,
|
|
18
|
-
head: Buffer,
|
|
19
|
-
callback: (ws: WSConnection) => void
|
|
20
|
-
): void;
|
|
21
|
-
close(): void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* PartyConnection class compatible with PartyKit's Party.Connection interface
|
|
26
|
-
*
|
|
27
|
-
* This class implements the Connection interface expected by RPG-JS server,
|
|
28
|
-
* providing WebSocket communication capabilities and connection state management.
|
|
29
|
-
* Includes optional packet loss simulation for testing network conditions.
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* ```typescript
|
|
33
|
-
* const connection = new PartyConnection(websocket, 'player123');
|
|
34
|
-
* connection.send('Hello player!');
|
|
35
|
-
* connection.setState({ username: 'Alice' });
|
|
36
|
-
* ```
|
|
37
|
-
*/
|
|
38
|
-
class PartyConnection {
|
|
39
|
-
public id: string;
|
|
40
|
-
public uri: string;
|
|
41
|
-
private _state: any = {};
|
|
42
|
-
private messageQueue: Array<{ message: string; timestamp: number; sequence: number }> = [];
|
|
43
|
-
private isProcessingQueue: boolean = false;
|
|
44
|
-
private sequenceCounter: number = 0;
|
|
45
|
-
private incomingQueue: Array<{
|
|
46
|
-
message: string;
|
|
47
|
-
timestamp: number;
|
|
48
|
-
processor: (messages: string[]) => Promise<void>;
|
|
49
|
-
}> = [];
|
|
50
|
-
private isProcessingIncomingQueue: boolean = false;
|
|
51
|
-
public static packetLossRate: number = parseFloat(process.env.RPGJS_PACKET_LOSS_RATE || '0.1');
|
|
52
|
-
public static packetLossEnabled: boolean = process.env.RPGJS_ENABLE_PACKET_LOSS === 'true';
|
|
53
|
-
public static packetLossFilter: string = process.env.RPGJS_PACKET_LOSS_FILTER || '';
|
|
54
|
-
public static bandwidthEnabled: boolean = process.env.RPGJS_ENABLE_BANDWIDTH === 'true';
|
|
55
|
-
public static bandwidthKbps: number = parseInt(process.env.RPGJS_BANDWIDTH_KBPS || '100'); // Kilobits per second
|
|
56
|
-
public static bandwidthFilter: string = process.env.RPGJS_BANDWIDTH_FILTER || '';
|
|
57
|
-
public static latencyEnabled: boolean = process.env.RPGJS_ENABLE_LATENCY === 'true';
|
|
58
|
-
public static latencyMs: number = parseInt(process.env.RPGJS_LATENCY_MS || '50'); // Fixed latency in milliseconds
|
|
59
|
-
public static latencyFilter: string = process.env.RPGJS_LATENCY_FILTER || '';
|
|
60
|
-
|
|
61
|
-
constructor(private ws: WSConnection, id?: string, uri?: string) {
|
|
62
|
-
this.id = id || this.generateId();
|
|
63
|
-
this.uri = uri || "";
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Generates a unique identifier for the connection
|
|
68
|
-
*
|
|
69
|
-
* @returns {string} Unique identifier based on timestamp and random number
|
|
70
|
-
*/
|
|
71
|
-
private generateId(): string {
|
|
72
|
-
return `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Sends data to the client via WebSocket with bandwidth and latency simulation
|
|
77
|
-
*
|
|
78
|
-
* Messages are queued and sent in the order they were called, with bandwidth limitations
|
|
79
|
-
* and network latency that simulate a slow, distant network connection. This ensures that
|
|
80
|
-
* if send(A) is called before send(B), A will always be sent before B, but both will be
|
|
81
|
-
* slowed down by bandwidth constraints and network latency.
|
|
82
|
-
*
|
|
83
|
-
* @param {any} data - Data to send (automatically serialized to JSON if not string)
|
|
84
|
-
*/
|
|
85
|
-
async send(data: any): Promise<void> {
|
|
86
|
-
if (this.ws.readyState !== 1) {
|
|
87
|
-
// WebSocket not open
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const message = typeof data === "string" ? data : JSON.stringify(data);
|
|
92
|
-
const timestamp = Date.now();
|
|
93
|
-
const sequence = ++this.sequenceCounter;
|
|
94
|
-
|
|
95
|
-
// Add message to queue
|
|
96
|
-
this.messageQueue.push({ message, timestamp, sequence });
|
|
97
|
-
|
|
98
|
-
// Start processing queue if not already processing
|
|
99
|
-
if (!this.isProcessingQueue) {
|
|
100
|
-
this.processMessageQueue();
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Processes the outgoing queue in order.
|
|
106
|
-
*
|
|
107
|
-
* Each message receives its own fixed latency (if enabled), while preserving
|
|
108
|
-
* original spacing and order.
|
|
109
|
-
*/
|
|
110
|
-
private async processMessageQueue(): Promise<void> {
|
|
111
|
-
await this.flushSendQueue();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Flushes the send queue sequentially, respecting bandwidth constraints.
|
|
116
|
-
*/
|
|
117
|
-
private async flushSendQueue(): Promise<void> {
|
|
118
|
-
if (this.isProcessingQueue) return;
|
|
119
|
-
this.isProcessingQueue = true;
|
|
120
|
-
|
|
121
|
-
while (this.messageQueue.length > 0) {
|
|
122
|
-
const queueItem = this.messageQueue.shift()!;
|
|
123
|
-
|
|
124
|
-
// Apply fixed one-way latency per message (not batched bursts).
|
|
125
|
-
if (this.shouldApplyLatency(queueItem.message)) {
|
|
126
|
-
await this.waitUntil(queueItem.timestamp + PartyConnection.latencyMs);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Bandwidth simulation per message
|
|
130
|
-
if (PartyConnection.bandwidthEnabled && PartyConnection.bandwidthKbps > 0) {
|
|
131
|
-
if (!PartyConnection.bandwidthFilter || queueItem.message.includes(PartyConnection.bandwidthFilter)) {
|
|
132
|
-
const messageSizeBits = queueItem.message.length * 8;
|
|
133
|
-
const transmissionTimeMs = (messageSizeBits / (PartyConnection.bandwidthKbps * 1000)) * 1000;
|
|
134
|
-
const minDelayMs = 10;
|
|
135
|
-
const bandwidthDelayMs = Math.max(transmissionTimeMs, minDelayMs);
|
|
136
|
-
console.log(`\x1b[34m[BANDWIDTH SIMULATION]\x1b[0m Connection ${this.id}: Message #${queueItem.sequence} transmission time: ${bandwidthDelayMs.toFixed(1)}ms`);
|
|
137
|
-
await new Promise(resolve => setTimeout(resolve, bandwidthDelayMs));
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
this.ws.send(queueItem.message);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
this.isProcessingQueue = false;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
private shouldApplyLatency(message: string): boolean {
|
|
148
|
-
if (!PartyConnection.latencyEnabled || PartyConnection.latencyMs <= 0) {
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
if (!PartyConnection.latencyFilter) {
|
|
152
|
-
return true;
|
|
153
|
-
}
|
|
154
|
-
return message.includes(PartyConnection.latencyFilter);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
private async waitUntil(targetTimestamp: number): Promise<void> {
|
|
158
|
-
const delayMs = targetTimestamp - Date.now();
|
|
159
|
-
if (delayMs <= 0) {
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Closes the WebSocket connection
|
|
167
|
-
*/
|
|
168
|
-
close(): void {
|
|
169
|
-
if (this.ws.readyState === 1) {
|
|
170
|
-
// WebSocket.OPEN
|
|
171
|
-
this.ws.close();
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Sets state data for this connection
|
|
177
|
-
*
|
|
178
|
-
* @param {any} value - State data to store (max 2KB as per PartyKit spec)
|
|
179
|
-
*/
|
|
180
|
-
setState(value: any): void {
|
|
181
|
-
this._state = value;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Gets the current state of this connection
|
|
186
|
-
*
|
|
187
|
-
* @returns {any} Current connection state
|
|
188
|
-
*/
|
|
189
|
-
get state(): any {
|
|
190
|
-
return this._state;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Buffers incoming messages to simulate TCP latency on reception.
|
|
195
|
-
*
|
|
196
|
-
* Messages are processed in strict order. Each message keeps its own fixed
|
|
197
|
-
* latency delay relative to the moment it arrived.
|
|
198
|
-
*
|
|
199
|
-
* @param {string} message - Raw incoming message
|
|
200
|
-
* @param {(messages: string[]) => Promise<void>} processor - Async batch processor
|
|
201
|
-
*
|
|
202
|
-
* @example
|
|
203
|
-
* await connection.bufferIncoming(raw, async (batch) => {
|
|
204
|
-
* for (const msg of batch) await handle(msg)
|
|
205
|
-
* })
|
|
206
|
-
*/
|
|
207
|
-
bufferIncoming(message: string, processor: (messages: string[]) => Promise<void>): void {
|
|
208
|
-
this.incomingQueue.push({
|
|
209
|
-
message,
|
|
210
|
-
timestamp: Date.now(),
|
|
211
|
-
processor,
|
|
212
|
-
});
|
|
213
|
-
if (!this.isProcessingIncomingQueue) {
|
|
214
|
-
void this.processIncomingQueue();
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
private async processIncomingQueue(): Promise<void> {
|
|
219
|
-
if (this.isProcessingIncomingQueue) {
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
this.isProcessingIncomingQueue = true;
|
|
223
|
-
while (this.incomingQueue.length > 0) {
|
|
224
|
-
const item = this.incomingQueue.shift()!;
|
|
225
|
-
if (this.shouldApplyLatency(item.message)) {
|
|
226
|
-
await this.waitUntil(item.timestamp + PartyConnection.latencyMs);
|
|
227
|
-
}
|
|
228
|
-
try {
|
|
229
|
-
await item.processor([item.message]);
|
|
230
|
-
} catch (err) {
|
|
231
|
-
console.error('Error processing incoming message:', err);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
this.isProcessingIncomingQueue = false;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Configures packet loss simulation settings
|
|
239
|
-
*
|
|
240
|
-
* @param {boolean} enabled - Whether to enable packet loss simulation
|
|
241
|
-
* @param {number} rate - Packet loss rate (0.0 to 1.0, e.g., 0.1 = 10% loss)
|
|
242
|
-
* @param {string} filter - Optional filter string to only simulate loss for messages containing this string
|
|
243
|
-
*
|
|
244
|
-
* @example
|
|
245
|
-
* ```typescript
|
|
246
|
-
* PartyConnection.configurePacketLoss(true, 0.15); // 15% packet loss
|
|
247
|
-
* PartyConnection.configurePacketLoss(true, 0.2, 'sync'); // 20% loss only for sync messages
|
|
248
|
-
* ```
|
|
249
|
-
*/
|
|
250
|
-
static configurePacketLoss(enabled: boolean, rate: number, filter?: string): void {
|
|
251
|
-
PartyConnection.packetLossEnabled = enabled;
|
|
252
|
-
PartyConnection.packetLossRate = Math.max(0, Math.min(1, rate)); // Clamp between 0 and 1
|
|
253
|
-
PartyConnection.packetLossFilter = filter || '';
|
|
254
|
-
|
|
255
|
-
if (enabled && rate > 0) {
|
|
256
|
-
const filterInfo = filter ? ` (filtered: "${filter}")` : '';
|
|
257
|
-
console.log(`\x1b[35m[PACKET LOSS SIMULATION]\x1b[0m Enabled with ${(rate * 100).toFixed(1)}% loss rate${filterInfo}`);
|
|
258
|
-
} else if (enabled) {
|
|
259
|
-
console.log(`\x1b[35m[PACKET LOSS SIMULATION]\x1b[0m Enabled but rate is 0% (no messages will be dropped)`);
|
|
260
|
-
} else {
|
|
261
|
-
console.log(`\x1b[35m[PACKET LOSS SIMULATION]\x1b[0m Disabled`);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Gets current packet loss simulation status
|
|
267
|
-
*
|
|
268
|
-
* @returns {Object} Current configuration
|
|
269
|
-
*/
|
|
270
|
-
static getPacketLossStatus(): { enabled: boolean; rate: number; filter: string } {
|
|
271
|
-
return {
|
|
272
|
-
enabled: PartyConnection.packetLossEnabled,
|
|
273
|
-
rate: PartyConnection.packetLossRate,
|
|
274
|
-
filter: PartyConnection.packetLossFilter
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Configures bandwidth simulation settings
|
|
280
|
-
*
|
|
281
|
-
* @param {boolean} enabled - Whether to enable bandwidth simulation
|
|
282
|
-
* @param {number} kbps - Bandwidth in kilobits per second (e.g., 100 = 100 kbps)
|
|
283
|
-
* @param {string} filter - Optional filter string to only simulate bandwidth for messages containing this string
|
|
284
|
-
*
|
|
285
|
-
* @example
|
|
286
|
-
* ```typescript
|
|
287
|
-
* PartyConnection.configureBandwidth(true, 50); // 50 kbps (very slow connection)
|
|
288
|
-
* PartyConnection.configureBandwidth(true, 1000, 'sync'); // 1 Mbps only for sync messages
|
|
289
|
-
* ```
|
|
290
|
-
*/
|
|
291
|
-
static configureBandwidth(enabled: boolean, kbps: number, filter?: string): void {
|
|
292
|
-
PartyConnection.bandwidthEnabled = enabled;
|
|
293
|
-
PartyConnection.bandwidthKbps = Math.max(1, kbps); // Minimum 1 kbps
|
|
294
|
-
PartyConnection.bandwidthFilter = filter || '';
|
|
295
|
-
|
|
296
|
-
if (enabled && kbps > 0) {
|
|
297
|
-
const filterInfo = filter ? ` (filtered: "${filter}")` : '';
|
|
298
|
-
console.log(`\x1b[35m[BANDWIDTH SIMULATION]\x1b[0m Enabled with ${kbps} kbps bandwidth${filterInfo}`);
|
|
299
|
-
} else if (enabled) {
|
|
300
|
-
console.log(`\x1b[35m[BANDWIDTH SIMULATION]\x1b[0m Enabled but bandwidth is 0 kbps (no delay will be applied)`);
|
|
301
|
-
} else {
|
|
302
|
-
console.log(`\x1b[35m[BANDWIDTH SIMULATION]\x1b[0m Disabled`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Gets current bandwidth simulation status
|
|
308
|
-
*
|
|
309
|
-
* @returns {Object} Current configuration
|
|
310
|
-
*/
|
|
311
|
-
static getBandwidthStatus(): { enabled: boolean; kbps: number; filter: string } {
|
|
312
|
-
return {
|
|
313
|
-
enabled: PartyConnection.bandwidthEnabled,
|
|
314
|
-
kbps: PartyConnection.bandwidthKbps,
|
|
315
|
-
filter: PartyConnection.bandwidthFilter
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Configures latency simulation settings
|
|
321
|
-
*
|
|
322
|
-
* Latency simulates the ping time to a distant server. Each message gets the same fixed delay,
|
|
323
|
-
* regardless of when it was sent. This means if you send 3 messages rapidly, they will all
|
|
324
|
-
* have the same latency delay applied to them.
|
|
325
|
-
*
|
|
326
|
-
* @param {boolean} enabled - Whether to enable latency simulation
|
|
327
|
-
* @param {number} ms - Fixed latency in milliseconds (simulates ping to distant server)
|
|
328
|
-
* @param {string} filter - Optional filter string to only simulate latency for messages containing this string
|
|
329
|
-
*
|
|
330
|
-
* @example
|
|
331
|
-
* ```typescript
|
|
332
|
-
* PartyConnection.configureLatency(true, 100); // 100ms latency (distant server)
|
|
333
|
-
* PartyConnection.configureLatency(true, 200, 'sync'); // 200ms latency only for sync messages
|
|
334
|
-
* ```
|
|
335
|
-
*/
|
|
336
|
-
static configureLatency(enabled: boolean, ms: number, filter?: string): void {
|
|
337
|
-
PartyConnection.latencyEnabled = enabled;
|
|
338
|
-
PartyConnection.latencyMs = Math.max(0, ms);
|
|
339
|
-
PartyConnection.latencyFilter = filter || '';
|
|
340
|
-
|
|
341
|
-
if (enabled && ms > 0) {
|
|
342
|
-
const filterInfo = filter ? ` (filtered: "${filter}")` : '';
|
|
343
|
-
console.log(`\x1b[35m[LATENCY SIMULATION]\x1b[0m Enabled with ${ms}ms fixed latency${filterInfo}`);
|
|
344
|
-
} else if (enabled) {
|
|
345
|
-
console.log(`\x1b[35m[LATENCY SIMULATION]\x1b[0m Enabled but latency is 0ms (no delay will be applied)`);
|
|
346
|
-
} else {
|
|
347
|
-
console.log(`\x1b[35m[LATENCY SIMULATION]\x1b[0m Disabled`);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Gets current latency simulation status
|
|
353
|
-
*
|
|
354
|
-
* @returns {Object} Current configuration
|
|
355
|
-
*/
|
|
356
|
-
static getLatencyStatus(): { enabled: boolean; ms: number; filter: string } {
|
|
357
|
-
return {
|
|
358
|
-
enabled: PartyConnection.latencyEnabled,
|
|
359
|
-
ms: PartyConnection.latencyMs,
|
|
360
|
-
filter: PartyConnection.latencyFilter
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Room class compatible with PartyKit's Party.Room interface
|
|
367
|
-
*
|
|
368
|
-
* This class manages multiple WebSocket connections and provides broadcasting
|
|
369
|
-
* capabilities, storage, and connection management as expected by RPG-JS server.
|
|
370
|
-
*
|
|
371
|
-
* @example
|
|
372
|
-
* ```typescript
|
|
373
|
-
* const room = new Room('lobby-1');
|
|
374
|
-
* room.broadcast('Game started!');
|
|
375
|
-
* const playerCount = [...room.getConnections()].length;
|
|
376
|
-
* ```
|
|
377
|
-
*/
|
|
378
|
-
class Room {
|
|
379
|
-
public id: string;
|
|
380
|
-
public internalID: string;
|
|
381
|
-
public env: Record<string, any> = {};
|
|
382
|
-
public context: any = {};
|
|
383
|
-
private connections: Map<string, PartyConnection> = new Map();
|
|
384
|
-
private storageData: Map<string, any> = new Map();
|
|
385
|
-
|
|
386
|
-
constructor(id: string) {
|
|
387
|
-
this.id = id;
|
|
388
|
-
this.internalID = `internal_${id}_${Date.now()}`;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Broadcasts a message to all connected clients with bandwidth simulation
|
|
393
|
-
*
|
|
394
|
-
* Messages are sent to each connection in parallel, but each connection maintains
|
|
395
|
-
* its own ordered queue of messages with bandwidth limitations. This ensures that
|
|
396
|
-
* broadcast messages are queued in the correct order for each individual connection,
|
|
397
|
-
* while being slowed down by simulated bandwidth constraints.
|
|
398
|
-
*
|
|
399
|
-
* @param {any} message - Message to broadcast
|
|
400
|
-
* @param {string[]} except - Array of connection IDs to exclude from broadcast
|
|
401
|
-
*/
|
|
402
|
-
async broadcast(message: any, except: string[] = []): Promise<void> {
|
|
403
|
-
const data =
|
|
404
|
-
typeof message === "string" ? message : JSON.stringify(message);
|
|
405
|
-
|
|
406
|
-
const sendPromises: Promise<void>[] = [];
|
|
407
|
-
|
|
408
|
-
for (const [connectionId, connection] of this.connections) {
|
|
409
|
-
if (!except.includes(connectionId)) {
|
|
410
|
-
// Each connection will handle its own queue ordering
|
|
411
|
-
sendPromises.push(connection.send(data));
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Wait for all messages to be queued (actual sending happens asynchronously in each connection's queue)
|
|
416
|
-
await Promise.all(sendPromises);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Gets a connection by its ID
|
|
421
|
-
*
|
|
422
|
-
* @param {string} id - Connection ID
|
|
423
|
-
* @returns {PartyConnection | undefined} The connection or undefined if not found
|
|
424
|
-
*/
|
|
425
|
-
getConnection(id: string): PartyConnection | undefined {
|
|
426
|
-
return this.connections.get(id);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Gets all currently connected clients
|
|
431
|
-
*
|
|
432
|
-
* @param {string} tag - Optional tag to filter connections (not implemented yet)
|
|
433
|
-
* @returns {IterableIterator<PartyConnection>} Iterator of all connections
|
|
434
|
-
*/
|
|
435
|
-
getConnections(tag?: string): IterableIterator<PartyConnection> {
|
|
436
|
-
// TODO: Implement tag filtering if needed
|
|
437
|
-
return this.connections.values();
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Adds a connection to this room
|
|
442
|
-
*
|
|
443
|
-
* @param {PartyConnection} connection - Connection to add
|
|
444
|
-
*/
|
|
445
|
-
addConnection(connection: PartyConnection): void {
|
|
446
|
-
this.connections.set(connection.id, connection);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* Removes a connection from this room
|
|
451
|
-
*
|
|
452
|
-
* @param {string} connectionId - ID of connection to remove
|
|
453
|
-
*/
|
|
454
|
-
removeConnection(connectionId: string): void {
|
|
455
|
-
this.connections.delete(connectionId);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Simple key-value storage for the room
|
|
460
|
-
*/
|
|
461
|
-
get storage() {
|
|
462
|
-
return {
|
|
463
|
-
put: async (key: string, value: any) => {
|
|
464
|
-
this.storageData.set(key, value);
|
|
465
|
-
},
|
|
466
|
-
get: async <T = any>(key: string): Promise<T | undefined> => {
|
|
467
|
-
return this.storageData.get(key) as T;
|
|
468
|
-
},
|
|
469
|
-
delete: async (key: string) => {
|
|
470
|
-
this.storageData.delete(key);
|
|
471
|
-
},
|
|
472
|
-
list: async () => {
|
|
473
|
-
// Return entries to match expected PartyKit/Server semantics
|
|
474
|
-
// Consumers often iterate as: for (const [key, value] of await storage.list())
|
|
475
|
-
return Array.from(this.storageData.entries());
|
|
476
|
-
},
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Utility function to safely import WebSocketServer
|
|
483
|
-
*
|
|
484
|
-
* This function checks if we are in a Node.js environment
|
|
485
|
-
* before trying to import the ws module, thus avoiding
|
|
486
|
-
* browser compatibility errors.
|
|
487
|
-
*
|
|
488
|
-
* @returns {Promise<any>} The WebSocketServer class or null if not available
|
|
489
|
-
*/
|
|
490
5
|
async function importWebSocketServer(): Promise<any> {
|
|
491
|
-
// Check if we are in a Node.js environment
|
|
492
6
|
if (typeof process === "undefined" || !process.versions?.node) {
|
|
493
7
|
console.warn("Not in Node.js environment, WebSocket server not available");
|
|
494
8
|
return null;
|
|
495
9
|
}
|
|
496
10
|
|
|
497
11
|
try {
|
|
498
|
-
// Use createRequire to import ws in an ES module context
|
|
499
12
|
const { createRequire } = await import("module");
|
|
500
13
|
const require = createRequire(import.meta.url);
|
|
501
14
|
const ws = require("ws");
|
|
@@ -506,168 +19,14 @@ async function importWebSocketServer(): Promise<any> {
|
|
|
506
19
|
}
|
|
507
20
|
}
|
|
508
21
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
try {
|
|
515
|
-
const mapId = roomId.startsWith('map-') ? roomId.slice(4) : roomId;
|
|
516
|
-
const defaultMapPayload = {
|
|
517
|
-
id: mapId,
|
|
518
|
-
width: 0,
|
|
519
|
-
height: 0,
|
|
520
|
-
events: [] as any[],
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
const req = {
|
|
524
|
-
url: `http://localhost/parties/main/${roomId}/map/update`,
|
|
525
|
-
method: 'POST',
|
|
526
|
-
headers: new Headers({}),
|
|
527
|
-
json: async () => defaultMapPayload,
|
|
528
|
-
text: async () => JSON.stringify(defaultMapPayload)
|
|
529
|
-
} as any;
|
|
530
|
-
|
|
531
|
-
await (rpgServer as any).onRequest(req);
|
|
532
|
-
console.log(`Initialized map for room ${roomId} via POST /map/update`);
|
|
533
|
-
} catch (error) {
|
|
534
|
-
console.warn(`Failed initializing map for room ${roomId}:`, error);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Creates a Vite plugin for integrating RPG-JS server functionality
|
|
540
|
-
*
|
|
541
|
-
* This plugin configures the development server to automatically start
|
|
542
|
-
* an RPG-JS server instance when Vite's dev server starts. It handles
|
|
543
|
-
* the instantiation and initialization of the server module, and sets up
|
|
544
|
-
* HTTP request and WebSocket connection forwarding to the RPG-JS server.
|
|
545
|
-
*
|
|
546
|
-
* The plugin intercepts:
|
|
547
|
-
* - HTTP requests to `/parties/*` paths and forwards them to the RPG-JS server
|
|
548
|
-
* - WebSocket upgrade requests and establishes connections with the RPG-JS server
|
|
549
|
-
*
|
|
550
|
-
* @param {new () => RpgServerEngine} serverModule - A class constructor that extends RpgServerEngine
|
|
551
|
-
* @returns {Object} Vite plugin configuration object
|
|
552
|
-
*
|
|
553
|
-
* @example
|
|
554
|
-
* ```typescript
|
|
555
|
-
* // In vite.config.ts
|
|
556
|
-
* import { serverPlugin } from '@rpgjs/vite';
|
|
557
|
-
* import startServer from './src/server';
|
|
558
|
-
*
|
|
559
|
-
* export default defineConfig({
|
|
560
|
-
* plugins: [
|
|
561
|
-
* serverPlugin(startServer)
|
|
562
|
-
* ]
|
|
563
|
-
* });
|
|
564
|
-
* ```
|
|
565
|
-
*/
|
|
566
|
-
export function serverPlugin(
|
|
567
|
-
serverModule: new (room: Room) => RpgServerEngine
|
|
568
|
-
) {
|
|
569
|
-
let wsServer: WSServer | null = null;
|
|
570
|
-
let rooms: Map<string, Room> = new Map();
|
|
571
|
-
let servers: Map<string, RpgServerEngine> = new Map();
|
|
572
|
-
|
|
573
|
-
// Ensure a room and its server instance exist for a given roomId
|
|
574
|
-
async function ensureRoomAndServer(roomId: string) {
|
|
575
|
-
let room = rooms.get(roomId);
|
|
576
|
-
if (!room) {
|
|
577
|
-
room = new Room(roomId);
|
|
578
|
-
rooms.set(roomId, room);
|
|
579
|
-
console.log(`Created new room: ${roomId}`);
|
|
580
|
-
}
|
|
581
|
-
let rpgServer = servers.get(roomId);
|
|
582
|
-
if (!rpgServer) {
|
|
583
|
-
rpgServer = new serverModule(room);
|
|
584
|
-
servers.set(roomId, rpgServer);
|
|
585
|
-
console.log(`Created new server instance for room: ${roomId}`);
|
|
586
|
-
if (typeof rpgServer.onStart === "function") {
|
|
587
|
-
try {
|
|
588
|
-
await rpgServer.onStart();
|
|
589
|
-
console.log(`Server started for room: ${roomId}`);
|
|
590
|
-
} catch (error) {
|
|
591
|
-
console.error(`Error starting server for room ${roomId}:`, error);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
await updateMap(roomId, rpgServer);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// Make sure parties context is available on the room
|
|
599
|
-
room.context.parties = buildPartiesContext();
|
|
600
|
-
return { room, rpgServer };
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// Build a parties context compatible with "room.context.parties"
|
|
604
|
-
function buildPartiesContext() {
|
|
605
|
-
return {
|
|
606
|
-
main: {
|
|
607
|
-
get: async (targetRoomId: string) => {
|
|
608
|
-
const { rpgServer } = await ensureRoomAndServer(targetRoomId);
|
|
609
|
-
return {
|
|
610
|
-
fetch: async (path: string, init?: { method?: string; body?: any; headers?: Record<string, string> }) => {
|
|
611
|
-
try {
|
|
612
|
-
const url = `http://localhost/parties/main/${targetRoomId}${path}`;
|
|
613
|
-
const method = (init?.method || 'GET').toUpperCase();
|
|
614
|
-
const headers = new Headers(init?.headers || {});
|
|
615
|
-
const bodyRaw = init?.body;
|
|
616
|
-
|
|
617
|
-
const req = {
|
|
618
|
-
url,
|
|
619
|
-
method,
|
|
620
|
-
headers,
|
|
621
|
-
json: async () => {
|
|
622
|
-
if (!bodyRaw) return undefined as any;
|
|
623
|
-
return typeof bodyRaw === 'string' ? JSON.parse(bodyRaw) : bodyRaw;
|
|
624
|
-
},
|
|
625
|
-
text: async () => {
|
|
626
|
-
if (typeof bodyRaw === 'string') return bodyRaw;
|
|
627
|
-
if (typeof bodyRaw === 'undefined') return '';
|
|
628
|
-
return JSON.stringify(bodyRaw);
|
|
629
|
-
}
|
|
630
|
-
} as any;
|
|
631
|
-
|
|
632
|
-
const result = await (rpgServer as any).onRequest(req);
|
|
633
|
-
const ok = !!result && (result.ok === true || typeof result !== 'undefined');
|
|
634
|
-
const response = {
|
|
635
|
-
ok,
|
|
636
|
-
status: ok ? 200 : 404,
|
|
637
|
-
async json() {
|
|
638
|
-
if (result && typeof result.json === 'function') return result.json();
|
|
639
|
-
return result ?? {};
|
|
640
|
-
},
|
|
641
|
-
async text() {
|
|
642
|
-
if (typeof result === 'string') return result;
|
|
643
|
-
try { return JSON.stringify(result ?? {}); } catch { return ''; }
|
|
644
|
-
}
|
|
645
|
-
} as any;
|
|
646
|
-
return response;
|
|
647
|
-
} catch (error) {
|
|
648
|
-
return {
|
|
649
|
-
ok: false,
|
|
650
|
-
status: 500,
|
|
651
|
-
async json() {
|
|
652
|
-
return { error: (error as Error)?.message || 'Internal error' };
|
|
653
|
-
},
|
|
654
|
-
async text() {
|
|
655
|
-
return (error as Error)?.message || 'Internal error';
|
|
656
|
-
}
|
|
657
|
-
} as any;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
} as any;
|
|
664
|
-
}
|
|
22
|
+
export function serverPlugin(serverModule: RpgTransportServerConstructor) {
|
|
23
|
+
let wsServer: RpgWebSocketServer | null = null;
|
|
24
|
+
const transport = createRpgServerTransport(serverModule);
|
|
665
25
|
|
|
666
26
|
return {
|
|
667
27
|
name: "server-plugin",
|
|
668
28
|
|
|
669
29
|
async configureServer(server: ViteDevServer) {
|
|
670
|
-
// Dynamic import of WebSocketServer to avoid compatibility issues
|
|
671
30
|
try {
|
|
672
31
|
const WebSocketServerClass = await importWebSocketServer();
|
|
673
32
|
if (WebSocketServerClass) {
|
|
@@ -683,283 +42,22 @@ export function serverPlugin(
|
|
|
683
42
|
wsServer = null;
|
|
684
43
|
}
|
|
685
44
|
|
|
686
|
-
console.log(
|
|
687
|
-
|
|
688
|
-
// Display network simulation status
|
|
689
|
-
const packetLossStatus = PartyConnection.getPacketLossStatus();
|
|
690
|
-
const bandwidthStatus = PartyConnection.getBandwidthStatus();
|
|
691
|
-
const latencyStatus = PartyConnection.getLatencyStatus();
|
|
692
|
-
|
|
693
|
-
if (packetLossStatus.enabled) {
|
|
694
|
-
const filterInfo = packetLossStatus.filter ? ` (filter: "${packetLossStatus.filter}")` : '';
|
|
695
|
-
console.log(`\x1b[36m[NETWORK SIMULATION]\x1b[0m Packet loss simulation: ${(packetLossStatus.rate * 100).toFixed(1)}% loss rate${filterInfo}`);
|
|
696
|
-
} else {
|
|
697
|
-
console.log(`\x1b[36m[NETWORK SIMULATION]\x1b[0m Packet loss simulation: disabled`);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
if (bandwidthStatus.enabled) {
|
|
701
|
-
const filterInfo = bandwidthStatus.filter ? ` (filter: "${bandwidthStatus.filter}")` : '';
|
|
702
|
-
console.log(`\x1b[36m[NETWORK SIMULATION]\x1b[0m Bandwidth simulation: ${bandwidthStatus.kbps} kbps${filterInfo}`);
|
|
703
|
-
} else {
|
|
704
|
-
console.log(`\x1b[36m[NETWORK SIMULATION]\x1b[0m Bandwidth simulation: disabled`);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
if (latencyStatus.enabled) {
|
|
708
|
-
const filterInfo = latencyStatus.filter ? ` (filter: "${latencyStatus.filter}")` : '';
|
|
709
|
-
console.log(`\x1b[36m[NETWORK SIMULATION]\x1b[0m Latency simulation: ${latencyStatus.ms}ms ping${filterInfo}`);
|
|
710
|
-
} else {
|
|
711
|
-
console.log(`\x1b[36m[NETWORK SIMULATION]\x1b[0m Latency simulation: disabled`);
|
|
712
|
-
}
|
|
45
|
+
console.log("RPG-JS server plugin initialized");
|
|
46
|
+
logNetworkSimulationStatus();
|
|
713
47
|
|
|
714
|
-
// HTTP request interception for /parties/* routes
|
|
715
48
|
server.middlewares.use("/parties", async (req, res, next) => {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const parsedUrl = new URL(incomingUrl, `http://${host}`);
|
|
720
|
-
const normalizedPath = parsedUrl.pathname.startsWith("/parties")
|
|
721
|
-
? parsedUrl.pathname
|
|
722
|
-
: `/parties${parsedUrl.pathname.startsWith("/") ? parsedUrl.pathname : `/${parsedUrl.pathname}`}`;
|
|
723
|
-
const pathParts = normalizedPath.split("/").filter(Boolean);
|
|
724
|
-
|
|
725
|
-
if (pathParts[0] !== "parties" || pathParts[1] !== "main" || pathParts.length < 4) {
|
|
726
|
-
next();
|
|
727
|
-
return;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
const roomId = pathParts[2];
|
|
731
|
-
const requestPath = `/${pathParts.slice(3).join("/")}`;
|
|
732
|
-
const { room, rpgServer } = await ensureRoomAndServer(roomId);
|
|
733
|
-
room.context.parties = buildPartiesContext();
|
|
734
|
-
|
|
735
|
-
const bodyText = await new Promise<string>((resolve, reject) => {
|
|
736
|
-
const chunks: Buffer[] = [];
|
|
737
|
-
req.on("data", (chunk: Buffer | string) => {
|
|
738
|
-
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
739
|
-
});
|
|
740
|
-
req.on("end", () => {
|
|
741
|
-
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
742
|
-
});
|
|
743
|
-
req.on("error", reject);
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
const requestHeaders = new Headers();
|
|
747
|
-
Object.entries(req.headers).forEach(([key, value]) => {
|
|
748
|
-
if (Array.isArray(value)) {
|
|
749
|
-
if (value[0] !== undefined) requestHeaders.set(key, value[0]);
|
|
750
|
-
return;
|
|
751
|
-
}
|
|
752
|
-
if (typeof value === "string") {
|
|
753
|
-
requestHeaders.set(key, value);
|
|
754
|
-
}
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
const requestLike = {
|
|
758
|
-
url: `http://${host}/parties/main/${roomId}${requestPath}${parsedUrl.search}`,
|
|
759
|
-
method: (req.method || "GET").toUpperCase(),
|
|
760
|
-
headers: requestHeaders,
|
|
761
|
-
json: async () => {
|
|
762
|
-
if (!bodyText) return undefined as any;
|
|
763
|
-
return JSON.parse(bodyText);
|
|
764
|
-
},
|
|
765
|
-
text: async () => bodyText,
|
|
766
|
-
} as any;
|
|
767
|
-
|
|
768
|
-
const result = await (rpgServer as any).onRequest(requestLike);
|
|
769
|
-
|
|
770
|
-
if (result instanceof Response) {
|
|
771
|
-
res.statusCode = result.status;
|
|
772
|
-
result.headers.forEach((value, key) => {
|
|
773
|
-
res.setHeader(key, value);
|
|
774
|
-
});
|
|
775
|
-
res.end(await result.text());
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
if (typeof result === "string") {
|
|
780
|
-
res.statusCode = 200;
|
|
781
|
-
res.setHeader("Content-Type", "text/plain");
|
|
782
|
-
res.end(result);
|
|
783
|
-
return;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
res.statusCode = 200;
|
|
787
|
-
res.setHeader("Content-Type", "application/json");
|
|
788
|
-
res.end(JSON.stringify(result ?? {}));
|
|
789
|
-
} catch (error) {
|
|
790
|
-
console.error("Error handling RPG-JS request:", error);
|
|
791
|
-
res.statusCode = 500;
|
|
792
|
-
res.setHeader("Content-Type", "application/json");
|
|
793
|
-
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
794
|
-
}
|
|
49
|
+
await transport.handleNodeRequest(req, res, next, {
|
|
50
|
+
mountedPath: "/parties",
|
|
51
|
+
});
|
|
795
52
|
});
|
|
796
|
-
// WebSocket upgrade handling (if available)
|
|
797
|
-
if (wsServer) {
|
|
798
|
-
server.httpServer?.on(
|
|
799
|
-
"upgrade",
|
|
800
|
-
(request: IncomingMessage, socket: Duplex, head: Buffer) => {
|
|
801
|
-
const url = new URL(request.url!, `http://${request.headers.host}`);
|
|
802
|
-
|
|
803
|
-
// Check if it's a WebSocket connection for RPG-JS
|
|
804
|
-
if (url.pathname.startsWith("/parties/")) {
|
|
805
|
-
console.log(`WebSocket upgrade request: ${url.pathname}`);
|
|
806
|
-
|
|
807
|
-
wsServer!.handleUpgrade(
|
|
808
|
-
request,
|
|
809
|
-
socket,
|
|
810
|
-
head,
|
|
811
|
-
async (ws: WSConnection) => {
|
|
812
|
-
try {
|
|
813
|
-
// Extract room name from URL: /parties/main/lobby-1 -> lobby-1
|
|
814
|
-
const pathParts = url.pathname.split("/");
|
|
815
|
-
const roomName = pathParts[pathParts.length - 1]; // Get the last part (lobby-1)
|
|
816
|
-
|
|
817
|
-
// Extract query parameters (like _pk)
|
|
818
|
-
const queryParams = Object.fromEntries(
|
|
819
|
-
url.searchParams.entries()
|
|
820
|
-
);
|
|
821
|
-
console.log(
|
|
822
|
-
`Room: ${roomName}, Query params:`,
|
|
823
|
-
queryParams
|
|
824
|
-
);
|
|
825
|
-
|
|
826
|
-
// Get or create the room and its server
|
|
827
|
-
const ensured = await ensureRoomAndServer(roomName);
|
|
828
|
-
const room = ensured.room;
|
|
829
|
-
const rpgServer = ensured.rpgServer;
|
|
830
|
-
// Inject a compatible parties context for cross-room calls
|
|
831
|
-
room.context.parties = buildPartiesContext();
|
|
832
|
-
|
|
833
|
-
// Create a connection instance
|
|
834
|
-
const connection = new PartyConnection(
|
|
835
|
-
ws,
|
|
836
|
-
queryParams._pk,
|
|
837
|
-
request.url
|
|
838
|
-
);
|
|
839
|
-
|
|
840
|
-
// Add connection to the room
|
|
841
|
-
room.addConnection(connection);
|
|
842
|
-
|
|
843
|
-
console.log(
|
|
844
|
-
`WebSocket connection established: ${connection.id} in room: ${roomName}`
|
|
845
|
-
);
|
|
846
|
-
|
|
847
|
-
// Set up WebSocket event handlers
|
|
848
|
-
ws.on("message", async (data: Buffer) => {
|
|
849
|
-
try {
|
|
850
|
-
const rawMessage = data.toString();
|
|
851
53
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
if (random < PartyConnection.packetLossRate) {
|
|
857
|
-
console.log(`\x1b[31m[PACKET LOSS]\x1b[0m Connection ${connection.id}: Server dropped an incoming packet (${(PartyConnection.packetLossRate * 100).toFixed(1)}% loss rate)`);
|
|
858
|
-
console.log(`\x1b[33m[PACKET DATA]\x1b[0m ${rawMessage.substring(0, 100)}${rawMessage.length > 100 ? '...' : ''}`);
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// Buffer incoming messages to simulate TCP latency on reception
|
|
865
|
-
connection.bufferIncoming(rawMessage, async (batch: string[]) => {
|
|
866
|
-
// Process in order
|
|
867
|
-
for (const msg of batch) {
|
|
868
|
-
if (typeof rpgServer.onMessage === "function") {
|
|
869
|
-
await rpgServer.onMessage(msg, connection as any);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
});
|
|
873
|
-
} catch (error) {
|
|
874
|
-
console.error(
|
|
875
|
-
"Error processing WebSocket message:",
|
|
876
|
-
error
|
|
877
|
-
);
|
|
878
|
-
}
|
|
879
|
-
});
|
|
880
|
-
|
|
881
|
-
ws.on("close", async () => {
|
|
882
|
-
console.log(
|
|
883
|
-
`WebSocket connection closed: ${connection.id} from room: ${roomName}`
|
|
884
|
-
);
|
|
885
|
-
// Remove connection from room
|
|
886
|
-
room.removeConnection(connection.id);
|
|
887
|
-
// Call onClose on the RPG-JS server
|
|
888
|
-
if (typeof rpgServer.onClose === "function") {
|
|
889
|
-
await rpgServer.onClose(connection as any);
|
|
890
|
-
}
|
|
891
|
-
});
|
|
892
|
-
|
|
893
|
-
ws.on("error", async (error: Error) => {
|
|
894
|
-
console.error("WebSocket error:", error);
|
|
895
|
-
// Remove connection from room
|
|
896
|
-
room.removeConnection(connection.id);
|
|
897
|
-
// Call onClose on the RPG-JS server
|
|
898
|
-
if (typeof rpgServer.onClose === "function") {
|
|
899
|
-
await rpgServer.onClose(connection as any);
|
|
900
|
-
}
|
|
901
|
-
});
|
|
902
|
-
|
|
903
|
-
// Call onConnect on the RPG-JS server if the method exists
|
|
904
|
-
if (typeof rpgServer.onConnect === "function") {
|
|
905
|
-
// Create a compatible connection context with Headers-like interface
|
|
906
|
-
const headers = new Map();
|
|
907
|
-
if (request.headers) {
|
|
908
|
-
Object.entries(request.headers).forEach(
|
|
909
|
-
([key, value]) => {
|
|
910
|
-
headers.set(
|
|
911
|
-
key.toLowerCase(),
|
|
912
|
-
Array.isArray(value) ? value[0] : value
|
|
913
|
-
);
|
|
914
|
-
}
|
|
915
|
-
);
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
const connectionContext = {
|
|
919
|
-
request: {
|
|
920
|
-
headers: {
|
|
921
|
-
has: (name: string) =>
|
|
922
|
-
headers.has(name.toLowerCase()),
|
|
923
|
-
get: (name: string) =>
|
|
924
|
-
headers.get(name.toLowerCase()),
|
|
925
|
-
entries: () => headers.entries(),
|
|
926
|
-
keys: () => headers.keys(),
|
|
927
|
-
values: () => headers.values(),
|
|
928
|
-
},
|
|
929
|
-
url: url.toString(),
|
|
930
|
-
method: request.method,
|
|
931
|
-
},
|
|
932
|
-
url: url,
|
|
933
|
-
};
|
|
934
|
-
await rpgServer.onConnect(
|
|
935
|
-
connection as any,
|
|
936
|
-
connectionContext as any
|
|
937
|
-
);
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
// Send connection confirmation
|
|
941
|
-
connection.send({
|
|
942
|
-
type: "connected",
|
|
943
|
-
id: connection.id,
|
|
944
|
-
message: "Connected to RPG-JS server",
|
|
945
|
-
});
|
|
946
|
-
} catch (error) {
|
|
947
|
-
console.error(
|
|
948
|
-
"Error establishing WebSocket connection:",
|
|
949
|
-
error
|
|
950
|
-
);
|
|
951
|
-
ws.close();
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
);
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
);
|
|
54
|
+
if (wsServer) {
|
|
55
|
+
server.httpServer?.on("upgrade", (request, socket, head) => {
|
|
56
|
+
void transport.handleUpgrade(wsServer!, request, socket, head);
|
|
57
|
+
});
|
|
958
58
|
}
|
|
959
59
|
|
|
960
|
-
console.log(
|
|
961
|
-
"RPG-JS server plugin configured with HTTP and WebSocket forwarding"
|
|
962
|
-
);
|
|
60
|
+
console.log("RPG-JS server plugin configured with HTTP and WebSocket forwarding !");
|
|
963
61
|
},
|
|
964
62
|
|
|
965
63
|
buildStart() {
|
|
@@ -967,7 +65,6 @@ export function serverPlugin(
|
|
|
967
65
|
},
|
|
968
66
|
|
|
969
67
|
buildEnd() {
|
|
970
|
-
// Cleanup when server stops
|
|
971
68
|
if (wsServer) {
|
|
972
69
|
wsServer.close();
|
|
973
70
|
}
|