@series-inc/rundot-syncplay 5.23.0 → 5.24.0-beta.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.
Files changed (131) hide show
  1. package/README.md +53 -12
  2. package/dist/authority-room.d.ts +11 -0
  3. package/dist/authority-room.js +43 -0
  4. package/dist/browser.d.ts +11 -1
  5. package/dist/browser.js +5 -0
  6. package/dist/checksum-basis.d.ts +4 -0
  7. package/dist/checksum-basis.js +17 -0
  8. package/dist/cjs/authority-room.js +43 -0
  9. package/dist/cjs/browser.js +18 -2
  10. package/dist/cjs/checksum-basis.js +22 -0
  11. package/dist/cjs/collider-cooking.js +834 -0
  12. package/dist/cjs/deterministic-session.js +2 -0
  13. package/dist/cjs/engine-complete.js +1 -4
  14. package/dist/cjs/engine-parity-matrix.js +41 -9
  15. package/dist/cjs/engine-parity.js +2 -2
  16. package/dist/cjs/index.js +24 -2
  17. package/dist/cjs/input-authority.js +34 -2
  18. package/dist/cjs/instant-replay.js +4 -9
  19. package/dist/cjs/local-authority-server-runner.js +3 -1
  20. package/dist/cjs/local-authority-wire.js +20 -1
  21. package/dist/cjs/match-log.js +27 -0
  22. package/dist/cjs/movement3d.js +17 -1
  23. package/dist/cjs/networked-client.js +70 -20
  24. package/dist/cjs/node.js +23 -1
  25. package/dist/cjs/offline-session.js +52 -24
  26. package/dist/cjs/physics-cert.js +8 -3
  27. package/dist/cjs/physics3d-heightfield.js +51 -0
  28. package/dist/cjs/physics3d-hull.js +381 -0
  29. package/dist/cjs/physics3d-joints.js +255 -49
  30. package/dist/cjs/physics3d-mass.js +131 -0
  31. package/dist/cjs/physics3d-recipes.js +197 -0
  32. package/dist/cjs/physics3d-shared.js +11 -1
  33. package/dist/cjs/physics3d-solver.js +845 -98
  34. package/dist/cjs/physics3d-stacking.js +154 -0
  35. package/dist/cjs/physics3d-toi.js +274 -0
  36. package/dist/cjs/physics3d-vehicle.js +35 -10
  37. package/dist/cjs/physics3d.js +601 -38
  38. package/dist/cjs/replay.js +25 -1
  39. package/dist/cjs/runtime-support.js +71 -0
  40. package/dist/cjs/sdk-offline.js +9 -1
  41. package/dist/cjs/sdk-session.js +8 -0
  42. package/dist/cjs/secret-authority.js +647 -0
  43. package/dist/cjs/secret-client.js +615 -0
  44. package/dist/cjs/secret-command-classification.js +8 -0
  45. package/dist/cjs/secret-config.js +211 -0
  46. package/dist/cjs/secret-crypto.js +208 -0
  47. package/dist/cjs/secret-protocol.js +238 -0
  48. package/dist/cjs/session-build.js +7 -4
  49. package/dist/cjs/session-promotion.js +5 -5
  50. package/dist/cjs/synctest.js +49 -0
  51. package/dist/cjs/testing.js +4 -1
  52. package/dist/cjs/tools/static-checker.js +61 -13
  53. package/dist/cli.js +193 -48
  54. package/dist/collider-cooking.d.ts +141 -0
  55. package/dist/collider-cooking.js +829 -0
  56. package/dist/deterministic-primitives.d.ts +1 -0
  57. package/dist/deterministic-session.js +2 -0
  58. package/dist/engine-complete.js +1 -4
  59. package/dist/engine-parity-matrix.js +41 -9
  60. package/dist/engine-parity.js +2 -2
  61. package/dist/index.d.ts +12 -1
  62. package/dist/index.js +6 -0
  63. package/dist/input-authority.d.ts +3 -0
  64. package/dist/input-authority.js +35 -3
  65. package/dist/instant-replay.d.ts +2 -2
  66. package/dist/instant-replay.js +4 -9
  67. package/dist/local-authority-server-runner.d.ts +1 -0
  68. package/dist/local-authority-server-runner.js +2 -1
  69. package/dist/local-authority-wire.js +20 -1
  70. package/dist/match-log.d.ts +4 -1
  71. package/dist/match-log.js +27 -0
  72. package/dist/movement3d.js +18 -2
  73. package/dist/networked-client.d.ts +8 -0
  74. package/dist/networked-client.js +70 -20
  75. package/dist/node.d.ts +7 -0
  76. package/dist/node.js +6 -0
  77. package/dist/offline-session.js +53 -25
  78. package/dist/physics-cert.js +8 -3
  79. package/dist/physics3d-heightfield.d.ts +17 -0
  80. package/dist/physics3d-heightfield.js +47 -0
  81. package/dist/physics3d-hull.d.ts +53 -0
  82. package/dist/physics3d-hull.js +376 -0
  83. package/dist/physics3d-joints.d.ts +13 -1
  84. package/dist/physics3d-joints.js +256 -50
  85. package/dist/physics3d-mass.d.ts +11 -0
  86. package/dist/physics3d-mass.js +126 -0
  87. package/dist/physics3d-recipes.d.ts +29 -0
  88. package/dist/physics3d-recipes.js +193 -0
  89. package/dist/physics3d-shared.d.ts +1 -0
  90. package/dist/physics3d-shared.js +10 -1
  91. package/dist/physics3d-solver.d.ts +78 -2
  92. package/dist/physics3d-solver.js +842 -98
  93. package/dist/physics3d-stacking.d.ts +26 -0
  94. package/dist/physics3d-stacking.js +147 -0
  95. package/dist/physics3d-toi.d.ts +79 -0
  96. package/dist/physics3d-toi.js +267 -0
  97. package/dist/physics3d-vehicle.js +35 -10
  98. package/dist/physics3d.d.ts +28 -3
  99. package/dist/physics3d.js +601 -40
  100. package/dist/replay.d.ts +5 -0
  101. package/dist/replay.js +25 -1
  102. package/dist/reports.d.ts +6 -1
  103. package/dist/runtime-support.d.ts +60 -0
  104. package/dist/runtime-support.js +66 -0
  105. package/dist/sdk-offline.d.ts +4 -0
  106. package/dist/sdk-offline.js +9 -1
  107. package/dist/sdk-session.d.ts +4 -0
  108. package/dist/sdk-session.js +8 -0
  109. package/dist/secret-authority.d.ts +90 -0
  110. package/dist/secret-authority.js +643 -0
  111. package/dist/secret-client.d.ts +89 -0
  112. package/dist/secret-client.js +611 -0
  113. package/dist/secret-command-classification.d.ts +1 -0
  114. package/dist/secret-command-classification.js +5 -0
  115. package/dist/secret-config.d.ts +45 -0
  116. package/dist/secret-config.js +206 -0
  117. package/dist/secret-crypto.d.ts +29 -0
  118. package/dist/secret-crypto.js +190 -0
  119. package/dist/secret-protocol.d.ts +181 -0
  120. package/dist/secret-protocol.js +230 -0
  121. package/dist/session-build.d.ts +16 -0
  122. package/dist/session-build.js +7 -4
  123. package/dist/session-promotion.js +5 -5
  124. package/dist/synctest.d.ts +1 -1
  125. package/dist/synctest.js +49 -0
  126. package/dist/testing.d.ts +2 -0
  127. package/dist/testing.js +1 -0
  128. package/dist/tools/static-checker.d.ts +11 -0
  129. package/dist/tools/static-checker.js +60 -13
  130. package/dist/types.d.ts +21 -2
  131. package/package.json +8 -3
