@series-inc/rundot-syncplay 5.23.0 → 5.24.0-beta.0

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 (72) hide show
  1. package/README.md +20 -0
  2. package/dist/browser.d.ts +4 -1
  3. package/dist/browser.js +1 -0
  4. package/dist/checksum-basis.d.ts +4 -0
  5. package/dist/checksum-basis.js +17 -0
  6. package/dist/cjs/browser.js +4 -2
  7. package/dist/cjs/checksum-basis.js +22 -0
  8. package/dist/cjs/collider-cooking.js +834 -0
  9. package/dist/cjs/deterministic-session.js +2 -0
  10. package/dist/cjs/engine-parity-matrix.js +32 -9
  11. package/dist/cjs/engine-parity.js +1 -1
  12. package/dist/cjs/index.js +8 -2
  13. package/dist/cjs/instant-replay.js +4 -9
  14. package/dist/cjs/movement3d.js +17 -1
  15. package/dist/cjs/networked-client.js +11 -9
  16. package/dist/cjs/node.js +5 -1
  17. package/dist/cjs/offline-session.js +52 -24
  18. package/dist/cjs/physics-cert.js +7 -2
  19. package/dist/cjs/physics3d-hull.js +381 -0
  20. package/dist/cjs/physics3d-joints.js +163 -31
  21. package/dist/cjs/physics3d-recipes.js +197 -0
  22. package/dist/cjs/physics3d-shared.js +6 -0
  23. package/dist/cjs/physics3d-solver.js +746 -89
  24. package/dist/cjs/physics3d-stacking.js +154 -0
  25. package/dist/cjs/physics3d-toi.js +251 -0
  26. package/dist/cjs/physics3d-vehicle.js +35 -10
  27. package/dist/cjs/physics3d.js +401 -20
  28. package/dist/cjs/runtime-support.js +71 -0
  29. package/dist/cjs/session-promotion.js +5 -5
  30. package/dist/cjs/synctest.js +49 -0
  31. package/dist/cjs/testing.js +4 -1
  32. package/dist/cli.js +163 -41
  33. package/dist/collider-cooking.d.ts +141 -0
  34. package/dist/collider-cooking.js +829 -0
  35. package/dist/deterministic-session.js +2 -0
  36. package/dist/engine-parity-matrix.js +32 -9
  37. package/dist/engine-parity.js +1 -1
  38. package/dist/index.d.ts +3 -1
  39. package/dist/index.js +1 -0
  40. package/dist/instant-replay.d.ts +2 -2
  41. package/dist/instant-replay.js +4 -9
  42. package/dist/movement3d.js +18 -2
  43. package/dist/networked-client.js +11 -9
  44. package/dist/node.d.ts +2 -0
  45. package/dist/node.js +2 -0
  46. package/dist/offline-session.js +53 -25
  47. package/dist/physics-cert.js +7 -2
  48. package/dist/physics3d-hull.d.ts +53 -0
  49. package/dist/physics3d-hull.js +376 -0
  50. package/dist/physics3d-joints.d.ts +7 -0
  51. package/dist/physics3d-joints.js +163 -31
  52. package/dist/physics3d-recipes.d.ts +29 -0
  53. package/dist/physics3d-recipes.js +193 -0
  54. package/dist/physics3d-shared.js +6 -0
  55. package/dist/physics3d-solver.d.ts +65 -1
  56. package/dist/physics3d-solver.js +744 -89
  57. package/dist/physics3d-stacking.d.ts +26 -0
  58. package/dist/physics3d-stacking.js +147 -0
  59. package/dist/physics3d-toi.d.ts +70 -0
  60. package/dist/physics3d-toi.js +245 -0
  61. package/dist/physics3d-vehicle.js +35 -10
  62. package/dist/physics3d.d.ts +20 -2
  63. package/dist/physics3d.js +400 -21
  64. package/dist/runtime-support.d.ts +60 -0
  65. package/dist/runtime-support.js +66 -0
  66. package/dist/session-promotion.js +5 -5
  67. package/dist/synctest.d.ts +1 -1
  68. package/dist/synctest.js +49 -0
  69. package/dist/testing.d.ts +2 -0
  70. package/dist/testing.js +1 -0
  71. package/dist/types.d.ts +21 -2
  72. package/package.json +8 -3
@@ -7,11 +7,13 @@ exports.runDeterministicSessionCoreFixture = runDeterministicSessionCoreFixture;
7
7
  exports.runDeterministicStructuredSessionInputFixture = runDeterministicStructuredSessionInputFixture;
8
8
  exports.runDeterministicSessionAuthorityFixture = runDeterministicSessionAuthorityFixture;
9
9
  exports.runDeterministicSessionLifecycleProtocolFixture = runDeterministicSessionLifecycleProtocolFixture;
10
+ const runtime_support_js_1 = require("./runtime-support.js");
10
11
  const canonical_js_1 = require("./canonical.js");
11
12
  const commands_js_1 = require("./commands.js");
12
13
  const engine_runtime_js_1 = require("./engine-runtime.js");
13
14
  const random_js_1 = require("./random.js");
