@carverjs/multiplayer 0.0.1
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/NetworkManager-DrKM2tEx.d.mts +369 -0
- package/dist/NetworkManager-nvVAOr1O.d.ts +369 -0
- package/dist/chunk-3KT73N2S.mjs +655 -0
- package/dist/chunk-3KT73N2S.mjs.map +1 -0
- package/dist/chunk-EO3YNPRQ.mjs +817 -0
- package/dist/chunk-EO3YNPRQ.mjs.map +1 -0
- package/dist/chunk-UD6FDZMX.mjs +581 -0
- package/dist/chunk-UD6FDZMX.mjs.map +1 -0
- package/dist/firebase-CPu87KA0.d.ts +100 -0
- package/dist/firebase-PE6MxGdJ.d.mts +100 -0
- package/dist/index.d.mts +316 -0
- package/dist/index.d.ts +316 -0
- package/dist/index.js +3817 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1743 -0
- package/dist/index.mjs.map +1 -0
- package/dist/strategy.d.mts +7 -0
- package/dist/strategy.d.ts +7 -0
- package/dist/strategy.js +619 -0
- package/dist/strategy.js.map +1 -0
- package/dist/strategy.mjs +11 -0
- package/dist/strategy.mjs.map +1 -0
- package/dist/sync.d.mts +212 -0
- package/dist/sync.d.ts +212 -0
- package/dist/sync.js +845 -0
- package/dist/sync.js.map +1 -0
- package/dist/sync.mjs +11 -0
- package/dist/sync.mjs.map +1 -0
- package/dist/transport.d.mts +159 -0
- package/dist/transport.d.ts +159 -0
- package/dist/transport.js +1274 -0
- package/dist/transport.js.map +1 -0
- package/dist/transport.mjs +19 -0
- package/dist/transport.mjs.map +1 -0
- package/dist/types-5LHBOW08.d.mts +74 -0
- package/dist/types-5LHBOW08.d.ts +74 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/dist/types.mjs +1 -0
- package/dist/types.mjs.map +1 -0
- package/package.json +73 -0
package/dist/sync.js
ADDED
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/sync/index.ts
|
|
21
|
+
var sync_exports = {};
|
|
22
|
+
__export(sync_exports, {
|
|
23
|
+
EventSync: () => EventSync,
|
|
24
|
+
PredictionSync: () => PredictionSync,
|
|
25
|
+
SnapshotSync: () => SnapshotSync
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(sync_exports);
|
|
28
|
+
|
|
29
|
+
// src/sync/EventSync.ts
|
|
30
|
+
var EventSync = class {
|
|
31
|
+
constructor(transport, options) {
|
|
32
|
+
this._handlers = /* @__PURE__ */ new Map();
|
|
33
|
+
this._transport = transport;
|
|
34
|
+
this._hostValidation = options?.hostValidation ?? false;
|
|
35
|
+
this._channel = transport.createChannel("carver:events", {
|
|
36
|
+
reliable: true,
|
|
37
|
+
ordered: true
|
|
38
|
+
});
|
|
39
|
+
this._channel.onReceive((rawData, peerId) => {
|
|
40
|
+
try {
|
|
41
|
+
const packet = typeof rawData === "string" ? JSON.parse(rawData) : rawData;
|
|
42
|
+
if (this._hostValidation && this._transport.isHost && packet.sender !== this._transport.peerId) {
|
|
43
|
+
const targets = Array.from(this._transport.peers).filter((p) => p !== peerId);
|
|
44
|
+
if (targets.length > 0) {
|
|
45
|
+
this._channel.send(JSON.stringify(packet), targets);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (this._hostValidation && !this._transport.isHost && peerId !== this._transport.hostId) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const handlers = this._handlers.get(packet.type);
|
|
52
|
+
if (handlers) {
|
|
53
|
+
for (const handler of handlers) {
|
|
54
|
+
handler(packet.payload, packet.sender);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Send a typed event to a specific peer or all peers.
|
|
63
|
+
*/
|
|
64
|
+
sendEvent(type, payload, target) {
|
|
65
|
+
const packet = {
|
|
66
|
+
type,
|
|
67
|
+
payload,
|
|
68
|
+
sender: this._transport.peerId,
|
|
69
|
+
target
|
|
70
|
+
};
|
|
71
|
+
const serialized = JSON.stringify(packet);
|
|
72
|
+
if (this._hostValidation && !this._transport.isHost) {
|
|
73
|
+
this._channel.send(serialized, this._transport.hostId);
|
|
74
|
+
} else if (target) {
|
|
75
|
+
this._channel.send(serialized, target);
|
|
76
|
+
} else {
|
|
77
|
+
this._channel.send(serialized);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Broadcast a typed event to all connected peers.
|
|
82
|
+
*/
|
|
83
|
+
broadcast(type, payload) {
|
|
84
|
+
this.sendEvent(type, payload);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Register a handler for a specific event type.
|
|
88
|
+
* Returns an unsubscribe function.
|
|
89
|
+
*/
|
|
90
|
+
onEvent(type, callback) {
|
|
91
|
+
let handlers = this._handlers.get(type);
|
|
92
|
+
if (!handlers) {
|
|
93
|
+
handlers = [];
|
|
94
|
+
this._handlers.set(type, handlers);
|
|
95
|
+
}
|
|
96
|
+
handlers.push(callback);
|
|
97
|
+
return () => {
|
|
98
|
+
const arr = this._handlers.get(type);
|
|
99
|
+
if (arr) {
|
|
100
|
+
const idx = arr.indexOf(callback);
|
|
101
|
+
if (idx >= 0) arr.splice(idx, 1);
|
|
102
|
+
if (arr.length === 0) this._handlers.delete(type);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Clean up the event channel.
|
|
108
|
+
*/
|
|
109
|
+
destroy() {
|
|
110
|
+
this._channel.close();
|
|
111
|
+
this._handlers.clear();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// src/core/HostAuthority.ts
|
|
116
|
+
var HostAuthority = class {
|
|
117
|
+
constructor(transport, codec, snapshotBuffer, options) {
|
|
118
|
+
this._tick = 0;
|
|
119
|
+
this._broadcastAccumulator = 0;
|
|
120
|
+
// Per-client last ACK'd tick for delta compression
|
|
121
|
+
this._clientBaselines = /* @__PURE__ */ new Map();
|
|
122
|
+
// Per-client last keyframe tick for scheduling
|
|
123
|
+
this._clientLastKeyframeTick = /* @__PURE__ */ new Map();
|
|
124
|
+
// Interest management callback (optional)
|
|
125
|
+
this._interestFilter = null;
|
|
126
|
+
this._transport = transport;
|
|
127
|
+
this._codec = codec;
|
|
128
|
+
this._snapshotBuffer = snapshotBuffer;
|
|
129
|
+
this._broadcastRate = options?.broadcastRate ?? 20;
|
|
130
|
+
this._keyframeInterval = options?.keyframeInterval ?? 300;
|
|
131
|
+
this._snapshotChannel = transport.createChannel(
|
|
132
|
+
"carver:snapshots",
|
|
133
|
+
{
|
|
134
|
+
reliable: false,
|
|
135
|
+
ordered: false,
|
|
136
|
+
maxRetransmits: 0
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
this._ackChannel = transport.createChannel("carver:acks", {
|
|
140
|
+
reliable: true,
|
|
141
|
+
ordered: true
|
|
142
|
+
});
|
|
143
|
+
this._ackChannel.onReceive((data, peerId) => {
|
|
144
|
+
try {
|
|
145
|
+
const ackTick = typeof data === "string" ? parseInt(data, 10) : data;
|
|
146
|
+
if (ackTick === -1) {
|
|
147
|
+
this._clientBaselines.delete(peerId);
|
|
148
|
+
} else {
|
|
149
|
+
this._clientBaselines.set(peerId, ackTick);
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
transport.onPeerJoin((peerId) => {
|
|
155
|
+
this._clientBaselines.delete(peerId);
|
|
156
|
+
});
|
|
157
|
+
transport.onPeerLeave((peerId) => {
|
|
158
|
+
this._clientBaselines.delete(peerId);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/** Set optional interest management filter */
|
|
162
|
+
setInterestFilter(filter) {
|
|
163
|
+
this._interestFilter = filter;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Called every fixed tick by the sync engine.
|
|
167
|
+
* Collects entity states and decides whether to broadcast.
|
|
168
|
+
*/
|
|
169
|
+
tick(currentTick, entities, delta) {
|
|
170
|
+
this._tick = currentTick;
|
|
171
|
+
this._snapshotBuffer.store(currentTick, new Map(entities));
|
|
172
|
+
this._broadcastAccumulator += delta;
|
|
173
|
+
const broadcastInterval = 1 / this._broadcastRate;
|
|
174
|
+
if (this._broadcastAccumulator < broadcastInterval) return;
|
|
175
|
+
this._broadcastAccumulator -= broadcastInterval;
|
|
176
|
+
for (const peerId of this._transport.peers) {
|
|
177
|
+
this._broadcastToClient(peerId, currentTick, entities);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** Force a keyframe broadcast to all clients (e.g., after host migration) */
|
|
181
|
+
forceKeyframe(currentTick, entities) {
|
|
182
|
+
this._clientBaselines.clear();
|
|
183
|
+
this._clientLastKeyframeTick.clear();
|
|
184
|
+
this._snapshotBuffer.store(currentTick, new Map(entities));
|
|
185
|
+
for (const peerId of this._transport.peers) {
|
|
186
|
+
this._broadcastToClient(peerId, currentTick, entities);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
destroy() {
|
|
190
|
+
this._snapshotChannel.close();
|
|
191
|
+
this._ackChannel.close();
|
|
192
|
+
this._clientBaselines.clear();
|
|
193
|
+
this._clientLastKeyframeTick.clear();
|
|
194
|
+
}
|
|
195
|
+
_broadcastToClient(peerId, currentTick, entities) {
|
|
196
|
+
let clientEntities = entities;
|
|
197
|
+
if (this._interestFilter) {
|
|
198
|
+
clientEntities = /* @__PURE__ */ new Map();
|
|
199
|
+
for (const [id, entity] of entities) {
|
|
200
|
+
if (this._interestFilter(id, peerId)) {
|
|
201
|
+
clientEntities.set(id, entity);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const clientBaseTick = this._clientBaselines.get(peerId);
|
|
206
|
+
const clientLastKeyframe = this._clientLastKeyframeTick.get(peerId) ?? 0;
|
|
207
|
+
const needsKeyframe = clientBaseTick === void 0 || currentTick - clientLastKeyframe >= this._keyframeInterval;
|
|
208
|
+
let baseline;
|
|
209
|
+
if (!needsKeyframe && clientBaseTick !== void 0) {
|
|
210
|
+
baseline = this._snapshotBuffer.get(clientBaseTick);
|
|
211
|
+
}
|
|
212
|
+
if (needsKeyframe) {
|
|
213
|
+
this._clientLastKeyframeTick.set(peerId, currentTick);
|
|
214
|
+
}
|
|
215
|
+
const packet = this._codec.serializeDelta(
|
|
216
|
+
currentTick,
|
|
217
|
+
needsKeyframe ? -1 : clientBaseTick ?? -1,
|
|
218
|
+
clientEntities,
|
|
219
|
+
baseline
|
|
220
|
+
);
|
|
221
|
+
if (packet) {
|
|
222
|
+
this._snapshotChannel.send(packet, peerId);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// src/core/ClientReceiver.ts
|
|
228
|
+
var ClientReceiver = class {
|
|
229
|
+
constructor(transport, codec, options) {
|
|
230
|
+
// Snapshot buffer (ring buffer of last N snapshots)
|
|
231
|
+
this._buffer = [];
|
|
232
|
+
// Current interpolated state
|
|
233
|
+
this._interpolatedState = /* @__PURE__ */ new Map();
|
|
234
|
+
// Network quality tracking
|
|
235
|
+
this._lastSnapshotTime = 0;
|
|
236
|
+
this._networkQuality = "good";
|
|
237
|
+
this._packetLossCount = 0;
|
|
238
|
+
this._packetCount = 0;
|
|
239
|
+
// Entity state: full accumulated state from keyframes + deltas
|
|
240
|
+
this._fullState = /* @__PURE__ */ new Map();
|
|
241
|
+
this._transport = transport;
|
|
242
|
+
this._codec = codec;
|
|
243
|
+
this._bufferSize = options?.bufferSize ?? 3;
|
|
244
|
+
this._method = options?.method ?? "hermite";
|
|
245
|
+
this._extrapolateMs = options?.extrapolateMs ?? 250;
|
|
246
|
+
this._is2D = options?.is2D ?? false;
|
|
247
|
+
this._snapshotChannel = transport.createChannel(
|
|
248
|
+
"carver:snapshots",
|
|
249
|
+
{
|
|
250
|
+
reliable: false,
|
|
251
|
+
ordered: false,
|
|
252
|
+
maxRetransmits: 0
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
this._ackChannel = transport.createChannel("carver:acks", {
|
|
256
|
+
reliable: true,
|
|
257
|
+
ordered: true
|
|
258
|
+
});
|
|
259
|
+
this._snapshotChannel.onReceive((data) => {
|
|
260
|
+
this._handleSnapshot(data);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/** Get the current interpolated entity states */
|
|
264
|
+
get state() {
|
|
265
|
+
return this._interpolatedState;
|
|
266
|
+
}
|
|
267
|
+
get networkQuality() {
|
|
268
|
+
return this._networkQuality;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Called every render frame to interpolate between buffered snapshots.
|
|
272
|
+
* @param renderTime - current render time in ms
|
|
273
|
+
*/
|
|
274
|
+
interpolate(renderTime) {
|
|
275
|
+
if (this._buffer.length < 2) {
|
|
276
|
+
return this._buffer.length > 0 ? this._buffer[this._buffer.length - 1].entities : this._interpolatedState;
|
|
277
|
+
}
|
|
278
|
+
const interpDelay = (this._bufferSize - 1) * (1e3 / 20);
|
|
279
|
+
const targetTime = renderTime - interpDelay;
|
|
280
|
+
let from = null;
|
|
281
|
+
let to = null;
|
|
282
|
+
for (let i = 0; i < this._buffer.length - 1; i++) {
|
|
283
|
+
if (this._buffer[i].receivedAt <= targetTime && this._buffer[i + 1].receivedAt > targetTime) {
|
|
284
|
+
from = this._buffer[i];
|
|
285
|
+
to = this._buffer[i + 1];
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (!from || !to) {
|
|
290
|
+
const latest = this._buffer[this._buffer.length - 1];
|
|
291
|
+
const timeSinceLatest = renderTime - latest.receivedAt;
|
|
292
|
+
if (timeSinceLatest > this._extrapolateMs) {
|
|
293
|
+
this._updateNetworkQuality("poor");
|
|
294
|
+
return latest.entities;
|
|
295
|
+
}
|
|
296
|
+
if (this._buffer.length >= 2) {
|
|
297
|
+
from = this._buffer[this._buffer.length - 2];
|
|
298
|
+
to = latest;
|
|
299
|
+
this._updateNetworkQuality("degraded");
|
|
300
|
+
} else {
|
|
301
|
+
return latest.entities;
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
this._updateNetworkQuality("good");
|
|
305
|
+
}
|
|
306
|
+
const range = to.receivedAt - from.receivedAt;
|
|
307
|
+
const t = range > 0 ? Math.min(1, Math.max(0, (targetTime - from.receivedAt) / range)) : 1;
|
|
308
|
+
const result = /* @__PURE__ */ new Map();
|
|
309
|
+
const allIds = /* @__PURE__ */ new Set([...from.entities.keys(), ...to.entities.keys()]);
|
|
310
|
+
for (const id of allIds) {
|
|
311
|
+
const fromEntity = from.entities.get(id);
|
|
312
|
+
const toEntity = to.entities.get(id);
|
|
313
|
+
if (toEntity && toEntity.c?.__removed) continue;
|
|
314
|
+
if (fromEntity && toEntity) {
|
|
315
|
+
result.set(id, this._interpolateEntity(fromEntity, toEntity, t));
|
|
316
|
+
} else if (toEntity) {
|
|
317
|
+
result.set(id, toEntity);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
this._interpolatedState = result;
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
/** Request a keyframe from the host */
|
|
324
|
+
requestKeyframe() {
|
|
325
|
+
this._ackChannel.send("-1");
|
|
326
|
+
}
|
|
327
|
+
destroy() {
|
|
328
|
+
this._snapshotChannel.close();
|
|
329
|
+
this._ackChannel.close();
|
|
330
|
+
this._buffer = [];
|
|
331
|
+
this._interpolatedState.clear();
|
|
332
|
+
this._fullState.clear();
|
|
333
|
+
}
|
|
334
|
+
_handleSnapshot(data) {
|
|
335
|
+
try {
|
|
336
|
+
const { tick, baseTick, entities } = this._codec.deserializePacket(data);
|
|
337
|
+
const now = performance.now();
|
|
338
|
+
if (baseTick === -1) {
|
|
339
|
+
this._fullState.clear();
|
|
340
|
+
for (const entity of entities) {
|
|
341
|
+
this._fullState.set(entity.id, entity);
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
for (const entity of entities) {
|
|
345
|
+
if (entity.c?.__removed) {
|
|
346
|
+
this._fullState.delete(entity.id);
|
|
347
|
+
} else {
|
|
348
|
+
this._fullState.set(entity.id, entity);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
this._buffer.push({
|
|
353
|
+
tick,
|
|
354
|
+
entities: new Map(this._fullState),
|
|
355
|
+
receivedAt: now
|
|
356
|
+
});
|
|
357
|
+
while (this._buffer.length > this._bufferSize * 2) {
|
|
358
|
+
this._buffer.shift();
|
|
359
|
+
}
|
|
360
|
+
this._ackChannel.send(String(tick));
|
|
361
|
+
this._lastSnapshotTime = now;
|
|
362
|
+
this._packetCount++;
|
|
363
|
+
} catch {
|
|
364
|
+
this._packetLossCount++;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
_interpolateEntity(from, to, t) {
|
|
368
|
+
if (this._is2D || !("z" in from)) {
|
|
369
|
+
return this._interpolateEntity2D(
|
|
370
|
+
from,
|
|
371
|
+
to,
|
|
372
|
+
t
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return this._interpolateEntity3D(
|
|
376
|
+
from,
|
|
377
|
+
to,
|
|
378
|
+
t
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
_interpolateEntity2D(from, to, t) {
|
|
382
|
+
if (this._method === "hermite") {
|
|
383
|
+
return {
|
|
384
|
+
id: to.id,
|
|
385
|
+
x: hermite(from.x, from.vx, to.x, to.vx, t),
|
|
386
|
+
y: hermite(from.y, from.vy, to.y, to.vy, t),
|
|
387
|
+
a: lerpAngle(from.a, to.a, t),
|
|
388
|
+
vx: lerp(from.vx, to.vx, t),
|
|
389
|
+
vy: lerp(from.vy, to.vy, t),
|
|
390
|
+
va: lerp(from.va, to.va, t),
|
|
391
|
+
c: interpolateCustom(from.c, to.c, t)
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
id: to.id,
|
|
396
|
+
x: lerp(from.x, to.x, t),
|
|
397
|
+
y: lerp(from.y, to.y, t),
|
|
398
|
+
a: lerpAngle(from.a, to.a, t),
|
|
399
|
+
vx: lerp(from.vx, to.vx, t),
|
|
400
|
+
vy: lerp(from.vy, to.vy, t),
|
|
401
|
+
va: lerp(from.va, to.va, t),
|
|
402
|
+
c: interpolateCustom(from.c, to.c, t)
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
_interpolateEntity3D(from, to, t) {
|
|
406
|
+
const [qx, qy, qz, qw] = slerp(
|
|
407
|
+
from.qx,
|
|
408
|
+
from.qy,
|
|
409
|
+
from.qz,
|
|
410
|
+
from.qw,
|
|
411
|
+
to.qx,
|
|
412
|
+
to.qy,
|
|
413
|
+
to.qz,
|
|
414
|
+
to.qw,
|
|
415
|
+
t
|
|
416
|
+
);
|
|
417
|
+
if (this._method === "hermite") {
|
|
418
|
+
return {
|
|
419
|
+
id: to.id,
|
|
420
|
+
x: hermite(from.x, from.vx, to.x, to.vx, t),
|
|
421
|
+
y: hermite(from.y, from.vy, to.y, to.vy, t),
|
|
422
|
+
z: hermite(from.z, from.vz, to.z, to.vz, t),
|
|
423
|
+
qx,
|
|
424
|
+
qy,
|
|
425
|
+
qz,
|
|
426
|
+
qw,
|
|
427
|
+
vx: lerp(from.vx, to.vx, t),
|
|
428
|
+
vy: lerp(from.vy, to.vy, t),
|
|
429
|
+
vz: lerp(from.vz, to.vz, t),
|
|
430
|
+
wx: lerp(from.wx, to.wx, t),
|
|
431
|
+
wy: lerp(from.wy, to.wy, t),
|
|
432
|
+
wz: lerp(from.wz, to.wz, t),
|
|
433
|
+
c: interpolateCustom(from.c, to.c, t)
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
id: to.id,
|
|
438
|
+
x: lerp(from.x, to.x, t),
|
|
439
|
+
y: lerp(from.y, to.y, t),
|
|
440
|
+
z: lerp(from.z, to.z, t),
|
|
441
|
+
qx,
|
|
442
|
+
qy,
|
|
443
|
+
qz,
|
|
444
|
+
qw,
|
|
445
|
+
vx: lerp(from.vx, to.vx, t),
|
|
446
|
+
vy: lerp(from.vy, to.vy, t),
|
|
447
|
+
vz: lerp(from.vz, to.vz, t),
|
|
448
|
+
wx: lerp(from.wx, to.wx, t),
|
|
449
|
+
wy: lerp(from.wy, to.wy, t),
|
|
450
|
+
wz: lerp(from.wz, to.wz, t),
|
|
451
|
+
c: interpolateCustom(from.c, to.c, t)
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
_updateNetworkQuality(quality) {
|
|
455
|
+
this._networkQuality = quality;
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
function lerp(a, b, t) {
|
|
459
|
+
return a + (b - a) * t;
|
|
460
|
+
}
|
|
461
|
+
function lerpAngle(a, b, t) {
|
|
462
|
+
let diff = b - a;
|
|
463
|
+
while (diff > Math.PI) diff -= Math.PI * 2;
|
|
464
|
+
while (diff < -Math.PI) diff += Math.PI * 2;
|
|
465
|
+
return a + diff * t;
|
|
466
|
+
}
|
|
467
|
+
function hermite(p0, v0, p1, v1, t) {
|
|
468
|
+
const t2 = t * t;
|
|
469
|
+
const t3 = t2 * t;
|
|
470
|
+
return (2 * t3 - 3 * t2 + 1) * p0 + (t3 - 2 * t2 + t) * v0 + (-2 * t3 + 3 * t2) * p1 + (t3 - t2) * v1;
|
|
471
|
+
}
|
|
472
|
+
function slerp(ax, ay, az, aw, bx, by, bz, bw, t) {
|
|
473
|
+
let dot = ax * bx + ay * by + az * bz + aw * bw;
|
|
474
|
+
if (dot < 0) {
|
|
475
|
+
bx = -bx;
|
|
476
|
+
by = -by;
|
|
477
|
+
bz = -bz;
|
|
478
|
+
bw = -bw;
|
|
479
|
+
dot = -dot;
|
|
480
|
+
}
|
|
481
|
+
if (dot > 0.9995) {
|
|
482
|
+
return [lerp(ax, bx, t), lerp(ay, by, t), lerp(az, bz, t), lerp(aw, bw, t)];
|
|
483
|
+
}
|
|
484
|
+
const theta = Math.acos(Math.min(1, Math.max(-1, dot)));
|
|
485
|
+
const sinTheta = Math.sin(theta);
|
|
486
|
+
const wa = Math.sin((1 - t) * theta) / sinTheta;
|
|
487
|
+
const wb = Math.sin(t * theta) / sinTheta;
|
|
488
|
+
return [
|
|
489
|
+
ax * wa + bx * wb,
|
|
490
|
+
ay * wa + by * wb,
|
|
491
|
+
az * wa + bz * wb,
|
|
492
|
+
aw * wa + bw * wb
|
|
493
|
+
];
|
|
494
|
+
}
|
|
495
|
+
function interpolateCustom(from, to, t) {
|
|
496
|
+
if (!from && !to) return void 0;
|
|
497
|
+
if (!from) return to;
|
|
498
|
+
if (!to) return from;
|
|
499
|
+
const result = {};
|
|
500
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(from), ...Object.keys(to)]);
|
|
501
|
+
for (const key of allKeys) {
|
|
502
|
+
const fromVal = from[key];
|
|
503
|
+
const toVal = to[key];
|
|
504
|
+
if (typeof fromVal === "number" && typeof toVal === "number") {
|
|
505
|
+
result[key] = lerp(fromVal, toVal, t);
|
|
506
|
+
} else {
|
|
507
|
+
result[key] = t >= 0.5 ? toVal : fromVal;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return result;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/sync/SnapshotSync.ts
|
|
514
|
+
var SnapshotSync = class {
|
|
515
|
+
constructor(transport, codec, snapshotBuffer, options) {
|
|
516
|
+
this._hostAuthority = null;
|
|
517
|
+
this._clientReceiver = null;
|
|
518
|
+
this._transport = transport;
|
|
519
|
+
this._codec = codec;
|
|
520
|
+
this._snapshotBuffer = snapshotBuffer;
|
|
521
|
+
if (transport.isHost) {
|
|
522
|
+
this._hostAuthority = new HostAuthority(
|
|
523
|
+
transport,
|
|
524
|
+
codec,
|
|
525
|
+
snapshotBuffer,
|
|
526
|
+
{
|
|
527
|
+
broadcastRate: options?.broadcastRate,
|
|
528
|
+
keyframeInterval: options?.keyframeInterval
|
|
529
|
+
}
|
|
530
|
+
);
|
|
531
|
+
} else {
|
|
532
|
+
this._clientReceiver = new ClientReceiver(transport, codec, {
|
|
533
|
+
bufferSize: options?.bufferSize,
|
|
534
|
+
method: options?.interpolationMethod,
|
|
535
|
+
extrapolateMs: options?.extrapolateMs,
|
|
536
|
+
is2D: options?.is2D
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
get isHost() {
|
|
541
|
+
return this._hostAuthority !== null;
|
|
542
|
+
}
|
|
543
|
+
get hostAuthority() {
|
|
544
|
+
return this._hostAuthority;
|
|
545
|
+
}
|
|
546
|
+
get clientReceiver() {
|
|
547
|
+
return this._clientReceiver;
|
|
548
|
+
}
|
|
549
|
+
/** Host: called every fixed tick to potentially broadcast state */
|
|
550
|
+
hostTick(tick, entities, delta) {
|
|
551
|
+
this._hostAuthority?.tick(tick, entities, delta);
|
|
552
|
+
}
|
|
553
|
+
/** Client: called every render frame to interpolate */
|
|
554
|
+
clientInterpolate(renderTime) {
|
|
555
|
+
return this._clientReceiver?.interpolate(renderTime) ?? /* @__PURE__ */ new Map();
|
|
556
|
+
}
|
|
557
|
+
/** Set interest filter on host authority */
|
|
558
|
+
setInterestFilter(filter) {
|
|
559
|
+
this._hostAuthority?.setInterestFilter(filter);
|
|
560
|
+
}
|
|
561
|
+
/** Handle host migration: switch from client to host mode */
|
|
562
|
+
promoteToHost(options) {
|
|
563
|
+
this._clientReceiver?.destroy();
|
|
564
|
+
this._clientReceiver = null;
|
|
565
|
+
this._hostAuthority = new HostAuthority(
|
|
566
|
+
this._transport,
|
|
567
|
+
this._codec,
|
|
568
|
+
this._snapshotBuffer,
|
|
569
|
+
{
|
|
570
|
+
broadcastRate: options?.broadcastRate,
|
|
571
|
+
keyframeInterval: options?.keyframeInterval
|
|
572
|
+
}
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
/** Handle host migration: switch from host to client mode */
|
|
576
|
+
demoteToClient(options) {
|
|
577
|
+
this._hostAuthority?.destroy();
|
|
578
|
+
this._hostAuthority = null;
|
|
579
|
+
this._clientReceiver = new ClientReceiver(
|
|
580
|
+
this._transport,
|
|
581
|
+
this._codec,
|
|
582
|
+
{
|
|
583
|
+
bufferSize: options?.bufferSize,
|
|
584
|
+
method: options?.interpolationMethod,
|
|
585
|
+
extrapolateMs: options?.extrapolateMs,
|
|
586
|
+
is2D: options?.is2D
|
|
587
|
+
}
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
destroy() {
|
|
591
|
+
this._hostAuthority?.destroy();
|
|
592
|
+
this._clientReceiver?.destroy();
|
|
593
|
+
this._hostAuthority = null;
|
|
594
|
+
this._clientReceiver = null;
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
// src/sync/PredictionSync.ts
|
|
599
|
+
var import_msgpackr = require("msgpackr");
|
|
600
|
+
var DEFAULT_OPTIONS = {
|
|
601
|
+
maxRewindTicks: 15,
|
|
602
|
+
errorSmoothingDecay: 0.85,
|
|
603
|
+
maxErrorPerFrame: 5,
|
|
604
|
+
snapThreshold: 15,
|
|
605
|
+
lagCompensation: false
|
|
606
|
+
};
|
|
607
|
+
var PredictionSync = class {
|
|
608
|
+
constructor(transport, codec, tickKeeper, options) {
|
|
609
|
+
// Input buffer: ring buffer of recent inputs keyed by tick
|
|
610
|
+
this._inputBuffer = [];
|
|
611
|
+
// Per-client last processed input tick (host-side tracking)
|
|
612
|
+
this._clientLastProcessedTick = /* @__PURE__ */ new Map();
|
|
613
|
+
// Predicted state (client-side)
|
|
614
|
+
this._predictedState = /* @__PURE__ */ new Map();
|
|
615
|
+
// Error correction vectors per entity
|
|
616
|
+
this._errorCorrections = /* @__PURE__ */ new Map();
|
|
617
|
+
// Server state (last received authoritative snapshot)
|
|
618
|
+
this._serverState = /* @__PURE__ */ new Map();
|
|
619
|
+
this._serverTick = 0;
|
|
620
|
+
// Physics step callback (provided by developer)
|
|
621
|
+
this._onPhysicsStep = null;
|
|
622
|
+
// Own input for current tick
|
|
623
|
+
this._currentInput = null;
|
|
624
|
+
this._transport = transport;
|
|
625
|
+
this._codec = codec;
|
|
626
|
+
this._tickKeeper = tickKeeper;
|
|
627
|
+
this._options = { ...DEFAULT_OPTIONS, ...options };
|
|
628
|
+
this._isHost = transport.isHost;
|
|
629
|
+
this._inputChannel = transport.createChannel("carver:inputs", {
|
|
630
|
+
reliable: true,
|
|
631
|
+
ordered: true
|
|
632
|
+
});
|
|
633
|
+
this._stateChannel = transport.createChannel("carver:pred-state", {
|
|
634
|
+
reliable: false,
|
|
635
|
+
ordered: false,
|
|
636
|
+
maxRetransmits: 0
|
|
637
|
+
});
|
|
638
|
+
this._ackChannel = transport.createChannel("carver:pred-acks", {
|
|
639
|
+
reliable: true,
|
|
640
|
+
ordered: true
|
|
641
|
+
});
|
|
642
|
+
if (this._isHost) {
|
|
643
|
+
this._setupHostListeners();
|
|
644
|
+
} else {
|
|
645
|
+
this._setupClientListeners();
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/** Set the physics step callback (required for rollback re-simulation) */
|
|
649
|
+
setPhysicsStep(cb) {
|
|
650
|
+
this._onPhysicsStep = cb;
|
|
651
|
+
}
|
|
652
|
+
/** Set the current input for this tick (client-side) */
|
|
653
|
+
setInput(input) {
|
|
654
|
+
this._currentInput = input;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Called every fixed tick on the client.
|
|
658
|
+
* Applies input locally (prediction), buffers it, and sends to host.
|
|
659
|
+
*/
|
|
660
|
+
clientTick(tick) {
|
|
661
|
+
if (this._isHost) return;
|
|
662
|
+
if (this._currentInput !== null) {
|
|
663
|
+
this._inputBuffer.push({ tick, input: this._currentInput });
|
|
664
|
+
const packet = {
|
|
665
|
+
t: tick,
|
|
666
|
+
i: this._currentInput,
|
|
667
|
+
p: this._transport.peerId
|
|
668
|
+
};
|
|
669
|
+
this._inputChannel.send(JSON.stringify(packet));
|
|
670
|
+
if (this._onPhysicsStep) {
|
|
671
|
+
const inputs = /* @__PURE__ */ new Map();
|
|
672
|
+
inputs.set(this._transport.peerId, this._currentInput);
|
|
673
|
+
this._onPhysicsStep(inputs, tick, false);
|
|
674
|
+
}
|
|
675
|
+
this._currentInput = null;
|
|
676
|
+
}
|
|
677
|
+
const minTick = tick - this._options.maxRewindTicks * 2;
|
|
678
|
+
while (this._inputBuffer.length > 0 && this._inputBuffer[0].tick < minTick) {
|
|
679
|
+
this._inputBuffer.shift();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Called every fixed tick on the host.
|
|
684
|
+
* Processes received inputs and broadcasts authoritative state.
|
|
685
|
+
*/
|
|
686
|
+
hostTick(tick, entities, _delta) {
|
|
687
|
+
if (!this._isHost) return;
|
|
688
|
+
const stateArray = Array.from(entities.values());
|
|
689
|
+
const data = this._codec.serialize(stateArray);
|
|
690
|
+
for (const peerId of this._transport.peers) {
|
|
691
|
+
const lastTick = this._clientLastProcessedTick.get(peerId) ?? -1;
|
|
692
|
+
const packet = {
|
|
693
|
+
t: tick,
|
|
694
|
+
s: data,
|
|
695
|
+
li: lastTick
|
|
696
|
+
};
|
|
697
|
+
this._stateChannel.send((0, import_msgpackr.pack)(packet), peerId);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Called every render frame on the client to apply visual error smoothing.
|
|
702
|
+
* Returns the corrected entity states.
|
|
703
|
+
*/
|
|
704
|
+
applyErrorSmoothing(entities) {
|
|
705
|
+
const result = /* @__PURE__ */ new Map();
|
|
706
|
+
const decay = this._options.errorSmoothingDecay;
|
|
707
|
+
for (const [id, entity] of entities) {
|
|
708
|
+
const correction = this._errorCorrections.get(id);
|
|
709
|
+
if (!correction) {
|
|
710
|
+
result.set(id, entity);
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
const corrected = { ...entity };
|
|
714
|
+
corrected.x += correction.x;
|
|
715
|
+
corrected.y += correction.y;
|
|
716
|
+
if ("z" in corrected) {
|
|
717
|
+
corrected.z += correction.z;
|
|
718
|
+
}
|
|
719
|
+
correction.x *= decay;
|
|
720
|
+
correction.y *= decay;
|
|
721
|
+
correction.z *= decay;
|
|
722
|
+
const mag = Math.abs(correction.x) + Math.abs(correction.y) + Math.abs(correction.z);
|
|
723
|
+
if (mag < 1e-3) {
|
|
724
|
+
this._errorCorrections.delete(id);
|
|
725
|
+
}
|
|
726
|
+
result.set(id, corrected);
|
|
727
|
+
}
|
|
728
|
+
return result;
|
|
729
|
+
}
|
|
730
|
+
get predictedState() {
|
|
731
|
+
return this._predictedState;
|
|
732
|
+
}
|
|
733
|
+
get serverTick() {
|
|
734
|
+
return this._serverTick;
|
|
735
|
+
}
|
|
736
|
+
destroy() {
|
|
737
|
+
this._inputChannel.close();
|
|
738
|
+
this._stateChannel.close();
|
|
739
|
+
this._ackChannel.close();
|
|
740
|
+
this._inputBuffer = [];
|
|
741
|
+
this._predictedState.clear();
|
|
742
|
+
this._errorCorrections.clear();
|
|
743
|
+
this._serverState.clear();
|
|
744
|
+
}
|
|
745
|
+
// ── Private: Host-side ──
|
|
746
|
+
_setupHostListeners() {
|
|
747
|
+
this._inputChannel.onReceive((rawData, peerId) => {
|
|
748
|
+
try {
|
|
749
|
+
const packet = JSON.parse(rawData);
|
|
750
|
+
if (this._onPhysicsStep) {
|
|
751
|
+
const inputs = /* @__PURE__ */ new Map();
|
|
752
|
+
inputs.set(peerId, packet.i);
|
|
753
|
+
this._onPhysicsStep(inputs, packet.t, false);
|
|
754
|
+
}
|
|
755
|
+
const prevTick = this._clientLastProcessedTick.get(peerId) ?? -1;
|
|
756
|
+
this._clientLastProcessedTick.set(peerId, Math.max(prevTick, packet.t));
|
|
757
|
+
} catch (err) {
|
|
758
|
+
if (typeof console !== "undefined") console.debug("[CarverJS] Malformed input packet:", err);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
// ── Private: Client-side ──
|
|
763
|
+
_setupClientListeners() {
|
|
764
|
+
this._stateChannel.onReceive((data) => {
|
|
765
|
+
try {
|
|
766
|
+
const packet = (0, import_msgpackr.unpack)(data);
|
|
767
|
+
const entities = this._codec.deserialize(packet.s);
|
|
768
|
+
const serverTick = packet.t;
|
|
769
|
+
const lastInputTick = packet.li;
|
|
770
|
+
this._serverTick = serverTick;
|
|
771
|
+
this._tickKeeper.setServerTick(serverTick);
|
|
772
|
+
this._serverState.clear();
|
|
773
|
+
for (const entity of entities) {
|
|
774
|
+
this._serverState.set(entity.id, entity);
|
|
775
|
+
}
|
|
776
|
+
this._reconcile(lastInputTick);
|
|
777
|
+
} catch (err) {
|
|
778
|
+
if (typeof console !== "undefined") console.debug("[CarverJS] Malformed state packet:", err);
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
_reconcile(lastInputTick) {
|
|
783
|
+
this._inputBuffer = this._inputBuffer.filter((entry) => entry.tick > lastInputTick);
|
|
784
|
+
let needsRollback = false;
|
|
785
|
+
let maxError = 0;
|
|
786
|
+
for (const [id, serverEntity] of this._serverState) {
|
|
787
|
+
const predicted = this._predictedState.get(id);
|
|
788
|
+
if (!predicted) continue;
|
|
789
|
+
const error = this._computeError(predicted, serverEntity);
|
|
790
|
+
maxError = Math.max(maxError, error);
|
|
791
|
+
if (error > this._options.maxErrorPerFrame) {
|
|
792
|
+
needsRollback = true;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (maxError > this._options.snapThreshold) {
|
|
796
|
+
this._predictedState = new Map(this._serverState);
|
|
797
|
+
this._errorCorrections.clear();
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (needsRollback) {
|
|
801
|
+
const oldPositions = /* @__PURE__ */ new Map();
|
|
802
|
+
for (const [id, entity] of this._predictedState) {
|
|
803
|
+
oldPositions.set(id, {
|
|
804
|
+
x: entity.x,
|
|
805
|
+
y: entity.y,
|
|
806
|
+
z: "z" in entity ? entity.z : 0
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
this._predictedState = new Map(this._serverState);
|
|
810
|
+
if (this._onPhysicsStep) {
|
|
811
|
+
for (const entry of this._inputBuffer) {
|
|
812
|
+
const inputs = /* @__PURE__ */ new Map();
|
|
813
|
+
inputs.set(this._transport.peerId, entry.input);
|
|
814
|
+
this._onPhysicsStep(inputs, entry.tick, true);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
for (const [id, newEntity] of this._predictedState) {
|
|
818
|
+
const oldPos = oldPositions.get(id);
|
|
819
|
+
if (oldPos) {
|
|
820
|
+
this._errorCorrections.set(id, {
|
|
821
|
+
x: oldPos.x - newEntity.x,
|
|
822
|
+
y: oldPos.y - newEntity.y,
|
|
823
|
+
z: oldPos.z - ("z" in newEntity ? newEntity.z : 0)
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
_computeError(predicted, server) {
|
|
830
|
+
const dx = predicted.x - server.x;
|
|
831
|
+
const dy = predicted.y - server.y;
|
|
832
|
+
let dz = 0;
|
|
833
|
+
if ("z" in predicted && "z" in server) {
|
|
834
|
+
dz = predicted.z - server.z;
|
|
835
|
+
}
|
|
836
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
840
|
+
0 && (module.exports = {
|
|
841
|
+
EventSync,
|
|
842
|
+
PredictionSync,
|
|
843
|
+
SnapshotSync
|
|
844
|
+
});
|
|
845
|
+
//# sourceMappingURL=sync.js.map
|