@series-inc/rundot-syncplay 5.23.0-beta.7 → 5.23.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 (75) hide show
  1. package/README.md +116 -15
  2. package/dist/browser.d.ts +87 -55
  3. package/dist/browser.js +47 -52
  4. package/dist/certification.d.ts +32 -3
  5. package/dist/certification.js +88 -3
  6. package/dist/cjs/browser.js +259 -87
  7. package/dist/cjs/certification.js +87 -2
  8. package/dist/cjs/collision.js +58 -0
  9. package/dist/cjs/creator.js +2 -1
  10. package/dist/cjs/engine-complete.js +213 -3
  11. package/dist/cjs/engine-parity-matrix.js +19 -4
  12. package/dist/cjs/index.js +199 -132
  13. package/dist/cjs/math.js +342 -15
  14. package/dist/cjs/movement3d.js +230 -1
  15. package/dist/cjs/node.js +5 -1
  16. package/dist/cjs/noise.js +58 -0
  17. package/dist/cjs/physics-cert.js +147 -6
  18. package/dist/cjs/physics2d.js +10 -4
  19. package/dist/cjs/physics3d-joints.js +637 -0
  20. package/dist/cjs/physics3d-shared.js +288 -0
  21. package/dist/cjs/physics3d-solver.js +1351 -0
  22. package/dist/cjs/physics3d-vehicle.js +467 -0
  23. package/dist/cjs/physics3d.js +1057 -223
  24. package/dist/cjs/random.js +49 -0
  25. package/dist/cjs/replay-bundle.js +127 -20
  26. package/dist/cjs/sample-scenes.js +3 -0
  27. package/dist/cjs/sdk-offline.js +6 -32
  28. package/dist/cjs/sdk-session.js +156 -0
  29. package/dist/cjs/session-build.js +42 -0
  30. package/dist/cjs/testing.js +107 -0
  31. package/dist/cli.js +40 -12
  32. package/dist/collision.d.ts +12 -0
  33. package/dist/collision.js +52 -0
  34. package/dist/creator.d.ts +1 -1
  35. package/dist/creator.js +1 -1
  36. package/dist/engine-complete.js +214 -4
  37. package/dist/engine-parity-matrix.js +19 -4
  38. package/dist/index.d.ts +94 -89
  39. package/dist/index.js +45 -56
  40. package/dist/math.d.ts +59 -6
  41. package/dist/math.js +342 -15
  42. package/dist/movement3d.d.ts +73 -0
  43. package/dist/movement3d.js +229 -1
  44. package/dist/node.d.ts +4 -0
  45. package/dist/node.js +2 -0
  46. package/dist/noise.d.ts +7 -0
  47. package/dist/noise.js +51 -0
  48. package/dist/physics-cert.d.ts +1 -1
  49. package/dist/physics-cert.js +147 -6
  50. package/dist/physics2d.js +10 -4
  51. package/dist/physics3d-joints.d.ts +94 -0
  52. package/dist/physics3d-joints.js +634 -0
  53. package/dist/physics3d-shared.d.ts +72 -0
  54. package/dist/physics3d-shared.js +257 -0
  55. package/dist/physics3d-solver.d.ts +197 -0
  56. package/dist/physics3d-solver.js +1346 -0
  57. package/dist/physics3d-vehicle.d.ts +84 -0
  58. package/dist/physics3d-vehicle.js +463 -0
  59. package/dist/physics3d.d.ts +108 -8
  60. package/dist/physics3d.js +1006 -177
  61. package/dist/random.d.ts +7 -0
  62. package/dist/random.js +49 -0
  63. package/dist/replay-bundle.d.ts +40 -1
  64. package/dist/replay-bundle.js +126 -20
  65. package/dist/sample-scenes.js +3 -0
  66. package/dist/sdk-offline.js +6 -32
  67. package/dist/sdk-session.d.ts +47 -0
  68. package/dist/sdk-session.js +153 -0
  69. package/dist/session-build.d.ts +21 -0
  70. package/dist/session-build.js +39 -0
  71. package/dist/testing.d.ts +52 -0
  72. package/dist/testing.js +45 -0
  73. package/dist/tools/vite-plugin.d.ts +8 -0
  74. package/dist/tools/vite-plugin.js +63 -9
  75. package/package.json +11 -3
