@carverjs/multiplayer 0.0.1 → 0.0.2

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.
Files changed (42) hide show
  1. package/dist/InputBuffer-J6XT_Tt0.d.mts +61 -0
  2. package/dist/InputBuffer-V7XfHbc6.d.ts +61 -0
  3. package/dist/{NetworkManager-nvVAOr1O.d.ts → NetworkManager-D-DxFgdM.d.mts} +66 -14
  4. package/dist/{NetworkManager-DrKM2tEx.d.mts → NetworkManager-DH9uGVMg.d.ts} +66 -14
  5. package/dist/{chunk-UD6FDZMX.mjs → chunk-CBTAOVXP.mjs} +34 -3
  6. package/dist/chunk-CBTAOVXP.mjs.map +1 -0
  7. package/dist/{chunk-EO3YNPRQ.mjs → chunk-Q25TJEY4.mjs} +494 -204
  8. package/dist/chunk-Q25TJEY4.mjs.map +1 -0
  9. package/dist/{chunk-3KT73N2S.mjs → chunk-UKEFWQ76.mjs} +0 -0
  10. package/dist/chunk-UKEFWQ76.mjs.map +1 -0
  11. package/dist/{firebase-CPu87KA0.d.ts → firebase-B5MgLlHk.d.ts} +6 -1
  12. package/dist/{firebase-PE6MxGdJ.d.mts → firebase-GrbVrNgs.d.mts} +6 -1
  13. package/dist/index.d.mts +27 -6
  14. package/dist/index.d.ts +27 -6
  15. package/dist/index.js +744 -245
  16. package/dist/index.js.map +1 -1
  17. package/dist/index.mjs +172 -37
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/strategy.d.mts +2 -2
  20. package/dist/strategy.d.ts +2 -2
  21. package/dist/strategy.js +33 -2
  22. package/dist/strategy.js.map +1 -1
  23. package/dist/strategy.mjs +1 -1
  24. package/dist/sync.d.mts +134 -50
  25. package/dist/sync.d.ts +134 -50
  26. package/dist/sync.js +499 -205
  27. package/dist/sync.js.map +1 -1
  28. package/dist/sync.mjs +15 -3
  29. package/dist/transport.d.mts +0 -0
  30. package/dist/transport.d.ts +0 -0
  31. package/dist/transport.js +0 -0
  32. package/dist/transport.js.map +1 -1
  33. package/dist/transport.mjs +2 -2
  34. package/dist/{types-5LHBOW08.d.mts → types-hNfCIBzj.d.mts} +7 -0
  35. package/dist/{types-5LHBOW08.d.ts → types-hNfCIBzj.d.ts} +7 -0
  36. package/dist/types.d.mts +2 -2
  37. package/dist/types.d.ts +2 -2
  38. package/dist/types.js.map +1 -1
  39. package/package.json +26 -5
  40. package/dist/chunk-3KT73N2S.mjs.map +0 -1
  41. package/dist/chunk-EO3YNPRQ.mjs.map +0 -1
  42. 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-3KT73N2S.mjs";
3
+ } from "./chunk-UKEFWQ76.mjs";
4
4
  import {
5
5
  FirebaseStrategy,
6
6
  MqttStrategy
7
- } from "./chunk-UD6FDZMX.mjs";
7
+ } from "./chunk-CBTAOVXP.mjs";
8
8
  import {
9
9
  EventSync,
10
+ InputBuffer,
10
11
  PredictionSync,
11
- SnapshotSync
12
- } from "./chunk-EO3YNPRQ.mjs";
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
- strategyRef.current?.destroy();
520
- strategyRef.current = null;
521
- managerRef.current?.destroy();
522
- managerRef.current = null;
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 (isHost) {
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
- if (mode === "snapshot" || mode === "prediction") {
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
- if (mode === "snapshot" && snapshotSyncRef.current) {
1193
- const renderTime = performance.now();
1194
- const interpolated = snapshotSyncRef.current.clientInterpolate(renderTime);
1195
- applyStatesToActors(interpolated, actorRegistry.current, is2D);
1196
- } else if (mode === "prediction" && predictionSyncRef.current) {
1197
- const predicted = predictionSyncRef.current.predictedState;
1198
- const smoothed = predictionSyncRef.current.applyErrorSmoothing(predicted);
1199
- applyStatesToActors(smoothed, actorRegistry.current, is2D);
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,