@series-inc/rundot-syncplay 5.26.0-beta.2 → 5.26.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser.d.ts CHANGED
@@ -27,6 +27,7 @@ export { fbm2D, hash2, hashUint32, valueNoise1D, valueNoise2D } from './noise.js
27
27
  export { circleCastDeterministicPhysics2D, createDeterministicPhysicsWorld2D, nearestDeterministicPhysicsHit2D, queryDeterministicPhysicsAabb2D, queryDeterministicPhysicsShapeOverlap2D, raycastAllDeterministicPhysics2D, raycastDeterministicPhysics2D, stepDeterministicPhysicsWorld2D } from './physics2d.js';
28
28
  export { applyDeterministicImpulse3D, applyDeterministicImpulses3D, applyDeterministicWorldEdit3D, contactDetailsDeterministicPhysics3D, createDeterministicPhysicsWorld3D, createTrustedDeterministicPhysicsWorld3D, deriveInverseInertia, deterministicPhysicsBodyById3D, deterministicPhysicsCallback3D, overlapDeterministicPhysics3D, raycastDeterministicPhysics3D, raycastDeterministicPhysics3DDetailed, runDeterministicPhysics3DStress, setDeterministicBodyVelocity3D, shapeCastDeterministicPhysics3D, shapeCastDeterministicPhysics3DDetailed, stepDeterministicPhysicsWorld3D } from './physics3d.js';
29
29
  export { cookDeterministicVoxelChunkCollider3D } from './physics3d-voxel.js';
30
+ export { cylinderHullVertices } from './physics3d-hull.js';
30
31
  export { createDeterministicCosmeticPhysicsWorld3D, setDeterministicCosmeticPhysicsAuthoritativeBodyProxies3D, stepDeterministicCosmeticPhysicsWorld3D } from './cosmetic-physics3d.js';
31
32
  export { stepDeterministicVehicle3D } from './physics3d-vehicle.js';
32
33
  export { createDeterministicR3fRenderAdapter as createDeterministicR3fAdapter, createDeterministicR3fRenderAdapter, interpolateDeterministicRenderNumber, interpolateDeterministicRenderVec2 } from './r3f-render-adapter.js';
package/dist/browser.js CHANGED
@@ -28,6 +28,7 @@ export { fbm2D, hash2, hashUint32, valueNoise1D, valueNoise2D } from './noise.js
28
28
  export { circleCastDeterministicPhysics2D, createDeterministicPhysicsWorld2D, nearestDeterministicPhysicsHit2D, queryDeterministicPhysicsAabb2D, queryDeterministicPhysicsShapeOverlap2D, raycastAllDeterministicPhysics2D, raycastDeterministicPhysics2D, stepDeterministicPhysicsWorld2D } from './physics2d.js';
29
29
  export { applyDeterministicImpulse3D, applyDeterministicImpulses3D, applyDeterministicWorldEdit3D, contactDetailsDeterministicPhysics3D, createDeterministicPhysicsWorld3D, createTrustedDeterministicPhysicsWorld3D, deriveInverseInertia, deterministicPhysicsBodyById3D, deterministicPhysicsCallback3D, overlapDeterministicPhysics3D, raycastDeterministicPhysics3D, raycastDeterministicPhysics3DDetailed, runDeterministicPhysics3DStress, setDeterministicBodyVelocity3D, shapeCastDeterministicPhysics3D, shapeCastDeterministicPhysics3DDetailed, stepDeterministicPhysicsWorld3D } from './physics3d.js';
30
30
  export { cookDeterministicVoxelChunkCollider3D } from './physics3d-voxel.js';
31
+ export { cylinderHullVertices } from './physics3d-hull.js';
31
32
  export { createDeterministicCosmeticPhysicsWorld3D, setDeterministicCosmeticPhysicsAuthoritativeBodyProxies3D, stepDeterministicCosmeticPhysicsWorld3D } from './cosmetic-physics3d.js';
32
33
  export { stepDeterministicVehicle3D } from './physics3d-vehicle.js';
33
34
  export { createDeterministicR3fRenderAdapter as createDeterministicR3fAdapter, createDeterministicR3fRenderAdapter, interpolateDeterministicRenderNumber, interpolateDeterministicRenderVec2 } from './r3f-render-adapter.js';
package/dist/creator.d.ts CHANGED
@@ -14,6 +14,7 @@ export type { SyncplaySession, SyncplaySessionPolicy } from './sdk-session.js';
14
14
  export type { InstantReplayInputFrame, InstantReplayResult, InstantReplayRunner, InstantReplayRunnerOptions, InstantReplayRunOptions, RunInstantReplayOptions, } from './instant-replay.js';
15
15
  export { applyDeterministicImpulse3D, applyDeterministicImpulses3D, applyDeterministicWorldEdit3D, contactDetailsDeterministicPhysics3D, createDeterministicPhysicsWorld3D, deriveInverseInertia, deterministicPhysicsBodyById3D, deterministicPhysicsCallback3D, overlapDeterministicPhysics3D, raycastDeterministicPhysics3D, raycastDeterministicPhysics3DDetailed, runDeterministicPhysics3DStress, setDeterministicBodyVelocity3D, shapeCastDeterministicPhysics3D, shapeCastDeterministicPhysics3DDetailed, stepDeterministicPhysicsWorld3D, } from './physics3d.js';
16
16
  export { cookDeterministicVoxelChunkCollider3D } from './physics3d-voxel.js';
17
+ export { cylinderHullVertices } from './physics3d-hull.js';
17
18
  export { stepDeterministicVehicle3D } from './physics3d-vehicle.js';
18
19
  export type { DeterministicContactCacheEntry3D, DeterministicJointCacheEntry3D, DeterministicPhysicsActivePair3D, DeterministicPhysicsAxisLock3D, DeterministicPhysicsBody3D, DeterministicPhysicsCallbackMask3D, DeterministicPhysicsCompoundChild3D, DeterministicPhysicsCompoundChildShape3D, DeterministicPhysicsConstraint3D, DeterministicPhysicsContactDetail3D, DeterministicPhysicsContactEvent3D, DeterministicPhysicsDetailedHit3D, DeterministicPhysicsHit3D, DeterministicPhysicsHullShape3D, DeterministicPhysicsImpulseApplication3D, DeterministicPhysicsOverlapQuery3D, DeterministicPhysicsPrimitiveShape3D, DeterministicPhysicsQuaternion3D, DeterministicPhysicsQueryOptions3D, DeterministicPhysicsRaycastQuery3D, DeterministicPhysicsScheduledQuery3D, DeterministicPhysicsShape3D, DeterministicPhysicsShapeCastQuery3D, DeterministicPhysicsStepResult3D, DeterministicPhysicsVec3, DeterministicPhysicsWorld3D, DeterministicPhysicsWorldEdit3D, } from './physics3d.js';