14
15
  function createDeterministicSessionCore(options) {
16
+ (0, runtime_support_js_1.assertSupportedJsRuntime)();
15
17
  assertPositiveInteger(options.players, 'players');
16
18
  if (options.reconnectGraceFrames !== undefined) {
17
19
  assertNonNegativeInteger(options.reconnectGraceFrames, 'reconnectGraceFrames');
@@ -623,30 +623,47 @@ const engineParityMatrixEntries = [
623
623
  'packages/syncplay/src/physics2d.ts',
624
624
  'packages/syncplay/src/physics3d.ts',
625
625
  'packages/syncplay/src/physics3d-solver.ts',
626
+ 'packages/syncplay/src/physics3d-toi.ts',
626
627
  'packages/syncplay/src/physics3d-joints.ts',
627
628
  'packages/syncplay/tests/physics2d.test.ts',
628
629
  'packages/syncplay/tests/physics3d-solver.test.ts',
629
630
  'packages/syncplay/tests/physics3d-joints.test.ts',
630
631
  'packages/syncplay/tests/physics3d-ccd.test.ts',
632
+ 'packages/syncplay/tests/physics3d-toi.test.ts',
631
633
  'packages/syncplay/tests/engine-complete.test.ts',
632
634
  'packages/syncplay/src/physics3d-vehicle.ts',
633
635
  'packages/syncplay/tests/physics3d-vehicle.test.ts',
634
- 'warm-started sequential-impulse solver: stable stacking, tumbling, sleeping, axis locks, joints (ball/hinge/weld/rope/grab), speculative-contact CCD, compound colliders, orientation-aware queries',
636
+ 'packages/syncplay/src/physics3d-stacking.ts',
637
+ 'packages/syncplay/tests/physics3d-stacking.test.ts',
638
+ 'packages/syncplay/src/physics3d-recipes.ts',
639
+ 'packages/syncplay/tests/physics3d-recipes.test.ts',
640
+ 'packages/syncplay/src/physics3d-hull.ts',
641
+ 'packages/syncplay/tests/physics3d-hull.test.ts',
642
+ 'TGS-Soft (sub-stepped soft-constraint) rigid solver: box3d-class stable stacking, tumbling, sleeping, axis locks, joints (ball/hinge/weld/rope/prismatic/spring/grab), opt-in TOI-bound CCD, compound colliders, orientation-aware queries',
643
+ 'extreme-speed CCD (box3d model, src/physics3d-toi.ts): an opt-in ccd body whose per-tick motion exceeds half its smallest extent has its exact first time-of-impact found by GJK conservative advancement; it is advanced there and the impact resolved as a perfectly-inelastic collision (momentum transfers to the struck body — a fast bullet punches through and knocks targets loose, no non-physical stop-dead) with its position TOI-clamped so it never tunnels. A bullet at 60 units/tick into a resting stack or a frame+door cluster stays finite and bounded (fixes the #2821 relax divergence via witness-point contact anchors + a small speculative margin + shared multi-body seam impulse). Linear-sweep + stationary-target scope; glancing bodies slide (not frozen); non-ccd bodies still tunnel (opt-in). physics3d-ccd.test.ts covers momentum-transfer / no-freeze / finite-at-vx60 / rollback + permutation equivalence',
644
+ 'stacking stability gated in CI: tower-15 (<1° tilt), 50:1 mass-ratio column, pyramid, kick-recovery, and a 20-layer stack all meet drift/penetration/up-alignment thresholds with rollback + authoring-order-permutation equivalence',
645
+ 'convex-hull collision shape (4–16 verts): deterministic supporting-plane preprocessing, SAT+clip hull-vs-hull/box, closest-point hull-vs-sphere/capsule, support-sample hull-vs-static-mesh, exact ray/overlap/shape-cast queries; golden hull-pair battery pinned',
646
+ 'cone-twist ball joint (swing cone + twist limits via fixed-point cosine) drives a 6-body ragdoll that settles kinetically and stays articulated; rigid-wheel vehicle recipe (sphere wheels + hinge motors) certified alongside the raycast controller',
635
647
  'builtin-rigid physics-cert lane passes all 4 determinism gates; contact/joint caches are checksummed rollback state',
636
- 'raycast-wheel vehicle controller (stepDeterministicVehicle3D): spring-damper suspension, friction-circle drive/steer/brake, fixed-point steering trig; certified via the physics-rigid-vehicle fixture, the builtin-rigid arena car, and a rollback-every-frame synctest scene',
637
- 'per-world static AABB-tree query index: raycast/overlap/shape-cast are O(log n + hits) with bit-identical results (oracle-tested; cert-lane checksums unchanged); batched applyDeterministicImpulses3D applies a frame of controller impulses in one canonical rebuild',
638
- 'pooled narrow-phase/constraint scratch (scalarized box face-clip over ping-pong clip slots, step-scoped constraint pool) pulled the 128-body pile impact-burst p99 from 2.06 ms to 1.54 ms (p50 0.85 → 0.72 ms, median-of-5); prismatic/breakable/collision-suppressed joints, a soft frequencyHz/dampingRatio spring joint, and per-body gravityScale round out the joint/body surface — joint breaks are sticky checksummed cache state',
648
+ 'raycast-wheel vehicle controller (stepDeterministicVehicle3D): spring-damper suspension, friction-circle drive/steer/brake, fixed-point steering trig, moving-ground reaction (slip relative to the contact body; equal-and-opposite reaction on dynamic ground)',
649
+ 'per-world static AABB-tree query index: raycast/overlap/shape-cast are O(log n + hits) with bit-identical results; batched applyDeterministicImpulses3D applies a frame of controller impulses in one canonical rebuild',
650
+ 'pooled narrow-phase/constraint scratch (scalarized box face-clip over ping-pong clip slots, step-scoped constraint pool); prismatic/breakable/collision-suppressed joints, a soft frequencyHz/dampingRatio spring joint (breakable/disable-collision), and per-body gravityScale — joint breaks are sticky checksummed cache state',
651
+ 'kinematic bodies integrate their authored velocity (linear + angular) inside the solver, drive the dynamic bodies they touch through the contact relative-velocity term, absorb the equal-and-opposite reaction (zero inverse mass), and wake the sleeping piles they push; a kinematic mesh is therefore a moving platform / conveyor collider. A dynamic mesh is rejected at world creation',
652
+ 'one-sided mesh contacts resolve penetration for zero-radius shapes: a support sample projecting onto a triangle face uses its signed plane distance (capped at the sample shape\'s diameter), so a box or hull rests on and is pushed by a mesh instead of falling through it — previously only sphere/capsule (radius-bearing) samples could generate a resolvable mesh contact',
653
+ 'compound colliders accept hull children (packages/syncplay/src/collider-cooking.ts): an offline V-HACD-shaped cooker voxelizes a concave triangle soup, splits it on the axis-aligned plane that best reduces concavity, and emits a compound of ≤16-vert convex hulls that the runtime validates with buildSolverHull. The cook is order-invariant and byte-reproducible (exact integer-lattice convex hull, lexicographic tie-breaks); no decomposition code runs in a simulation step',
639
654
  ],
640
655
  cliProofs: [
641
656
  'npm run lab:physics3d -- --bodies 128 --frames 1000 --no-fast-stress --json artifacts/reports/physics3d-no-fast.json',
657
+ 'npm run lab:physics3d-stacks -- --json artifacts/reports/physics3d-stacks.json',
642
658
  'npm run certify:engine-complete -- --filter physics3d --players 128 --frames 5000 --no-fast-stress',
643
659
  'npm run lab:physics-cert -- --engine builtin-rigid --shape fixed-arena --players 4 --frames 600 --seed 7 --snapshot-every 8',
644
660
  'npm run coverage:package:physics-vehicle',
661
+ 'npm run collider:validate -- <mesh.json> --json artifacts/reports/collider-validate.json',
645
662
  ],
646
663
  gaps: [
647
- 'Vehicles are raycast-wheel only (static ground, flat/axis-aligned contact normals); moving-ground reaction and rigid-wheel vehicles are tracked follow-ons.',
648
- 'Dynamic mesh-vs-mesh collision, convex-hull decomposition / visual collider import, and ragdolls-in-checksummed-state remain out of scope.',
649
- 'Cross-VM certification posture unchanged (V8↔V8 certified; WebKit nightly; Hermes documented gap).',
664
+ 'Mesh-vs-mesh collision remains unsupported by design: a mesh collider is static or kinematic, and a dynamic concave body must be cooked to a compound of hulls (collider:cook).',
665
+ 'Kinematic bodies resolve contacts discretely — a kinematic mesh driven faster than its own extent per tick can pass through a thin body (no CCD for kinematic; ccd is a dynamic-body opt-in).',
666
+ 'Convex decomposition is offline only: the cooked hull set is conservative (it contains the source, inflated by up to one voxel of the chosen resolution) and hulls stay capped at 16 verts.',
650
667
  ],
651
668
  nextCliWork: [
652
669
  'Profile the remaining burst cost before further pooling; no known single lever.',
@@ -829,20 +846,26 @@ const engineParityMatrixEntries = [
829
846
  canCloseWithCli: true,
830
847
  currentEvidence: [
831
848
  'packages/syncplay/src/browser.ts',
849
+ 'packages/syncplay/src/runtime-support.ts',
850
+ 'packages/syncplay/tests/runtime-support.test.ts',
832
851
  'packages/sdk/src/syncplay/index.ts',
833
852
  'packages/sdk/src/syncplay/browser.ts',
834
853
  'packages/sdk/src/syncplay/node.ts',
835
854
  'packages/sdk/tests/deterministicMultiplayer.test.ts',
855
+ 'cross-VM certification: Chromium (V8) AND WebKit (JavaScriptCore, the executable proxy for iOS WKWebView) are blocking gates on every engine PR (.github/workflows/ci-syncplay.yml cross-vm-browser). Each runs the replay proof, the number lab, and the 128-player/1000-frame engine-core numeric proof, and each must reproduce Node\'s checksums bit-for-bit; static:determinism requires a passing proof artifact per certified runtime, not just chromium',
856
+ 'Hermes is an explicit non-goal with an enforced runtime guard: H5 games run in the platform WebView (V8 on Android, JavaScriptCore on iOS), never in React Native\'s VM, and no lane can execute Hermes — so createDeterministicSessionCore refuses to start there rather than emit checksums the authority server would silently reject (src/runtime-support.ts)',
836
857
  ],
837
858
  cliProofs: [
838
859
  'npm run lab:replay-verify-browser -- artifacts/replays/browser-proof-current.json --runtime chromium --json artifacts/certification/browser-replay-chromium-current.json',
860
+ 'npm run lab:replay-verify-browser -- artifacts/replays/browser-proof-current.json --runtime webkit --json artifacts/certification/browser-replay-webkit-current.json',
861
+ 'npm run lab:engine-core-browser -- --runtime webkit --json artifacts/certification/engine-core-numeric-webkit-current.json',
839
862
  'cd packages/sdk && npm test -- deterministicMultiplayer.test.ts --runInBand && npm run build:sdk',
840
863
  ],
841
864
  gaps: [
842
- 'Browser proof artifacts are not always present and are not yet mandatory for every parity report.',
865
+ 'Android WebView is proven only by proxy (Chromium/V8 on Linux); the on-device APK replay harness stays a manual per-release step.',
843
866
  ],
844
867
  nextCliWork: [
845
- 'Require Chromium browser replay and SDK test-app proof artifacts in static:determinism certification mode.',
868
+ 'Automate the Android WebView APK replay harness so the on-device proof stops being manual.',
846
869
  ],
847
870
  referenceDocs: [
848
871
  'external-deterministic-engine-reference:manual/replay',
@@ -2531,7 +2531,7 @@ function runReplayWorkload128(frames, seed) {
2531
2531
  mismatches: verification.mismatches,
2532
2532
  finalChecksum: verification.finalChecksum,
2533
2533
  replayBytes: JSON.stringify(replay).length,
2534
- snapshotBytes: session.getSnapshot(session.currentFrame).stateBytes.byteLength,
2534
+ snapshotBytes: session.getSnapshotBytes(session.currentFrame).byteLength,
2535
2535
  };
2536
2536
  }
2537
2537
  function runSecurityHostileClientCategory(config) {
package/dist/cjs/index.js CHANGED
@@ -4,8 +4,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.createDeterministicCommandTimelineSnapshot = exports.createDeterministicCommandTimeline = exports.assertDeterministicPayloadFields = exports.translateAabb = exports.sortAabbsByStableId = exports.segmentsIntersect = exports.segmentSegmentDistance = exports.segmentPointDistance = exports.pointOnSegment = exports.orientation = exports.fixedVec2 = exports.fixedCircle = exports.fixedAabb = exports.closestPointOnSegment = exports.circleIntersectsAabb = exports.circleIntersects = exports.aabbIntersects = exports.aabbContainsPoint = exports.stableSortedKeys = exports.stableEntries = exports.deterministicShuffle = exports.createStableIndexedMap = exports.encodeUtf8 = exports.defaultChecksum = exports.decodeUtf8 = exports.decodeCanonicalValue = exports.cloneCanonical = exports.canonicalStringify = exports.canonicalize = exports.tickDeterministicHfsm = exports.tickDeterministicBehaviorTreeWithMemory = exports.tickDeterministicBehaviorTree = exports.selectDeterministicUtility = exports.runDeterministicBotSimulation = exports.getBlackboardValue = exports.createDeterministicBotBlackboard = exports.cookDeterministicBotAsset = exports.traceDeterministicBotDocument = exports.summarizeDeterministicBotDocumentTraceCoverage = exports.summarizeDeterministicBotDocumentTraceCounterDelta = exports.compileDeterministicBotDocument = exports.createDeterministicAuthorityRoom = exports.stableAssetCookJson = exports.materializePrototypeEntities = exports.cookDeterministicAssets = exports.assertAssetManifestMatches = exports.stepDeterministicAnimation = exports.reconstructDeterministicAnimationEvents = exports.createDeterministicAnimationRuntimeState = exports.cookDeterministicAnimation = void 0;
5
5
  exports.createDeterministicLocalAuthorityCorrectionEnvelope = exports.createDeterministicLocalAuthorityCommandBundleEnvelope = exports.createDeterministicLocalAuthorityAckEnvelope = exports.applyDeterministicLocalAuthorityInputBundleEnvelope = exports.applyDeterministicLocalAuthorityCommandBundleEnvelope = exports.createDeterministicLocalAuthorityRuntime = exports.validateDeterministicLagCompensatedShot3D = exports.validateDeterministicAuthoritativeLagCompensatedShot3D = exports.rewindDeterministicLagCompensatedTargets3D = exports.createDeterministicLagCompensation3DHistory = exports.buildDeterministicLagCompensatedHitboxProxies3D = exports.sortDeterministicInputTimelineFlags = exports.evaluateDeterministicInputTimelineResult = exports.accumulateDeterministicInputTimelineState = exports.createInputAuthority = exports.filterDeterministicEventsForPlayer = exports.createDeterministicEventTimeline = exports.SyncplayError = exports.validateDeterministicFrameSystemsStateless = exports.runDeterministicFrameRuntimeMutationCorpus = exports.isDeterministicFrameHeapStaleHandleError = exports.generateDeterministicFrameRuntimeSource = exports.createDeterministicSparseSetFrameRuntime = exports.createDeterministicSlotMap = exports.createDeterministicFrameRuntimeAccessors = exports.createDeterministicFrameRuntime = exports.createDeterministicBitset = exports.compileDeterministicDslRuntimeDescriptor = exports.assertDeterministicFrameSystemsStateless = exports.assertDeterministicFrameHeapStaleHandleError = exports.stableDeterministicEngineCompleteJson = exports.runDeterministicEngineCompleteSuite = exports.engineCompleteExecutionProofHash = exports.createDeterministicEventEffectsAdapter = exports.createEcsLiteWorld = exports.verifyDeterministicSessionRuntimeReplay = exports.createDeterministicSessionReplayBundle = exports.createDeterministicSessionCore = exports.stepTopDownBody = exports.stepKccLiteBody = exports.quantizeFpsAim = exports.detectVerticalCcdHit = exports.cookStaticColliders = exports.combineDeterministicPhysicsMaterial = exports.verifyDeterministicConfigBundle = exports.summarizeDeterministicConfigBundleFailureReasons = exports.evaluateDeterministicConfigBundleParityResult = exports.cookDeterministicConfigBundle = exports.evaluateDeterministicCommandTimelineMigrationResult = exports.deterministicPayloadMatchesFields = void 0;
6
6
  exports.queryDeterministicPhysicsShapeOverlap2D = exports.queryDeterministicPhysicsAabb2D = exports.nearestDeterministicPhysicsHit2D = exports.createDeterministicPhysicsWorld2D = exports.circleCastDeterministicPhysics2D = exports.createOfflineSession = exports.valueNoise2D = exports.valueNoise1D = exports.hashUint32 = exports.hash2 = exports.fbm2D = exports.createNetworkedSyncplayClient = exports.stepDeterministicNavmeshOffMeshTraversal = exports.stepDeterministicNavmeshAgents = exports.smoothDeterministicNavmeshPath = exports.runDeterministicNavmeshStress = exports.locateDeterministicNavmeshPolygon = exports.findDeterministicNavmeshPointPath = exports.findDeterministicNavmeshPath = exports.cookImportedDeterministicNavmesh = exports.cookDeterministicNavmesh = exports.stepDeterministicNavigationAgents = exports.smoothDeterministicPath = exports.findDeterministicPath = exports.createDeterministicFlowField = exports.cookTilemapNavigationGraph = exports.cookDeterministicNavigationGraph = exports.applyDeterministicReciprocalAvoidance = exports.stepDeterministicKcc3D = exports.stepDeterministicCarry3D = exports.validateDeterministicLagCompensatedHit = exports.stepDeterministicTopDownMover = exports.stepDeterministicKcc2D = exports.stepDeterministicFpsAim = exports.runDeterministicMovementSimulation = exports.runDeterministicMovementCrowdSimulation = exports.rewindDeterministicTargets = exports.createDeterministicMath = exports.encodeDeterministicLocalAuthorityWireEnvelope = exports.deterministicLocalAuthorityWireProtocol = exports.decodeDeterministicLocalAuthorityWireEnvelope = exports.createDeterministicLocalAuthorityWireReceiver = exports.createDeterministicLocalAuthorityWireAuth = exports.createDeterministicLocalAuthoritySnapshotTransfer = exports.createDeterministicLocalAuthoritySnapshotRequestEnvelope = exports.createDeterministicLocalAuthoritySnapshotOfferEnvelope = exports.createDeterministicLocalAuthoritySnapshotEnvelope = exports.createDeterministicLocalAuthoritySnapshotCompleteEnvelope = exports.createDeterministicLocalAuthoritySnapshotChunkEnvelope = exports.createDeterministicLocalAuthorityInputBundleEnvelope = void 0;
7
- exports.withRuntimeDeterminismGuards = exports.createDeterministicRollbackHistory = exports.stableReportJson = exports.stableReportDiffJson = exports.renderDeterministicReportHtml = exports.diffDeterministicReports = exports.createProtocolRollbackReport = exports.verifyScoredReplay = exports.stableFrameDumpJson = exports.stableFrameDiffJson = exports.inspectReplayFrameWithDescriptor = exports.inspectReplayFrame = exports.explainReplayChecksum = exports.diffReplayFrames = exports.verifyReplayScoreAgainstTrustedRuntime = exports.verifyDeterministicSelfContainedFrameRuntimeReplayBundle = exports.verifyDeterministicReplayBundle = exports.verifyDeterministicFrameRuntimeReplayBundle = exports.sliceDeterministicReplayBundle = exports.minimizeDeterministicReplayDesyncBundle = exports.minimizeDeterministicReplayDesync = exports.createDeterministicReplayRuntimeManifest = exports.createDeterministicReplayDebugTables = exports.createDeterministicReplayBundle = exports.verifyReplayWithDescriptor = exports.verifyReplay = exports.DeterministicRandom = exports.interpolateDeterministicRenderVec2 = exports.interpolateDeterministicRenderNumber = exports.createDeterministicR3fRenderAdapter = exports.createDeterministicR3fDebugOverlay = exports.stepDeterministicVehicle3D = exports.stepDeterministicPhysicsWorld3D = exports.shapeCastDeterministicPhysics3DDetailed = exports.shapeCastDeterministicPhysics3D = exports.setDeterministicBodyVelocity3D = exports.runDeterministicPhysics3DStress = exports.raycastDeterministicPhysics3DDetailed = exports.raycastDeterministicPhysics3D = exports.overlapDeterministicPhysics3D = exports.deterministicPhysicsCallback3D = exports.deterministicPhysicsBodyById3D = exports.deriveInverseInertia = exports.createDeterministicPhysicsWorld3D = exports.contactDetailsDeterministicPhysics3D = exports.applyDeterministicImpulses3D = exports.applyDeterministicImpulse3D = exports.stepDeterministicPhysicsWorld2D = exports.raycastDeterministicPhysics2D = exports.raycastAllDeterministicPhysics2D = void 0;
8
- exports.tickCooldown = exports.scheduleFrameEvent = exports.popDueFrameEvents = exports.millisecondsToFrames = exports.framesToMilliseconds = exports.createCooldown = exports.summarizeDeterministicLifecycleCounters = exports.createDeterministicLifecycleRunner = exports.createDeterministicSparseSet = exports.createDeterministicSignalBus = exports.encodeDeterministicSessionMessage = exports.deterministicSessionWireVersion = exports.decodeDeterministicSessionMessage = exports.encodeDeterministicStateSnapshot = exports.deterministicRoomDescriptorHash = exports.DEFAULT_SNAPSHOT_CHUNK_SIZE = exports.DEFAULT_MAX_SNAPSHOT_BYTES = exports.decodeDeterministicStateSnapshot = exports.createSnapshotTransferFromBytes = exports.createSnapshotTransferCollector = exports.bytesToBase64 = exports.base64ToBytes = exports.createSyncplayOfflineRuntime = exports.stableDeterministicDslSchemaJson = exports.parseDeterministicSchemaDsl = exports.deterministicDslMaxPlayers = exports.createDeterministicBinarySerializer = exports.proveSchemaBinaryRoundTrip = exports.generateSchemaTypes = exports.generateSchemaBinarySource = exports.createDeterministicDescriptorBinarySchema = exports.createSchemaArtifacts = exports.stableDescriptorId = exports.defineSyncplayGame = void 0;
7
+ exports.diffDeterministicReports = exports.createProtocolRollbackReport = exports.verifyScoredReplay = exports.stableFrameDumpJson = exports.stableFrameDiffJson = exports.inspectReplayFrameWithDescriptor = exports.inspectReplayFrame = exports.explainReplayChecksum = exports.diffReplayFrames = exports.verifyReplayScoreAgainstTrustedRuntime = exports.verifyDeterministicSelfContainedFrameRuntimeReplayBundle = exports.verifyDeterministicReplayBundle = exports.verifyDeterministicFrameRuntimeReplayBundle = exports.sliceDeterministicReplayBundle = exports.minimizeDeterministicReplayDesyncBundle = exports.minimizeDeterministicReplayDesync = exports.createDeterministicReplayRuntimeManifest = exports.createDeterministicReplayDebugTables = exports.createDeterministicReplayBundle = exports.verifyReplayWithDescriptor = exports.verifyReplay = exports.DeterministicRandom = exports.interpolateDeterministicRenderVec2 = exports.interpolateDeterministicRenderNumber = exports.createDeterministicR3fRenderAdapter = exports.createDeterministicR3fDebugOverlay = exports.stepDeterministicVehicle3D = exports.runDeterministicTower3DScenario = exports.runDeterministicStackingSuite3D = exports.runDeterministicPyramid3DScenario = exports.runDeterministicMassRatio3DScenario = exports.runDeterministicKickRecovery3DScenario = exports.stepDeterministicPhysicsWorld3D = exports.shapeCastDeterministicPhysics3DDetailed = exports.shapeCastDeterministicPhysics3D = exports.setDeterministicBodyVelocity3D = exports.runDeterministicPhysics3DStress = exports.raycastDeterministicPhysics3DDetailed = exports.raycastDeterministicPhysics3D = exports.overlapDeterministicPhysics3D = exports.deterministicPhysicsCallback3D = exports.deterministicPhysicsBodyById3D = exports.deriveInverseInertia = exports.createDeterministicPhysicsWorld3D = exports.contactDetailsDeterministicPhysics3D = exports.applyDeterministicImpulses3D = exports.applyDeterministicImpulse3D = exports.stepDeterministicPhysicsWorld2D = exports.raycastDeterministicPhysics2D = exports.raycastAllDeterministicPhysics2D = void 0;
8
+ exports.tickCooldown = exports.scheduleFrameEvent = exports.popDueFrameEvents = exports.millisecondsToFrames = exports.framesToMilliseconds = exports.createCooldown = exports.summarizeDeterministicLifecycleCounters = exports.createDeterministicLifecycleRunner = exports.createDeterministicSparseSet = exports.createDeterministicSignalBus = exports.encodeDeterministicSessionMessage = exports.deterministicSessionWireVersion = exports.decodeDeterministicSessionMessage = exports.encodeDeterministicStateSnapshot = exports.deterministicRoomDescriptorHash = exports.DEFAULT_SNAPSHOT_CHUNK_SIZE = exports.DEFAULT_MAX_SNAPSHOT_BYTES = exports.decodeDeterministicStateSnapshot = exports.createSnapshotTransferFromBytes = exports.createSnapshotTransferCollector = exports.bytesToBase64 = exports.base64ToBytes = exports.createSyncplayOfflineRuntime = exports.stableDeterministicDslSchemaJson = exports.parseDeterministicSchemaDsl = exports.deterministicDslMaxPlayers = exports.createDeterministicBinarySerializer = exports.proveSchemaBinaryRoundTrip = exports.generateSchemaTypes = exports.generateSchemaBinarySource = exports.createDeterministicDescriptorBinarySchema = exports.createSchemaArtifacts = exports.stableDescriptorId = exports.defineSyncplayGame = exports.withRuntimeDeterminismGuards = exports.createDeterministicRollbackHistory = exports.stableReportJson = exports.stableReportDiffJson = exports.renderDeterministicReportHtml = void 0;
9
9
  var animation_js_1 = require("./animation.js");
10
10
  Object.defineProperty(exports, "cookDeterministicAnimation", { enumerable: true, get: function () { return animation_js_1.cookDeterministicAnimation; } });
11
11
  Object.defineProperty(exports, "createDeterministicAnimationRuntimeState", { enumerable: true, get: function () { return animation_js_1.createDeterministicAnimationRuntimeState; } });
@@ -207,6 +207,12 @@ Object.defineProperty(exports, "setDeterministicBodyVelocity3D", { enumerable: t
207
207
  Object.defineProperty(exports, "shapeCastDeterministicPhysics3D", { enumerable: true, get: function () { return physics3d_js_1.shapeCastDeterministicPhysics3D; } });
208
208
  Object.defineProperty(exports, "shapeCastDeterministicPhysics3DDetailed", { enumerable: true, get: function () { return physics3d_js_1.shapeCastDeterministicPhysics3DDetailed; } });
209
209
  Object.defineProperty(exports, "stepDeterministicPhysicsWorld3D", { enumerable: true, get: function () { return physics3d_js_1.stepDeterministicPhysicsWorld3D; } });
210
+ var physics3d_stacking_js_1 = require("./physics3d-stacking.js");
211
+ Object.defineProperty(exports, "runDeterministicKickRecovery3DScenario", { enumerable: true, get: function () { return physics3d_stacking_js_1.runDeterministicKickRecovery3DScenario; } });
212
+ Object.defineProperty(exports, "runDeterministicMassRatio3DScenario", { enumerable: true, get: function () { return physics3d_stacking_js_1.runDeterministicMassRatio3DScenario; } });
213
+ Object.defineProperty(exports, "runDeterministicPyramid3DScenario", { enumerable: true, get: function () { return physics3d_stacking_js_1.runDeterministicPyramid3DScenario; } });
214
+ Object.defineProperty(exports, "runDeterministicStackingSuite3D", { enumerable: true, get: function () { return physics3d_stacking_js_1.runDeterministicStackingSuite3D; } });
215
+ Object.defineProperty(exports, "runDeterministicTower3DScenario", { enumerable: true, get: function () { return physics3d_stacking_js_1.runDeterministicTower3DScenario; } });
210
216
  var physics3d_vehicle_js_1 = require("./physics3d-vehicle.js");
211
217
  Object.defineProperty(exports, "stepDeterministicVehicle3D", { enumerable: true, get: function () { return physics3d_vehicle_js_1.stepDeterministicVehicle3D; } });
212
218
  var r3f_render_adapter_js_1 = require("./r3f-render-adapter.js");
@@ -22,15 +22,10 @@ function runInstantReplay(options) {
22
22
  options.onFrame(session.getState(), session.currentFrame);
23
23
  expectedFrame += 1;
24
24
  }
25
- const finalSnapshot = session.getSnapshot(session.currentFrame);
26
- /* istanbul ignore if -- the session records a snapshot on every stepped frame. */
27
- if (finalSnapshot === undefined) {
28
- throw new errors_js_1.SyncplayError('Instant replay session has no snapshot at its final frame', {
29
- code: 'instant-replay.missing-final-snapshot',
30
- frame: session.currentFrame,
31
- });
32
- }
33
- return { framesPlayed: options.inputs.length, finalChecksum: finalSnapshot.checksum };
25
+ return {
26
+ framesPlayed: options.inputs.length,
27
+ finalChecksum: session.computeRecordedChecksum(session.currentFrame),
28
+ };
34
29
  }
35
30
  /**
36
31
  * Binds a session config to a replay window. Each `run` simulates on a fresh
@@ -188,8 +188,11 @@ function computeKccPushImpulses(physics, x, y, z, velocity, geometry, maxPushImp
188
188
  const magnitude = Math.min(speedIntoBody * KCC_PUSH_MASS, maxPushImpulse);
189
189
  impulses.push({
190
190
  bodyId: body.id,
191
+ // Apply through the body's center of mass so the horizontal push slides the
192
+ // body rather than torquing it — the soft-step solver's livelier angular
193
+ // response otherwise lets an above-COM push tumble a light crate.
191
194
  impulse: { x: dirX * magnitude, y: 0, z: dirZ * magnitude },
192
- point: { x: body.x, y, z: body.z },
195
+ point: { x: body.x, y: body.y, z: body.z },
193
196
  });
194
197
  }
195
198
  return impulses;
@@ -260,6 +263,19 @@ function primitiveHalfExtent(shape, m) {
260
263
  if (shape.type === 'sphere') {
261
264
  return { x: shape.radius, y: shape.radius, z: shape.radius };
262
265
  }
266
+ if (shape.type === 'hull') {
267
+ // Same symmetric bound the solver uses (physics3d-solver shapeHalfExtent).
268
+ let ext = { x: 0, y: 0, z: 0 };
269
+ for (const vertex of shape.vertices) {
270
+ const rotated = (0, physics3d_shared_js_1.mulMat3Vec)(m, vertex);
271
+ ext = {
272
+ x: Math.max(ext.x, Math.abs(rotated.x)),
273
+ y: Math.max(ext.y, Math.abs(rotated.y)),
274
+ z: Math.max(ext.z, Math.abs(rotated.z)),
275
+ };
276
+ }
277
+ return ext;
278
+ }
263
279
  if (shape.type === 'box') {
264
280
  return {
265
281
  x: Math.abs(m[0]) * shape.halfX + Math.abs(m[1]) * shape.halfY + Math.abs(m[2]) * shape.halfZ,
@@ -394,7 +394,7 @@ function createNetworkedSyncplayClient(options) {
394
394
  return '';
395
395
  }
396
396
  active.restoreToFrame(frame);
397
- return String((0, canonical_js_1.defaultChecksum)(active.getState()));
397
+ return active.getWireStateChecksum(frame);
398
398
  }
399
399
  function applyConfirmedFrames(frames) {
400
400
  for (const frame of frames) {
@@ -457,9 +457,9 @@ function createNetworkedSyncplayClient(options) {
457
457
  /**
458
458
  * Report the confirmed-state checksum at `tick`, read from the rolling
459
459
  * 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.
460
+ * session out from under prediction. Quorum consistency comes from the wire
461
+ * checksum's full-state serialize/deserialize round-trip; synctest's codec
462
+ * phase verifies that round-trip is canonical on every retained frame.
463
463
  * Ticks that fell out of the buffer are skipped (the client simply is not
464
464
  * donor-eligible for them).
465
465
  */
@@ -469,7 +469,7 @@ function createNetworkedSyncplayClient(options) {
469
469
  if (active === undefined || tick < 0 || role === 'spectator') {
470
470
  return;
471
471
  }
472
- const snapshot = active.getSnapshot(tick + 1);
472
+ const snapshot = active.peekSnapshot(tick + 1);
473
473
  if (snapshot === undefined) {
474
474
  return;
475
475
  }
@@ -477,7 +477,7 @@ function createNetworkedSyncplayClient(options) {
477
477
  kind: 'checksum-report',
478
478
  slot,
479
479
  tick,
480
- checksum: (0, canonical_js_1.defaultChecksum)(snapshot.state),
480
+ checksum: active.getWireStateChecksum(snapshot.frame),
481
481
  }));
482
482
  reportedTicks.push(tick);
483
483
  if (reportedTicks.length > REPORTED_TICKS_RETAINED) {
@@ -531,7 +531,7 @@ function createNetworkedSyncplayClient(options) {
531
531
  const minTick = minFrame - 1;
532
532
  const candidates = reportedTicks.filter((tick) => tick >= minTick).sort((a, b) => a - b);
533
533
  for (const tick of candidates) {
534
- const snapshot = active.getSnapshot(tick + 1);
534
+ const snapshot = active.peekSnapshot(tick + 1);
535
535
  const confirmedAtTick = timeline[tick];
536
536
  if (snapshot === undefined || confirmedAtTick === undefined) {
537
537
  continue;
@@ -540,9 +540,9 @@ function createNetworkedSyncplayClient(options) {
540
540
  v: 1,
541
541
  tick,
542
542
  sessionFrame: tick + 1,
543
- stateBytes: (0, session_snapshot_js_1.bytesToBase64)(snapshot.stateBytes),
543
+ stateBytes: (0, session_snapshot_js_1.bytesToBase64)(active.getSnapshotBytes(snapshot.frame)),
544
544
  randomState: snapshot.randomState,
545
- stateChecksum: (0, canonical_js_1.defaultChecksum)(snapshot.state),
545
+ stateChecksum: active.getWireStateChecksum(snapshot.frame),
546
546
  confirmedInputs: confirmedAtTick.map((input) => (0, canonical_js_1.cloneCanonical)(input)),
547
547
  });
548
548
  donorTransferCounter += 1;
@@ -613,6 +613,8 @@ function createNetworkedSyncplayClient(options) {
613
613
  randomState: snapshot.randomState,
614
614
  inputs: snapshot.confirmedInputs.map((input) => decodeInputSafe(input)),
615
615
  });
616
+ // The active state was just deserialized, so this is already the same
617
+ // round-tripped full-state basis used by the donor's wire checksum.
616
618
  const actualChecksum = (0, canonical_js_1.defaultChecksum)(active.getState());
617
619
  if (actualChecksum !== snapshot.stateChecksum) {
618
620
  throw new errors_js_1.SyncplayError('Hydrated snapshot state does not match its declared checksum', {
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.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);
@@ -49,3 +49,7 @@ var engine_parity_js_1 = require("./engine-parity.js");
49
49
  Object.defineProperty(exports, "explainEngineParityFixture", { enumerable: true, get: function () { return engine_parity_js_1.explainEngineParityFixture; } });
50
50
  Object.defineProperty(exports, "runEngineParitySuite", { enumerable: true, get: function () { return engine_parity_js_1.runEngineParitySuite; } });
51
51
  Object.defineProperty(exports, "stableEngineParityJson", { enumerable: true, get: function () { return engine_parity_js_1.stableEngineParityJson; } });
52
+ // Offline collider cooking — build-time only, never part of a simulation step.
53
+ var collider_cooking_js_1 = require("./collider-cooking.js");
54
+ Object.defineProperty(exports, "assertCookedColliderIsRuntimeValid", { enumerable: true, get: function () { return collider_cooking_js_1.assertCookedColliderIsRuntimeValid; } });
55
+ 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
  }
@@ -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 });