@watchtower-sdk/core 0.2.2 → 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 +23 -2
- package/dist/index.d.ts +23 -2
- package/dist/index.js +144 -22
- package/dist/index.mjs +144 -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
|
|
@@ -303,7 +317,7 @@ declare class Sync<T extends Record<string, unknown>> {
|
|
|
303
317
|
/**
|
|
304
318
|
* Subscribe to sync events
|
|
305
319
|
*/
|
|
306
|
-
on(event: 'join' | 'leave' | 'error' | 'connected' | 'disconnected' | 'message', callback: Function): void;
|
|
320
|
+
on(event: 'join' | 'leave' | 'error' | 'connected' | 'disconnected' | 'reconnecting' | 'reconnected' | 'message', callback: Function): void;
|
|
307
321
|
/**
|
|
308
322
|
* Unsubscribe from sync events
|
|
309
323
|
*/
|
|
@@ -313,13 +327,20 @@ declare class Sync<T extends Record<string, unknown>> {
|
|
|
313
327
|
private handleMessage;
|
|
314
328
|
private applyFullState;
|
|
315
329
|
private applyPlayerState;
|
|
330
|
+
private addSnapshot;
|
|
331
|
+
private applyStateDirect;
|
|
316
332
|
private removePlayer;
|
|
333
|
+
private attemptReconnect;
|
|
317
334
|
private clearRemotePlayers;
|
|
318
335
|
private findPlayersKey;
|
|
319
336
|
private startSyncLoop;
|
|
337
|
+
private startInterpolationLoop;
|
|
338
|
+
private stopInterpolationLoop;
|
|
339
|
+
private processJitterQueue;
|
|
320
340
|
private stopSyncLoop;
|
|
321
341
|
private syncMyState;
|
|
322
342
|
private updateInterpolation;
|
|
343
|
+
private lerpState;
|
|
323
344
|
private generateRoomCode;
|
|
324
345
|
private getHeaders;
|
|
325
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
|
|
@@ -303,7 +317,7 @@ declare class Sync<T extends Record<string, unknown>> {
|
|
|
303
317
|
/**
|
|
304
318
|
* Subscribe to sync events
|
|
305
319
|
*/
|
|
306
|
-
on(event: 'join' | 'leave' | 'error' | 'connected' | 'disconnected' | 'message', callback: Function): void;
|
|
320
|
+
on(event: 'join' | 'leave' | 'error' | 'connected' | 'disconnected' | 'reconnecting' | 'reconnected' | 'message', callback: Function): void;
|
|
307
321
|
/**
|
|
308
322
|
* Unsubscribe from sync events
|
|
309
323
|
*/
|
|
@@ -313,13 +327,20 @@ declare class Sync<T extends Record<string, unknown>> {
|
|
|
313
327
|
private handleMessage;
|
|
314
328
|
private applyFullState;
|
|
315
329
|
private applyPlayerState;
|
|
330
|
+
private addSnapshot;
|
|
331
|
+
private applyStateDirect;
|
|
316
332
|
private removePlayer;
|
|
333
|
+
private attemptReconnect;
|
|
317
334
|
private clearRemotePlayers;
|
|
318
335
|
private findPlayersKey;
|
|
319
336
|
private startSyncLoop;
|
|
337
|
+
private startInterpolationLoop;
|
|
338
|
+
private stopInterpolationLoop;
|
|
339
|
+
private processJitterQueue;
|
|
320
340
|
private stopSyncLoop;
|
|
321
341
|
private syncMyState;
|
|
322
342
|
private updateInterpolation;
|
|
343
|
+
private lerpState;
|
|
323
344
|
private generateRoomCode;
|
|
324
345
|
private getHeaders;
|
|
325
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,23 +328,34 @@ 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
|
}
|
|
336
361
|
/**
|
|
@@ -424,6 +449,9 @@ var Sync = class {
|
|
|
424
449
|
this.ws.onclose = () => {
|
|
425
450
|
this.stopSyncLoop();
|
|
426
451
|
this.emit("disconnected");
|
|
452
|
+
if (this.options.autoReconnect && this._roomId && !this.isReconnecting) {
|
|
453
|
+
this.attemptReconnect();
|
|
454
|
+
}
|
|
427
455
|
};
|
|
428
456
|
this.ws.onmessage = (event) => {
|
|
429
457
|
try {
|
|
@@ -463,15 +491,43 @@ var Sync = class {
|
|
|
463
491
|
}
|
|
464
492
|
}
|
|
465
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) {
|
|
466
528
|
const playersKey = this.findPlayersKey();
|
|
467
529
|
if (!playersKey) return;
|
|
468
530
|
const players = this.state[playersKey];
|
|
469
|
-
if (this.options.interpolate && players[playerId]) {
|
|
470
|
-
this.interpolationTargets.set(playerId, {
|
|
471
|
-
target: { ...playerState },
|
|
472
|
-
current: { ...players[playerId] }
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
531
|
players[playerId] = playerState;
|
|
476
532
|
}
|
|
477
533
|
removePlayer(playerId) {
|
|
@@ -479,7 +535,28 @@ var Sync = class {
|
|
|
479
535
|
if (!playersKey) return;
|
|
480
536
|
const players = this.state[playersKey];
|
|
481
537
|
delete players[playerId];
|
|
482
|
-
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);
|
|
483
560
|
}
|
|
484
561
|
clearRemotePlayers() {
|
|
485
562
|
const playersKey = this.findPlayersKey();
|
|
@@ -490,7 +567,8 @@ var Sync = class {
|
|
|
490
567
|
delete players[playerId];
|
|
491
568
|
}
|
|
492
569
|
}
|
|
493
|
-
this.
|
|
570
|
+
this.snapshots.clear();
|
|
571
|
+
this.jitterQueue = [];
|
|
494
572
|
}
|
|
495
573
|
findPlayersKey() {
|
|
496
574
|
const candidates = ["players", "entities", "gnomes", "users", "clients"];
|
|
@@ -511,11 +589,30 @@ var Sync = class {
|
|
|
511
589
|
const intervalMs = 1e3 / this.options.tickRate;
|
|
512
590
|
this.syncInterval = setInterval(() => {
|
|
513
591
|
this.syncMyState();
|
|
514
|
-
if (this.options.interpolate) {
|
|
515
|
-
this.updateInterpolation();
|
|
516
|
-
}
|
|
517
592
|
}, intervalMs);
|
|
518
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
|
+
}
|
|
519
616
|
stopSyncLoop() {
|
|
520
617
|
if (this.syncInterval) {
|
|
521
618
|
clearInterval(this.syncInterval);
|
|
@@ -541,16 +638,41 @@ var Sync = class {
|
|
|
541
638
|
const playersKey = this.findPlayersKey();
|
|
542
639
|
if (!playersKey) return;
|
|
543
640
|
const players = this.state[playersKey];
|
|
544
|
-
const
|
|
545
|
-
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;
|
|
546
644
|
const player = players[playerId];
|
|
547
645
|
if (!player) continue;
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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;
|
|
554
676
|
}
|
|
555
677
|
}
|
|
556
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,23 +301,34 @@ 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
|
}
|
|
309
334
|
/**
|
|
@@ -397,6 +422,9 @@ var Sync = class {
|
|
|
397
422
|
this.ws.onclose = () => {
|
|
398
423
|
this.stopSyncLoop();
|
|
399
424
|
this.emit("disconnected");
|
|
425
|
+
if (this.options.autoReconnect && this._roomId && !this.isReconnecting) {
|
|
426
|
+
this.attemptReconnect();
|
|
427
|
+
}
|
|
400
428
|
};
|
|
401
429
|
this.ws.onmessage = (event) => {
|
|
402
430
|
try {
|
|
@@ -436,15 +464,43 @@ var Sync = class {
|
|
|
436
464
|
}
|
|
437
465
|
}
|
|
438
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) {
|
|
439
501
|
const playersKey = this.findPlayersKey();
|
|
440
502
|
if (!playersKey) return;
|
|
441
503
|
const players = this.state[playersKey];
|
|
442
|
-
if (this.options.interpolate && players[playerId]) {
|
|
443
|
-
this.interpolationTargets.set(playerId, {
|
|
444
|
-
target: { ...playerState },
|
|
445
|
-
current: { ...players[playerId] }
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
504
|
players[playerId] = playerState;
|
|
449
505
|
}
|
|
450
506
|
removePlayer(playerId) {
|
|
@@ -452,7 +508,28 @@ var Sync = class {
|
|
|
452
508
|
if (!playersKey) return;
|
|
453
509
|
const players = this.state[playersKey];
|
|
454
510
|
delete players[playerId];
|
|
455
|
-
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);
|
|
456
533
|
}
|
|
457
534
|
clearRemotePlayers() {
|
|
458
535
|
const playersKey = this.findPlayersKey();
|
|
@@ -463,7 +540,8 @@ var Sync = class {
|
|
|
463
540
|
delete players[playerId];
|
|
464
541
|
}
|
|
465
542
|
}
|
|
466
|
-
this.
|
|
543
|
+
this.snapshots.clear();
|
|
544
|
+
this.jitterQueue = [];
|
|
467
545
|
}
|
|
468
546
|
findPlayersKey() {
|
|
469
547
|
const candidates = ["players", "entities", "gnomes", "users", "clients"];
|
|
@@ -484,11 +562,30 @@ var Sync = class {
|
|
|
484
562
|
const intervalMs = 1e3 / this.options.tickRate;
|
|
485
563
|
this.syncInterval = setInterval(() => {
|
|
486
564
|
this.syncMyState();
|
|
487
|
-
if (this.options.interpolate) {
|
|
488
|
-
this.updateInterpolation();
|
|
489
|
-
}
|
|
490
565
|
}, intervalMs);
|
|
491
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
|
+
}
|
|
492
589
|
stopSyncLoop() {
|
|
493
590
|
if (this.syncInterval) {
|
|
494
591
|
clearInterval(this.syncInterval);
|
|
@@ -514,16 +611,41 @@ var Sync = class {
|
|
|
514
611
|
const playersKey = this.findPlayersKey();
|
|
515
612
|
if (!playersKey) return;
|
|
516
613
|
const players = this.state[playersKey];
|
|
517
|
-
const
|
|
518
|
-
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;
|
|
519
617
|
const player = players[playerId];
|
|
520
618
|
if (!player) continue;
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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;
|
|
527
649
|
}
|
|
528
650
|
}
|
|
529
651
|
}
|