rollback-netcode 0.0.4
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/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/debug.d.ts +29 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +56 -0
- package/dist/debug.js.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/encoding.d.ts +80 -0
- package/dist/protocol/encoding.d.ts.map +1 -0
- package/dist/protocol/encoding.js +992 -0
- package/dist/protocol/encoding.js.map +1 -0
- package/dist/protocol/messages.d.ts +271 -0
- package/dist/protocol/messages.d.ts.map +1 -0
- package/dist/protocol/messages.js +114 -0
- package/dist/protocol/messages.js.map +1 -0
- package/dist/rollback/engine.d.ts +261 -0
- package/dist/rollback/engine.d.ts.map +1 -0
- package/dist/rollback/engine.js +543 -0
- package/dist/rollback/engine.js.map +1 -0
- package/dist/rollback/input-buffer.d.ts +225 -0
- package/dist/rollback/input-buffer.d.ts.map +1 -0
- package/dist/rollback/input-buffer.js +483 -0
- package/dist/rollback/input-buffer.js.map +1 -0
- package/dist/rollback/snapshot-buffer.d.ts +119 -0
- package/dist/rollback/snapshot-buffer.d.ts.map +1 -0
- package/dist/rollback/snapshot-buffer.js +256 -0
- package/dist/rollback/snapshot-buffer.js.map +1 -0
- package/dist/session/desync-manager.d.ts +106 -0
- package/dist/session/desync-manager.d.ts.map +1 -0
- package/dist/session/desync-manager.js +136 -0
- package/dist/session/desync-manager.js.map +1 -0
- package/dist/session/lag-monitor.d.ts +69 -0
- package/dist/session/lag-monitor.d.ts.map +1 -0
- package/dist/session/lag-monitor.js +74 -0
- package/dist/session/lag-monitor.js.map +1 -0
- package/dist/session/message-builders.d.ts +86 -0
- package/dist/session/message-builders.d.ts.map +1 -0
- package/dist/session/message-builders.js +199 -0
- package/dist/session/message-builders.js.map +1 -0
- package/dist/session/message-router.d.ts +61 -0
- package/dist/session/message-router.d.ts.map +1 -0
- package/dist/session/message-router.js +105 -0
- package/dist/session/message-router.js.map +1 -0
- package/dist/session/player-manager.d.ts +100 -0
- package/dist/session/player-manager.d.ts.map +1 -0
- package/dist/session/player-manager.js +160 -0
- package/dist/session/player-manager.js.map +1 -0
- package/dist/session/session.d.ts +379 -0
- package/dist/session/session.d.ts.map +1 -0
- package/dist/session/session.js +1294 -0
- package/dist/session/session.js.map +1 -0
- package/dist/session/topology.d.ts +66 -0
- package/dist/session/topology.d.ts.map +1 -0
- package/dist/session/topology.js +72 -0
- package/dist/session/topology.js.map +1 -0
- package/dist/transport/adapter.d.ts +99 -0
- package/dist/transport/adapter.d.ts.map +1 -0
- package/dist/transport/adapter.js +8 -0
- package/dist/transport/adapter.js.map +1 -0
- package/dist/transport/local.d.ts +192 -0
- package/dist/transport/local.d.ts.map +1 -0
- package/dist/transport/local.js +435 -0
- package/dist/transport/local.js.map +1 -0
- package/dist/transport/transforming.d.ts +177 -0
- package/dist/transport/transforming.d.ts.map +1 -0
- package/dist/transport/transforming.js +407 -0
- package/dist/transport/transforming.js.map +1 -0
- package/dist/transport/webrtc.d.ts +285 -0
- package/dist/transport/webrtc.d.ts.map +1 -0
- package/dist/transport/webrtc.js +734 -0
- package/dist/transport/webrtc.js.map +1 -0
- package/dist/types.d.ts +394 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +256 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +59 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +93 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ring buffer for storing game state snapshots.
|
|
3
|
+
*
|
|
4
|
+
* Used for rollback: when a misprediction is detected, the game state
|
|
5
|
+
* is restored from a snapshot and resimulated forward.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* A ring buffer that stores game state snapshots for rollback.
|
|
9
|
+
*
|
|
10
|
+
* Snapshots are stored at specific ticks and can be retrieved by tick.
|
|
11
|
+
* When the buffer is full, the oldest snapshot is evicted.
|
|
12
|
+
* Uses a tick-to-index map for O(1) lookup performance.
|
|
13
|
+
*
|
|
14
|
+
* INVARIANT: Snapshots must be saved in ascending tick order. This is required
|
|
15
|
+
* for O(log n) binary search in getAtOrBefore(). The invariant is maintained by:
|
|
16
|
+
* - Normal simulation: ticks advance sequentially (0, 1, 2, ...)
|
|
17
|
+
* - Resimulation: existing ticks are updated in-place, preserving order
|
|
18
|
+
*
|
|
19
|
+
* Saving a non-existing tick out of order will corrupt the sort order and
|
|
20
|
+
* cause getAtOrBefore() to return incorrect results.
|
|
21
|
+
*/
|
|
22
|
+
export class SnapshotBuffer {
|
|
23
|
+
capacity;
|
|
24
|
+
buffer;
|
|
25
|
+
tickToIndex = new Map();
|
|
26
|
+
head = 0; // Next write position
|
|
27
|
+
count = 0;
|
|
28
|
+
_oldestTick;
|
|
29
|
+
_newestTick;
|
|
30
|
+
/**
|
|
31
|
+
* Create a new snapshot buffer.
|
|
32
|
+
*
|
|
33
|
+
* @param capacity - Maximum number of snapshots to store
|
|
34
|
+
*/
|
|
35
|
+
constructor(capacity) {
|
|
36
|
+
this.capacity = capacity;
|
|
37
|
+
if (capacity <= 0) {
|
|
38
|
+
throw new Error("Capacity must be positive");
|
|
39
|
+
}
|
|
40
|
+
this.buffer = new Array(capacity);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Number of snapshots currently stored.
|
|
44
|
+
*/
|
|
45
|
+
get size() {
|
|
46
|
+
return this.count;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The oldest tick in the buffer, or undefined if empty.
|
|
50
|
+
*/
|
|
51
|
+
get oldestTick() {
|
|
52
|
+
return this._oldestTick;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The newest tick in the buffer, or undefined if empty.
|
|
56
|
+
*/
|
|
57
|
+
get newestTick() {
|
|
58
|
+
return this._newestTick;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Save a snapshot at a specific tick.
|
|
62
|
+
*
|
|
63
|
+
* If the tick already exists, updates in-place to maintain sort order.
|
|
64
|
+
* If the buffer is full, the oldest snapshot is evicted.
|
|
65
|
+
* The state is copied to prevent external mutation.
|
|
66
|
+
*
|
|
67
|
+
* PRECONDITION: New ticks (not already in buffer) must be >= all existing ticks.
|
|
68
|
+
* Saving an out-of-order new tick corrupts sort order. See class invariant.
|
|
69
|
+
*
|
|
70
|
+
* @param tick - The tick this snapshot was taken at
|
|
71
|
+
* @param state - The serialized game state
|
|
72
|
+
* @param hash - The hash of the game state
|
|
73
|
+
*/
|
|
74
|
+
save(tick, state, hash) {
|
|
75
|
+
// Copy the state to prevent external mutation
|
|
76
|
+
const stateCopy = new Uint8Array(state.length);
|
|
77
|
+
stateCopy.set(state);
|
|
78
|
+
const snapshot = {
|
|
79
|
+
tick,
|
|
80
|
+
state: stateCopy,
|
|
81
|
+
hash,
|
|
82
|
+
};
|
|
83
|
+
// Check if this tick already exists - update in-place to maintain order
|
|
84
|
+
const existingIdx = this.tickToIndex.get(tick);
|
|
85
|
+
if (existingIdx !== undefined) {
|
|
86
|
+
this.buffer[existingIdx] = snapshot;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Warn on invariant violation: new ticks must be >= newest for binary search
|
|
90
|
+
if (this._newestTick !== undefined && tick < this._newestTick) {
|
|
91
|
+
console.error(`SnapshotBuffer: tick ${tick} is out of order (newest: ${this._newestTick}). This will corrupt binary search in getAtOrBefore().`);
|
|
92
|
+
}
|
|
93
|
+
// New tick - append or evict oldest
|
|
94
|
+
if (this.count === this.capacity) {
|
|
95
|
+
// Remove old tick from index map
|
|
96
|
+
const oldSnapshot = this.buffer[this.head];
|
|
97
|
+
if (oldSnapshot) {
|
|
98
|
+
this.tickToIndex.delete(oldSnapshot.tick);
|
|
99
|
+
}
|
|
100
|
+
// The slot at head contains the oldest snapshot
|
|
101
|
+
this.buffer[this.head] = snapshot;
|
|
102
|
+
this.tickToIndex.set(tick, this.head);
|
|
103
|
+
this.head = (this.head + 1) % this.capacity;
|
|
104
|
+
// Update oldest tick to the new oldest
|
|
105
|
+
const oldestSnapshot = this.buffer[this.head];
|
|
106
|
+
this._oldestTick = oldestSnapshot?.tick;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// Buffer not full, just append
|
|
110
|
+
const writePos = (this.head + this.count) % this.capacity;
|
|
111
|
+
this.buffer[writePos] = snapshot;
|
|
112
|
+
this.tickToIndex.set(tick, writePos);
|
|
113
|
+
this.count++;
|
|
114
|
+
if (this.count === 1) {
|
|
115
|
+
this._oldestTick = tick;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
this._newestTick = tick;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Get a snapshot at a specific tick.
|
|
122
|
+
* Uses O(1) map lookup for performance.
|
|
123
|
+
*
|
|
124
|
+
* @param tick - The tick to retrieve
|
|
125
|
+
* @returns The snapshot at that tick, or undefined if not found
|
|
126
|
+
*/
|
|
127
|
+
get(tick) {
|
|
128
|
+
const idx = this.tickToIndex.get(tick);
|
|
129
|
+
if (idx === undefined) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
return this.buffer[idx];
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get the oldest snapshot in the buffer.
|
|
136
|
+
*
|
|
137
|
+
* @returns The oldest snapshot, or undefined if empty
|
|
138
|
+
*/
|
|
139
|
+
getOldest() {
|
|
140
|
+
if (this.count === 0) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
return this.buffer[this.head];
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Get the newest snapshot in the buffer.
|
|
147
|
+
*
|
|
148
|
+
* @returns The newest snapshot, or undefined if empty
|
|
149
|
+
*/
|
|
150
|
+
getNewest() {
|
|
151
|
+
if (this.count === 0) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const newestIdx = (this.head + this.count - 1) % this.capacity;
|
|
155
|
+
return this.buffer[newestIdx];
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Clear all snapshots from the buffer.
|
|
159
|
+
*/
|
|
160
|
+
clear() {
|
|
161
|
+
this.buffer.fill(undefined);
|
|
162
|
+
this.tickToIndex.clear();
|
|
163
|
+
this.head = 0;
|
|
164
|
+
this.count = 0;
|
|
165
|
+
this._oldestTick = undefined;
|
|
166
|
+
this._newestTick = undefined;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get the snapshot at or before a given tick.
|
|
170
|
+
* Useful for finding the closest snapshot for rollback.
|
|
171
|
+
*
|
|
172
|
+
* O(log n) binary search. Snapshots are stored in ascending tick order.
|
|
173
|
+
*
|
|
174
|
+
* @param tick - The target tick
|
|
175
|
+
* @returns The snapshot at or before that tick, or undefined if none exists
|
|
176
|
+
*/
|
|
177
|
+
getAtOrBefore(tick) {
|
|
178
|
+
if (this.count === 0) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
// Binary search to find the rightmost snapshot with tick <= target
|
|
182
|
+
// Logical indices: 0 to count-1, physical: (head + logical) % capacity
|
|
183
|
+
let lo = 0;
|
|
184
|
+
let hi = this.count - 1;
|
|
185
|
+
let result;
|
|
186
|
+
while (lo <= hi) {
|
|
187
|
+
const mid = (lo + hi) >>> 1;
|
|
188
|
+
const idx = (this.head + mid) % this.capacity;
|
|
189
|
+
const snapshot = this.buffer[idx];
|
|
190
|
+
if (snapshot && snapshot.tick <= tick) {
|
|
191
|
+
// This snapshot is valid, but there might be a better one to the right
|
|
192
|
+
result = snapshot;
|
|
193
|
+
lo = mid + 1;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// Snapshot tick is too high, search left
|
|
197
|
+
hi = mid - 1;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Remove all snapshots before a given tick.
|
|
204
|
+
* Used to clean up old snapshots that are no longer needed.
|
|
205
|
+
*
|
|
206
|
+
* @param tick - Remove all snapshots with tick < this value
|
|
207
|
+
*/
|
|
208
|
+
pruneBeforeTick(tick) {
|
|
209
|
+
while (this.count > 0) {
|
|
210
|
+
const oldest = this.buffer[this.head];
|
|
211
|
+
if (oldest && oldest.tick < tick) {
|
|
212
|
+
// Remove from index map
|
|
213
|
+
this.tickToIndex.delete(oldest.tick);
|
|
214
|
+
this.buffer[this.head] = undefined;
|
|
215
|
+
this.head = (this.head + 1) % this.capacity;
|
|
216
|
+
this.count--;
|
|
217
|
+
if (this.count === 0) {
|
|
218
|
+
this._oldestTick = undefined;
|
|
219
|
+
this._newestTick = undefined;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
this._oldestTick = this.buffer[this.head]?.tick;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Check if a snapshot exists at a given tick.
|
|
232
|
+
*
|
|
233
|
+
* @param tick - The tick to check
|
|
234
|
+
* @returns true if a snapshot exists at that tick
|
|
235
|
+
*/
|
|
236
|
+
has(tick) {
|
|
237
|
+
return this.get(tick) !== undefined;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Get all ticks that have snapshots, in order from oldest to newest.
|
|
241
|
+
*
|
|
242
|
+
* @returns Array of ticks with snapshots
|
|
243
|
+
*/
|
|
244
|
+
getTicks() {
|
|
245
|
+
const ticks = [];
|
|
246
|
+
for (let i = 0; i < this.count; i++) {
|
|
247
|
+
const idx = (this.head + i) % this.capacity;
|
|
248
|
+
const snapshot = this.buffer[idx];
|
|
249
|
+
if (snapshot) {
|
|
250
|
+
ticks.push(snapshot.tick);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return ticks;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=snapshot-buffer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-buffer.js","sourceRoot":"","sources":["../../src/rollback/snapshot-buffer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desync detection and management.
|
|
3
|
+
*
|
|
4
|
+
* Supports both host-authority mode (host collects and compares all hashes)
|
|
5
|
+
* and peer mode (each peer compares independently).
|
|
6
|
+
*/
|
|
7
|
+
import { DesyncAuthority, type PlayerId, type Tick, Topology } from "../types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Configuration for the desync manager.
|
|
10
|
+
*/
|
|
11
|
+
export interface DesyncManagerConfig {
|
|
12
|
+
/** Network topology */
|
|
13
|
+
topology: Topology;
|
|
14
|
+
/** Desync authority mode */
|
|
15
|
+
desyncAuthority: DesyncAuthority;
|
|
16
|
+
/** Hash interval for pruning calculations */
|
|
17
|
+
hashInterval: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Result when a desync is detected.
|
|
21
|
+
*/
|
|
22
|
+
export interface DesyncResult {
|
|
23
|
+
/** The tick where desync was detected */
|
|
24
|
+
tick: Tick;
|
|
25
|
+
/** The player who desynced (in host-authority mode) */
|
|
26
|
+
desyncedPlayerId: PlayerId;
|
|
27
|
+
/** The host/reference hash */
|
|
28
|
+
referenceHash: number;
|
|
29
|
+
/** The player's actual hash */
|
|
30
|
+
playerHash: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Result of checking a hash in peer mode.
|
|
34
|
+
* Returned only when a desync is detected (non-null means desync).
|
|
35
|
+
*/
|
|
36
|
+
export interface PeerDesyncResult {
|
|
37
|
+
/** The tick where desync was detected */
|
|
38
|
+
tick: Tick;
|
|
39
|
+
/** Local hash */
|
|
40
|
+
localHash: number;
|
|
41
|
+
/** Remote hash */
|
|
42
|
+
remoteHash: number;
|
|
43
|
+
/** Remote player ID */
|
|
44
|
+
remotePlayerId: PlayerId;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Manages desync detection for both host-authority and peer modes.
|
|
48
|
+
*/
|
|
49
|
+
export declare class DesyncManager {
|
|
50
|
+
private readonly topology;
|
|
51
|
+
private readonly desyncAuthority;
|
|
52
|
+
private readonly hashInterval;
|
|
53
|
+
/** Host-authority mode: hashes received from players, keyed by tick then playerId */
|
|
54
|
+
private readonly receivedHashes;
|
|
55
|
+
constructor(config: DesyncManagerConfig);
|
|
56
|
+
/**
|
|
57
|
+
* Whether this is host-authority mode.
|
|
58
|
+
*/
|
|
59
|
+
get isHostAuthority(): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Record a hash from a player (host-authority mode).
|
|
62
|
+
*
|
|
63
|
+
* @param tick - The tick the hash is for
|
|
64
|
+
* @param playerId - The player who sent the hash
|
|
65
|
+
* @param hash - The hash value
|
|
66
|
+
*/
|
|
67
|
+
recordHash(tick: Tick, playerId: PlayerId, hash: number): void;
|
|
68
|
+
/**
|
|
69
|
+
* Check if we have all hashes for a tick (host-authority mode).
|
|
70
|
+
*
|
|
71
|
+
* @param tick - The tick to check
|
|
72
|
+
* @param expectedPlayerCount - Number of players expected to submit hashes
|
|
73
|
+
* @returns true if all hashes have been received
|
|
74
|
+
*/
|
|
75
|
+
hasAllHashes(tick: Tick, expectedPlayerCount: number): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Check for desyncs at a tick (host-authority mode).
|
|
78
|
+
* Compares all player hashes against the reference (host) hash.
|
|
79
|
+
*
|
|
80
|
+
* @param tick - The tick to check
|
|
81
|
+
* @param hostPlayerId - The host's player ID
|
|
82
|
+
* @returns Array of desync results for any desynced players
|
|
83
|
+
*/
|
|
84
|
+
checkDesyncs(tick: Tick, hostPlayerId: PlayerId): DesyncResult[];
|
|
85
|
+
/**
|
|
86
|
+
* Check for desync in peer mode (comparing local vs remote hash).
|
|
87
|
+
*
|
|
88
|
+
* @param tick - The tick to check
|
|
89
|
+
* @param localHash - Local hash value (or undefined if not available)
|
|
90
|
+
* @param remoteHash - Remote hash value
|
|
91
|
+
* @param remotePlayerId - Remote player's ID
|
|
92
|
+
* @returns Desync result if a desync is detected, null otherwise
|
|
93
|
+
*/
|
|
94
|
+
checkPeerDesync(tick: Tick, localHash: number | undefined, remoteHash: number, remotePlayerId: PlayerId): PeerDesyncResult | null;
|
|
95
|
+
/**
|
|
96
|
+
* Remove old hash entries to prevent memory growth.
|
|
97
|
+
*
|
|
98
|
+
* @param currentTick - Current simulation tick
|
|
99
|
+
*/
|
|
100
|
+
pruneOldHashes(currentTick: Tick): void;
|
|
101
|
+
/**
|
|
102
|
+
* Clear all stored hashes.
|
|
103
|
+
*/
|
|
104
|
+
clear(): void;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=desync-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"desync-manager.d.ts","sourceRoot":"","sources":["../../src/session/desync-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACN,eAAe,EACf,KAAK,QAAQ,EACb,KAAK,IAAI,EACT,QAAQ,EAER,MAAM,aAAa,CAAC;AAErB;;GAEG;AACH,MAAM,WAAW,mBAAmB;IACnC,uBAAuB;IACvB,QAAQ,EAAE,QAAQ,CAAC;IACnB,4BAA4B;IAC5B,eAAe,EAAE,eAAe,CAAC;IACjC,6CAA6C;IAC7C,YAAY,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B,yCAAyC;IACzC,IAAI,EAAE,IAAI,CAAC;IACX,uDAAuD;IACvD,gBAAgB,EAAE,QAAQ,CAAC;IAC3B,8BAA8B;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,+BAA+B;IAC/B,UAAU,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAChC,yCAAyC;IACzC,IAAI,EAAE,IAAI,CAAC;IACX,iBAAiB;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,uBAAuB;IACvB,cAAc,EAAE,QAAQ,CAAC;CACzB;AAED;;GAEG;AACH,qBAAa,aAAa;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IACpC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IAEtC,qFAAqF;IACrF,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA+C;gBAElE,MAAM,EAAE,mBAAmB;IAMvC;;OAEG;IACH,IAAI,eAAe,IAAI,OAAO,CAO7B;IAED;;;;;;OAMG;IACH,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAS9D;;;;;;OAMG;IACH,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,mBAAmB,EAAE,MAAM,GAAG,OAAO;IAQ9D;;;;;;;OAOG;IACH,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,GAAG,YAAY,EAAE;IA6BhE;;;;;;;;OAQG;IACH,eAAe,CACd,IAAI,EAAE,IAAI,EACV,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,QAAQ,GACtB,gBAAgB,GAAG,IAAI;IAiB1B;;;;OAIG;IACH,cAAc,CAAC,WAAW,EAAE,IAAI,GAAG,IAAI;IASvC;;OAEG;IACH,KAAK,IAAI,IAAI;CAGb"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desync detection and management.
|
|
3
|
+
*
|
|
4
|
+
* Supports both host-authority mode (host collects and compares all hashes)
|
|
5
|
+
* and peer mode (each peer compares independently).
|
|
6
|
+
*/
|
|
7
|
+
import { DesyncAuthority, Topology, asTick, } from "../types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Manages desync detection for both host-authority and peer modes.
|
|
10
|
+
*/
|
|
11
|
+
export class DesyncManager {
|
|
12
|
+
topology;
|
|
13
|
+
desyncAuthority;
|
|
14
|
+
hashInterval;
|
|
15
|
+
/** Host-authority mode: hashes received from players, keyed by tick then playerId */
|
|
16
|
+
receivedHashes = new Map();
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.topology = config.topology;
|
|
19
|
+
this.desyncAuthority = config.desyncAuthority;
|
|
20
|
+
this.hashInterval = config.hashInterval;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Whether this is host-authority mode.
|
|
24
|
+
*/
|
|
25
|
+
get isHostAuthority() {
|
|
26
|
+
// Only Mesh + Host authority uses host-authority mode
|
|
27
|
+
// Star topology and Mesh + Peer use peer-based detection
|
|
28
|
+
return (this.topology === Topology.Mesh &&
|
|
29
|
+
this.desyncAuthority === DesyncAuthority.Host);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Record a hash from a player (host-authority mode).
|
|
33
|
+
*
|
|
34
|
+
* @param tick - The tick the hash is for
|
|
35
|
+
* @param playerId - The player who sent the hash
|
|
36
|
+
* @param hash - The hash value
|
|
37
|
+
*/
|
|
38
|
+
recordHash(tick, playerId, hash) {
|
|
39
|
+
let tickHashes = this.receivedHashes.get(tick);
|
|
40
|
+
if (!tickHashes) {
|
|
41
|
+
tickHashes = new Map();
|
|
42
|
+
this.receivedHashes.set(tick, tickHashes);
|
|
43
|
+
}
|
|
44
|
+
tickHashes.set(playerId, hash);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Check if we have all hashes for a tick (host-authority mode).
|
|
48
|
+
*
|
|
49
|
+
* @param tick - The tick to check
|
|
50
|
+
* @param expectedPlayerCount - Number of players expected to submit hashes
|
|
51
|
+
* @returns true if all hashes have been received
|
|
52
|
+
*/
|
|
53
|
+
hasAllHashes(tick, expectedPlayerCount) {
|
|
54
|
+
const tickHashes = this.receivedHashes.get(tick);
|
|
55
|
+
if (!tickHashes) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return tickHashes.size >= expectedPlayerCount;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check for desyncs at a tick (host-authority mode).
|
|
62
|
+
* Compares all player hashes against the reference (host) hash.
|
|
63
|
+
*
|
|
64
|
+
* @param tick - The tick to check
|
|
65
|
+
* @param hostPlayerId - The host's player ID
|
|
66
|
+
* @returns Array of desync results for any desynced players
|
|
67
|
+
*/
|
|
68
|
+
checkDesyncs(tick, hostPlayerId) {
|
|
69
|
+
const tickHashes = this.receivedHashes.get(tick);
|
|
70
|
+
if (!tickHashes) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const hostHash = tickHashes.get(hostPlayerId);
|
|
74
|
+
if (hostHash === undefined) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const desyncs = [];
|
|
78
|
+
for (const [playerId, playerHash] of tickHashes) {
|
|
79
|
+
if (playerId === hostPlayerId) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (playerHash !== hostHash) {
|
|
83
|
+
desyncs.push({
|
|
84
|
+
tick,
|
|
85
|
+
desyncedPlayerId: playerId,
|
|
86
|
+
referenceHash: hostHash,
|
|
87
|
+
playerHash,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return desyncs;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check for desync in peer mode (comparing local vs remote hash).
|
|
95
|
+
*
|
|
96
|
+
* @param tick - The tick to check
|
|
97
|
+
* @param localHash - Local hash value (or undefined if not available)
|
|
98
|
+
* @param remoteHash - Remote hash value
|
|
99
|
+
* @param remotePlayerId - Remote player's ID
|
|
100
|
+
* @returns Desync result if a desync is detected, null otherwise
|
|
101
|
+
*/
|
|
102
|
+
checkPeerDesync(tick, localHash, remoteHash, remotePlayerId) {
|
|
103
|
+
if (localHash === undefined) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (localHash !== remoteHash) {
|
|
107
|
+
return {
|
|
108
|
+
tick,
|
|
109
|
+
localHash,
|
|
110
|
+
remoteHash,
|
|
111
|
+
remotePlayerId,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Remove old hash entries to prevent memory growth.
|
|
118
|
+
*
|
|
119
|
+
* @param currentTick - Current simulation tick
|
|
120
|
+
*/
|
|
121
|
+
pruneOldHashes(currentTick) {
|
|
122
|
+
const pruneBelow = asTick(currentTick - this.hashInterval * 2);
|
|
123
|
+
for (const tick of this.receivedHashes.keys()) {
|
|
124
|
+
if (tick < pruneBelow) {
|
|
125
|
+
this.receivedHashes.delete(tick);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Clear all stored hashes.
|
|
131
|
+
*/
|
|
132
|
+
clear() {
|
|
133
|
+
this.receivedHashes.clear();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=desync-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"desync-manager.js","sourceRoot":"","sources":["../../src/session/desync-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lag monitoring for detecting and reporting players that fall behind.
|
|
3
|
+
*/
|
|
4
|
+
import type { PlayerId, Tick } from "../types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for the lag monitor.
|
|
7
|
+
*/
|
|
8
|
+
export interface LagMonitorConfig {
|
|
9
|
+
/**
|
|
10
|
+
* Number of ticks behind before a player is considered lagging.
|
|
11
|
+
* Set to 0 to disable lag monitoring.
|
|
12
|
+
*/
|
|
13
|
+
threshold: number;
|
|
14
|
+
/**
|
|
15
|
+
* Minimum ticks between lag reports for the same player.
|
|
16
|
+
* Prevents spam when a player is consistently lagging.
|
|
17
|
+
*/
|
|
18
|
+
cooldownTicks: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Result when a player is detected as lagging.
|
|
22
|
+
*/
|
|
23
|
+
export interface LagReport {
|
|
24
|
+
/** The player who is lagging */
|
|
25
|
+
laggyPlayerId: PlayerId;
|
|
26
|
+
/** How many ticks behind they are */
|
|
27
|
+
ticksBehind: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Default cooldown between lag reports for the same player.
|
|
31
|
+
*/
|
|
32
|
+
export declare const DEFAULT_LAG_REPORT_COOLDOWN_TICKS = 60;
|
|
33
|
+
/**
|
|
34
|
+
* Monitors player lag and generates reports when players fall too far behind.
|
|
35
|
+
*
|
|
36
|
+
* Uses a cooldown system to prevent flooding the host with lag reports
|
|
37
|
+
* for persistently lagging players.
|
|
38
|
+
*/
|
|
39
|
+
export declare class LagMonitor {
|
|
40
|
+
private readonly threshold;
|
|
41
|
+
private readonly cooldownTicks;
|
|
42
|
+
private readonly lastReportTick;
|
|
43
|
+
constructor(config: LagMonitorConfig);
|
|
44
|
+
/**
|
|
45
|
+
* Check if a player is lagging and should be reported.
|
|
46
|
+
*
|
|
47
|
+
* @param playerId - The player to check
|
|
48
|
+
* @param ticksBehind - How many ticks behind the player is
|
|
49
|
+
* @param currentTick - The current simulation tick
|
|
50
|
+
* @returns A LagReport if the player is lagging and cooldown has passed, null otherwise
|
|
51
|
+
*/
|
|
52
|
+
checkPlayer(playerId: PlayerId, ticksBehind: number, currentTick: Tick): LagReport | null;
|
|
53
|
+
/**
|
|
54
|
+
* Reset the cooldown for a player.
|
|
55
|
+
* Use this when a player recovers from lag.
|
|
56
|
+
*
|
|
57
|
+
* @param playerId - The player to reset
|
|
58
|
+
*/
|
|
59
|
+
reset(playerId: PlayerId): void;
|
|
60
|
+
/**
|
|
61
|
+
* Clear all lag tracking state.
|
|
62
|
+
*/
|
|
63
|
+
clear(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Whether lag monitoring is enabled.
|
|
66
|
+
*/
|
|
67
|
+
get isEnabled(): boolean;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=lag-monitor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lag-monitor.d.ts","sourceRoot":"","sources":["../../src/session/lag-monitor.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAElD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB,gCAAgC;IAChC,aAAa,EAAE,QAAQ,CAAC;IACxB,qCAAqC;IACrC,WAAW,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,eAAO,MAAM,iCAAiC,KAAK,CAAC;AAEpD;;;;;GAKG;AACH,qBAAa,UAAU;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAkC;gBAErD,MAAM,EAAE,gBAAgB;IAKpC;;;;;;;OAOG;IACH,WAAW,CACV,QAAQ,EAAE,QAAQ,EAClB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,IAAI,GACf,SAAS,GAAG,IAAI;IA4BnB;;;;;OAKG;IACH,KAAK,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAI/B;;OAEG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACH,IAAI,SAAS,IAAI,OAAO,CAEvB;CACD"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lag monitoring for detecting and reporting players that fall behind.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Default cooldown between lag reports for the same player.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_LAG_REPORT_COOLDOWN_TICKS = 60;
|
|
8
|
+
/**
|
|
9
|
+
* Monitors player lag and generates reports when players fall too far behind.
|
|
10
|
+
*
|
|
11
|
+
* Uses a cooldown system to prevent flooding the host with lag reports
|
|
12
|
+
* for persistently lagging players.
|
|
13
|
+
*/
|
|
14
|
+
export class LagMonitor {
|
|
15
|
+
threshold;
|
|
16
|
+
cooldownTicks;
|
|
17
|
+
lastReportTick = new Map();
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.threshold = config.threshold;
|
|
20
|
+
this.cooldownTicks = config.cooldownTicks;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Check if a player is lagging and should be reported.
|
|
24
|
+
*
|
|
25
|
+
* @param playerId - The player to check
|
|
26
|
+
* @param ticksBehind - How many ticks behind the player is
|
|
27
|
+
* @param currentTick - The current simulation tick
|
|
28
|
+
* @returns A LagReport if the player is lagging and cooldown has passed, null otherwise
|
|
29
|
+
*/
|
|
30
|
+
checkPlayer(playerId, ticksBehind, currentTick) {
|
|
31
|
+
// Disabled if threshold is 0 or negative
|
|
32
|
+
if (this.threshold <= 0) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
// Not lagging enough
|
|
36
|
+
if (ticksBehind < this.threshold) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
// Check cooldown
|
|
40
|
+
const lastReport = this.lastReportTick.get(playerId);
|
|
41
|
+
if (lastReport !== undefined &&
|
|
42
|
+
currentTick - lastReport < this.cooldownTicks) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
// Record this report and return
|
|
46
|
+
this.lastReportTick.set(playerId, currentTick);
|
|
47
|
+
return {
|
|
48
|
+
laggyPlayerId: playerId,
|
|
49
|
+
ticksBehind,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Reset the cooldown for a player.
|
|
54
|
+
* Use this when a player recovers from lag.
|
|
55
|
+
*
|
|
56
|
+
* @param playerId - The player to reset
|
|
57
|
+
*/
|
|
58
|
+
reset(playerId) {
|
|
59
|
+
this.lastReportTick.delete(playerId);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Clear all lag tracking state.
|
|
63
|
+
*/
|
|
64
|
+
clear() {
|
|
65
|
+
this.lastReportTick.clear();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Whether lag monitoring is enabled.
|
|
69
|
+
*/
|
|
70
|
+
get isEnabled() {
|
|
71
|
+
return this.threshold > 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=lag-monitor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lag-monitor.js","sourceRoot":"","sources":["../../src/session/lag-monitor.ts"],"names":[],"mappings":"AAAA;;GAEG;AA+BH;;GAEG;AACH,MAAM,CAAC,MAAM,iCAAiC,GAAG,EAAE,CAAC;AAEpD;;;;;GAKG;AACH,MAAM,OAAO,UAAU;IACL,SAAS,CAAS;IAClB,aAAa,CAAS;IACtB,cAAc,GAAwB,IAAI,GAAG,EAAE,CAAC;IAEjE,YAAY,MAAwB;QACnC,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;QAClC,IAAI,CAAC,aAAa,GAAG,MAAM,CAAC,aAAa,CAAC;IAC3C,CAAC;IAED;;;;;;;OAOG;IACH,WAAW,CACV,QAAkB,EAClB,WAAmB,EACnB,WAAiB;QAEjB,yCAAyC;QACzC,IAAI,IAAI,CAAC,SAAS,IAAI,CAAC,EAAE,CAAC;YACzB,OAAO,IAAI,CAAC;QACb,CAAC;QAED,qBAAqB;QACrB,IAAI,WAAW,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;YAClC,OAAO,IAAI,CAAC;QACb,CAAC;QAED,iBAAiB;QACjB,MAAM,UAAU,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACrD,IACC,UAAU,KAAK,SAAS;YACxB,WAAW,GAAG,UAAU,GAAG,IAAI,CAAC,aAAa,EAC5C,CAAC;YACF,OAAO,IAAI,CAAC;QACb,CAAC;QAED,gCAAgC;QAChC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QAC/C,OAAO;YACN,aAAa,EAAE,QAAQ;YACvB,WAAW;SACX,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,QAAkB;QACvB,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACtC,CAAC;IAED;;OAEG;IACH,KAAK;QACJ,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC;IAC7B,CAAC;IAED;;OAEG;IACH,IAAI,SAAS;QACZ,OAAO,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC;IAC3B,CAAC;CACD"}
|