19
20
  export type { DeterministicVoxelChunk3D, DeterministicVoxelCompoundShape3D } from './physics3d-voxel.js';
package/dist/creator.js CHANGED
@@ -11,4 +11,5 @@ export { runSyncplaySynctest } from './synctest.js';
11
11
  // stay off it — the first two are not requested, the third reads wall clock.
12
12
  export { applyDeterministicImpulse3D, applyDeterministicImpulses3D, applyDeterministicWorldEdit3D, contactDetailsDeterministicPhysics3D, createDeterministicPhysicsWorld3D, deriveInverseInertia, deterministicPhysicsBodyById3D, deterministicPhysicsCallback3D, overlapDeterministicPhysics3D, raycastDeterministicPhysics3D, raycastDeterministicPhysics3DDetailed, runDeterministicPhysics3DStress, setDeterministicBodyVelocity3D, shapeCastDeterministicPhysics3D, shapeCastDeterministicPhysics3DDetailed, stepDeterministicPhysicsWorld3D, } from './physics3d.js';
13
13
  export { cookDeterministicVoxelChunkCollider3D } from './physics3d-voxel.js';
14
+ export { cylinderHullVertices } from './physics3d-hull.js';
14
15
  export { stepDeterministicVehicle3D } from './physics3d-vehicle.js';
package/dist/index.d.ts CHANGED
@@ -30,6 +30,7 @@ export { fbm2D, hash2, hashUint32, valueNoise1D, valueNoise2D } from './noise.js
30
30
  export { circleCastDeterministicPhysics2D, createDeterministicPhysicsWorld2D, nearestDeterministicPhysicsHit2D, queryDeterministicPhysicsAabb2D, queryDeterministicPhysicsShapeOverlap2D, raycastAllDeterministicPhysics2D, raycastDeterministicPhysics2D, stepDeterministicPhysicsWorld2D } from './physics2d.js';
31
31
  export { applyDeterministicImpulse3D, applyDeterministicImpulses3D, applyDeterministicWorldEdit3D, contactDetailsDeterministicPhysics3D, createDeterministicPhysicsWorld3D, createTrustedDeterministicPhysicsWorld3D, deriveInverseInertia, deterministicPhysicsBodyById3D, deterministicPhysicsCallback3D, overlapDeterministicPhysics3D, raycastDeterministicPhysics3D, raycastDeterministicPhysics3DDetailed, runDeterministicPhysics3DStress, setDeterministicBodyVelocity3D, shapeCastDeterministicPhysics3D, shapeCastDeterministicPhysics3DDetailed, stepDeterministicPhysicsWorld3D } from './physics3d.js';
32
32
  export { cookDeterministicVoxelChunkCollider3D } from './physics3d-voxel.js';
33
+ export { cylinderHullVertices } from './physics3d-hull.js';
33
34
  export { runDeterministicPhysics3DDestructionCert } from './physics3d-destruction-cert.js';
34
35
  export { createDeterministicCosmeticPhysicsWorld3D, setDeterministicCosmeticPhysicsAuthoritativeBodyProxies3D, stepDeterministicCosmeticPhysicsWorld3D } from './cosmetic-physics3d.js';
35
36
  export { runDeterministicKickRecovery3DScenario, runDeterministicMassRatio3DScenario, runDeterministicPyramid3DScenario, runDeterministicStackingSuite3D, runDeterministicTower3DScenario } from './physics3d-stacking.js';
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ export { fbm2D, hash2, hashUint32, valueNoise1D, valueNoise2D } from './noise.js
31
31
  export { circleCastDeterministicPhysics2D, createDeterministicPhysicsWorld2D, nearestDeterministicPhysicsHit2D, queryDeterministicPhysicsAabb2D, queryDeterministicPhysicsShapeOverlap2D, raycastAllDeterministicPhysics2D, raycastDeterministicPhysics2D, stepDeterministicPhysicsWorld2D } from './physics2d.js';
32
32
  export { applyDeterministicImpulse3D, applyDeterministicImpulses3D, applyDeterministicWorldEdit3D, contactDetailsDeterministicPhysics3D, createDeterministicPhysicsWorld3D, createTrustedDeterministicPhysicsWorld3D, deriveInverseInertia, deterministicPhysicsBodyById3D, deterministicPhysicsCallback3D, overlapDeterministicPhysics3D, raycastDeterministicPhysics3D, raycastDeterministicPhysics3DDetailed, runDeterministicPhysics3DStress, setDeterministicBodyVelocity3D, shapeCastDeterministicPhysics3D, shapeCastDeterministicPhysics3DDetailed, stepDeterministicPhysicsWorld3D } from './physics3d.js';
33
33
  export { cookDeterministicVoxelChunkCollider3D } from './physics3d-voxel.js';
34
+ export { cylinderHullVertices } from './physics3d-hull.js';
34
35
  export { runDeterministicPhysics3DDestructionCert } from './physics3d-destruction-cert.js';
35
36
  export { createDeterministicCosmeticPhysicsWorld3D, setDeterministicCosmeticPhysicsAuthoritativeBodyProxies3D, stepDeterministicCosmeticPhysicsWorld3D } from './cosmetic-physics3d.js';
36
37
  export { runDeterministicKickRecovery3DScenario, runDeterministicMassRatio3DScenario, runDeterministicPyramid3DScenario, runDeterministicStackingSuite3D, runDeterministicTower3DScenario } from './physics3d-stacking.js';
@@ -189,6 +189,15 @@ export declare function stepDeterministicKcc3D(body: DeterministicKccBody3D, inp
189
189
  readonly maxPushImpulse?: number;
190
190
  /** Enable standing on (and inheriting the velocity of) dynamic physics bodies. */
191
191
  readonly standOnDynamicBodies?: boolean;
192
+ /**
193
+ * Ground the capsule on STATIC physics bodies, including heightfields.
194
+ * Off by default: without it the physics world only blocks horizontally and
195
+ * vertical support comes from `world.slopes` / `steps` / platforms / `minY`,
196
+ * so a streaming-terrain game had to rebuild slope geometry to stand on the
197
+ * same terrain it already streams as colliders. Opt-in so shipped games keep
198
+ * bit-identical checksums.
199
+ */
200
+ readonly groundOnStaticBodies?: boolean;
192
201
  }): DeterministicKccStepResult3D;
