@carverjs/multiplayer 0.0.1 → 0.0.3
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/README.md +154 -0
- package/dist/InputBuffer-J6XT_Tt0.d.mts +61 -0
- package/dist/InputBuffer-V7XfHbc6.d.ts +61 -0
- package/dist/{NetworkManager-nvVAOr1O.d.ts → NetworkManager-D-DxFgdM.d.mts} +66 -14
- package/dist/{NetworkManager-DrKM2tEx.d.mts → NetworkManager-DH9uGVMg.d.ts} +66 -14
- package/dist/{chunk-UD6FDZMX.mjs → chunk-GOTAQDBJ.mjs} +47 -4
- package/dist/chunk-GOTAQDBJ.mjs.map +1 -0
- package/dist/{chunk-3KT73N2S.mjs → chunk-LPNEP2VH.mjs} +0 -0
- package/dist/chunk-LPNEP2VH.mjs.map +1 -0
- package/dist/{chunk-EO3YNPRQ.mjs → chunk-Q25TJEY4.mjs} +494 -204
- package/dist/chunk-Q25TJEY4.mjs.map +1 -0
- package/dist/{firebase-CPu87KA0.d.ts → firebase-B5MgLlHk.d.ts} +6 -1
- package/dist/{firebase-PE6MxGdJ.d.mts → firebase-GrbVrNgs.d.mts} +6 -1
- package/dist/index.d.mts +27 -6
- package/dist/index.d.ts +27 -6
- package/dist/index.js +821 -258
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +172 -37
- package/dist/index.mjs.map +1 -1
- package/dist/strategy.d.mts +2 -2
- package/dist/strategy.d.ts +2 -2
- package/dist/strategy.js +46 -3
- package/dist/strategy.js.map +1 -1
- package/dist/strategy.mjs +1 -1
- package/dist/sync.d.mts +134 -50
- package/dist/sync.d.ts +134 -50
- package/dist/sync.js +499 -205
- package/dist/sync.js.map +1 -1
- package/dist/sync.mjs +15 -3
- package/dist/transport.d.mts +0 -0
- package/dist/transport.d.ts +0 -0
- package/dist/transport.js +0 -0
- package/dist/transport.js.map +1 -1
- package/dist/transport.mjs +2 -2
- package/dist/{types-5LHBOW08.d.mts → types-hNfCIBzj.d.mts} +7 -0
- package/dist/{types-5LHBOW08.d.ts → types-hNfCIBzj.d.ts} +7 -0
- package/dist/types.d.mts +2 -2
- package/dist/types.d.ts +2 -2
- package/dist/types.js.map +1 -1
- package/package.json +26 -5
- package/dist/chunk-3KT73N2S.mjs.map +0 -1
- package/dist/chunk-EO3YNPRQ.mjs.map +0 -1
- package/dist/chunk-UD6FDZMX.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
WebRTCTransport
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-LPNEP2VH.mjs";
|
|
4
4
|
import {
|
|
5
5
|
FirebaseStrategy,
|
|
6
6
|
MqttStrategy
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-GOTAQDBJ.mjs";
|
|
8
8
|
import {
|
|
9
9
|
EventSync,
|
|
10
|
+
InputBuffer,
|
|
10
11
|
PredictionSync,
|
|
11
|
-
SnapshotSync
|
|
12
|
-
|
|
12
|
+
SnapshotSync,
|
|
13
|
+
computeJustPressed,
|
|
14
|
+
quatInvert,
|
|
15
|
+
quatMultiply
|
|
16
|
+
} from "./chunk-Q25TJEY4.mjs";
|
|
13
17
|
|
|
14
18
|
// src/components/MultiplayerProvider.ts
|
|
15
19
|
import { createElement, useRef, useEffect } from "react";
|
|
@@ -54,6 +58,11 @@ var _TickKeeper = class _TickKeeper {
|
|
|
54
58
|
this._serverTick = serverTick;
|
|
55
59
|
this._updateDriftCorrection();
|
|
56
60
|
}
|
|
61
|
+
/** Hard-set the local tick (prediction rollback snap). Does not touch the accumulator. */
|
|
62
|
+
snapTick(tick) {
|
|
63
|
+
this._tick = tick;
|
|
64
|
+
this._updateDriftCorrection();
|
|
65
|
+
}
|
|
57
66
|
/**
|
|
58
67
|
* Accumulate time and return the number of fixed ticks to process.
|
|
59
68
|
* Call this once per render frame with the raw frame delta.
|
|
@@ -178,15 +187,16 @@ var Codec = class {
|
|
|
178
187
|
}
|
|
179
188
|
return changed.length > 0 ? changed : null;
|
|
180
189
|
}
|
|
181
|
-
/** Serialize a delta snapshot packet */
|
|
182
|
-
serializeDelta(tick, baseTick, current, baseline) {
|
|
190
|
+
/** Serialize a delta snapshot packet (optionally embedding the host's own input as `hi`) */
|
|
191
|
+
serializeDelta(tick, baseTick, current, baseline, hostInput) {
|
|
183
192
|
const delta = this.computeDelta(current, baseline);
|
|
184
193
|
if (!delta) return null;
|
|
185
194
|
const packet = {
|
|
186
195
|
t: tick,
|
|
187
196
|
b: baseline ? baseTick : -1,
|
|
188
197
|
// -1 = keyframe
|
|
189
|
-
s: this.serialize(delta)
|
|
198
|
+
s: this.serialize(delta),
|
|
199
|
+
...hostInput !== void 0 ? { hi: hostInput } : {}
|
|
190
200
|
};
|
|
191
201
|
return pack(packet);
|
|
192
202
|
}
|
|
@@ -196,7 +206,8 @@ var Codec = class {
|
|
|
196
206
|
return {
|
|
197
207
|
tick: packet.t,
|
|
198
208
|
baseTick: packet.b,
|
|
199
|
-
entities: this.deserialize(packet.s)
|
|
209
|
+
entities: this.deserialize(packet.s),
|
|
210
|
+
hostInput: packet.hi
|
|
200
211
|
};
|
|
201
212
|
}
|
|
202
213
|
_hasChanged(current, prev) {
|
|
@@ -508,6 +519,7 @@ function MultiplayerProvider({
|
|
|
508
519
|
}) {
|
|
509
520
|
const managerRef = useRef(null);
|
|
510
521
|
const strategyRef = useRef(null);
|
|
522
|
+
const destroyTimerRef = useRef(null);
|
|
511
523
|
if (!managerRef.current) {
|
|
512
524
|
managerRef.current = new NetworkManager();
|
|
513
525
|
}
|
|
@@ -515,11 +527,18 @@ function MultiplayerProvider({
|
|
|
515
527
|
strategyRef.current = createStrategy(appId, strategyConfig);
|
|
516
528
|
}
|
|
517
529
|
useEffect(() => {
|
|
530
|
+
if (destroyTimerRef.current) {
|
|
531
|
+
clearTimeout(destroyTimerRef.current);
|
|
532
|
+
destroyTimerRef.current = null;
|
|
533
|
+
}
|
|
518
534
|
return () => {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
535
|
+
destroyTimerRef.current = setTimeout(() => {
|
|
536
|
+
destroyTimerRef.current = null;
|
|
537
|
+
strategyRef.current?.destroy();
|
|
538
|
+
strategyRef.current = null;
|
|
539
|
+
managerRef.current?.destroy();
|
|
540
|
+
managerRef.current = null;
|
|
541
|
+
}, 0);
|
|
523
542
|
};
|
|
524
543
|
}, []);
|
|
525
544
|
const value = {
|
|
@@ -708,7 +727,7 @@ function announcementToRoom(ann) {
|
|
|
708
727
|
isPrivate: ann.isPrivate,
|
|
709
728
|
metadata: ann.metadata,
|
|
710
729
|
createdAt: ann.createdAt,
|
|
711
|
-
state: "lobby"
|
|
730
|
+
state: ann.state ?? "lobby"
|
|
712
731
|
};
|
|
713
732
|
}
|
|
714
733
|
function useLobby(options) {
|
|
@@ -754,7 +773,7 @@ function useLobby(options) {
|
|
|
754
773
|
hostId: strategy.selfId,
|
|
755
774
|
playerCount: 0,
|
|
756
775
|
maxPlayers: config.maxPlayers ?? 8,
|
|
757
|
-
gameMode: config.metadata?.gameMode,
|
|
776
|
+
gameMode: config.gameMode ?? config.metadata?.gameMode,
|
|
758
777
|
isPrivate: config.isPrivate ?? false,
|
|
759
778
|
metadata: config.metadata ?? {},
|
|
760
779
|
createdAt: Date.now(),
|
|
@@ -1037,6 +1056,34 @@ function applyState3D(ref, state) {
|
|
|
1037
1056
|
}
|
|
1038
1057
|
}
|
|
1039
1058
|
}
|
|
1059
|
+
function applyStateHard2D(ref, state) {
|
|
1060
|
+
applyState2D(ref, state);
|
|
1061
|
+
if (ref.rigidBody) {
|
|
1062
|
+
try {
|
|
1063
|
+
if (typeof ref.rigidBody.setLinvel === "function") {
|
|
1064
|
+
ref.rigidBody.setLinvel({ x: state.vx, y: state.vy }, true);
|
|
1065
|
+
}
|
|
1066
|
+
if (typeof ref.rigidBody.setAngvel === "function") {
|
|
1067
|
+
ref.rigidBody.setAngvel(state.va, true);
|
|
1068
|
+
}
|
|
1069
|
+
} catch {
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
function applyStateHard3D(ref, state) {
|
|
1074
|
+
applyState3D(ref, state);
|
|
1075
|
+
if (ref.rigidBody) {
|
|
1076
|
+
try {
|
|
1077
|
+
if (typeof ref.rigidBody.setLinvel === "function") {
|
|
1078
|
+
ref.rigidBody.setLinvel({ x: state.vx, y: state.vy, z: state.vz }, true);
|
|
1079
|
+
}
|
|
1080
|
+
if (typeof ref.rigidBody.setAngvel === "function") {
|
|
1081
|
+
ref.rigidBody.setAngvel({ x: state.wx, y: state.wy, z: state.wz }, true);
|
|
1082
|
+
}
|
|
1083
|
+
} catch {
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1040
1087
|
function applyEntityState(ref, state, is2D) {
|
|
1041
1088
|
if (is2D) {
|
|
1042
1089
|
applyState2D(ref, state);
|
|
@@ -1052,6 +1099,10 @@ function applyStatesToActors(states, registry, is2D) {
|
|
|
1052
1099
|
applyEntityState(ref, state, is2D);
|
|
1053
1100
|
}
|
|
1054
1101
|
}
|
|
1102
|
+
var IDENTITY_QUAT = { x: 0, y: 0, z: 0, w: 1 };
|
|
1103
|
+
function isIdentityQuat(q) {
|
|
1104
|
+
return q.x === 0 && q.y === 0 && q.z === 0 && q.w === 1;
|
|
1105
|
+
}
|
|
1055
1106
|
function useMultiplayer(options = {}) {
|
|
1056
1107
|
const { networkManager } = useMultiplayerContext();
|
|
1057
1108
|
const mode = options.mode ?? networkManager.syncMode;
|
|
@@ -1067,6 +1118,7 @@ function useMultiplayer(options = {}) {
|
|
|
1067
1118
|
const [drift, setDrift] = useState4(0);
|
|
1068
1119
|
const actorRegistry = useRef4(getActorRegistry());
|
|
1069
1120
|
const wasHostRef = useRef4(null);
|
|
1121
|
+
const appliedErrorsRef = useRef4(/* @__PURE__ */ new Map());
|
|
1070
1122
|
const buildSnapshotOpts = useCallback5(() => {
|
|
1071
1123
|
return {
|
|
1072
1124
|
broadcastRate: options.broadcastRate,
|
|
@@ -1083,6 +1135,36 @@ function useMultiplayer(options = {}) {
|
|
|
1083
1135
|
options.interpolation?.method,
|
|
1084
1136
|
options.interpolation?.extrapolateMs
|
|
1085
1137
|
]);
|
|
1138
|
+
const undoAppliedErrorOffsets = useCallback5(() => {
|
|
1139
|
+
const registry = actorRegistry.current;
|
|
1140
|
+
for (const [id, e] of appliedErrorsRef.current) {
|
|
1141
|
+
const ref = registry.get(id);
|
|
1142
|
+
if (!ref) {
|
|
1143
|
+
appliedErrorsRef.current.delete(id);
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
const pos = ref.object3D.position;
|
|
1147
|
+
const untouched = Math.abs(pos.x - e.px) < 1e-9 && Math.abs(pos.y - e.py) < 1e-9 && Math.abs(pos.z - e.pz) < 1e-9;
|
|
1148
|
+
if (untouched) {
|
|
1149
|
+
pos.x -= e.x;
|
|
1150
|
+
pos.y -= e.y;
|
|
1151
|
+
pos.z -= e.z;
|
|
1152
|
+
if (e.a !== 0) {
|
|
1153
|
+
ref.object3D.rotation.z -= e.a;
|
|
1154
|
+
}
|
|
1155
|
+
if (!isIdentityQuat(e.q)) {
|
|
1156
|
+
const inv = quatInvert(e.q);
|
|
1157
|
+
const cur = ref.object3D.quaternion;
|
|
1158
|
+
const r = quatMultiply(inv, { x: cur.x, y: cur.y, z: cur.z, w: cur.w });
|
|
1159
|
+
cur.set(r.x, r.y, r.z, r.w);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
appliedErrorsRef.current.clear();
|
|
1164
|
+
}, []);
|
|
1165
|
+
const setInput = useCallback5((input) => {
|
|
1166
|
+
predictionSyncRef.current?.setInput(input);
|
|
1167
|
+
}, []);
|
|
1086
1168
|
useEffect5(() => {
|
|
1087
1169
|
const transport = networkManager.transport;
|
|
1088
1170
|
if (!transport) return;
|
|
@@ -1111,15 +1193,30 @@ function useMultiplayer(options = {}) {
|
|
|
1111
1193
|
);
|
|
1112
1194
|
}
|
|
1113
1195
|
if (mode === "prediction") {
|
|
1196
|
+
const driver = {
|
|
1197
|
+
captureState: () => buildEntityMap(actorRegistry.current.getNetworked(), is2DRef.current ?? true),
|
|
1198
|
+
applyState: (entities) => {
|
|
1199
|
+
const is2D = is2DRef.current ?? true;
|
|
1200
|
+
for (const state of entities) {
|
|
1201
|
+
if (state.c && state.c.__removed) continue;
|
|
1202
|
+
const ref = actorRegistry.current.get(state.id);
|
|
1203
|
+
if (!ref) continue;
|
|
1204
|
+
if (is2D) applyStateHard2D(ref, state);
|
|
1205
|
+
else applyStateHard3D(ref, state);
|
|
1206
|
+
}
|
|
1207
|
+
},
|
|
1208
|
+
...options.stepWorld ? { stepWorld: options.stepWorld } : {}
|
|
1209
|
+
};
|
|
1114
1210
|
predictionSyncRef.current = new PredictionSync(
|
|
1115
1211
|
transport,
|
|
1116
|
-
networkManager.codec,
|
|
1117
1212
|
networkManager.tickKeeper,
|
|
1213
|
+
snapshotSyncRef.current,
|
|
1118
1214
|
options.prediction
|
|
1119
1215
|
);
|
|
1120
1216
|
if (options.onPhysicsStep) {
|
|
1121
1217
|
predictionSyncRef.current.setPhysicsStep(options.onPhysicsStep);
|
|
1122
1218
|
}
|
|
1219
|
+
predictionSyncRef.current.setWorldDriver(driver);
|
|
1123
1220
|
}
|
|
1124
1221
|
wasHostRef.current = networkManager.isHost;
|
|
1125
1222
|
setIsActive(true);
|
|
@@ -1148,9 +1245,10 @@ function useMultiplayer(options = {}) {
|
|
|
1148
1245
|
snapshotSyncRef.current = null;
|
|
1149
1246
|
predictionSyncRef.current = null;
|
|
1150
1247
|
networkSimulatorRef.current = null;
|
|
1248
|
+
appliedErrorsRef.current.clear();
|
|
1151
1249
|
setIsActive(false);
|
|
1152
1250
|
};
|
|
1153
|
-
}, [networkManager, mode, buildSnapshotOpts, options.prediction, options.onPhysicsStep, options.debug]);
|
|
1251
|
+
}, [networkManager, mode, buildSnapshotOpts, options.prediction, options.onPhysicsStep, options.stepWorld, options.debug]);
|
|
1154
1252
|
useEffect5(() => {
|
|
1155
1253
|
const unsub = networkManager.onConnectionStateChange(() => {
|
|
1156
1254
|
setNetworkQuality(networkManager.networkQuality);
|
|
@@ -1170,33 +1268,67 @@ function useMultiplayer(options = {}) {
|
|
|
1170
1268
|
}
|
|
1171
1269
|
const is2D = is2DRef.current ?? true;
|
|
1172
1270
|
if (mode === "events") return;
|
|
1271
|
+
if (mode === "prediction" && predictionSyncRef.current) {
|
|
1272
|
+
undoAppliedErrorOffsets();
|
|
1273
|
+
predictionSyncRef.current.beginFrame();
|
|
1274
|
+
}
|
|
1173
1275
|
const ticksThisFrame = tickKeeper.update(delta);
|
|
1174
1276
|
for (let i = 0; i < ticksThisFrame; i++) {
|
|
1175
1277
|
const currentTick = tickKeeper.tick - (ticksThisFrame - 1 - i);
|
|
1176
|
-
if (
|
|
1278
|
+
if (mode === "prediction" && predictionSyncRef.current) {
|
|
1279
|
+
predictionSyncRef.current.tick(currentTick);
|
|
1280
|
+
if (isHost) {
|
|
1281
|
+
const entities = buildEntityMap(actorRegistry.current.getNetworked(), is2D);
|
|
1282
|
+
snapshotSyncRef.current?.hostTick(
|
|
1283
|
+
currentTick,
|
|
1284
|
+
entities,
|
|
1285
|
+
tickKeeper.tickDelta,
|
|
1286
|
+
predictionSyncRef.current.getLocalInput(currentTick)
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
} else if (mode === "snapshot" && isHost) {
|
|
1177
1290
|
const networked = actorRegistry.current.getNetworked();
|
|
1178
1291
|
const entities = buildEntityMap(networked, is2D);
|
|
1179
|
-
|
|
1180
|
-
snapshotSyncRef.current?.hostTick(currentTick, entities, tickKeeper.tickDelta);
|
|
1181
|
-
}
|
|
1182
|
-
if (mode === "prediction") {
|
|
1183
|
-
predictionSyncRef.current?.hostTick(currentTick, entities, tickKeeper.tickDelta);
|
|
1184
|
-
}
|
|
1185
|
-
} else {
|
|
1186
|
-
if (mode === "prediction" && predictionSyncRef.current) {
|
|
1187
|
-
predictionSyncRef.current.clientTick(currentTick);
|
|
1188
|
-
}
|
|
1292
|
+
snapshotSyncRef.current?.hostTick(currentTick, entities, tickKeeper.tickDelta);
|
|
1189
1293
|
}
|
|
1190
1294
|
}
|
|
1191
|
-
if (!isHost) {
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1295
|
+
if (!isHost && mode === "snapshot" && snapshotSyncRef.current) {
|
|
1296
|
+
const renderTime = performance.now();
|
|
1297
|
+
const interpolated = snapshotSyncRef.current.clientInterpolate(renderTime);
|
|
1298
|
+
applyStatesToActors(interpolated, actorRegistry.current, is2D);
|
|
1299
|
+
}
|
|
1300
|
+
if (mode === "prediction" && predictionSyncRef.current) {
|
|
1301
|
+
const offsets = predictionSyncRef.current.getRenderErrorOffsets();
|
|
1302
|
+
for (const [id, off] of offsets) {
|
|
1303
|
+
const ref = actorRegistry.current.get(id);
|
|
1304
|
+
if (!ref) continue;
|
|
1305
|
+
const pos = ref.object3D.position;
|
|
1306
|
+
pos.x += off.x;
|
|
1307
|
+
pos.y += off.y;
|
|
1308
|
+
if (!is2D) pos.z += off.z;
|
|
1309
|
+
let appliedA = 0;
|
|
1310
|
+
let appliedQ = IDENTITY_QUAT;
|
|
1311
|
+
if (is2D) {
|
|
1312
|
+
ref.object3D.rotation.z += off.a;
|
|
1313
|
+
appliedA = off.a;
|
|
1314
|
+
} else {
|
|
1315
|
+
appliedQ = { x: off.qx, y: off.qy, z: off.qz, w: off.qw };
|
|
1316
|
+
if (!isIdentityQuat(appliedQ)) {
|
|
1317
|
+
const cur = ref.object3D.quaternion;
|
|
1318
|
+
const r = quatMultiply(appliedQ, { x: cur.x, y: cur.y, z: cur.z, w: cur.w });
|
|
1319
|
+
cur.set(r.x, r.y, r.z, r.w);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
appliedErrorsRef.current.set(id, {
|
|
1323
|
+
x: off.x,
|
|
1324
|
+
y: off.y,
|
|
1325
|
+
z: is2D ? 0 : off.z,
|
|
1326
|
+
a: appliedA,
|
|
1327
|
+
q: appliedQ,
|
|
1328
|
+
px: pos.x,
|
|
1329
|
+
py: pos.y,
|
|
1330
|
+
pz: pos.z
|
|
1331
|
+
});
|
|
1200
1332
|
}
|
|
1201
1333
|
}
|
|
1202
1334
|
if (ticksThisFrame > 0) {
|
|
@@ -1216,7 +1348,8 @@ function useMultiplayer(options = {}) {
|
|
|
1216
1348
|
tick,
|
|
1217
1349
|
serverTick,
|
|
1218
1350
|
drift,
|
|
1219
|
-
syncEngine: mode
|
|
1351
|
+
syncEngine: mode,
|
|
1352
|
+
setInput
|
|
1220
1353
|
};
|
|
1221
1354
|
}
|
|
1222
1355
|
|
|
@@ -1727,11 +1860,13 @@ var InterestManager = class {
|
|
|
1727
1860
|
export {
|
|
1728
1861
|
DebugOverlay,
|
|
1729
1862
|
FirebaseStrategy,
|
|
1863
|
+
InputBuffer,
|
|
1730
1864
|
InterestManager,
|
|
1731
1865
|
MqttStrategy,
|
|
1732
1866
|
MultiplayerBridge,
|
|
1733
1867
|
MultiplayerProvider,
|
|
1734
1868
|
NetworkSimulator,
|
|
1869
|
+
computeJustPressed,
|
|
1735
1870
|
useHost,
|
|
1736
1871
|
useLobby,
|
|
1737
1872
|
useMultiplayer,
|