@@ -6,6 +6,7 @@ const errors_js_1 = require("./errors.js");
6
6
  const local_authority_wire_js_1 = require("./local-authority-wire.js");
7
7
  const session_snapshot_js_1 = require("./session-snapshot.js");
8
8
  const session_wire_js_1 = require("./session-wire.js");
9
+ const secret_client_js_1 = require("./secret-client.js");
9
10
  const DEFAULT_MSG_TYPE = 'rdm';
10
11
  const DEFAULT_MAX_PREDICTION = 8;
11
12
  const DEFAULT_CHECKSUM_CADENCE_TICKS = 120;
@@ -82,6 +83,9 @@ function createNetworkedSyncplayClient(options) {
82
83
  const ownInputs = new Map();
83
84
  const confirmed = new Map();
84
85
  let lastConfirmedBySlot = [];
86
+ let confirmationQueue;
87
+ let secretFailure;
88
+ let secrets;
85
89
  // M5.5 snapshot late-join state: identities from session-start (descriptor)
86
90
  // and the session config (schema), the donor upload queue, and the joiner's
87
91
  // transfer collector.
@@ -147,12 +151,25 @@ function createNetworkedSyncplayClient(options) {
147
151
  }
148
152
  else if (message.kind === 'confirmed-input') {
149
153
  observeAuthorityTick(message.authorityTick, wallClock());
150
- applyConfirmedFrames(message.frames);
154
+ receiveConfirmedFrames(message.frames);
151
155
  }
152
156
  else if (message.kind === 'time-sync-pong') {
153
157
  handleTimeSyncPong(message.nonce, message.authorityTick);
154
158
  }
155
159
  });
160
+ // Attach the public channel first: both channels may have buffered their
161
+ // join-time greetings, and secret-start is bound to the slot established by
162
+ // session-start. Flushing the private channel first would reject an honest
163
+ // greeting against the initial slot -1.
164
+ secrets = (0, secret_client_js_1.createSyncplaySecretsClient)({
165
+ transport: options.transport,
166
+ connection: () => ({
167
+ playerId: options.playerId ?? options.transport.playerId,
168
+ slot,
169
+ role,
170
+ }),
171
+ ...(options.secretCrypto === undefined ? {} : { crypto: options.secretCrypto }),
172
+ });
156
173
  // ── G1: authority-clock estimation + drift-corrected timescale ──
