@watchtower-sdk/core 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +29 -2
- package/dist/index.d.ts +29 -2
- package/dist/index.js +157 -22
- package/dist/index.mjs +157 -22
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -211,6 +211,14 @@ interface SyncOptions {
|
|
|
211
211
|
tickRate?: number;
|
|
212
212
|
/** Enable interpolation for remote entities (default: true) */
|
|
213
213
|
interpolate?: boolean;
|
|
214
|
+
/** Interpolation delay in ms - how far "in the past" to render others (default: 100) */
|
|
215
|
+
interpolationDelay?: number;
|
|
216
|
+
/** Jitter buffer size in ms - smooths network variance (default: 50) */
|
|
217
|
+
jitterBuffer?: number;
|
|
218
|
+
/** Enable auto-reconnection on disconnect (default: true) */
|
|
219
|
+
autoReconnect?: boolean;
|
|
220
|
+
/** Max reconnection attempts (default: 10) */
|
|
221
|
+
maxReconnectAttempts?: number;
|
|
214
222
|
}
|
|
215
223
|
interface JoinOptions {
|
|
216
224
|
/** Create room if it doesn't exist */
|
|
@@ -268,9 +276,15 @@ declare class Sync<T extends Record<string, unknown>> {
|
|
|
268
276
|
private _roomId;
|
|
269
277
|
private ws;
|
|
270
278
|
private syncInterval;
|
|
279
|
+
private interpolationInterval;
|
|
271
280
|
private lastSentState;
|
|
272
|
-
private interpolationTargets;
|
|
273
281
|
private listeners;
|
|
282
|
+
private snapshots;
|
|
283
|
+
private jitterQueue;
|
|
284
|
+
private reconnectAttempts;
|
|
285
|
+
private reconnectTimeout;
|
|
286
|
+
private lastJoinOptions;
|
|
287
|
+
private isReconnecting;
|
|
274
288
|
constructor(state: T, config: Required<WatchtowerConfig>, options?: SyncOptions);
|
|
275
289
|
/**
|
|
276
290
|
* Join a room - your state will sync with everyone in this room
|
|
@@ -283,6 +297,12 @@ declare class Sync<T extends Record<string, unknown>> {
|
|
|
283
297
|
* Leave the current room
|
|
284
298
|
*/
|
|
285
299
|
leave(): Promise<void>;
|
|
300
|
+
/**
|
|
301
|
+
* Send a one-off message to all players in the room
|
|
302
|
+
*
|
|
303
|
+
* @param data - Any JSON-serializable data
|
|
304
|
+
*/
|
|
305
|
+
broadcast(data: unknown): void;
|
|
286
306
|
/**
|
|
287
307
|
* Create a new room and join it
|
|
288
308
|
*
|
|
@@ -297,7 +317,7 @@ declare class Sync<T extends Record<string, unknown>> {
|
|
|
297
317
|
/**
|
|
298
318
|
* Subscribe to sync events
|
|
299
319
|
*/
|
|
300
|
-
on(event: 'join' | 'leave' | 'error' | 'connected' | 'disconnected', callback: Function): void;
|
|
320
|
+
on(event: 'join' | 'leave' | 'error' | 'connected' | 'disconnected' | 'reconnecting' | 'reconnected' | 'message', callback: Function): void;
|
|
301
321
|
/**
|
|
302
322
|
* Unsubscribe from sync events
|
|
303
323
|
*/
|
|
@@ -307,13 +327,20 @@ declare class Sync<T extends Record<string, unknown>> {
|
|
|
307
327
|
private handleMessage;
|
|
308
328
|
private applyFullState;
|
|
309
329
|
private applyPlayerState;
|
|
330
|
+
private addSnapshot;
|
|
331
|
+
private applyStateDirect;
|
|
310
332
|
private removePlayer;
|
|
333
|
+
private attemptReconnect;
|
|
311
334
|
private clearRemotePlayers;
|
|
312
335
|
private findPlayersKey;
|
|
313
336
|
private startSyncLoop;
|
|
337
|
+
private startInterpolationLoop;
|
|
338
|
+
private stopInterpolationLoop;
|
|
339
|
+
private processJitterQueue;
|
|
314
340
|
private stopSyncLoop;
|
|
315
341
|
private syncMyState;
|
|
316
342
|
private updateInterpolation;
|
|
343
|
+
private lerpState;
|
|
317
344
|
private generateRoomCode;
|
|
318
345
|
private getHeaders;
|
|
319
346
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -211,6 +211,14 @@ interface SyncOptions {
|
|
|
211
211
|
tickRate?: number;
|
|
212
212
|
/** Enable interpolation for remote entities (default: true) */
|
|
213
213
|
interpolate?: boolean;
|
|
214
|
+
/** Interpolation delay in ms - how far "in the past" to render others (default: 100) */
|
|
215
|
+
interpolationDelay?: number;
|
|
216
|
+
/** Jitter buffer size in ms - smooths network variance (default: 50) */
|
|
217
|
+
jitterBuffer?: number;
|
|
218
|
+
/** Enable auto-reconnection on disconnect (default: true) */
|
|
219
|
+
autoReconnect?: boolean;
|
|
220
|
+
/** Max reconnection attempts (default: 10) */
|
|
221
|
+
maxReconnectAttempts?: number;
|
|
214
222
|
}
|
|
215
223
|
interface JoinOptions {
|
|
216
224
|
/** Create room if it doesn't exist */
|
|
@@ -268,9 +276,15 @@ declare class Sync<T extends Record<string, unknown>> {
|
|
|
268
276
|
private _roomId;
|
|
269
277
|
private ws;
|
|
270
278
|
private syncInterval;
|
|
279
|
+
private interpolationInterval;
|
|
271
280
|
private lastSentState;
|
|
272
|
-
private interpolationTargets;
|
|
273
281
|
private listeners;
|
|
282
|
+
private snapshots;
|
|
283
|
+
private jitterQueue;
|
|
284
|
+
private reconnectAttempts;
|
|
285
|
+
private reconnectTimeout;
|
|
286
|
+
private lastJoinOptions;
|
|
287
|
+
private isReconnecting;
|
|
274
288
|
constructor(state: T, config: Required<WatchtowerConfig>, options?: SyncOptions);
|
|
275
289
|
/**
|
|
276
290
|
* Join a room - your state will sync with everyone in this room
|
|
@@ -283,6 +297,12 @@ declare class Sync<T extends Record<string, unknown>> {
|
|
|
283
297
|
* Leave the current room
|
|
284
298
|
*/
|
|
285
299
|
leave(): Promise<void>;
|
|
300
|
+
/**
|
|
301
|
+
* Send a one-off message to all players in the room
|
|
302
|
+
*
|
|
303
|
+
* @param data - Any JSON-serializable data
|
|
304
|
+
*/
|
|
305
|
+
broadcast(data: unknown): void;
|
|
286
306
|
/**
|
|
287
307
|
* Create a new room and join it
|
|
288
308
|
*
|
|
@@ -297,7 +317,7 @@ declare class Sync<T extends Record<string, unknown>> {
|
|
|
297
317
|
/**
|
|
298
318
|
* Subscribe to sync events
|
|
299
319
|
*/
|
|
300
|
-
on(event: 'join' | 'leave' | 'error' | 'connected' | 'disconnected', callback: Function): void;
|
|
320
|
+
on(event: 'join' | 'leave' | 'error' | 'connected' | 'disconnected' | 'reconnecting' | 'reconnected' | 'message', callback: Function): void;
|
|
301
321
|
/**
|
|
302
322
|
* Unsubscribe from sync events
|
|
303
323
|
*/
|
|
@@ -307,13 +327,20 @@ declare class Sync<T extends Record<string, unknown>> {
|
|
|
307
327
|
private handleMessage;
|
|
308
328
|
private applyFullState;
|
|
309
329
|
private applyPlayerState;
|
|
330
|
+
private addSnapshot;
|
|
331
|
+
private applyStateDirect;
|
|
310
332
|
private removePlayer;
|
|
333
|
+
private attemptReconnect;
|
|
311
334
|
private clearRemotePlayers;
|
|
312
335
|
private findPlayersKey;
|
|
313
336
|
private startSyncLoop;
|
|
337
|
+
private startInterpolationLoop;
|
|
338
|
+
private stopInterpolationLoop;
|
|
339
|
+
private processJitterQueue;
|
|
314
340
|
private stopSyncLoop;
|
|
315
341
|
private syncMyState;
|
|
316
342
|
private updateInterpolation;
|
|
343
|
+
private lerpState;
|
|
317
344
|
private generateRoomCode;
|
|
318
345
|
private getHeaders;
|
|
319
346
|
}
|
package/dist/index.js
CHANGED
|
@@ -288,15 +288,29 @@ var Sync = class {
|
|
|
288
288
|
this._roomId = null;
|
|
289
289
|
this.ws = null;
|
|
290
290
|
this.syncInterval = null;
|
|
291
|
+
this.interpolationInterval = null;
|
|
291
292
|
this.lastSentState = "";
|
|
292
|
-
this.interpolationTargets = /* @__PURE__ */ new Map();
|
|
293
293
|
this.listeners = /* @__PURE__ */ new Map();
|
|
294
|
+
// Snapshot-based interpolation: store timestamped snapshots per player
|
|
295
|
+
this.snapshots = /* @__PURE__ */ new Map();
|
|
296
|
+
// Jitter buffer: queue incoming updates before applying
|
|
297
|
+
this.jitterQueue = [];
|
|
298
|
+
// Auto-reconnect state
|
|
299
|
+
this.reconnectAttempts = 0;
|
|
300
|
+
this.reconnectTimeout = null;
|
|
301
|
+
this.lastJoinOptions = void 0;
|
|
302
|
+
this.isReconnecting = false;
|
|
294
303
|
this.state = state;
|
|
295
304
|
this.myId = config.playerId;
|
|
296
305
|
this.config = config;
|
|
297
306
|
this.options = {
|
|
298
307
|
tickRate: options?.tickRate ?? 20,
|
|
299
|
-
interpolate: options?.interpolate ?? true
|
|
308
|
+
interpolate: options?.interpolate ?? true,
|
|
309
|
+
interpolationDelay: options?.interpolationDelay ?? 100,
|
|
310
|
+
jitterBuffer: options?.jitterBuffer ?? 0,
|
|
311
|
+
// 0 = immediate, set to 50+ for smoothing
|
|
312
|
+
autoReconnect: options?.autoReconnect ?? true,
|
|
313
|
+
maxReconnectAttempts: options?.maxReconnectAttempts ?? 10
|
|
300
314
|
};
|
|
301
315
|
}
|
|
302
316
|
/** Current room ID (null if not in a room) */
|
|
@@ -314,25 +328,46 @@ var Sync = class {
|
|
|
314
328
|
* @param options - Join options
|
|
315
329
|
*/
|
|
316
330
|
async join(roomId, options) {
|
|
317
|
-
if (this._roomId) {
|
|
331
|
+
if (this._roomId && !this.isReconnecting) {
|
|
318
332
|
await this.leave();
|
|
319
333
|
}
|
|
320
334
|
this._roomId = roomId;
|
|
335
|
+
this.lastJoinOptions = options;
|
|
336
|
+
this.reconnectAttempts = 0;
|
|
321
337
|
await this.connectWebSocket(roomId, options);
|
|
322
338
|
this.startSyncLoop();
|
|
339
|
+
this.startInterpolationLoop();
|
|
323
340
|
}
|
|
324
341
|
/**
|
|
325
342
|
* Leave the current room
|
|
326
343
|
*/
|
|
327
344
|
async leave() {
|
|
345
|
+
if (this.reconnectTimeout) {
|
|
346
|
+
clearTimeout(this.reconnectTimeout);
|
|
347
|
+
this.reconnectTimeout = null;
|
|
348
|
+
}
|
|
349
|
+
this.isReconnecting = false;
|
|
328
350
|
this.stopSyncLoop();
|
|
351
|
+
this.stopInterpolationLoop();
|
|
329
352
|
if (this.ws) {
|
|
330
353
|
this.ws.close();
|
|
331
354
|
this.ws = null;
|
|
332
355
|
}
|
|
333
356
|
this.clearRemotePlayers();
|
|
357
|
+
this.snapshots.clear();
|
|
358
|
+
this.jitterQueue = [];
|
|
334
359
|
this._roomId = null;
|
|
335
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* Send a one-off message to all players in the room
|
|
363
|
+
*
|
|
364
|
+
* @param data - Any JSON-serializable data
|
|
365
|
+
*/
|
|
366
|
+
broadcast(data) {
|
|
367
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
368
|
+
this.ws.send(JSON.stringify({ type: "broadcast", data }));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
336
371
|
/**
|
|
337
372
|
* Create a new room and join it
|
|
338
373
|
*
|
|
@@ -414,6 +449,9 @@ var Sync = class {
|
|
|
414
449
|
this.ws.onclose = () => {
|
|
415
450
|
this.stopSyncLoop();
|
|
416
451
|
this.emit("disconnected");
|
|
452
|
+
if (this.options.autoReconnect && this._roomId && !this.isReconnecting) {
|
|
453
|
+
this.attemptReconnect();
|
|
454
|
+
}
|
|
417
455
|
};
|
|
418
456
|
this.ws.onmessage = (event) => {
|
|
419
457
|
try {
|
|
@@ -440,6 +478,9 @@ var Sync = class {
|
|
|
440
478
|
this.removePlayer(data.playerId);
|
|
441
479
|
this.emit("leave", data.playerId);
|
|
442
480
|
break;
|
|
481
|
+
case "message":
|
|
482
|
+
this.emit("message", data.from, data.data);
|
|
483
|
+
break;
|
|
443
484
|
}
|
|
444
485
|
}
|
|
445
486
|
applyFullState(fullState) {
|
|
@@ -450,15 +491,43 @@ var Sync = class {
|
|
|
450
491
|
}
|
|
451
492
|
}
|
|
452
493
|
applyPlayerState(playerId, playerState) {
|
|
494
|
+
if (this.options.interpolate && this.options.jitterBuffer > 0) {
|
|
495
|
+
this.jitterQueue.push({
|
|
496
|
+
deliverAt: Date.now() + this.options.jitterBuffer,
|
|
497
|
+
playerId,
|
|
498
|
+
state: { ...playerState }
|
|
499
|
+
});
|
|
500
|
+
} else if (this.options.interpolate) {
|
|
501
|
+
this.addSnapshot(playerId, playerState);
|
|
502
|
+
} else {
|
|
503
|
+
this.applyStateDirect(playerId, playerState);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
addSnapshot(playerId, playerState) {
|
|
507
|
+
const isNewPlayer = !this.snapshots.has(playerId);
|
|
508
|
+
if (isNewPlayer) {
|
|
509
|
+
this.snapshots.set(playerId, []);
|
|
510
|
+
}
|
|
511
|
+
const playerSnapshots = this.snapshots.get(playerId);
|
|
512
|
+
playerSnapshots.push({
|
|
513
|
+
time: Date.now(),
|
|
514
|
+
state: { ...playerState }
|
|
515
|
+
});
|
|
516
|
+
while (playerSnapshots.length > 10) {
|
|
517
|
+
playerSnapshots.shift();
|
|
518
|
+
}
|
|
519
|
+
const playersKey = this.findPlayersKey();
|
|
520
|
+
if (playersKey) {
|
|
521
|
+
const players = this.state[playersKey];
|
|
522
|
+
if (isNewPlayer || !players[playerId]) {
|
|
523
|
+
players[playerId] = { ...playerState };
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
applyStateDirect(playerId, playerState) {
|
|
453
528
|
const playersKey = this.findPlayersKey();
|
|
454
529
|
if (!playersKey) return;
|
|
455
530
|
const players = this.state[playersKey];
|
|
456
|
-
if (this.options.interpolate && players[playerId]) {
|
|
457
|
-
this.interpolationTargets.set(playerId, {
|
|
458
|
-
target: { ...playerState },
|
|
459
|
-
current: { ...players[playerId] }
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
531
|
players[playerId] = playerState;
|
|
463
532
|
}
|
|
464
533
|
removePlayer(playerId) {
|
|
@@ -466,7 +535,28 @@ var Sync = class {
|
|
|
466
535
|
if (!playersKey) return;
|
|
467
536
|
const players = this.state[playersKey];
|
|
468
537
|
delete players[playerId];
|
|
469
|
-
this.
|
|
538
|
+
this.snapshots.delete(playerId);
|
|
539
|
+
}
|
|
540
|
+
attemptReconnect() {
|
|
541
|
+
if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
|
|
542
|
+
this.emit("error", new Error("Max reconnection attempts reached"));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
this.isReconnecting = true;
|
|
546
|
+
this.reconnectAttempts++;
|
|
547
|
+
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 3e4);
|
|
548
|
+
this.emit("reconnecting", { attempt: this.reconnectAttempts, delay });
|
|
549
|
+
this.reconnectTimeout = setTimeout(async () => {
|
|
550
|
+
try {
|
|
551
|
+
await this.connectWebSocket(this._roomId, this.lastJoinOptions);
|
|
552
|
+
this.startSyncLoop();
|
|
553
|
+
this.isReconnecting = false;
|
|
554
|
+
this.reconnectAttempts = 0;
|
|
555
|
+
this.emit("reconnected");
|
|
556
|
+
} catch (e) {
|
|
557
|
+
this.isReconnecting = false;
|
|
558
|
+
}
|
|
559
|
+
}, delay);
|
|
470
560
|
}
|
|
471
561
|
clearRemotePlayers() {
|
|
472
562
|
const playersKey = this.findPlayersKey();
|
|
@@ -477,7 +567,8 @@ var Sync = class {
|
|
|
477
567
|
delete players[playerId];
|
|
478
568
|
}
|
|
479
569
|
}
|
|
480
|
-
this.
|
|
570
|
+
this.snapshots.clear();
|
|
571
|
+
this.jitterQueue = [];
|
|
481
572
|
}
|
|
482
573
|
findPlayersKey() {
|
|
483
574
|
const candidates = ["players", "entities", "gnomes", "users", "clients"];
|
|
@@ -498,11 +589,30 @@ var Sync = class {
|
|
|
498
589
|
const intervalMs = 1e3 / this.options.tickRate;
|
|
499
590
|
this.syncInterval = setInterval(() => {
|
|
500
591
|
this.syncMyState();
|
|
501
|
-
if (this.options.interpolate) {
|
|
502
|
-
this.updateInterpolation();
|
|
503
|
-
}
|
|
504
592
|
}, intervalMs);
|
|
505
593
|
}
|
|
594
|
+
startInterpolationLoop() {
|
|
595
|
+
if (this.interpolationInterval) return;
|
|
596
|
+
if (!this.options.interpolate) return;
|
|
597
|
+
this.interpolationInterval = setInterval(() => {
|
|
598
|
+
this.processJitterQueue();
|
|
599
|
+
this.updateInterpolation();
|
|
600
|
+
}, 16);
|
|
601
|
+
}
|
|
602
|
+
stopInterpolationLoop() {
|
|
603
|
+
if (this.interpolationInterval) {
|
|
604
|
+
clearInterval(this.interpolationInterval);
|
|
605
|
+
this.interpolationInterval = null;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
processJitterQueue() {
|
|
609
|
+
const now = Date.now();
|
|
610
|
+
const ready = this.jitterQueue.filter((item) => item.deliverAt <= now);
|
|
611
|
+
this.jitterQueue = this.jitterQueue.filter((item) => item.deliverAt > now);
|
|
612
|
+
for (const item of ready) {
|
|
613
|
+
this.addSnapshot(item.playerId, item.state);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
506
616
|
stopSyncLoop() {
|
|
507
617
|
if (this.syncInterval) {
|
|
508
618
|
clearInterval(this.syncInterval);
|
|
@@ -528,16 +638,41 @@ var Sync = class {
|
|
|
528
638
|
const playersKey = this.findPlayersKey();
|
|
529
639
|
if (!playersKey) return;
|
|
530
640
|
const players = this.state[playersKey];
|
|
531
|
-
const
|
|
532
|
-
for (const [playerId,
|
|
641
|
+
const renderTime = Date.now() - this.options.interpolationDelay;
|
|
642
|
+
for (const [playerId, playerSnapshots] of this.snapshots) {
|
|
643
|
+
if (playerId === this.myId) continue;
|
|
533
644
|
const player = players[playerId];
|
|
534
645
|
if (!player) continue;
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
646
|
+
let before = null;
|
|
647
|
+
let after = null;
|
|
648
|
+
for (const snapshot of playerSnapshots) {
|
|
649
|
+
if (snapshot.time <= renderTime) {
|
|
650
|
+
before = snapshot;
|
|
651
|
+
} else if (!after) {
|
|
652
|
+
after = snapshot;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (before && after) {
|
|
656
|
+
const total = after.time - before.time;
|
|
657
|
+
const elapsed = renderTime - before.time;
|
|
658
|
+
const alpha = total > 0 ? Math.min(1, elapsed / total) : 1;
|
|
659
|
+
this.lerpState(player, before.state, after.state, alpha);
|
|
660
|
+
} else if (before) {
|
|
661
|
+
this.lerpState(player, player, before.state, 0.3);
|
|
662
|
+
} else if (after) {
|
|
663
|
+
this.lerpState(player, player, after.state, 0.3);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
lerpState(target, from, to, alpha) {
|
|
668
|
+
for (const key of Object.keys(to)) {
|
|
669
|
+
const fromVal = from[key];
|
|
670
|
+
const toVal = to[key];
|
|
671
|
+
if (typeof fromVal === "number" && typeof toVal === "number") {
|
|
672
|
+
target[key] = fromVal + (toVal - fromVal) * alpha;
|
|
673
|
+
} else {
|
|
674
|
+
if (alpha > 0.5) {
|
|
675
|
+
target[key] = toVal;
|
|
541
676
|
}
|
|
542
677
|
}
|
|
543
678
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -261,15 +261,29 @@ var Sync = class {
|
|
|
261
261
|
this._roomId = null;
|
|
262
262
|
this.ws = null;
|
|
263
263
|
this.syncInterval = null;
|
|
264
|
+
this.interpolationInterval = null;
|
|
264
265
|
this.lastSentState = "";
|
|
265
|
-
this.interpolationTargets = /* @__PURE__ */ new Map();
|
|
266
266
|
this.listeners = /* @__PURE__ */ new Map();
|
|
267
|
+
// Snapshot-based interpolation: store timestamped snapshots per player
|
|
268
|
+
this.snapshots = /* @__PURE__ */ new Map();
|
|
269
|
+
// Jitter buffer: queue incoming updates before applying
|
|
270
|
+
this.jitterQueue = [];
|
|
271
|
+
// Auto-reconnect state
|
|
272
|
+
this.reconnectAttempts = 0;
|
|
273
|
+
this.reconnectTimeout = null;
|
|
274
|
+
this.lastJoinOptions = void 0;
|
|
275
|
+
this.isReconnecting = false;
|
|
267
276
|
this.state = state;
|
|
268
277
|
this.myId = config.playerId;
|
|
269
278
|
this.config = config;
|
|
270
279
|
this.options = {
|
|
271
280
|
tickRate: options?.tickRate ?? 20,
|
|
272
|
-
interpolate: options?.interpolate ?? true
|
|
281
|
+
interpolate: options?.interpolate ?? true,
|
|
282
|
+
interpolationDelay: options?.interpolationDelay ?? 100,
|
|
283
|
+
jitterBuffer: options?.jitterBuffer ?? 0,
|
|
284
|
+
// 0 = immediate, set to 50+ for smoothing
|
|
285
|
+
autoReconnect: options?.autoReconnect ?? true,
|
|
286
|
+
maxReconnectAttempts: options?.maxReconnectAttempts ?? 10
|
|
273
287
|
};
|
|
274
288
|
}
|
|
275
289
|
/** Current room ID (null if not in a room) */
|
|
@@ -287,25 +301,46 @@ var Sync = class {
|
|
|
287
301
|
* @param options - Join options
|
|
288
302
|
*/
|
|
289
303
|
async join(roomId, options) {
|
|
290
|
-
if (this._roomId) {
|
|
304
|
+
if (this._roomId && !this.isReconnecting) {
|
|
291
305
|
await this.leave();
|
|
292
306
|
}
|
|
293
307
|
this._roomId = roomId;
|
|
308
|
+
this.lastJoinOptions = options;
|
|
309
|
+
this.reconnectAttempts = 0;
|
|
294
310
|
await this.connectWebSocket(roomId, options);
|
|
295
311
|
this.startSyncLoop();
|
|
312
|
+
this.startInterpolationLoop();
|
|
296
313
|
}
|
|
297
314
|
/**
|
|
298
315
|
* Leave the current room
|
|
299
316
|
*/
|
|
300
317
|
async leave() {
|
|
318
|
+
if (this.reconnectTimeout) {
|
|
319
|
+
clearTimeout(this.reconnectTimeout);
|
|
320
|
+
this.reconnectTimeout = null;
|
|
321
|
+
}
|
|
322
|
+
this.isReconnecting = false;
|
|
301
323
|
this.stopSyncLoop();
|
|
324
|
+
this.stopInterpolationLoop();
|
|
302
325
|
if (this.ws) {
|
|
303
326
|
this.ws.close();
|
|
304
327
|
this.ws = null;
|
|
305
328
|
}
|
|
306
329
|
this.clearRemotePlayers();
|
|
330
|
+
this.snapshots.clear();
|
|
331
|
+
this.jitterQueue = [];
|
|
307
332
|
this._roomId = null;
|
|
308
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Send a one-off message to all players in the room
|
|
336
|
+
*
|
|
337
|
+
* @param data - Any JSON-serializable data
|
|
338
|
+
*/
|
|
339
|
+
broadcast(data) {
|
|
340
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
341
|
+
this.ws.send(JSON.stringify({ type: "broadcast", data }));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
309
344
|
/**
|
|
310
345
|
* Create a new room and join it
|
|
311
346
|
*
|
|
@@ -387,6 +422,9 @@ var Sync = class {
|
|
|
387
422
|
this.ws.onclose = () => {
|
|
388
423
|
this.stopSyncLoop();
|
|
389
424
|
this.emit("disconnected");
|
|
425
|
+
if (this.options.autoReconnect && this._roomId && !this.isReconnecting) {
|
|
426
|
+
this.attemptReconnect();
|
|
427
|
+
}
|
|
390
428
|
};
|
|
391
429
|
this.ws.onmessage = (event) => {
|
|
392
430
|
try {
|
|
@@ -413,6 +451,9 @@ var Sync = class {
|
|
|
413
451
|
this.removePlayer(data.playerId);
|
|
414
452
|
this.emit("leave", data.playerId);
|
|
415
453
|
break;
|
|
454
|
+
case "message":
|
|
455
|
+
this.emit("message", data.from, data.data);
|
|
456
|
+
break;
|
|
416
457
|
}
|
|
417
458
|
}
|
|
418
459
|
applyFullState(fullState) {
|
|
@@ -423,15 +464,43 @@ var Sync = class {
|
|
|
423
464
|
}
|
|
424
465
|
}
|
|
425
466
|
applyPlayerState(playerId, playerState) {
|
|
467
|
+
if (this.options.interpolate && this.options.jitterBuffer > 0) {
|
|
468
|
+
this.jitterQueue.push({
|
|
469
|
+
deliverAt: Date.now() + this.options.jitterBuffer,
|
|
470
|
+
playerId,
|
|
471
|
+
state: { ...playerState }
|
|
472
|
+
});
|
|
473
|
+
} else if (this.options.interpolate) {
|
|
474
|
+
this.addSnapshot(playerId, playerState);
|
|
475
|
+
} else {
|
|
476
|
+
this.applyStateDirect(playerId, playerState);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
addSnapshot(playerId, playerState) {
|
|
480
|
+
const isNewPlayer = !this.snapshots.has(playerId);
|
|
481
|
+
if (isNewPlayer) {
|
|
482
|
+
this.snapshots.set(playerId, []);
|
|
483
|
+
}
|
|
484
|
+
const playerSnapshots = this.snapshots.get(playerId);
|
|
485
|
+
playerSnapshots.push({
|
|
486
|
+
time: Date.now(),
|
|
487
|
+
state: { ...playerState }
|
|
488
|
+
});
|
|
489
|
+
while (playerSnapshots.length > 10) {
|
|
490
|
+
playerSnapshots.shift();
|
|
491
|
+
}
|
|
492
|
+
const playersKey = this.findPlayersKey();
|
|
493
|
+
if (playersKey) {
|
|
494
|
+
const players = this.state[playersKey];
|
|
495
|
+
if (isNewPlayer || !players[playerId]) {
|
|
496
|
+
players[playerId] = { ...playerState };
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
applyStateDirect(playerId, playerState) {
|
|
426
501
|
const playersKey = this.findPlayersKey();
|
|
427
502
|
if (!playersKey) return;
|
|
428
503
|
const players = this.state[playersKey];
|
|
429
|
-
if (this.options.interpolate && players[playerId]) {
|
|
430
|
-
this.interpolationTargets.set(playerId, {
|
|
431
|
-
target: { ...playerState },
|
|
432
|
-
current: { ...players[playerId] }
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
504
|
players[playerId] = playerState;
|
|
436
505
|
}
|
|
437
506
|
removePlayer(playerId) {
|
|
@@ -439,7 +508,28 @@ var Sync = class {
|
|
|
439
508
|
if (!playersKey) return;
|
|
440
509
|
const players = this.state[playersKey];
|
|
441
510
|
delete players[playerId];
|
|
442
|
-
this.
|
|
511
|
+
this.snapshots.delete(playerId);
|
|
512
|
+
}
|
|
513
|
+
attemptReconnect() {
|
|
514
|
+
if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
|
|
515
|
+
this.emit("error", new Error("Max reconnection attempts reached"));
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
this.isReconnecting = true;
|
|
519
|
+
this.reconnectAttempts++;
|
|
520
|
+
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 3e4);
|
|
521
|
+
this.emit("reconnecting", { attempt: this.reconnectAttempts, delay });
|
|
522
|
+
this.reconnectTimeout = setTimeout(async () => {
|
|
523
|
+
try {
|
|
524
|
+
await this.connectWebSocket(this._roomId, this.lastJoinOptions);
|
|
525
|
+
this.startSyncLoop();
|
|
526
|
+
this.isReconnecting = false;
|
|
527
|
+
this.reconnectAttempts = 0;
|
|
528
|
+
this.emit("reconnected");
|
|
529
|
+
} catch (e) {
|
|
530
|
+
this.isReconnecting = false;
|
|
531
|
+
}
|
|
532
|
+
}, delay);
|
|
443
533
|
}
|
|
444
534
|
clearRemotePlayers() {
|
|
445
535
|
const playersKey = this.findPlayersKey();
|
|
@@ -450,7 +540,8 @@ var Sync = class {
|
|
|
450
540
|
delete players[playerId];
|
|
451
541
|
}
|
|
452
542
|
}
|
|
453
|
-
this.
|
|
543
|
+
this.snapshots.clear();
|
|
544
|
+
this.jitterQueue = [];
|
|
454
545
|
}
|
|
455
546
|
findPlayersKey() {
|
|
456
547
|
const candidates = ["players", "entities", "gnomes", "users", "clients"];
|
|
@@ -471,11 +562,30 @@ var Sync = class {
|
|
|
471
562
|
const intervalMs = 1e3 / this.options.tickRate;
|
|
472
563
|
this.syncInterval = setInterval(() => {
|
|
473
564
|
this.syncMyState();
|
|
474
|
-
if (this.options.interpolate) {
|
|
475
|
-
this.updateInterpolation();
|
|
476
|
-
}
|
|
477
565
|
}, intervalMs);
|
|
478
566
|
}
|
|
567
|
+
startInterpolationLoop() {
|
|
568
|
+
if (this.interpolationInterval) return;
|
|
569
|
+
if (!this.options.interpolate) return;
|
|
570
|
+
this.interpolationInterval = setInterval(() => {
|
|
571
|
+
this.processJitterQueue();
|
|
572
|
+
this.updateInterpolation();
|
|
573
|
+
}, 16);
|
|
574
|
+
}
|
|
575
|
+
stopInterpolationLoop() {
|
|
576
|
+
if (this.interpolationInterval) {
|
|
577
|
+
clearInterval(this.interpolationInterval);
|
|
578
|
+
this.interpolationInterval = null;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
processJitterQueue() {
|
|
582
|
+
const now = Date.now();
|
|
583
|
+
const ready = this.jitterQueue.filter((item) => item.deliverAt <= now);
|
|
584
|
+
this.jitterQueue = this.jitterQueue.filter((item) => item.deliverAt > now);
|
|
585
|
+
for (const item of ready) {
|
|
586
|
+
this.addSnapshot(item.playerId, item.state);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
479
589
|
stopSyncLoop() {
|
|
480
590
|
if (this.syncInterval) {
|
|
481
591
|
clearInterval(this.syncInterval);
|
|
@@ -501,16 +611,41 @@ var Sync = class {
|
|
|
501
611
|
const playersKey = this.findPlayersKey();
|
|
502
612
|
if (!playersKey) return;
|
|
503
613
|
const players = this.state[playersKey];
|
|
504
|
-
const
|
|
505
|
-
for (const [playerId,
|
|
614
|
+
const renderTime = Date.now() - this.options.interpolationDelay;
|
|
615
|
+
for (const [playerId, playerSnapshots] of this.snapshots) {
|
|
616
|
+
if (playerId === this.myId) continue;
|
|
506
617
|
const player = players[playerId];
|
|
507
618
|
if (!player) continue;
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
619
|
+
let before = null;
|
|
620
|
+
let after = null;
|
|
621
|
+
for (const snapshot of playerSnapshots) {
|
|
622
|
+
if (snapshot.time <= renderTime) {
|
|
623
|
+
before = snapshot;
|
|
624
|
+
} else if (!after) {
|
|
625
|
+
after = snapshot;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (before && after) {
|
|
629
|
+
const total = after.time - before.time;
|
|
630
|
+
const elapsed = renderTime - before.time;
|
|
631
|
+
const alpha = total > 0 ? Math.min(1, elapsed / total) : 1;
|
|
632
|
+
this.lerpState(player, before.state, after.state, alpha);
|
|
633
|
+
} else if (before) {
|
|
634
|
+
this.lerpState(player, player, before.state, 0.3);
|
|
635
|
+
} else if (after) {
|
|
636
|
+
this.lerpState(player, player, after.state, 0.3);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
lerpState(target, from, to, alpha) {
|
|
641
|
+
for (const key of Object.keys(to)) {
|
|
642
|
+
const fromVal = from[key];
|
|
643
|
+
const toVal = to[key];
|
|
644
|
+
if (typeof fromVal === "number" && typeof toVal === "number") {
|
|
645
|
+
target[key] = fromVal + (toVal - fromVal) * alpha;
|
|
646
|
+
} else {
|
|
647
|
+
if (alpha > 0.5) {
|
|
648
|
+
target[key] = toVal;
|
|
514
649
|
}
|
|
515
650
|
}
|
|
516
651
|
}
|