193
202
  export interface DeterministicKccCarryState3D {
194
203
  readonly bodyId: string;
@@ -1,5 +1,6 @@
1
1
  import { defaultChecksum } from './canonical.js';
2
2
  import { createDeterministicPhysicsWorld3D, overlapDeterministicPhysics3D, } from './physics3d.js';
3
+ import { heightfieldLocalSurfaceY } from './physics3d-heightfield.js';
3
4
  import { mulMat3Vec, quaternionFromQuarterTurns, quatMultiply, quatToMat3, rotatePointByQuat } from './physics3d-shared.js';
4
5
  export function stepDeterministicKcc3D(body, input, world, options) {
5
6
  let next = applyProcessors(body, input, options.frame, options.processors ?? []);
@@ -19,6 +20,7 @@ export function stepDeterministicKcc3D(body, input, world, options) {
19
20
  let z = next.z + vz;
20
21
  let grounded = false;
21
22
  let platformId = next.platformId;
23
+ let platformClaimedThisFrame = false;
22
24
  if (externalImpulse.applied) {
23
25
  events.push('external-impulse');
24
26
  }
@@ -36,6 +38,7 @@ export function stepDeterministicKcc3D(body, input, world, options) {
36
38
  vy = 0;
37
39
  grounded = true;
38
40
  platformId = platform.id;
41
+ platformClaimedThisFrame = true;
39
42
  events.push('moving-platform');
40
43
  }
41
44
  }
@@ -75,6 +78,58 @@ export function stepDeterministicKcc3D(body, input, world, options) {
75
78
  coyoteFrames = 0;
76
79
  events.push(next.grounded ? 'jump' : 'coyote-jump');
77
80
  }
81
+ const previousFeetY = next.y - kccFeetOffset(geometry);
82
+ let groundedOnStatic = false;
83
+ let groundedOnDynamic = false;
84
+ // Resolved before the dynamic path so the higher of the two wins: a crate
85
+ // standing on a floor must not lose its platform to the floor underneath it.
86
+ const staticGround = options.physics !== undefined && options.groundOnStaticBodies === true && vy <= 0
87
+ ? staticGroundUnderKcc(options.physics, x, y, z, previousFeetY, geometry, new Set(options.ignoredPhysicsBodyIds ?? []), world.stepHeight)
88
+ : undefined;
89
+ if (options.physics !== undefined && options.standOnDynamicBodies === true && vy <= 0) {
90
+ const ground = dynamicGroundUnderKcc(options.physics, x, y, z, geometry, new Set(options.ignoredPhysicsBodyIds ?? []));
91
+ if (ground !== undefined && (staticGround === undefined || ground.topY >= staticGround.topY)) {
92
+ groundedOnDynamic = true;
93
+ platformClaimedThisFrame = true;
94
+ y = ground.topY;
95
+ vy = 0;
96
+ grounded = true;
97
+ platformId = ground.bodyId;
98
+ // Carry the rider with the crate this frame (position delta) AND inherit its
99
+ // velocity, mirroring the kinematic moving-platform path — otherwise the
100
+ // rider lags one frame and slides off a moving crate.
101
+ x += ground.deltaX;
102
+ z += ground.deltaZ;
103
+ vx += ground.deltaX;
104
+ vz += ground.deltaZ;
105
+ events.push('stand-on-dynamic');
106
+ if (ground.deltaX !== 0 || ground.deltaZ !== 0) {
107
+ events.push('platform-velocity-transfer');
108
+ }
109
+ }
110
+ }
111
+ // Applied BEFORE the horizontal slide: the slide resolves overlap horizontally
112
+ // only, so a capsule that fell into terrain would be shoved sideways across
113
+ // the surface instead of being stood on top of it. Skipped when a dynamic
114
+ // body already claimed a higher surface this frame.
115
+ // platformId carries over from the previous frame, so a rider that simply
116
+ // walked off keeps pointing at a platform it has left. Clear it once nothing
117
+ // has claimed it this frame.
118
+ if (options.standOnDynamicBodies === true && !platformClaimedThisFrame) {
119
+ platformId = undefined;
120
+ }
121
+ // `grounded` here means an earlier source (moving platform, slope, step, or a
122
+ // dynamic body) already claimed a surface this frame; static only wins if it
123
+ // is higher. platformId cannot be used for this — it carries over from the
124
+ // previous frame, so stepping off a crate onto terrain would skip grounding.
125
+ if (staticGround !== undefined && (!grounded || staticGround.topY > y)) {
126
+ platformId = undefined;
127
+ y = staticGround.topY;
128
+ vy = 0;
129
+ grounded = true;
130
+ groundedOnStatic = true;
131
+ events.push('static-ground');
132
+ }
78
133
  // The KCC's intended (pre-slide) horizontal position and velocity — push
79
134
  // impulses are computed here because the slide blocks the KCC off the body.
80
135
  const pushCandidate = { x, z, vx, vz };
@@ -90,32 +145,41 @@ export function stepDeterministicKcc3D(body, input, world, options) {
90
145
  vz = resolved.vz;
91
146
  correctionCount += resolved.correctionCount;
92
147
  events.push(resolved.swept ? 'physics-sweep' : resolved.slid ? 'physics-slide' : 'physics-correction');
148
+ // The slide moved the footprint, so the height sampled before it belongs
149
+ // to the old x/z. On sloped ground that stale height reads as penetration
150
+ // next frame and freezes movement along walls and ledges — re-sample.
151
+ // Attachment must follow the FINAL footprint. A slide can carry the
152
+ // capsule off the platform it was standing on (leaving it "grounded" in
153
+ // mid-air) or onto one it had missed.
154
+ if (groundedOnDynamic && (x !== pushCandidate.x || z !== pushCandidate.z)) {
155
+ const reattached = dynamicGroundUnderKcc(options.physics, x, y, z, geometry, new Set(options.ignoredPhysicsBodyIds ?? []));
156
+ if (reattached === undefined) {
157
+ grounded = false;
158
+ platformId = undefined;
159
+ groundedOnDynamic = false;
160
+ }
161
+ else {
162
+ y = reattached.topY;
163
+ platformId = reattached.bodyId;
164
+ }
165
+ }
166
+ if (groundedOnStatic && (x !== pushCandidate.x || z !== pushCandidate.z)) {
167
+ const reground = staticGroundUnderKcc(options.physics, x, y, z,
168
+ // The capsule has already landed, so this is a local correction from
169
+ // the landed feet — not the start-of-frame ones. Reusing those would
170
+ // reopen the fall-crossing window and let a slide under an overhang
171
+ // yank the capsule onto its roof.
172
+ y - kccFeetOffset(geometry), geometry, new Set(options.ignoredPhysicsBodyIds ?? []), world.stepHeight);
173
+ if (reground !== undefined) {
174
+ y = reground.topY;
175
+ }
176
+ }
93
177
  }
94
178
  }
95
179
  const geometryForInteraction = normalizeKccGeometry3D(next);
96
180
  let physicsImpulses = [];
97
181
  // Stand on a dynamic body directly beneath the feet: attach as a platform and
98
182
  // inherit its per-frame delta (its velocity), mirroring the kinematic path.
99
- if (options.physics !== undefined && options.standOnDynamicBodies === true && vy <= 0) {
100
- const ground = dynamicGroundUnderKcc(options.physics, x, y, z, geometryForInteraction, new Set(options.ignoredPhysicsBodyIds ?? []));
101
- if (ground !== undefined) {
102
- y = ground.topY;
103
- vy = 0;
104
- grounded = true;
105
- platformId = ground.bodyId;
106
- // Carry the rider with the crate this frame (position delta) AND inherit its
107
- // velocity, mirroring the kinematic moving-platform path — otherwise the
108
- // rider lags one frame and slides off a moving crate.
109
- x += ground.deltaX;
110
- z += ground.deltaZ;
111
- vx += ground.deltaX;
112
- vz += ground.deltaZ;
113
- events.push('stand-on-dynamic');
114
- if (ground.deltaX !== 0 || ground.deltaZ !== 0) {
115
- events.push('platform-velocity-transfer');
116
- }
117
- }
118
- }
119
183
  // Push the dynamic bodies the KCC walked into this frame.
120
184
  if (options.physics !== undefined && (options.maxPushImpulse ?? 0) > 0) {
121
185
  physicsImpulses = computeKccPushImpulses(options.physics, pushCandidate.x, y, pushCandidate.z, { vx: pushCandidate.vx, vz: pushCandidate.vz }, geometryForInteraction, options.maxPushImpulse ?? 0, new Set(options.ignoredPhysicsBodyIds ?? []));
@@ -123,7 +187,10 @@ export function stepDeterministicKcc3D(body, input, world, options) {
123
187
  events.push('push-dynamic');
124
188
  }
125
189
  }
126
- const boundsCorrection = clampBodyToBounds({ x, y, z, vx, vy, vz, grounded, coyoteFrames, jumpBufferFrames, platformId }, world);
190
+ // Spread `next` first: the capsule's radius/halfHeight/skinWidth live on the
191
+ // body, and rebuilding it from a bare literal dropped them, so a caller's
192
+ // capsule silently reverted to the 1x2 default after a single step.
193
+ const boundsCorrection = clampBodyToBounds({ ...next, x, y, z, vx, vy, vz, grounded, coyoteFrames, jumpBufferFrames, platformId }, world);
127
194
  if (boundsCorrection.corrected) {
128
195
  correctionCount += boundsCorrection.corrections;
129
196
  events.push(...boundsCorrection.events);
@@ -247,7 +314,101 @@ function groundContactTopAt(body, x, z) {
247
314
  }
248
315
  return top;
249
316
  }
250
- return undefined; // mesh: static-only, not a stand-on platform
317
+ if (shape.type === 'hull') {
318
+ // Not primitiveHalfExtent: that returns symmetric max(|y|) half-extents, so
319
+ // an asymmetric hull would snap the capsule above its real top and treat
320
+ // the mirrored side of the AABB as walkable. The stand-on height is max(y)
321
+ // of the rotated vertices, and the footprint is their true XZ range.
322
+ const m = quatToMat3(q);
323
+ let minX = Infinity;
324
+ let maxX = -Infinity;
325
+ let minZ = Infinity;
326
+ let maxZ = -Infinity;
327
+ let maxY = -Infinity;
328
+ for (const vertex of shape.vertices) {
329
+ const rotated = mulMat3Vec(m, vertex);
330
+ minX = Math.min(minX, rotated.x);
331
+ maxX = Math.max(maxX, rotated.x);
332
+ minZ = Math.min(minZ, rotated.z);
333
+ maxZ = Math.max(maxZ, rotated.z);
334
+ maxY = Math.max(maxY, rotated.y);
335
+ }
336
+ const localX = x - body.x;
337
+ const localZ = z - body.z;
338
+ if (localX < minX || localX > maxX || localZ < minZ || localZ > maxZ) {
339
+ return undefined;
340
+ }
341
+ return body.y + maxY;
342
+ }
343
+ if (shape.type === 'heightfield') {
344
+ // Heightfields are static and never rotate (the solver maps them to a mesh
345
+ // in body-local space), so local == world minus the body position.
346
+ const localY = heightfieldLocalSurfaceY(shape, x - body.x, z - body.z);
347
+ return localY === undefined ? undefined : body.y + localY;
348
+ }
349
+ return undefined; // mesh: arbitrary triangle soup, not a stand-on platform
350
+ }
351
+ /**
352
+ * Highest static surface under the capsule within `snapDistance` of its feet.
353
+ *
354
+ * The overlap probe only narrows candidates (it rides the world's static query
355
+ * tree); the surface height itself comes from groundContactTopAt, so a capsule
356
+ * lands on the same triangle a projectile would hit.
357
+ */
358
+ /**
359
+ * Separation held between a grounded capsule and the surface under it, so
360
+ * resting contact never reads as penetration. Fixed rather than derived because
361
+ * skinWidth defaults to 0; a plain double is bit-identical on every peer.
362
+ */
363
+ const KCC_GROUND_CLEARANCE = 1e-4;
364
+ /**
365
+ * Distance from the capsule's centre to the bottom of its collision shape.
366
+ * A capsule's vertical extent is halfHeight + radius (see primitiveHalfExtent),
367
+ * so grounding on `halfHeight` alone buries it by a radius and the horizontal
368
+ * slide then fights the penetration every frame — the capsule cannot walk.
369
+ * skinWidth keeps a gap so resting contact is not treated as overlap.
370
+ */
371
+ function kccFeetOffset(geometry) {
372
+ // physicsOverlapsKcc probes with radius+skinWidth AND halfHeight+skinWidth,
373
+ // and counts exact touching as an overlap — at which point the slide reverts
374
+ // the whole horizontal move, so a capsule resting flush on terrain cannot
375
+ // walk. Rest one clearance above that probe instead of on the surface.
376
+ return geometry.halfHeight + geometry.radius + 2 * geometry.skinWidth + KCC_GROUND_CLEARANCE;
377
+ }
378
+ function staticGroundUnderKcc(physics, x, y, z, previousFeetY, geometry, ignoredBodyIds, snapDistance) {
379
+ const feetY = y - kccFeetOffset(geometry);
380
+ // Gravity is unbounded (vy -= 1 per frame), so a fast fall can clear the whole
381
+ // snap band in one step. Accept a surface the feet CROSSED this frame, not
382
+ // just one they ended near — otherwise terrain tunnels at terminal velocity.
383
+ // Requiring the frame to START at or above the surface is what keeps a capsule
384
+ // walking beneath a floating static box from being yanked onto its roof.
385
+ const fallDistance = Math.max(0, previousFeetY - feetY);
386
+ const reach = snapDistance + fallDistance;
387
+ const probe = {
388
+ shape: { type: 'sphere', radius: geometry.radius + reach },
389
+ x,
390
+ y: feetY,
391
+ z,
392
+ hitStatics: true,
393
+ hitDynamics: false,
394
+ hitKinematics: false,
395
+ };
396
+ const hitIds = new Set(overlapDeterministicPhysics3D(physics, probe));
397
+ const grounds = physics.bodies
398
+ .filter((body) => body.kind === 'static' && hitIds.has(body.id) && !ignoredBodyIds.has(body.id))
399
+ .map((body) => ({ body, top: groundContactTopAt(body, x, z) }))
400
+ .filter((candidate) => candidate.top !== undefined
401
+ && feetY <= candidate.top + snapDistance
402
+ && previousFeetY >= candidate.top - snapDistance)
403
+ .sort((left, right) => right.top - left.top || compareStrings(left.body.id, right.body.id));
404
+ const ground = grounds[0];
405
+ if (ground === undefined) {
406
+ return undefined;
407
+ }
408
+ return {
409
+ bodyId: ground.body.id,
410
+ topY: ground.top + kccFeetOffset(geometry),
411
+ };
251
412
  }