package/dist/cli.js CHANGED
@@ -3123,7 +3123,7 @@ async function runProjectCertify(args) {
3123
3123
  if (manifest.creatorEntrypoints !== undefined || requiredSections.includes('creator-boundary')) {
3124
3124
  sections.push(await certifyProjectCreatorBoundary(manifest, manifestDir));
3125
3125
  }
3126
- sections.push(await certifyProjectStatic(manifest, manifestDir, schemaStateFields));
3126
+ sections.push(await certifyProjectStatic(manifest, manifestDir, schemaStateFields, requiredSections.includes('static-determinism')));
3127
3127
  const sectionFailures = sections.flatMap((section) => section.failures.map((failure) => `${section.section}: ${failure}`));
3128
3128
  failures.push(...sectionFailures);
3129
3129
  const skippedRequiredSections = requiredSections.filter((section) => !projectRequirementSatisfied(section, sections));
@@ -3267,7 +3267,9 @@ async function runProjectCheck(args) {
3267
3267
  const browserJsonPath = readString(args, 'browser-json') ?? (browserRuntime === undefined ? undefined : join(outDir, `project-browser-proof-${browserRuntime}.json`));
3268
3268
  process.exitCode = undefined;
3269
3269
  await runProjectCertify(projectSubcommandArgs('project:certify', [manifestPath], {
3270
- require: readString(args, 'require'),
3270
+ // project:check is the deployable preflight, so it must certify a
3271
+ // file-backed simulation module even when the caller passes no --require.
3272
+ require: readString(args, 'require') ?? 'static-determinism',
3271
3273
  frames: readString(args, 'frames'),
3272
3274
  agents: readString(args, 'agents'),
3273
3275
  slots: readString(args, 'slots'),
@@ -6258,9 +6260,27 @@ async function certifyProjectAnimations(manifest, manifestDir, frames) {
6258
6260
  }
6259
6261
  return sections;
6260
6262
  }
6261
- async function certifyProjectStatic(manifest, manifestDir, schemaStateFields) {
6263
+ async function certifyProjectStatic(manifest, manifestDir, schemaStateFields, required) {
6262
6264
  const paths = manifest.staticPaths ?? [];
6263
6265
  if (paths.length === 0) {
6266
+ // A deployable project must prove its simulation is a real, file-backed
6267
+ // module. When the caller requires this section but the manifest lists no
6268
+ // staticPaths, fail loudly with a pointed message instead of silently
6269
+ // skipping. Advisory runs (section not required) still skip.
6270
+ if (required) {
6271
+ return {
6272
+ section: 'static-determinism',
6273
+ result: 'fail',
6274
+ metrics: {
6275
+ checkedPaths: 0,
6276
+ diagnostics: 0,
6277
+ errors: 0,
6278
+ warnings: 0,
6279
+ parser: getDeterminismParserKind(),
6280
+ },
6281
+ failures: ['static-determinism requires at least one staticPaths entry'],
6282
+ };
6283
+ }
6264
6284
  return skippedProjectSection('static-determinism');
6265
6285
  }
6266
6286
  const resolvedPaths = paths.map((path) => resolveManifestPath(manifestDir, path));
@@ -6484,6 +6504,14 @@ async function certifyProjectReplayRuntime(manifest, manifestDir, outDir, frames
6484
6504
  projectAuthoredSystemFrames: authoredSystemFrames,
6485
6505
  projectAuthoredSystemChecksum: authoredSystemChecksum,
6486
6506
  frames: replayFrames,
6507
+ // Game identity for the M8.5 deploy-time verification manifest: the
6508
+ // cloud-run deploy interceptor keys the stored trusted manifest by
6509
+ // deterministicVersion and the score re-run gates on all three. They live
6510
+ // on the game descriptor (not the runtimeReplay sub-object), so they are
6511
+ // copied into the bundle metadata explicitly here.
6512
+ deterministicVersion: projectDescriptor?.deterministicVersion ?? '',
6513
+ inputSchemaId: projectDescriptor?.input.id ?? '',
6514
+ stateSchemaId: projectDescriptor?.state.id ?? '',
6487
6515
  },
6488
6516
  });
6489
6517
  const verification = verifyDeterministicFrameRuntimeReplayBundle(bundle, {
@@ -8841,7 +8869,7 @@ async function runOfflineReplay(args) {
8841
8869
  checksums: checksumCount,
8842
8870
  mismatches: 0,
8843
8871
  finalChecksum,
8844
- replayRecordingMode: retainReplay ? 'memory' : 'metrics',
8872
+ replayRecordingMode: retainReplay ? 'memory' : 'none',
8845
8873
  replayBytes,
8846
8874
  inputBytesPerTick: (inputBytes / frames).toFixed(2),
8847
8875
  packedInputBytesPerTick: (packedInputBytes / frames).toFixed(2),
@@ -9271,7 +9299,7 @@ async function runReplayVerifyBrowser(args) {
9271
9299
  return;
9272
9300
  }
9273
9301
  const page = await browser.newPage();
9274
- await page.goto(`${baseUrl}/dist/browser.js`);
9302
+ await page.goto(`${baseUrl}/dist/testing.js`);
9275
9303
  const result = await page.evaluate(async ({ moduleUrl, replay: browserReplay }) => {
9276
9304
  const mod = (await import(moduleUrl));
9277
9305
  return mod.verifyReplay({
@@ -9283,7 +9311,7 @@ async function runReplayVerifyBrowser(args) {
9283
9311
  deterministicVersion: '0.0.0-source',
9284
9312
  hydrateInput: mod.hydrateLabInput,
9285
9313
  });
9286
- }, { moduleUrl: `${baseUrl}/dist/browser.js`, replay });
9314
+ }, { moduleUrl: `${baseUrl}/dist/testing.js`, replay });
9287
9315
  const elapsedMs = performance.now() - startedAt;
9288
9316
  const replayHash = defaultChecksum(replayText);
9289
9317
  const browserBundleHash = defaultChecksum(await readFile(resolve(packageRoot, 'dist/browser.js'), 'utf8'));
@@ -9369,11 +9397,11 @@ async function runNumberBrowser(args) {
9369
9397
  return;
9370
9398
  }
9371
9399
  const page = await browser.newPage();
9372
- await page.goto(`${baseUrl}/dist/browser.js`);
9400
+ await page.goto(`${baseUrl}/dist/testing.js`);
9373
9401
  const browserResult = await page.evaluate(async ({ moduleUrl, mode, frames, seed }) => {
9374
9402
  const mod = (await import(moduleUrl));
9375
9403
  return mod.runNumberLab({ mode, frames, seed });
9376
- }, { moduleUrl: `${baseUrl}/dist/browser.js`, mode, frames, seed });
9404
+ }, { moduleUrl: `${baseUrl}/dist/testing.js`, mode, frames, seed });
9377
9405
  const elapsedMs = performance.now() - startedAt;
9378
9406
  const checksumMatch = nodeResult.finalChecksum === browserResult.finalChecksum;
9379
9407
  const valueMatch = Object.is(nodeResult.finalValue, browserResult.finalValue);
@@ -9453,11 +9481,11 @@ async function runEngineCoreBrowser(args) {
9453
9481
  return;
9454
9482
  }
9455
9483
  const page = await browser.newPage();
9456
- await page.goto(`${baseUrl}/dist/browser.js`);
9484
+ await page.goto(`${baseUrl}/dist/testing.js`);
9457
9485
  const browserProof = await page.evaluate(async ({ moduleUrl, players, frames, seed }) => {
9458
9486
  const mod = (await import(moduleUrl));
9459
9487
  return mod.runDeterministicEngineCoreNumericProof({ players, frames, seed });
9460
- }, { moduleUrl: `${baseUrl}/dist/browser.js`, players, frames, seed });
9488
+ }, { moduleUrl: `${baseUrl}/dist/testing.js`, players, frames, seed });
9461
9489
  const elapsedMs = performance.now() - startedAt;
9462
9490
  const browserBundleHash = defaultChecksum(await readFile(resolve(packageRoot, 'dist/browser.js'), 'utf8'));
9463
9491
  const packageBuildHash = defaultChecksum(await readFile(resolve(packageRoot, 'dist/index.js'), 'utf8'));
@@ -10805,8 +10833,8 @@ function readOptionalProjectBrowserRuntime(args) {
10805
10833
  }
10806
10834
  function readPhysicsCertEngine(args) {
10807
10835
  const engine = readString(args, 'engine') ?? 'builtin-fixed';
10808
- if (engine !== 'builtin-fixed' && engine !== 'rapier2d-compat') {
10809
- throw new Error('--engine must be builtin-fixed or rapier2d-compat');
10836
+ if (engine !== 'builtin-fixed' && engine !== 'builtin-rigid' && engine !== 'rapier2d-compat') {
10837
+ throw new Error('--engine must be builtin-fixed, builtin-rigid, or rapier2d-compat');
10810
10838
  }
10811
10839
  return engine;
10812
10840
  }
@@ -1,3 +1,4 @@
1
+ import type { DeterministicMath } from './math.js';
1
2
  export type FixedVec2 = {
2
3
  readonly x: number;
3
4
  readonly y: number;
@@ -23,3 +24,14 @@ export declare function translateAabb(aabb: FixedAabb, delta: FixedVec2): FixedA
23
24
  export declare function sortAabbsByStableId<T extends {
24
25
  readonly id: string;
25
26
  }>(items: readonly T[]): readonly T[];
27
+ /**
28
+ * Orientation of the ordered triple (a, b, c): +1 counter-clockwise, -1
29
+ * clockwise, 0 collinear. Pure integer cross-product; scale-free.
30
+ */
31
+ export declare function orientation(a: FixedVec2, b: FixedVec2, c: FixedVec2): -1 | 0 | 1;
32
+ /** True when p lies within the axis-aligned bounding box of segment [a, b]. */
33
+ export declare function pointOnSegment(p: FixedVec2, a: FixedVec2, b: FixedVec2): boolean;
34
+ export declare function segmentsIntersect(a0: FixedVec2, a1: FixedVec2, b0: FixedVec2, b1: FixedVec2): boolean;
35
+ export declare function closestPointOnSegment(p: FixedVec2, a: FixedVec2, b: FixedVec2, math: DeterministicMath): FixedVec2;
36
+ export declare function segmentPointDistance(p: FixedVec2, a: FixedVec2, b: FixedVec2, math: DeterministicMath): number;
37
+ export declare function segmentSegmentDistance(a0: FixedVec2, a1: FixedVec2, b0: FixedVec2, b1: FixedVec2, math: DeterministicMath): number;
package/dist/collision.js CHANGED
@@ -83,3 +83,55 @@ function assertInteger(value, name) {
83
83
  throw new Error(`${name} must be an integer fixed-point value`);
84
84
  }
85
85
  }
86
+ /**
87
+ * Orientation of the ordered triple (a, b, c): +1 counter-clockwise, -1
88
+ * clockwise, 0 collinear. Pure integer cross-product; scale-free.
89
+ */
90
+ export function orientation(a, b, c) {
91
+ const v = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
92
+ return v > 0 ? 1 : v < 0 ? -1 : 0;
93
+ }
94
+ /** True when p lies within the axis-aligned bounding box of segment [a, b]. */
95
+ export function pointOnSegment(p, a, b) {
96
+ return (p.x >= Math.min(a.x, b.x) &&
97
+ p.x <= Math.max(a.x, b.x) &&
98
+ p.y >= Math.min(a.y, b.y) &&
99
+ p.y <= Math.max(a.y, b.y));
100
+ }
101
+ export function segmentsIntersect(a0, a1, b0, b1) {
102
+ const d1 = orientation(a0, a1, b0);
103
+ const d2 = orientation(a0, a1, b1);
104
+ const d3 = orientation(b0, b1, a0);
105
+ const d4 = orientation(b0, b1, a1);
106
+ if (d1 === 0 && pointOnSegment(b0, a0, a1))
107
+ return true;
108
+ if (d2 === 0 && pointOnSegment(b1, a0, a1))
109
+ return true;
110
+ if (d3 === 0 && pointOnSegment(a0, b0, b1))
111
+ return true;
112
+ if (d4 === 0 && pointOnSegment(a1, b0, b1))
113
+ return true;
114
+ return d1 !== d2 && d3 !== d4;
115
+ }
116
+ export function closestPointOnSegment(p, a, b, math) {
117
+ const abx = b.x - a.x;
118
+ const aby = b.y - a.y;
119
+ const abLenSq = math.mul(abx, abx) + math.mul(aby, aby);
120
+ if (abLenSq === 0) {
121
+ return { x: a.x, y: a.y };
122
+ }
123
+ const apx = p.x - a.x;
124
+ const apy = p.y - a.y;
125
+ const t = math.clamp(math.div(math.mul(apx, abx) + math.mul(apy, aby), abLenSq), 0, math.fixedScale);
126
+ return { x: a.x + math.mul(abx, t), y: a.y + math.mul(aby, t) };
127
+ }
128
+ export function segmentPointDistance(p, a, b, math) {
129
+ const c = closestPointOnSegment(p, a, b, math);
130
+ return math.hypot(p.x - c.x, p.y - c.y);
131
+ }
132
+ export function segmentSegmentDistance(a0, a1, b0, b1, math) {
133
+ if (segmentsIntersect(a0, a1, b0, b1)) {
134
+ return 0;
135
+ }
136
+ return math.min(math.min(segmentPointDistance(a0, b0, b1, math), segmentPointDistance(a1, b0, b1, math)), math.min(segmentPointDistance(b0, a0, a1, math), segmentPointDistance(b1, a0, a1, math)));
137
+ }
package/dist/creator.d.ts CHANGED
@@ -7,7 +7,7 @@ export { createSessionFromPromotionSnapshot, exportPromotionSnapshot } from './s
7
7
  export { verifyReplay, verifyReplayWithDescriptor } from './replay.js';
8
8
  export { defineSyncplayGame } from './schema.js';
9
9
  export { createSchemaArtifacts } from './schema-artifacts.js';
10
- export { createSyncplayOfflineRuntime } from './sdk-offline.js';
10
+ export { createSyncplayOfflineRuntime, createSyncplayOfflineRuntime as createSyncplayGameRuntime } from './sdk-offline.js';
11
11
  export { runSyncplaySynctest } from './synctest.js';
12
12
  export type { SyncplaySynctestConfig, SyncplaySynctestDivergence, SyncplaySynctestInputFuzz, SyncplaySynctestPhase, SyncplaySynctestResult, } from './synctest.js';
13
13
  export type { CanonicalValue } from './canonical.js';
package/dist/creator.js CHANGED
@@ -7,5 +7,5 @@ export { createSessionFromPromotionSnapshot, exportPromotionSnapshot } from './s
7
7
  export { verifyReplay, verifyReplayWithDescriptor } from './replay.js';
8
8
  export { defineSyncplayGame } from './schema.js';
9
9
  export { createSchemaArtifacts } from './schema-artifacts.js';
10
- export { createSyncplayOfflineRuntime } from './sdk-offline.js';
10
+ export { createSyncplayOfflineRuntime, createSyncplayOfflineRuntime as createSyncplayGameRuntime } from './sdk-offline.js';
11
11
  export { runSyncplaySynctest } from './synctest.js';
@@ -17,7 +17,8 @@ import { runDeterministicLocalAuthorityWireFixture } from './local-authority-wir
17
17
  import { runDeterministicMulticlientAuthorityFixture } from './multiclient-authority.js';
18
18
  import { runDeterministicKcc3DAdversarialFixture, runDeterministicKcc3DCallbackFilterFixture, runDeterministicKcc3DCrowdFixture, runDeterministicKcc3DFilterForceFixture, runDeterministicKcc3DGeometryFixture, stepDeterministicKcc3D } from './movement3d.js';
19
19
  import { cookDeterministicNavmesh, findDeterministicNavmeshPath, runDeterministicNavmeshImportTraversalFixture, runDeterministicNavmeshLayered3DFixture, runDeterministicNavmeshStress, runDeterministicNavmeshVelocityObstacleFixture, stepDeterministicNavmeshAgents } from './navmesh.js';
20
- import { createDeterministicPhysicsWorld3D, runDeterministicPhysics3DCallbackMaskFixture, runDeterministicPhysics3DDetailFixture, runDeterministicPhysics3DMaterialFixture, runDeterministicPhysics3DQueryOptionsFixture, runDeterministicPhysics3DStackFixture, runDeterministicPhysics3DStress, stepDeterministicPhysicsWorld3D, } from './physics3d.js';
20
+ import { createDeterministicPhysicsWorld3D, runDeterministicPhysics3DCallbackMaskFixture, runDeterministicPhysics3DDetailFixture, runDeterministicPhysics3DMaterialFixture, runDeterministicPhysics3DQueryOptionsFixture, runDeterministicPhysics3DStackFixture, runDeterministicPhysics3DStress, applyDeterministicImpulses3D, stepDeterministicPhysicsWorld3D, } from './physics3d.js';
21
+ import { stepDeterministicVehicle3D, } from './physics3d-vehicle.js';
21
22
  import { runDeterministicSignalFixture } from './signals.js';
22
23
  import { runDeterministicSparseSetFixture } from './sparse-set.js';
23
24
  import { runDeterministicLifecycleFixture } from './system-lifecycle.js';
@@ -1217,6 +1218,11 @@ function physicsCategory(bodies, requestedFrames, executedFrames, enforceThresho
1217
1218
  const lagComp3DApi = runDeterministicLagCompensation3DApiFixture();
1218
1219
  const authoritativeLagComp3D = runDeterministicAuthoritativeLagCompensation3DFixture();
1219
1220
  const fullStep = runPhysics3DFullStepMeasurementFixture(Math.min(requestedFrames, 300));
1221
+ // 128-dynamic-body interacting-pile timing sample. Only sampled on long
1222
+ // benchmark runs (>= 2000 frames) so its fixed wall-clock cost amortizes below
1223
+ // the per-frame mean budget; short ad-hoc runs skip it (gates default true).
1224
+ const rigidPile = benchmark && requestedFrames >= 2000 ? runPhysics3DRigidPileMeasurementFixture(128, 200) : undefined;
1225
+ const vehicleFleet = benchmark && requestedFrames >= 2000 ? runVehicle3DFleetMeasurementFixture(16, 200) : undefined;
1220
1226
  const highBodySample512 = requestedFrames > 512 ? runDeterministicPhysics3DStress(512, 128) : undefined;
1221
1227
  const highBodySample2048 = requestedFrames > 512 ? runDeterministicPhysics3DStress(2048, 32) : undefined;
1222
1228
  return categoryReport('physics3d', defaultChecksum({
@@ -1265,11 +1271,15 @@ function physicsCategory(bodies, requestedFrames, executedFrames, enforceThresho
1265
1271
  && stack.settledDynamicBodies === stack.stackLayers,
1266
1272
  stackRollbackEquivalent: stack.rollbackEquivalent,
1267
1273
  stackAuthoringOrderStable: stack.stableUnderAuthoringOrderPermutation,
1274
+ // Rigid-solver material response (impulse-based) — combine semantics unchanged
1275
+ // (floor-average friction, max restitution); the bounce/friction velocity
1276
+ // magnitudes come from the sequential-impulse solver, not the legacy pusher.
1268
1277
  materialResponseCovered: materials.combinedFriction === 3
1269
1278
  && materials.combinedRestitution === 7
1270
- && materials.normalVelocityAfterBounce === -2
1271
- && materials.tangentVelocityAfterFrictionY === 7
1272
- && materials.tangentVelocityAfterFrictionZ === -7,
1279
+ && materials.normalVelocityAfterBounce > 0
1280
+ && materials.tangentVelocityAfterFrictionY < 10
1281
+ && materials.tangentVelocityAfterFrictionZ > -10
1282
+ && materials.tangentVelocityAfterFrictionZ < 0,
1273
1283
  materialRollbackEquivalent: materials.rollbackEquivalent,
1274
1284
  callbackLifecycleCovered: callbackMasks.collisionEnter > 0
1275
1285
  && callbackMasks.collisionStay > 0
@@ -1309,6 +1319,17 @@ function physicsCategory(bodies, requestedFrames, executedFrames, enforceThresho
1309
1319
  && fullStep.p95MeasuredStepMs > 0
1310
1320
  && fullStep.p99MeasuredStepMs > 0,
1311
1321
  fullStepRollbackEquivalent: fullStep.rollbackEquivalent,
1322
+ rigidPileMeasuredTimingCovered: rigidPile === undefined ? true : rigidPile.bodies === 128 && rigidPile.p99MeasuredStepMs > 0,
1323
+ rigidPileRollbackEquivalent: rigidPile === undefined ? true : rigidPile.rollbackEquivalent,
1324
+ vehicleFleetMeasuredTimingCovered: vehicleFleet === undefined
1325
+ ? true
1326
+ : vehicleFleet.vehicles === 16 && vehicleFleet.groundedWheelSamples > 0 && vehicleFleet.p99MeasuredStepMs > 0,
1327
+ vehicleFleetRollbackEquivalent: vehicleFleet === undefined ? true : vehicleFleet.rollbackEquivalent,
1328
+ vehicleFleetP99WithinBudget: enforceThresholds && vehicleFleet !== undefined ? vehicleFleet.p99MeasuredStepMs <= 25 : true,
1329
+ // Measured wall-clock (never in checksummed state). The pooled clip scratch
1330
+ // + constraint pool (provenance pass 6) pulled the burst p99 to ~1.5 ms
1331
+ // locally; the 10 ms gate keeps generous headroom for CI variance.
1332
+ rigidPileP99WithinBudget: enforceThresholds && rigidPile !== undefined ? rigidPile.p99MeasuredStepMs <= 10 : true,
1312
1333
  fullStepContactsAndQueriesCovered: fullStep.contactCount > 0 && fullStep.queryCount === fullStep.frames,
1313
1334
  executedRequestedFrames: metrics.frames === requestedFrames,
1314
1335
  ...(metrics.extrapolatedStress ? {} : {
@@ -1435,6 +1456,13 @@ function physicsCategory(bodies, requestedFrames, executedFrames, enforceThresho
1435
1456
  highBodySample512Frames: highBodySample512?.frames ?? 0,
1436
1457
  highBodySample2048Checksum: highBodySample2048?.checksum ?? '',
1437
1458
  highBodySample2048Frames: highBodySample2048?.frames ?? 0,
1459
+ vehicleFleetChecksum: vehicleFleet?.checksum ?? '',
1460
+ vehicleFleetVehicles: vehicleFleet?.vehicles ?? 0,
1461
+ vehicleFleetFrames: vehicleFleet?.frames ?? 0,
1462
+ vehicleFleetGroundedWheelSamples: vehicleFleet?.groundedWheelSamples ?? 0,
1463
+ vehicleFleetP95MeasuredStepMs: vehicleFleet?.p95MeasuredStepMs ?? 0,
1464
+ vehicleFleetP99MeasuredStepMs: vehicleFleet?.p99MeasuredStepMs ?? 0,
1465
+ vehicleFleetMaxMeasuredStepMs: vehicleFleet?.maxMeasuredStepMs ?? 0,
1438
1466
  }, requestedFrames, metrics.frames));
1439
1467
  }
1440
1468
  function kccCategory(players, requestedFrames, executedFrames, benchmark, noFastStress) {
@@ -1616,6 +1644,188 @@ function runPhysics3DFullStepMeasurementFixture(frames) {
1616
1644
  wallClockElapsedMs: roundMetric(engineCompleteNowMs() - startedAt, 4),
1617
1645
  };
1618
1646
  }
1647
+ function createRigidPileBodies3D(bodies) {
1648
+ const out = [{
1649
+ id: 'pile-floor', kind: 'static', x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0,
1650
+ shape: { type: 'box', halfX: 40, halfY: 0.5, halfZ: 40 }, layer: 1, mask: 1,
1651
+ material: { friction: 8, restitution: 0 },
1652
+ }];
1653
+ const perRow = 8;
1654
+ for (let index = 0; index < bodies; index += 1) {
1655
+ const column = index % perRow;
1656
+ const rowInLayer = Math.floor(index / perRow) % perRow;
1657
+ const layer = Math.floor(index / (perRow * perRow));
1658
+ out.push({
1659
+ id: `pile-${String(index).padStart(3, '0')}`,
1660
+ kind: 'dynamic',
1661
+ x: (column - perRow / 2) * 1.1,
1662
+ y: 1 + layer * 1.05,
1663
+ z: (rowInLayer - perRow / 2) * 1.1,
1664
+ vx: 0, vy: 0, vz: 0, mass: 1,
1665
+ orientation: { x: 0, y: 0, z: 0, w: 1 }, angularVel: { x: 0, y: 0, z: 0 },
1666
+ shape: { type: 'box', halfX: 0.5, halfY: 0.5, halfZ: 0.5 }, layer: 1, mask: 1,
1667
+ material: { friction: 8, restitution: 0 },
1668
+ });
1669
+ }
1670
+ return out;
1671
+ }
1672
+ /**
1673
+ * 128-dynamic-body interacting-pile step-cost sample, timed via wall clock
1674
+ * (never enters checksummed state). CI-gated at measured p99 <= 10 ms
1675
+ * (rigidPileP99WithinBudget) — headroom over the ~1.5 ms local burst cost
1676
+ * after the pooled clip scratch + constraint pool (provenance pass 6).
1677
+ */
1678
+ function runPhysics3DRigidPileMeasurementFixture(bodies, frames) {
1679
+ const initial = createDeterministicPhysicsWorld3D(createRigidPileBodies3D(bodies));
1680
+ let world = initial;
1681
+ let snapshot = initial;
1682
+ const timings = [];
1683
+ const half = Math.floor(frames / 2);
1684
+ for (let frame = 0; frame < frames; frame += 1) {
1685
+ if (frame === half) {
1686
+ snapshot = world;
1687
+ }
1688
+ const stepStartedAt = engineCompleteNowMs();
1689
+ world = stepDeterministicPhysicsWorld3D(world, { gravityY: -0.02, computeChecksum: false }).world;
1690
+ timings.push(engineCompleteNowMs() - stepStartedAt);
1691
+ }
1692
+ const checksum = defaultChecksum(world);
1693
+ let restored = snapshot;
1694
+ for (let frame = half; frame < frames; frame += 1) {
1695
+ restored = stepDeterministicPhysicsWorld3D(restored, { gravityY: -0.02, computeChecksum: false }).world;
1696
+ }
1697
+ return {
1698
+ checksum,
1699
+ rollbackEquivalent: checksum === defaultChecksum(restored),
1700
+ bodies: world.bodies.filter((body) => body.kind === 'dynamic').length,
1701
+ frames,
1702
+ p95MeasuredStepMs: roundMetric(percentileNumber(timings, 95), 6),
1703
+ p99MeasuredStepMs: roundMetric(percentileNumber(timings, 99), 6),
1704
+ maxMeasuredStepMs: roundMetric(timings.reduce((max, value) => Math.max(max, value), 0), 6),
1705
+ };
1706
+ }
1707
+ function fleetVehicleWheel(localAnchor, steered) {
1708
+ return {
1709
+ localAnchor,
1710
+ suspensionDir: { x: 0, y: -1, z: 0 },
1711
+ suspensionRestLength: 0.6,
1712
+ suspensionMaxTravel: 0.3,
1713
+ radius: 0.3,
1714
+ stiffness: 60,
1715
+ damping: 220,
1716
+ grip: 1,
1717
+ maxSteerAngleTurns: 0.1,
1718
+ isSteered: steered,
1719
+ isDriven: steered,
1720
+ };
1721
+ }
1722
+ /**
1723
+ * Measured wall-clock sample of an N-car raycast-vehicle fleet running the full
1724
+ * controller→impulse→physics loop per frame. Timings never enter checksummed
1725
+ * state; the p99 is gated within the global 25 ms budget.
1726
+ */
1727
+ function runVehicle3DFleetMeasurementFixture(vehicleCount, frames) {
1728
+ const bodies = [{
1729
+ id: 'fleet-ground',
1730
+ kind: 'static',
1731
+ x: 0,
1732
+ y: 0,
1733
+ z: 0,
1734
+ vx: 0,
1735
+ vy: 0,
1736
+ vz: 0,
1737
+ shape: { type: 'box', halfX: 200, halfY: 0.5, halfZ: 200 },
1738
+ layer: 1,
1739
+ mask: 1,
1740
+ material: { friction: 8, restitution: 0 },
1741
+ }];
1742
+ const vehicles = [];
1743
+ for (let index = 0; index < vehicleCount; index += 1) {
1744
+ const id = `fleet-car-${index}`;
1745
+ bodies.push({
1746
+ id,
1747
+ kind: 'dynamic',
1748
+ x: (index % 4) * 12 - 18,
1749
+ y: 2.55,
1750
+ z: Math.floor(index / 4) * 12 - 18,
1751
+ vx: 0,
1752
+ vy: 0,
1753
+ vz: 0,
1754
+ mass: 1200,
1755
+ orientation: { x: 0, y: 0, z: 0, w: 1 },
1756
+ angularVel: { x: 0, y: 0, z: 0 },
1757
+ shape: { type: 'box', halfX: 1, halfY: 0.25, halfZ: 2 },
1758
+ layer: 1,
1759
+ mask: 1,
1760
+ });
1761
+ vehicles.push({
1762
+ chassisId: id,
1763
+ driveForce: 12,
1764
+ brakeForce: 40,
1765
+ wheels: [
1766
+ fleetVehicleWheel({ x: -0.9, y: -0.25, z: 1.5 }, true),
1767
+ fleetVehicleWheel({ x: 0.9, y: -0.25, z: 1.5 }, true),
1768
+ fleetVehicleWheel({ x: -0.9, y: -0.25, z: -1.5 }, false),
1769
+ fleetVehicleWheel({ x: 0.9, y: -0.25, z: -1.5 }, false),
1770
+ ],
1771
+ });
1772
+ }
1773
+ const stepFleetFrame = (world, frame) => {
1774
+ // every controller reads the same pre-impulse world (pure reads), then all
1775
+ // impulses land in one batched canonical rebuild — the recommended frame
1776
+ // shape for multi-vehicle games
1777
+ let groundedWheels = 0;
1778
+ const frameImpulses = [];
1779
+ for (let index = 0; index < vehicles.length; index += 1) {
1780
+ const input = {
1781
+ throttle: 10,
1782
+ steer: (Math.trunc(frame / 24) + index) % 2 === 0 ? 10 : -10,
1783
+ brake: 0,
1784
+ };
1785
+ const result = stepDeterministicVehicle3D(vehicles[index], input, world, { dtTicks: 1 });
1786
+ groundedWheels += result.wheels.filter((wheel) => wheel.grounded).length;
1787
+ for (const impulse of result.impulses) {
1788
+ frameImpulses.push({ bodyId: impulse.bodyId, impulse: impulse.impulse, worldPoint: impulse.point });
1789
+ }
1790
+ }
1791
+ const next = stepDeterministicPhysicsWorld3D(applyDeterministicImpulses3D(world, frameImpulses), {
1792
+ gravityY: -0.02,
1793
+ computeChecksum: false,
1794
+ }).world;
1795
+ return { world: next, groundedWheels };
1796
+ };
1797
+ const initial = createDeterministicPhysicsWorld3D(bodies);
1798
+ let world = initial;
1799
+ let snapshot = initial;
1800
+ let groundedWheelSamples = 0;
1801
+ const timings = [];
1802
+ const half = Math.floor(frames / 2);
1803
+ for (let frame = 0; frame < frames; frame += 1) {
1804
+ if (frame === half) {
1805
+ snapshot = world;
1806
+ }
1807
+ const stepStartedAt = engineCompleteNowMs();
1808
+ const stepped = stepFleetFrame(world, frame);
1809
+ timings.push(engineCompleteNowMs() - stepStartedAt);
1810
+ world = stepped.world;
1811
+ groundedWheelSamples += stepped.groundedWheels;
1812
+ }
1813
+ const checksum = defaultChecksum(world);
1814
+ let restored = snapshot;
1815
+ for (let frame = half; frame < frames; frame += 1) {
1816
+ restored = stepFleetFrame(restored, frame).world;
1817
+ }
1818
+ return {
1819
+ checksum,
1820
+ rollbackEquivalent: checksum === defaultChecksum(restored),
1821
+ vehicles: vehicles.length,
1822
+ frames,
1823
+ groundedWheelSamples,
1824
+ p95MeasuredStepMs: roundMetric(percentileNumber(timings, 95), 6),
1825
+ p99MeasuredStepMs: roundMetric(percentileNumber(timings, 99), 6),
1826
+ maxMeasuredStepMs: roundMetric(timings.reduce((max, value) => Math.max(max, value), 0), 6),
1827
+ };
1828
+ }
1619
1829
  function runKcc3DFullStepMeasurementFixture(players, frames) {
1620
1830
  let bodies = Array.from({ length: players }, (_, slot) => ({
1621
1831
  x: slot % 16,
@@ -617,19 +617,34 @@ const engineParityMatrixEntries = [
617
617
  currentEvidence: [
618
618
  'packages/syncplay/src/physics2d.ts',
619
619
  'packages/syncplay/src/physics3d.ts',
620
+ 'packages/syncplay/src/physics3d-solver.ts',
621
+ 'packages/syncplay/src/physics3d-joints.ts',
620
622
  'packages/syncplay/tests/physics2d.test.ts',
623
+ 'packages/syncplay/tests/physics3d-solver.test.ts',
624
+ 'packages/syncplay/tests/physics3d-joints.test.ts',
625
+ 'packages/syncplay/tests/physics3d-ccd.test.ts',
621
626
  'packages/syncplay/tests/engine-complete.test.ts',
627
+ 'packages/syncplay/src/physics3d-vehicle.ts',
628
+ 'packages/syncplay/tests/physics3d-vehicle.test.ts',
629
+ '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',
630
+ 'builtin-rigid physics-cert lane passes all 4 determinism gates; contact/joint caches are checksummed rollback state',
631
+ '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',
632
+ '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',
633
+ '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',
622
634
  ],
623
635
  cliProofs: [
624
636
  'npm run lab:physics3d -- --bodies 128 --frames 1000 --no-fast-stress --json artifacts/reports/physics3d-no-fast.json',
625
637
  'npm run certify:engine-complete -- --filter physics3d --players 128 --frames 5000 --no-fast-stress',
638
+ 'npm run lab:physics-cert -- --engine builtin-rigid --shape fixed-arena --players 4 --frames 600 --seed 7 --snapshot-every 8',
639
+ 'npm run coverage:package:physics-vehicle',
626
640
  ],
627
641
  gaps: [
628
- 'Production-quality stacked rigid-body solving, broad mesh import, full CCD, and long no-fast stress are not proven.',
629
- '100k stress can use optimized-tail mode unless no-fast-stress is required.',
642
+ 'Vehicles are raycast-wheel only (static ground, flat/axis-aligned contact normals); moving-ground reaction and rigid-wheel vehicles are tracked follow-ons.',
643
+ 'Dynamic mesh-vs-mesh collision, convex-hull decomposition / visual collider import, and ragdolls-in-checksummed-state remain out of scope.',
644
+ 'Cross-VM certification posture unchanged (V8↔V8 certified; WebKit nightly; Hermes documented gap).',
630
645
  ],
631
646
  nextCliWork: [
632
- 'Add adversarial no-fast fixtures for stacks, tunneling, joints, query ordering, 2.5D overload, and rollback equivalence.',
647
+ 'Profile the remaining burst cost before further pooling; no known single lever.',
633
648
  ],
634
649
  referenceDocs: [
635
650
  'external-deterministic-engine-reference:manual/physics/overview',
@@ -793,7 +808,7 @@ const engineParityMatrixEntries = [
793
808
  'Integrated samples need stronger per-scene mechanics assertions and replay artifacts.',
794
809
  ],
795
810
  nextCliWork: [
796
- 'Add CLI-rendered or artifact-traced Simple FPS, platform shooter, arena brawler, nav agents, animator, KCC playground, vehicle, and waiting-room samples.',
811
+ 'Add CLI-rendered or artifact-traced Simple FPS, platform shooter, arena brawler, nav agents, animator, KCC playground, vehicle (the sample-scenes vehicle is kinematic by design; raycast-wheel physics lives in physics3d-vehicle.ts), and waiting-room samples.',
797
812
  ],
798
813
  referenceDocs: [
799
814
  'external-deterministic-engine-reference:game-samples/engine-simple-fps/overview',