157
174
  function handleTimeSyncPong(nonce, authorityTick) {
158
175
  const sentAtMs = pendingPings.get(nonce);
@@ -278,7 +295,7 @@ function createNetworkedSyncplayClient(options) {
278
295
  function inputForSlot(targetSlot, tick) {
279
296
  const conf = confirmed.get(tick);
280
297
  if (conf !== undefined) {
281
- return conf[targetSlot];
298
+ return conf.inputs[targetSlot];
282
299
  }
283
300
  if (targetSlot === slot) {
284
301
  // Scheduled by the cursor (possibly delay-shifted). A tick with no
@@ -323,7 +340,8 @@ function createNetworkedSyncplayClient(options) {
323
340
  inputs.push(encoded);
324
341
  active.setInput(s, decodeInputSafe(encoded));
325
342
  }
326
- timeline[tick] = inputs;
343
+ timeline[tick] = { inputs, commands: [] };
344
+ active.recordCommands([]);
327
345
  active.stepFrames(1);
328
346
  predictedThrough = tick + 1;
329
347
  }
@@ -340,11 +358,15 @@ function createNetworkedSyncplayClient(options) {
340
358
  inputs.push(encoded);
341
359
  active.setInput(s, decodeInputSafe(encoded));
342
360
  }
343
- timeline[tick] = inputs;
361
+ const commands = confirmed.get(tick)?.commands ?? [];
362
+ timeline[tick] = { inputs, commands };
363
+ active.recordCommands(commands);
344
364
  active.stepFrames(1);
345
365
  }
346
366
  }
347
367
  function pump() {
368
+ if (secretFailure)
369
+ throw secretFailure;
348
370
  if (session === undefined) {
349
371
  return;
350
372
  }
@@ -354,6 +376,8 @@ function createNetworkedSyncplayClient(options) {
354
376
  }
355
377
  }
356
378
  function pumpPaced(nowMs) {
379
+ if (secretFailure)
380
+ throw secretFailure;
357
381
  flushOutgoingTransfer();
358
382
  maybeSendPing(nowMs);
359
383
  if (session === undefined || frameMs <= 0) {
@@ -394,20 +418,43 @@ function createNetworkedSyncplayClient(options) {
394
418
  return '';
395
419
  }
396
420
  active.restoreToFrame(frame);
397
- return String((0, canonical_js_1.defaultChecksum)(active.getState()));
421
+ return active.getWireStateChecksum(frame);
422
+ }
423
+ function receiveConfirmedFrames(frames) {
424
+ const containsSecretCommands = frames.some((frame) => (frame.commands ?? []).some((command) => (command !== null && typeof command === 'object' && !Array.isArray(command)
425
+ && typeof command.kind === 'string'
426
+ && command.kind.startsWith('syncplay.'))));
427
+ if (!containsSecretCommands && confirmationQueue === undefined) {
428
+ applyConfirmedFrames(frames);
429
+ return;
430
+ }
431
+ const prior = confirmationQueue ?? Promise.resolve();
432
+ const next = prior.then(async () => {
433
+ for (const frame of frames)
434
+ await secrets.acceptConfirmedCommands(frame.commands ?? []);
435
+ applyConfirmedFrames(frames);
436
+ });
437
+ confirmationQueue = next;
438
+ void next.catch((error) => {
439
+ secretFailure = error instanceof Error ? error : new Error('signature-invalid');
440
+ }).finally(() => {
441
+ if (confirmationQueue === next)
442
+ confirmationQueue = undefined;
443
+ });
398
444
  }
399
445
  function applyConfirmedFrames(frames) {
400
446
  for (const frame of frames) {
401
447
  if (frame.tick > appliedThrough) {
402
- confirmed.set(frame.tick, frame.inputs);
448
+ confirmed.set(frame.tick, { inputs: frame.inputs, commands: frame.commands ?? [] });
403
449
  }
404
450
  }
405
451
  while (confirmed.has(appliedThrough + 1)) {
406
452
  const tick = appliedThrough + 1;
407
- const confInputs = confirmed.get(tick);
408
- const predInputs = timeline[tick];
409
- if (predInputs === undefined || (0, canonical_js_1.canonicalStringify)(confInputs) !== (0, canonical_js_1.canonicalStringify)(predInputs)) {
410
- timeline[tick] = confInputs.slice();
453
+ const confFrame = confirmed.get(tick);
454
+ const confInputs = confFrame.inputs;
455
+ const predFrame = timeline[tick];
456
+ if (predFrame === undefined || (0, canonical_js_1.canonicalStringify)(confFrame) !== (0, canonical_js_1.canonicalStringify)(predFrame)) {
457
+ timeline[tick] = { inputs: confInputs.slice(), commands: confFrame.commands };
411
458
  if (predictedThrough > tick) {
412
459
  // Rollback: we had already predicted this tick and the confirmed inputs differ.
413
460
  rollbacks += 1;
@@ -457,9 +504,9 @@ function createNetworkedSyncplayClient(options) {
457
504
  /**
458
505
  * Report the confirmed-state checksum at `tick`, read from the rolling
459
506
  * snapshot buffer — NOT via restoreToFrame, which would rewind the live
460
- * session out from under prediction. Buffer state is the serialize
461
- * round-trip of the live state, i.e. exactly what a snapshot joiner will
462
- * hydrate, so quorum comparisons are round-trip-consistent by construction.
507
+ * session out from under prediction. Quorum consistency comes from the wire
508
+ * checksum's full-state serialize/deserialize round-trip; synctest's codec
509
+ * phase verifies that round-trip is canonical on every retained frame.
463
510
  * Ticks that fell out of the buffer are skipped (the client simply is not
464
511
  * donor-eligible for them).
465
512
  */
@@ -469,7 +516,7 @@ function createNetworkedSyncplayClient(options) {
469
516
  if (active === undefined || tick < 0 || role === 'spectator') {
470
517
  return;
471
518
  }
472
- const snapshot = active.getSnapshot(tick + 1);
519
+ const snapshot = active.peekSnapshot(tick + 1);
473
520
  if (snapshot === undefined) {
474
521
  return;
475
522
  }
@@ -477,7 +524,7 @@ function createNetworkedSyncplayClient(options) {
477
524
  kind: 'checksum-report',
478
525
  slot,
479
526
  tick,
480
- checksum: (0, canonical_js_1.defaultChecksum)(snapshot.state),
527
+ checksum: active.getWireStateChecksum(snapshot.frame),
481
528
  }));
482
529
  reportedTicks.push(tick);
483
530
  if (reportedTicks.length > REPORTED_TICKS_RETAINED) {
@@ -531,8 +578,8 @@ function createNetworkedSyncplayClient(options) {
531
578
  const minTick = minFrame - 1;
532
579
  const candidates = reportedTicks.filter((tick) => tick >= minTick).sort((a, b) => a - b);
533
580
  for (const tick of candidates) {
534
- const snapshot = active.getSnapshot(tick + 1);
535
- const confirmedAtTick = timeline[tick];
581
+ const snapshot = active.peekSnapshot(tick + 1);
582
+ const confirmedAtTick = timeline[tick]?.inputs;
536
583
  if (snapshot === undefined || confirmedAtTick === undefined) {
537
584
  continue;
538
585
  }
@@ -540,9 +587,9 @@ function createNetworkedSyncplayClient(options) {
540
587
  v: 1,
541
588
  tick,
542
589
  sessionFrame: tick + 1,
543
- stateBytes: (0, session_snapshot_js_1.bytesToBase64)(snapshot.stateBytes),
590
+ stateBytes: (0, session_snapshot_js_1.bytesToBase64)(active.getSnapshotBytes(snapshot.frame)),
544
591
  randomState: snapshot.randomState,
545
- stateChecksum: (0, canonical_js_1.defaultChecksum)(snapshot.state),
592
+ stateChecksum: active.getWireStateChecksum(snapshot.frame),
546
593
  confirmedInputs: confirmedAtTick.map((input) => (0, canonical_js_1.cloneCanonical)(input)),
547
594
  });
548
595
  donorTransferCounter += 1;
@@ -613,6 +660,8 @@ function createNetworkedSyncplayClient(options) {
613
660
  randomState: snapshot.randomState,
614
661
  inputs: snapshot.confirmedInputs.map((input) => decodeInputSafe(input)),
615
662
  });
663
+ // The active state was just deserialized, so this is already the same
664
+ // round-tripped full-state basis used by the donor's wire checksum.
616
665
  const actualChecksum = (0, canonical_js_1.defaultChecksum)(active.getState());
617
666
  if (actualChecksum !== snapshot.stateChecksum) {
618
667
  throw new errors_js_1.SyncplayError('Hydrated snapshot state does not match its declared checksum', {
@@ -627,7 +676,7 @@ function createNetworkedSyncplayClient(options) {
627
676
  ownScheduleCursor = Math.max(ownScheduleCursor, snapshot.tick + 1);
628
677
  lastConfirmedBySlot = snapshot.confirmedInputs.map((input) => (0, canonical_js_1.cloneCanonical)(input));
629
678
  timeline.length = 0;
630
- timeline[snapshot.tick] = lastConfirmedBySlot.slice();
679
+ timeline[snapshot.tick] = { inputs: lastConfirmedBySlot.slice(), commands: [] };
631
680
  for (const key of [...ownInputs.keys()]) {
632
681
  if (key <= snapshot.tick)
633
682
  ownInputs.delete(key);
@@ -669,6 +718,7 @@ function createNetworkedSyncplayClient(options) {
669
718
  maxRollbackDepth,
670
719
  };
671
720
  },
721
+ secrets,
672
722
  whenReady() {
673
723
  return readyPromise;
674
724
  },
package/dist/cjs/node.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.stableEngineParityJson = exports.runEngineParitySuite = exports.explainEngineParityFixture = exports.runChaosCertificationSuite = exports.explainChaosFixture = exports.startLocalWebSocketProtocolServer = exports.runLocalWebSocketAdapterTest = exports.runNetworkedInputAuthoritySpike = exports.defaultNetworkedInputAuthoritySpikeOptions = exports.verifyReplayScoreAgainstTrustedRuntime = exports.verifyReplay = exports.createSyncplaySession = exports.createOfflineSession = void 0;
17
+ exports.cookConvexDecomposition = exports.assertCookedColliderIsRuntimeValid = exports.stableEngineParityJson = exports.runEngineParitySuite = exports.explainEngineParityFixture = exports.runChaosCertificationSuite = exports.explainChaosFixture = exports.startLocalWebSocketProtocolServer = exports.runLocalWebSocketAdapterTest = exports.runNetworkedInputAuthoritySpike = exports.defaultNetworkedInputAuthoritySpikeOptions = exports.verifySyncplaySignedReceipt = exports.verifyMerkleProof = exports.verifyHashChainReveal = exports.signedReceiptBytes = exports.sha256Bytes = exports.merkleProof = exports.merkleNodeHash = exports.merkleLeafHash = exports.hexToBytes = exports.createBrowserSyncplaySecretCrypto = exports.constantTimeEqual = exports.concatBytes = exports.bytesToHex = exports.buildMerkleTree = exports.verifyReplayScoreAgainstTrustedRuntime = exports.verifyReplay = exports.createSyncplaySession = exports.createOfflineSession = void 0;
18
18
  __exportStar(require("./multiplayer-service.js"), exports);
19
19
  __exportStar(require("./input-authority.js"), exports);
20
20
  __exportStar(require("./local-authority-wire.js"), exports);
@@ -35,6 +35,24 @@ var replay_bundle_js_1 = require("./replay-bundle.js");
35
35
  Object.defineProperty(exports, "verifyReplayScoreAgainstTrustedRuntime", { enumerable: true, get: function () { return replay_bundle_js_1.verifyReplayScoreAgainstTrustedRuntime; } });
36
36
  __exportStar(require("./match-log.js"), exports);
37
37
  __exportStar(require("./authority-room.js"), exports);
38
+ __exportStar(require("./secret-config.js"), exports);
39
+ __exportStar(require("./secret-protocol.js"), exports);
40
+ var secret_crypto_js_1 = require("./secret-crypto.js");
41
+ Object.defineProperty(exports, "buildMerkleTree", { enumerable: true, get: function () { return secret_crypto_js_1.buildMerkleTree; } });
42
+ Object.defineProperty(exports, "bytesToHex", { enumerable: true, get: function () { return secret_crypto_js_1.bytesToHex; } });
43
+ Object.defineProperty(exports, "concatBytes", { enumerable: true, get: function () { return secret_crypto_js_1.concatBytes; } });
44
+ Object.defineProperty(exports, "constantTimeEqual", { enumerable: true, get: function () { return secret_crypto_js_1.constantTimeEqual; } });
45
+ Object.defineProperty(exports, "createBrowserSyncplaySecretCrypto", { enumerable: true, get: function () { return secret_crypto_js_1.createBrowserSyncplaySecretCrypto; } });
46
+ Object.defineProperty(exports, "hexToBytes", { enumerable: true, get: function () { return secret_crypto_js_1.hexToBytes; } });
47
+ Object.defineProperty(exports, "merkleLeafHash", { enumerable: true, get: function () { return secret_crypto_js_1.merkleLeafHash; } });
48
+ Object.defineProperty(exports, "merkleNodeHash", { enumerable: true, get: function () { return secret_crypto_js_1.merkleNodeHash; } });
49
+ Object.defineProperty(exports, "merkleProof", { enumerable: true, get: function () { return secret_crypto_js_1.merkleProof; } });
50
+ Object.defineProperty(exports, "sha256Bytes", { enumerable: true, get: function () { return secret_crypto_js_1.sha256Bytes; } });
51
+ Object.defineProperty(exports, "signedReceiptBytes", { enumerable: true, get: function () { return secret_crypto_js_1.signedReceiptBytes; } });
52
+ Object.defineProperty(exports, "verifyHashChainReveal", { enumerable: true, get: function () { return secret_crypto_js_1.verifyHashChainReveal; } });
53
+ Object.defineProperty(exports, "verifyMerkleProof", { enumerable: true, get: function () { return secret_crypto_js_1.verifyMerkleProof; } });
54
+ Object.defineProperty(exports, "verifySyncplaySignedReceipt", { enumerable: true, get: function () { return secret_crypto_js_1.verifySyncplaySignedReceipt; } });
55
+ __exportStar(require("./secret-authority.js"), exports);
38
56
  __exportStar(require("./ws-frame.js"), exports);
39
57
  var networked_input_authority_js_1 = require("./networked-input-authority.js");
40
58
  Object.defineProperty(exports, "defaultNetworkedInputAuthoritySpikeOptions", { enumerable: true, get: function () { return networked_input_authority_js_1.defaultNetworkedInputAuthoritySpikeOptions; } });
@@ -49,3 +67,7 @@ var engine_parity_js_1 = require("./engine-parity.js");
49
67
  Object.defineProperty(exports, "explainEngineParityFixture", { enumerable: true, get: function () { return engine_parity_js_1.explainEngineParityFixture; } });
50
68
  Object.defineProperty(exports, "runEngineParitySuite", { enumerable: true, get: function () { return engine_parity_js_1.runEngineParitySuite; } });
51
69
  Object.defineProperty(exports, "stableEngineParityJson", { enumerable: true, get: function () { return engine_parity_js_1.stableEngineParityJson; } });
70
+ // Offline collider cooking — build-time only, never part of a simulation step.
71
+ var collider_cooking_js_1 = require("./collider-cooking.js");
72
+ Object.defineProperty(exports, "assertCookedColliderIsRuntimeValid", { enumerable: true, get: function () { return collider_cooking_js_1.assertCookedColliderIsRuntimeValid; } });
73
+ Object.defineProperty(exports, "cookConvexDecomposition", { enumerable: true, get: function () { return collider_cooking_js_1.cookConvexDecomposition; } });
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createOfflineSession = createOfflineSession;
4
4
  const canonical_js_1 = require("./canonical.js");
5
+ const checksum_basis_js_1 = require("./checksum-basis.js");
5
6
  const errors_js_1 = require("./errors.js");
6
7
  const math_js_1 = require("./math.js");
7
8
  const random_js_1 = require("./random.js");
@@ -14,6 +15,8 @@ function createOfflineSession(config) {
14
15
  const randomSeed = config.randomSeed ?? 1;
15
16
  const fixedFrameMs = 1000 / config.tickRate;
16
17
  const math = (0, math_js_1.createDeterministicMath)();
18
+ // LOAD-BEARING: these two clones make frame-0 reference retention private to
19
+ // each session and insulate exportReplay's initialState.
17
20
  const initialState = (0, canonical_js_1.cloneCanonical)(config.initialState);
18
21
  const mutable = {
19
22
  frame: 0,
@@ -165,20 +168,39 @@ function createOfflineSession(config) {
165
168
  ],
166
169
  };
167
170
  },
171
+ peekSnapshot(frame) {
172
+ return mutable.snapshots.get(frame);
173
+ },
168
174
  getSnapshot(frame) {
169
175
  const snapshot = mutable.snapshots.get(frame);
170
176
  return snapshot === undefined ? undefined : cloneSnapshot(snapshot);
171
177
  },
178
+ getSnapshotBytes(frame) {
179
+ return (0, checksum_basis_js_1.serializeStateWith)(config, requireSnapshot(mutable, frame).state);
180
+ },
181
+ exportExternalSnapshot(frame) {
182
+ const snapshot = requireSnapshot(mutable, frame);
183
+ return {
184
+ frame: snapshot.frame,
185
+ stateBytes: (0, checksum_basis_js_1.serializeStateWith)(config, snapshot.state),
186
+ randomState: snapshot.randomState,
187
+ inputs: snapshot.inputs.map((input) => cloneInput(config, input)),
188
+ };
189
+ },
190
+ getRecordedChecksum(frame) {
191
+ return requireSnapshot(mutable, frame).checksum;
192
+ },
193
+ computeRecordedChecksum(frame) {
194
+ const snapshot = requireSnapshot(mutable, frame);
195
+ return snapshot.checksum ?? getChecksum(config, snapshot.state);
196
+ },
197
+ getWireStateChecksum(frame) {
198
+ return (0, checksum_basis_js_1.computeWireStateChecksum)(config, requireSnapshot(mutable, frame).state);
199
+ },
172
200
  restoreToFrame(frame) {
173
- const snapshot = mutable.snapshots.get(frame);
174
- if (snapshot === undefined) {
175
- throw new errors_js_1.SyncplayError(`No snapshot exists for frame ${frame}`, {
176
- code: 'session.missing-snapshot',
177
- frame,
178
- });
179
- }
201
+ const snapshot = requireSnapshot(mutable, frame);
180
202
  mutable.frame = snapshot.frame;
181
- mutable.state = (0, canonical_js_1.cloneCanonical)(snapshot.state);
203
+ mutable.state = snapshot.state;
182
204
  mutable.inputs = snapshot.inputs.map((input) => cloneInput(config, input));
183
205
  mutable.inputFlags = Array.from({ length: config.playerCount }, (_, slot) => defaultInputFlags(slot));
184
206
  mutable.random.restore({ state: snapshot.randomState });
@@ -193,7 +215,7 @@ function createOfflineSession(config) {
193
215
  (0, errors_js_1.invariant)(Number.isInteger(snapshot.frame) && snapshot.frame >= 0, 'External snapshot frame must be a non-negative integer', 'session.invalid-snapshot-frame');
194
216
  (0, errors_js_1.invariant)(snapshot.inputs.length === config.playerCount, 'External snapshot inputs must cover every player slot', 'session.invalid-snapshot-inputs');
195
217
  mutable.frame = snapshot.frame;
196
- mutable.state = deserializeState(config, snapshot.stateBytes);
218
+ mutable.state = (0, checksum_basis_js_1.deserializeStateWith)(config, snapshot.stateBytes);
197
219
  mutable.inputs = snapshot.inputs.map((input) => cloneInput(config, input));
198
220
  mutable.inputFlags = Array.from({ length: config.playerCount }, (_, slot) => defaultInputFlags(slot));
199
221
  mutable.random.restore({ state: snapshot.randomState });
@@ -205,13 +227,16 @@ function createOfflineSession(config) {
205
227
  mutable.snapshots = new Map();
206
228
  mutable.snapshotOrder = [];
207
229
  hydratedFromExternalSnapshot = true;
208
- recordSnapshot(config, mutable, snapshotBufferSize);
230
+ const hydratedChecksum = mutable.frame % checksumInterval === 0
231
+ ? getChecksum(config, mutable.state)
232
+ : undefined;
233
+ recordSnapshot(config, mutable, snapshotBufferSize, hydratedChecksum);
209
234
  },
210
235
  getSnapshotFrames() {
211
236
  return [...mutable.snapshotOrder];
212
237
  },
213
238
  };
214
- recordSnapshot(config, mutable, snapshotBufferSize);
239
+ recordSnapshot(config, mutable, snapshotBufferSize, getChecksum(config, mutable.state));
215
240
  return session;
216
241
  }
217
242
  function runFrames(config, mutable, checksumInterval, snapshotBufferSize, replayRecordingMode, math, count) {
@@ -251,8 +276,10 @@ function runFrames(config, mutable, checksumInterval, snapshotBufferSize, replay
251
276
  ? (0, runtime_guards_js_1.withRuntimeDeterminismGuards)(frame, () => config.step(mutable.state, effectiveInputs, frame, stepContext))
252
277
  : config.step(mutable.state, effectiveInputs, frame, stepContext);
253
278
  mutable.frame += 1;
254
- const checksum = getChecksum(config, mutable.state);
255
- if (mutable.frame % checksumInterval === 0) {
279
+ const checksum = mutable.frame % checksumInterval === 0
280
+ ? getChecksum(config, mutable.state)
281
+ : undefined;
282
+ if (checksum !== undefined) {
256
283
  const checksumRecord = { frame: mutable.frame, checksum };
257
284
  config.onChecksumRecorded?.(checksumRecord);
258
285
  if (replayRecordingMode === 'memory') {
@@ -322,26 +349,28 @@ function pruneSnapshotsAfterFrame(mutable, frame) {
322
349
  return true;
323
350
  });
324
351
  }
352
+ function requireSnapshot(mutable, frame) {
353
+ const snapshot = mutable.snapshots.get(frame);
354
+ if (snapshot === undefined) {
355
+ throw new errors_js_1.SyncplayError(`No snapshot exists for frame ${frame}`, {
356
+ code: 'session.missing-snapshot',
357
+ frame,
358
+ });
359
+ }
360
+ return snapshot;
361
+ }
325
362
  function getChecksum(config, state) {
326
363
  return String(config.checksum?.(state) ?? (0, canonical_js_1.defaultChecksum)(config.canonicalizeState?.(state) ?? state));
327
364
  }
328
365
  function cloneInput(config, input) {
329
366
  return (0, canonical_js_1.cloneCanonical)(input);
330
367
  }
331
- function serializeState(config, state) {
332
- return config.serializeState?.(state) ?? (0, canonical_js_1.encodeCanonicalBytes)(config.canonicalizeState?.(state) ?? state);
333
- }
334
- function deserializeState(config, bytes) {
335
- return config.deserializeState?.(bytes) ?? (0, canonical_js_1.decodeCanonicalBytes)(bytes);
336
- }
337
368
  function recordSnapshot(config, mutable, snapshotBufferSize, checksum) {
338
- const stateBytes = serializeState(config, mutable.state);
339
369
  const snapshot = {
340
370
  frame: mutable.frame,
341
371
  inputs: mutable.inputs.map((input) => cloneInput(config, input)),
342
- checksum: checksum ?? getChecksum(config, mutable.state),
343
- state: deserializeState(config, stateBytes),
344
- stateBytes,
372
+ checksum,
373
+ state: mutable.state,
345
374
  randomState: mutable.random.snapshot().state,
346
375
  };
347
376
  const alreadyStored = mutable.snapshots.has(snapshot.frame);
@@ -364,7 +393,6 @@ function cloneSnapshot(snapshot) {
364
393
  inputs: snapshot.inputs.map((input) => (0, canonical_js_1.cloneCanonical)(input)),
365
394
  checksum: snapshot.checksum,
366
395
  state: (0, canonical_js_1.cloneCanonical)(snapshot.state),
367
- stateBytes: new Uint8Array(snapshot.stateBytes),
368
396
  randomState: snapshot.randomState,
369
397
  };
370
398
  }
@@ -62,7 +62,7 @@ function runPhysicsCert(options) {
62
62
  snapshotHashStable;
63
63
  return {
64
64
  engine: options.engine,
65
- engineVersion: options.engine === 'builtin-rigid' ? 'builtin-rigid@1' : 'builtin-fixed@1',
65
+ engineVersion: options.engine === 'builtin-rigid' ? 'builtin-rigid@2' : 'builtin-fixed@1',
66
66
  adapterVersion: 'physics-cert@1',
67
67
  shape: options.shape,
68
68
  players: options.players,
@@ -319,13 +319,18 @@ function buildRigidArena(options) {
319
319
  // hinge door
320
320
  bodies.push({ id: `frame-${slot}`, kind: 'static', x: baseX + 3, y: 2, z: 0, vx: 0, vy: 0, vz: 0, shape: { type: 'box', halfX: 0.1, halfY: 0.5, halfZ: 0.5 }, layer: 1, mask: 1 });
321
321
  bodies.push({ id: `door-${slot}`, kind: 'dynamic', x: baseX + 4, y: 2, z: 0, vx: 0, vy: 0, vz: 0, mass: 1, orientation: ident, angularVel: { x: 0, y: 0, z: 0 }, shape: { type: 'box', halfX: 1, halfY: 0.5, halfZ: 0.1 }, layer: 1, mask: 1 });
322
- joints.push({ id: `hinge-${slot}`, type: 'hinge', a: `frame-${slot}`, b: `door-${slot}`, anchorA: { x: 0, y: 0, z: 0 }, anchorB: { x: -1, y: 0, z: 0 }, axisA: { x: 0, y: 1, z: 0 }, motorSpeed: 0.02, maxMotorTorque: 50 });
322
+ joints.push({ id: `hinge-${slot}`, type: 'hinge', a: `frame-${slot}`, b: `door-${slot}`, anchorA: { x: 0, y: 0, z: 0 }, anchorB: { x: -1, y: 0, z: 0 }, axisA: { x: 0, y: 1, z: 0 }, motorSpeed: 0.02, maxMotorTorque: 50, disableCollision: true });
323
323
  // grab-target box
324
324
  bodies.push({ id: `grab-${slot}`, kind: 'dynamic', x: baseX + 6, y: 3, z: 0, vx: 0, vy: 0, vz: 0, mass: 1, orientation: ident, angularVel: { x: 0, y: 0, z: 0 }, shape: { type: 'box', halfX: 0.5, halfY: 0.5, halfZ: 0.5 }, layer: 1, mask: 1 });
325
325
  joints.push({ id: `carry-${slot}`, type: 'grab', body: `grab-${slot}`, anchor: { x: 0, y: 0, z: 0 }, targetX: baseX + 6, targetY: 4, targetZ: 0, maxForce: 20 });
326
326
  // ccd projectile toward a thin wall
327
327
  bodies.push({ id: `wall-${slot}`, kind: 'static', x: baseX + 9, y: 2, z: 0, vx: 0, vy: 0, vz: 0, shape: { type: 'box', halfX: 0.05, halfY: 2, halfZ: 2 }, layer: 1, mask: 1 });
328
- bodies.push({ id: `bullet-${slot}`, kind: 'dynamic', x: baseX - 20, y: 2, z: 0, vx: 60, vy: 0, vz: 0, mass: 1, orientation: ident, angularVel: { x: 0, y: 0, z: 0 }, ccd: true, linearDamping: 1, angularDamping: 1, shape: { type: 'box', halfX: 0.2, halfY: 0.2, halfZ: 0.2 }, layer: 1, mask: 1 });
328
+ // vx 8 tunnels the 0.1-thick wall per step without CCD, so this exercises the
329
+ // exact-TOI ccd path while staying a settling-scale impact the convergence gate can
330
+ // reach. The extreme-speed case (vx 60 into a stack) is finiteness/determinism-tested
331
+ // in physics3d-ccd.test.ts — a physical punch-through does not re-settle, so it cannot
332
+ // live in a convergence budget.
333
+ bodies.push({ id: `bullet-${slot}`, kind: 'dynamic', x: baseX - 20, y: 2, z: 0, vx: 8, vy: 0, vz: 0, mass: 1, orientation: ident, angularVel: { x: 0, y: 0, z: 0 }, ccd: true, linearDamping: 1, angularDamping: 1, shape: { type: 'box', halfX: 0.2, halfY: 0.2, halfZ: 0.2 }, layer: 1, mask: 1 });
329
334
  // soft spring: bob hung 2 below a static anchor, restLength 1 → oscillates and settles
330
335
  bodies.push({ id: `spring-anchor-${slot}`, kind: 'static', x: baseX + 2, y: 6, z: 3, vx: 0, vy: 0, vz: 0, shape: { type: 'box', halfX: 0.1, halfY: 0.1, halfZ: 0.1 }, layer: 2, mask: 2 });
331
336
  bodies.push({ id: `spring-bob-${slot}`, kind: 'dynamic', x: baseX + 2, y: 4, z: 3, vx: 0, vy: 0, vz: 0, mass: 1, orientation: ident, angularVel: { x: 0, y: 0, z: 0 }, shape: { type: 'box', halfX: 0.3, halfY: 0.3, halfZ: 0.3 }, layer: 2, mask: 2 });
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.heightfieldLocalBounds = heightfieldLocalBounds;
4
+ exports.heightfieldLocalTriangles = heightfieldLocalTriangles;
5
+ function heightfieldLocalBounds(shape) {
6
+ let minHeight = shape.heights[0];
7
+ let maxHeight = shape.heights[0];
8
+ for (const height of shape.heights) {
9
+ minHeight = Math.min(minHeight, height);
10
+ maxHeight = Math.max(maxHeight, height);
11
+ }
12
+ return {
13
+ minX: 0,
14
+ maxX: (shape.columns - 1) * shape.scale.x,
15
+ minY: minHeight * shape.scale.y,
16
+ maxY: maxHeight * shape.scale.y,
17
+ minZ: 0,
18
+ maxZ: (shape.rows - 1) * shape.scale.z,
19
+ };
20
+ }
21
+ function vertex(shape, column, row) {
22
+ return {
23
+ x: column * shape.scale.x,
24
+ y: shape.heights[row * shape.columns + column] * shape.scale.y,
25
+ z: row * shape.scale.z,
26
+ };
27
+ }
28
+ function heightfieldLocalTriangles(shape, bounds) {
29
+ const minColumn = bounds === undefined ? 0 : Math.max(0, Math.floor(bounds.minX / shape.scale.x));
30
+ const maxColumn = bounds === undefined
31
+ ? shape.columns - 2
32
+ : Math.min(shape.columns - 2, Math.ceil(bounds.maxX / shape.scale.x));
33
+ const minRow = bounds === undefined ? 0 : Math.max(0, Math.floor(bounds.minZ / shape.scale.z));
34
+ const maxRow = bounds === undefined
35
+ ? shape.rows - 2
36
+ : Math.min(shape.rows - 2, Math.ceil(bounds.maxZ / shape.scale.z));
37
+ if (minColumn > maxColumn || minRow > maxRow) {
38
+ return [];
39
+ }
40
+ const triangles = [];
41
+ for (let row = minRow; row <= maxRow; row += 1) {
42
+ for (let column = minColumn; column <= maxColumn; column += 1) {
43
+ const a = vertex(shape, column, row);
44
+ const b = vertex(shape, column, row + 1);
45
+ const c = vertex(shape, column + 1, row);
46
+ const d = vertex(shape, column + 1, row + 1);
47
+ triangles.push([a, b, c], [d, c, b]);
48
+ }
49
+ }
50
+ return triangles;
51
+ }