252
413
  /** World half-extents (x,y,z) of a primitive shape under rotation matrix `m`. */
253
414
  function primitiveHalfExtent(shape, m) {
@@ -283,7 +444,7 @@ function primitiveHalfExtent(shape, m) {
283
444
  }
284
445
  /** A dynamic body directly under the KCC feet, with its per-frame delta. */
285
446
  function dynamicGroundUnderKcc(physics, x, y, z, geometry, ignoredBodyIds) {
286
- const feetY = y - geometry.halfHeight - geometry.skinWidth;
447
+ const feetY = y - kccFeetOffset(geometry);
287
448
  const probe = {
288
449
  shape: { type: 'sphere', radius: geometry.radius },
289
450
  x,
@@ -308,7 +469,7 @@ function dynamicGroundUnderKcc(physics, x, y, z, geometry, ignoredBodyIds) {
308
469
  }
309
470
  return {
310
471
  bodyId: ground.body.id,
311
- topY: ground.top + geometry.halfHeight + geometry.skinWidth,
472
+ topY: ground.top + kccFeetOffset(geometry),
312
473
  deltaX: ground.body.vx,
313
474
  deltaZ: ground.body.vz,
314
475
  };
@@ -14,4 +14,14 @@ export interface HeightfieldBounds3D {
14
14
  readonly maxZ: number;
15
15
  }
16
16
  export declare function heightfieldLocalBounds(shape: HeightfieldShapeData3D): HeightfieldBounds3D;
17
+ /**
18
+ * Local surface height at a local (x, z), or undefined outside the field.
19
+ *
20
+ * MUST stay consistent with heightfieldLocalTriangles' cell split: a cell emits
21
+ * [a,b,c] and [d,c,b] with a=(col,row) b=(col,row+1) c=(col+1,row)
22
+ * d=(col+1,row+1), so the diagonal runs b–c and the first triangle is the
23
+ * u+v<=1 half. Sampling a different diagonal would let a character stand where
24
+ * a projectile passes through.
25
+ */
26
+ export declare function heightfieldLocalSurfaceY(shape: HeightfieldShapeData3D, localX: number, localZ: number): number | undefined;
17
27
  export declare function heightfieldLocalTriangles(shape: HeightfieldShapeData3D, bounds?: HeightfieldBounds3D): readonly (readonly [Point3D, Point3D, Point3D])[];
@@ -21,6 +21,39 @@ function vertex(shape, column, row) {
21
21
  z: row * shape.scale.z,
22
22
  };
23
23
  }
24
+ function heightAt(shape, column, row) {
25
+ return shape.heights[row * shape.columns + column] * shape.scale.y;
26
+ }
27
+ /**
28
+ * Local surface height at a local (x, z), or undefined outside the field.
29
+ *
30
+ * MUST stay consistent with heightfieldLocalTriangles' cell split: a cell emits
31
+ * [a,b,c] and [d,c,b] with a=(col,row) b=(col,row+1) c=(col+1,row)
32
+ * d=(col+1,row+1), so the diagonal runs b–c and the first triangle is the
33
+ * u+v<=1 half. Sampling a different diagonal would let a character stand where
34
+ * a projectile passes through.
35
+ */
36
+ export function heightfieldLocalSurfaceY(shape, localX, localZ) {
37
+ const maxX = (shape.columns - 1) * shape.scale.x;
38
+ const maxZ = (shape.rows - 1) * shape.scale.z;
39
+ if (localX < 0 || localX > maxX || localZ < 0 || localZ > maxZ) {
40
+ return undefined;
41
+ }
42
+ const columnFloat = localX / shape.scale.x;
43
+ const rowFloat = localZ / shape.scale.z;
44
+ const column = Math.min(shape.columns - 2, Math.floor(columnFloat));
45
+ const row = Math.min(shape.rows - 2, Math.floor(rowFloat));
46
+ const u = columnFloat - column;
47
+ const v = rowFloat - row;
48
+ const a = heightAt(shape, column, row);
49
+ const b = heightAt(shape, column, row + 1);
50
+ const c = heightAt(shape, column + 1, row);
51
+ if (u + v <= 1) {
52
+ return a + (b - a) * v + (c - a) * u;
53
+ }
54
+ const d = heightAt(shape, column + 1, row + 1);
55
+ return d + (c - d) * (1 - v) + (b - d) * (1 - u);
56
+ }
24
57
  export function heightfieldLocalTriangles(shape, bounds) {
25
58
  const minColumn = bounds === undefined ? 0 : Math.max(0, Math.floor(bounds.minX / shape.scale.x));
26
59
  const maxColumn = bounds === undefined
@@ -51,3 +51,17 @@ export declare function closestPointOnHullLocal(point: Point3D, hull: SolverHull
51
51
  readonly normal: Point3D;
52
52
  readonly distance: number;
53
53
  };
54
+ /**
55
+ * Vertices for a cylinder approximated as a regular prism, for trunks, stumps,
56
+ * logs, pillars and barrels.
57
+ *
58
+ * There is no cylinder primitive in Kinetix, the TypeScript solver, or the
59
+ * Box3D parity set — a cylinder is a convex hull, and hulls are at parity. The
60
+ * TypeScript hull builder accepts 4-16 vertices, so `sides` tops out at 8 (an
61
+ * octagonal prism, exactly 16). A capsule is cheaper but its rounded caps let a
62
+ * character slide off a flat top and change how a body rolls off an edge.
63
+ *
64
+ * Call this when building the world, not inside the step: the vertices are
65
+ * constant for a given (radius, halfHeight, sides).
66
+ */
67
+ export declare function cylinderHullVertices(radius: number, halfHeight: number, sides?: number): readonly Point3D[];
@@ -10,6 +10,7 @@
10
10
  * Numeric regime: add/mul/cmp plus `solverSqrt` for the final unit normals only.
11
11
  * No transcendentals (banned in src/ by the static determinism checker).
12
12
  */
13
+ import { createDeterministicMath } from './math.js';
13
14
  import { addVec3, crossVec3, dotVec3, scaleVec3, solverSqrt, subVec3, } from './physics3d-shared.js';
14
15
  const PLANE_TOL = 1e-9;
15
16
  const COPLANAR_TOL = 1e-6;
@@ -374,3 +375,47 @@ function closestPointOnSegmentLocal(point, a, b) {
374
375
  t = t < 0 ? 0 : t > 1 ? 1 : t;
375
376
  return addVec3(a, scaleVec3(ab, t));
376
377
  }
378
+ /**
379
+ * Native Math.cos/Math.sin are not bit-identical across JS engines, and these
380
+ * vertices become checksummed collision geometry — two peers building the same
381
+ * trunk must agree exactly. sinTurns/cosTurns are pure integer arithmetic.
382
+ * 840 is divisible by every allowed side count, so each vertex lands on an
383
+ * exact turn boundary.
384
+ */
385
+ const PRISM_TURN_SCALE = 840_000;
386
+ const prismMath = createDeterministicMath(PRISM_TURN_SCALE);
387
+ /**
388
+ * Vertices for a cylinder approximated as a regular prism, for trunks, stumps,
389
+ * logs, pillars and barrels.
390
+ *
391
+ * There is no cylinder primitive in Kinetix, the TypeScript solver, or the
392
+ * Box3D parity set — a cylinder is a convex hull, and hulls are at parity. The
393
+ * TypeScript hull builder accepts 4-16 vertices, so `sides` tops out at 8 (an
394
+ * octagonal prism, exactly 16). A capsule is cheaper but its rounded caps let a
395
+ * character slide off a flat top and change how a body rolls off an edge.
396
+ *
397
+ * Call this when building the world, not inside the step: the vertices are
398
+ * constant for a given (radius, halfHeight, sides).
399
+ */
400
+ export function cylinderHullVertices(radius, halfHeight, sides = 8) {
401
+ if (!Number.isFinite(radius) || radius <= 0) {
402
+ throw new Error(`cylinder hull radius must be a finite positive number, received ${radius}`);
403
+ }
404
+ if (!Number.isFinite(halfHeight) || halfHeight <= 0) {
405
+ throw new Error(`cylinder hull halfHeight must be a finite positive number, received ${halfHeight}`);
406
+ }
407
+ if (!Number.isInteger(sides) || sides < 3 || sides > 8) {
408
+ throw new Error(`cylinder hull sides must be an integer in 3..8 (2 x sides must fit the 4-16 vertex hull limit), received ${sides}`);
409
+ }
410
+ const vertices = [];
411
+ for (let index = 0; index < sides; index += 1) {
412
+ // Turns, not radians: PRISM_TURN_SCALE is divisible by every allowed side
413
+ // count, so this is an exact integer and never rounds.
414
+ const turns = (PRISM_TURN_SCALE * index) / sides;
415
+ const x = (radius * prismMath.cosTurns(turns)) / PRISM_TURN_SCALE;
416
+ const z = (radius * prismMath.sinTurns(turns)) / PRISM_TURN_SCALE;
417
+ vertices.push({ x, y: halfHeight, z });
418
+ vertices.push({ x, y: -halfHeight, z });
419
+ }
420
+ return vertices;
421
+ }
package/dist/physics3d.js CHANGED
@@ -2080,6 +2080,33 @@ function warmMapToCache(warm, bodies) {
2080
2080
  || left.anchorY - right.anchorY
2081
2081
  || left.anchorZ - right.anchorZ);
2082
2082
  }
2083
+ /**
2084
+ * Exact-overlap memo for static/static pairs. Neither body can move, so the
2085
+ * result is invariant for the lifetime of both objects, and static bodies keep
2086
+ * their identity across steps (see the `kind === 'static'` passthrough in the
2087
+ * solver's body rebuild). Without this, every prop resting on a heightfield
2088
+ * regenerates the terrain's triangles on every step to re-derive an answer that
2089
+ * cannot change.
2090
+ */
2091
+ // rdm-ignore-next-line determinism/no-mutable-collection-state: pure derived overlap memo keyed by immutable body identity
2092
+ const staticOverlapCache = new WeakMap();
2093
+ function shapesOverlapForPair(left, right) {
2094
+ if (left.kind !== 'static' || right.kind !== 'static') {
2095
+ return shapesOverlap3D(bodyPose(left), bodyPose(right));
2096
+ }
2097
+ let memo = staticOverlapCache.get(left);
2098
+ if (memo === undefined) {
2099
+ memo = new WeakMap();
2100
+ staticOverlapCache.set(left, memo);
2101
+ }
2102
+ const cached = memo.get(right);
2103
+ if (cached !== undefined) {
2104
+ return cached;
2105
+ }
2106
+ const overlap = shapesOverlap3D(bodyPose(left), bodyPose(right));
2107
+ memo.set(right, overlap);
2108
+ return overlap;
2109
+ }
2083
2110
  function detectContacts(bodies, keepPair) {
2084
2111
  const contacts = [];
2085
2112
  for (const pair of broadphaseCandidatePairs(bodies)) {
@@ -2091,7 +2118,7 @@ function detectContacts(bodies, keepPair) {
2091
2118
  if ((left.mask & right.layer) === 0 ||
2092
2119
  (right.mask & left.layer) === 0 ||
2093
2120
  !boundsOverlap(pair.leftBounds, pair.rightBounds) ||
2094
- !shapesOverlap3D(bodyPose(left), bodyPose(right))) {
2121
+ !shapesOverlapForPair(left, right)) {
2095
2122
  continue;
2096
2123
  }
2097
2124
  const pairId = [left.id, right.id].sort(compareStrings).join('|');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@series-inc/rundot-syncplay",
3
- "version": "5.26.0-beta.2",
3
+ "version": "5.26.0-beta.3",
4
4
  "description": "Kinetix orchestration, rollback, replay, late-join, and deterministic networking for RUN.game",
5
5
  "repository": {
6
6
  "type": "git",
@@ -70,7 +70,9 @@
70
70
  "types": "./core/physics.d.ts",
71
71
  "default": "./core/src/physics/index.mjs"
72
72
  },
73
+ "./core/box3d-probe": "./core/src/physics/box3d-probe.mjs",
73
74
  "./core/build-native": "./core/scripts/build-native.mjs",
75
+ "./core/build-mobile-native": "./core/scripts/build-mobile-native.mjs",
74
76
  "./core/build-wasm-physics": "./core/scripts/build-wasm-physics.mjs",
75
77
  "./core/preflight": "./core/scripts/preflight.mjs"
76
78
  },
@@ -88,17 +90,19 @@
88
90
  "scripts": {
89
91
  "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
90
92
  "test": "npm run test:core",
91
- "test:core": "tsx --test tests/canonical.test.ts tests/runtime-session.test.ts tests/kinetix-runtime-session.test.ts tests/runtime-support.test.ts tests/runtime-guards.test.ts tests/r3f-render-adapter.test.ts tests/snapshot-cost.test.ts tests/sdkSession.test.ts tests/input-authority.test.ts tests/session-wire.test.ts tests/session-snapshot.test.ts tests/authority-room.test.ts tests/networked-client-snapshot.test.ts tests/runner.test.ts tests/testing.test.ts tests/browser-session.test.ts tests/secret-config.test.ts tests/secret-protocol.test.ts tests/secret-authority.test.ts tests/secret-client.test.ts tests/late-join-checksum.test.ts tests/late-join-input-backfill.test.ts tests/replay-scoring.test.ts tests/bot-backfill.test.ts tests/latency-simulation.test.ts tests/time-sync.test.ts tests/match-log.test.ts tests/spectator.test.ts tests/synctest.test.ts tests/instant-replay.test.ts tests/session-promotion.test.ts tests/physics3d-broadphase-stats.test.ts tests/entrypoints.test.ts tests/export-surface.test.ts tests/test-manifest.test.ts tests/coarse-region.test.ts tests/input-codec.test.ts tests/platformer-content.test.ts tests/command-ingress.test.ts tests/session-reconfigure.test.ts tests/prediction-mode.test.ts tests/ephemeral-channel.test.ts tests/runner-surface.test.ts tests/runner-harness.test.ts tests/effects-adapter-bounds.test.ts tests/simulated-match-secrets.test.ts tests/run-glue-surface.test.mjs core/tests/core-boundary.test.mjs tests/math-core-parity.test.ts",
93
+ "test:core": "tsx --test tests/canonical.test.ts tests/runtime-session.test.ts tests/kinetix-runtime-session.test.ts tests/runtime-support.test.ts tests/runtime-guards.test.ts tests/r3f-render-adapter.test.ts tests/snapshot-cost.test.ts tests/sdkSession.test.ts tests/input-authority.test.ts tests/session-wire.test.ts tests/session-snapshot.test.ts tests/authority-room.test.ts tests/networked-client-snapshot.test.ts tests/runner.test.ts tests/testing.test.ts tests/browser-session.test.ts tests/secret-config.test.ts tests/secret-protocol.test.ts tests/secret-authority.test.ts tests/secret-client.test.ts tests/late-join-checksum.test.ts tests/late-join-input-backfill.test.ts tests/replay-scoring.test.ts tests/bot-backfill.test.ts tests/latency-simulation.test.ts tests/time-sync.test.ts tests/match-log.test.ts tests/spectator.test.ts tests/synctest.test.ts tests/instant-replay.test.ts tests/session-promotion.test.ts tests/physics3d-broadphase-stats.test.ts tests/entrypoints.test.ts tests/export-surface.test.ts tests/test-manifest.test.ts tests/coarse-region.test.ts tests/input-codec.test.ts tests/platformer-content.test.ts tests/command-ingress.test.ts tests/session-reconfigure.test.ts tests/prediction-mode.test.ts tests/ephemeral-channel.test.ts tests/runner-surface.test.ts tests/runner-harness.test.ts tests/effects-adapter-bounds.test.ts tests/simulated-match-secrets.test.ts tests/run-glue-surface.test.mjs core/tests/core-boundary.test.mjs tests/math-core-parity.test.ts tests/movement3d-kcc-unit-contract.test.ts tests/movement3d-static-ground.test.ts",
92
94
  "test:soak": "tsx --test tests/runtime-soak.test.ts",
93
- "test:extended": "tsx --test tests/animation.test.ts tests/asset-cooking.test.ts tests/backend-proof-checksum-vectors.test.ts tests/bot-documents.test.ts tests/bot-primitives.test.ts tests/collider-cooking.test.ts tests/collision.test.ts tests/command-timeline.test.ts tests/config-bundle.test.ts tests/cosmetic-physics3d.test.ts tests/deterministic-primitives.test.ts tests/ecs-lite.test.ts tests/effects-adapter.test.ts tests/engine-runtime.test.ts tests/errors.test.ts tests/events.test.ts tests/input-button.test.ts tests/load-128-players.test.ts tests/math.test.ts tests/movement.test.ts tests/movement3d.test.ts tests/multiclient-authority.test.ts tests/navigation.test.ts tests/noise.test.ts tests/physics-cert.test.ts tests/physics2d.test.ts tests/physics3d-ccd.test.ts tests/physics3d-coverage.test.ts tests/physics3d-destruction-cert.test.ts tests/physics3d-heightfield.test.ts tests/physics3d-hull.test.ts tests/physics3d-joints.test.ts tests/physics3d-kinematic-mesh.test.ts tests/physics3d-mass.test.ts tests/physics3d-materials.test.ts tests/physics3d-mesh-onesided.test.ts tests/physics3d-query-index.test.ts tests/physics3d-recipes.test.ts tests/physics3d-rotation-convention.test.ts tests/physics3d-shared.test.ts tests/physics3d-sleeping-manifold-reuse.test.ts tests/physics3d-solver.test.ts tests/physics3d-stacking.test.ts tests/physics3d-toi.test.ts tests/physics3d-vehicle.test.ts tests/physics3d-voxel.test.ts tests/physics3d-world-edit.test.ts tests/physics3d.test.ts tests/physics-vector-generator.test.ts tests/random.test.ts tests/rollback-history.test.ts tests/schema-artifacts.test.ts tests/schema-codegen.test.ts tests/schema-dsl-binary.test.ts tests/schema.test.ts tests/signals.test.ts tests/sparse-set.test.ts tests/static-checker.test.ts tests/system-lifecycle.test.ts tests/wire-canonical-sentinels.test.ts tests/ws-frame.test.ts",
95
+ "test:extended": "tsx --test tests/kcc-vehicle-characterization.test.ts tests/animation.test.ts tests/asset-cooking.test.ts tests/backend-proof-checksum-vectors.test.ts tests/bot-documents.test.ts tests/bot-primitives.test.ts tests/collider-cooking.test.ts tests/collision.test.ts tests/command-timeline.test.ts tests/config-bundle.test.ts tests/cosmetic-physics3d.test.ts tests/deterministic-primitives.test.ts tests/ecs-lite.test.ts tests/effects-adapter.test.ts tests/engine-runtime.test.ts tests/errors.test.ts tests/events.test.ts tests/input-button.test.ts tests/load-128-players.test.ts tests/math.test.ts tests/movement.test.ts tests/movement3d.test.ts tests/multiclient-authority.test.ts tests/navigation.test.ts tests/noise.test.ts tests/physics-cert.test.ts tests/physics2d.test.ts tests/physics3d-ccd.test.ts tests/physics3d-coverage.test.ts tests/physics3d-destruction-cert.test.ts tests/physics3d-geometry-limits.test.ts tests/physics3d-heightfield.test.ts tests/physics3d-hull.test.ts tests/physics3d-joints.test.ts tests/physics3d-kinematic-mesh.test.ts tests/physics3d-mass.test.ts tests/physics3d-materials.test.ts tests/physics3d-mesh-onesided.test.ts tests/physics3d-query-index.test.ts tests/physics3d-recipes.test.ts tests/physics3d-rotation-convention.test.ts tests/physics3d-shared.test.ts tests/physics3d-sleeping-manifold-reuse.test.ts tests/physics3d-solver.test.ts tests/physics3d-static-memoization.test.ts tests/physics3d-static-overlap-memo.test.ts tests/physics3d-cylinder-hull.test.ts tests/physics3d-stacking.test.ts tests/physics3d-toi.test.ts tests/physics3d-vehicle.test.ts tests/physics3d-voxel.test.ts tests/physics3d-world-edit.test.ts tests/physics3d.test.ts tests/physics-vector-generator.test.ts tests/random.test.ts tests/rollback-history.test.ts tests/schema-artifacts.test.ts tests/schema-codegen.test.ts tests/schema-dsl-binary.test.ts tests/schema.test.ts tests/signals.test.ts tests/sparse-set.test.ts tests/static-checker.test.ts tests/system-lifecycle.test.ts tests/wire-canonical-sentinels.test.ts tests/ws-frame.test.ts",
94
96
  "test:ci": "npm run build && npm run test:core",
95
97
  "build:native": "node core/scripts/build-native.mjs",
98
+ "build:mobile-native": "node core/scripts/build-mobile-native.mjs",
96
99
  "build:wasm-physics": "node core/scripts/build-wasm-physics.mjs",
97
100
  "test:determinism": "node --test core/tests/*.test.mjs",
98
101
  "generate:platformer-manifest": "node core/scripts/generate-platformer2d-manifest.mjs",
99
102
  "generate:physics-vectors": "node scripts/gen-physics-vectors.mjs",
100
103
  "test:physics-vectors": "node --import tsx/esm --test tests/physics-vector-generator.test.ts",
101
- "pack:dry-run": "npm pack --dry-run"
104
+ "pack:dry-run": "npm pack --dry-run",
105
+ "bench:cross-lane": "node core/bench/cross-lane.mjs"
102
106
  },
103
107
  "license": "SEE LICENSE IN LICENSE.md",
104
108
  "keywords": [
@@ -1,34 +0,0 @@
1
- export type PredictionCullingMode = 'off' | 'local-area';
2
- export interface PredictionCullingOptions {
3
- readonly mode: PredictionCullingMode;
4
- readonly players: number;
5
- readonly objectsPerPlayer: number;
6
- readonly frames: number;
7
- readonly seed: number;
8
- readonly radius: number;
9
- readonly includeHiddenThreat?: boolean;
10
- }
11
- export interface PredictionCullingResult {
12
- readonly mode: PredictionCullingMode;
13
- readonly players: number;
14
- readonly objects: number;
15
- readonly frames: number;
16
- readonly seed: number;
17
- readonly radius: number;
18
- readonly averagePredictedObjects: string;
19
- readonly minPredictedObjects: number;
20
- readonly maxPredictedObjects: number;
21
- readonly visibleCorrections: number;
22
- readonly hiddenAuthoritativeInteractions: number;
23
- readonly predictedObjectSteps: number;
24
- readonly verifiedObjectSteps: number;
25
- readonly culledObjectSteps: number;
26
- readonly restoreFrame: number;
27
- readonly predictedChecksum: string;
28
- readonly verifiedChecksum: string;
29
- readonly resimulatedChecksum: string;
30
- readonly predictionFilterChecksum: string;
31
- readonly completeChecksum: string;
32
- readonly result: 'pass' | 'fail';
33
- }
34
- export declare function runPredictionCullingLab(options: PredictionCullingOptions): PredictionCullingResult;