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

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.
@@ -1,8 +1,10 @@
1
1
  #include "syncplay_native_probe_abi.h"
2
2
 
3
+ #include "../fixed_math.hpp"
3
4
  #include "../physics/physics.hpp"
4
5
  #include "box3d_probe_scenario.hpp"
5
6
 
7
+ #include <bit>
6
8
  #include <chrono>
7
9
  #include <cstdint>
8
10
  #include <mutex>
@@ -40,6 +42,35 @@ extern "C" std::int32_t rundot_syncplay_native_probe_run(
40
42
  output->status = RUNDOT_SYNCPLAY_NATIVE_PROBE_STATUS_UNSUPPORTED_ABI;
41
43
  return output->status;
42
44
  }
45
+ if (input->scenario_id == RUNDOT_SYNCPLAY_NATIVE_PROBE_SCENARIO_FIXED_MATH_V1) {
46
+ if (input->frames == 0 || input->frames > 1000000) {
47
+ output->status = RUNDOT_SYNCPLAY_NATIVE_PROBE_STATUS_INVALID_FRAMES;
48
+ return output->status;
49
+ }
50
+ namespace fx = rundot::fixedmath;
51
+ const fx::Config cfg = fx::defaultConfig();
52
+ bool ok = true;
53
+ fx::Vec3 p{0, 0, 0};
54
+ fx::Vec3 v{1000, 2000, -500};
55
+ double acc = 0;
56
+ const auto mathStart = std::chrono::steady_clock::now();
57
+ for (std::uint32_t i = 0; i < input->frames; i += 1) {
58
+ p = fx::vec3Add(cfg, p, fx::vec3Scale(cfg, v, 990, ok), ok);
59
+ acc += fx::vec3Length(cfg, p, ok);
60
+ v = fx::vec3Scale(cfg, v, 999, ok);
61
+ if (p.x > 2000000000.0) p = fx::Vec3{0, 0, 0};
62
+ }
63
+ output->elapsed_ns = static_cast<std::uint64_t>(
64
+ std::chrono::duration_cast<std::chrono::nanoseconds>(
65
+ std::chrono::steady_clock::now() - mathStart).count());
66
+ output->completed_frames = input->frames;
67
+ // The digest IS the f64 bit pattern, so it compares directly against the
68
+ // JavaScript lane's DataView-derived value with no formatting in between.
69
+ output->final_checksum = std::bit_cast<std::uint64_t>(acc);
70
+ output->status = ok ? RUNDOT_SYNCPLAY_NATIVE_PROBE_STATUS_OK
71
+ : RUNDOT_SYNCPLAY_NATIVE_PROBE_STATUS_STEP_FAILED;
72
+ return output->status;
73
+ }
43
74
  if (input->scenario_id != RUNDOT_SYNCPLAY_NATIVE_PROBE_SCENARIO_BOX3D_V1) {
44
75
  output->status = RUNDOT_SYNCPLAY_NATIVE_PROBE_STATUS_UNSUPPORTED_SCENARIO;
45
76
  return output->status;
@@ -4,6 +4,11 @@
4
4
 
5
5
  #define RUNDOT_SYNCPLAY_NATIVE_PROBE_ABI_VERSION 1u
6
6
  #define RUNDOT_SYNCPLAY_NATIVE_PROBE_SCENARIO_BOX3D_V1 1u
7
+ // Runs the fixed-point math kernel and returns its f64-bit digest in
8
+ // final_checksum. Exists so a device can compare its C++ result against the same
9
+ // kernel run in the app's JavaScript engine: with interpreted glue allowed on
10
+ // Android, that engine is on the consensus path and must agree bit-for-bit.
11
+ #define RUNDOT_SYNCPLAY_NATIVE_PROBE_SCENARIO_FIXED_MATH_V1 2u
7
12
 
8
13
  #define RUNDOT_SYNCPLAY_NATIVE_PROBE_STATUS_OK 0
9
14
  #define RUNDOT_SYNCPLAY_NATIVE_PROBE_STATUS_INVALID_ARGUMENT (-1)
@@ -21,6 +21,8 @@ export const MOBILE_NATIVE_FILES = Object.freeze([
21
21
  'physics/abi.cpp',
22
22
  'deterministic_math.hpp',
23
23
  'deterministic_math.cpp',
24
+ 'fixed_math.hpp',
25
+ 'fixed_math.cpp',
24
26
  ]);
25
27
 
26
28
  const sourceExtensions = new Set(['.cpp', '.hpp', '.h']);
package/dist/browser.d.ts CHANGED
@@ -61,7 +61,7 @@ export type { DeterministicAuthoritativeLagCompensation3DShotContext, Determinis
61
61
  export type { DeterministicLocalAuthoritySnapshotIdentity, DeterministicLocalAuthoritySnapshotTransfer, DeterministicLocalAuthorityWireEnvelope, DeterministicLocalAuthorityWireKind } from './local-authority-wire.js';
62
62
  export type { DeterministicFixedQuaternion, DeterministicFixedVec2, DeterministicFixedVec3, DeterministicMath } from './math.js';
63
63
  export type { DeterministicFpsAimState, DeterministicFpsInput, DeterministicKccBody, DeterministicKccInput, DeterministicKccStepResult, DeterministicLagCompensatedHitValidation, DeterministicLagCompensatedShot, DeterministicMovementCrowdMetrics, DeterministicMovementSimulationMetrics, DeterministicMovementStep, DeterministicMovementWorld, DeterministicMovingPlatform, DeterministicOneWayPlatform, DeterministicSlope, DeterministicTargetSnapshot, DeterministicTopDownBody, DeterministicTopDownBounds, DeterministicTopDownInput } from './movement.js';
64
- export type { DeterministicKccBody3D, DeterministicKccCarrierPose3D, DeterministicKccCarryResult3D, DeterministicKccCarryState3D, DeterministicKccCollisionCandidate3D, DeterministicKccCollisionFilter3D, DeterministicKccExternalImpulse3D, DeterministicKccGeometry3D, DeterministicKccInput3D, DeterministicKccPhysicsImpulse3D, DeterministicKccProcessor3D, DeterministicKccStepResult3D, DeterministicKccWorld3D, DeterministicMovingPlatform3D, DeterministicSlope3D, DeterministicStep3D } from './movement3d.js';
64
+ export type { DeterministicKccBody3D, DeterministicKccCarrierPose3D, DeterministicKccCarryResult3D, DeterministicKccCarryState3D, DeterministicKccCollisionCandidate3D, DeterministicKccCollisionFilter3D, DeterministicKccExternalImpulse3D, DeterministicKccGeometry3D, DeterministicKccInput3D, DeterministicKccJumpTuning3D, DeterministicKccPhysicsImpulse3D, DeterministicKccProcessor3D, DeterministicKccStepResult3D, DeterministicKccWorld3D, DeterministicMovingPlatform3D, DeterministicSlope3D, DeterministicStep3D } from './movement3d.js';
65
65
  export type { CookedDeterministicNavigationGraph, DeterministicFlowField, DeterministicNavEdge, DeterministicNavigationAgent, DeterministicNavigationGraphSource, DeterministicNavigationStepOptions, DeterministicNavigationStepResult, DeterministicNavNode, DeterministicNavRegion, DeterministicPathQuery, DeterministicPathResult, DeterministicTilemapSource } from './navigation.js';
66
66
  export type { CookedDeterministicNavmesh, DeterministicNavmeshAgent, DeterministicNavmeshAgentStepResult, DeterministicNavmeshImportSource, DeterministicNavmeshLink, DeterministicNavmeshMovingAvoidanceObstacle, DeterministicNavmeshOffMeshTraversalState, DeterministicNavmeshOffMeshTraversalStep, DeterministicNavmeshPath, DeterministicNavmeshPoint, DeterministicNavmeshPolygon, DeterministicNavmeshSource } from './navmesh.js';
67
67
  export type { ConfirmedSyncplayPresentationFrame, KinetixRuntimeFactory, NetworkedClientTransport, NetworkedSyncplayClient, NetworkedSyncplayClientInputDelayOptions, NetworkedSyncplayClientNetStats, NetworkedSyncplayClientOptions, NetworkedSyncplayClientPacingOptions, NetworkedSyncplayPreparationOptions, NetworkedSyncplaySessionDescriptor, NetworkedSyncplaySlotPresence, PreparedNetworkedSyncplayTransport, SyncplayPredictionMode } from './networked-client.js';
package/dist/index.d.ts CHANGED
@@ -73,7 +73,7 @@ export type { DeterministicAuthoritativeLagCompensation3DShotContext, Determinis
73
73
  export type { DeterministicLocalAuthoritySnapshotIdentity, DeterministicLocalAuthoritySnapshotTransfer, DeterministicLocalAuthorityWireEnvelope, DeterministicLocalAuthorityWireKind } from './local-authority-wire.js';
74
74
  export type { DeterministicFixedQuaternion, DeterministicFixedVec2, DeterministicFixedVec3, DeterministicMath } from './math.js';
75
75
  export type { DeterministicFpsAimState, DeterministicFpsInput, DeterministicKccBody, DeterministicKccInput, DeterministicKccStepResult, DeterministicMovementCrowdMetrics, DeterministicMovementSimulationMetrics, DeterministicMovementStep, DeterministicMovementWorld, DeterministicMovingPlatform, DeterministicOneWayPlatform, DeterministicSlope, DeterministicTargetSnapshot, DeterministicTopDownBody, DeterministicTopDownBounds, DeterministicTopDownInput } from './movement.js';
76
- export type { DeterministicKccBody3D, DeterministicKccCarrierPose3D, DeterministicKccCarryResult3D, DeterministicKccCarryState3D, DeterministicKccCollisionCandidate3D, DeterministicKccCollisionFilter3D, DeterministicKccExternalImpulse3D, DeterministicKccGeometry3D, DeterministicKccInput3D, DeterministicKccPhysicsImpulse3D, DeterministicKccProcessor3D, DeterministicKccStepResult3D, DeterministicKccWorld3D, DeterministicMovingPlatform3D, DeterministicSlope3D, DeterministicStep3D } from './movement3d.js';
76
+ export type { DeterministicKccBody3D, DeterministicKccCarrierPose3D, DeterministicKccCarryResult3D, DeterministicKccCarryState3D, DeterministicKccCollisionCandidate3D, DeterministicKccCollisionFilter3D, DeterministicKccExternalImpulse3D, DeterministicKccGeometry3D, DeterministicKccInput3D, DeterministicKccJumpTuning3D, DeterministicKccPhysicsImpulse3D, DeterministicKccProcessor3D, DeterministicKccStepResult3D, DeterministicKccWorld3D, DeterministicMovingPlatform3D, DeterministicSlope3D, DeterministicStep3D } from './movement3d.js';
77
77
  export type { CookedDeterministicNavigationGraph, DeterministicFlowField, DeterministicNavEdge, DeterministicNavigationAgent, DeterministicNavigationGraphSource, DeterministicNavigationStepOptions, DeterministicNavigationStepResult, DeterministicNavNode, DeterministicNavRegion, DeterministicPathQuery, DeterministicPathResult, DeterministicTilemapSource } from './navigation.js';
78
78
  export type { CookedDeterministicNavmesh, DeterministicNavmeshAgent, DeterministicNavmeshAgentStepResult, DeterministicNavmeshImportSource, DeterministicNavmeshLink, DeterministicNavmeshMovingAvoidanceObstacle, DeterministicNavmeshOffMeshTraversalState, DeterministicNavmeshOffMeshTraversalStep, DeterministicNavmeshPath, DeterministicNavmeshPoint, DeterministicNavmeshPolygon, DeterministicNavmeshSource } from './navmesh.js';
79
79
  export type { ConfirmedSyncplayPresentationFrame, KinetixRuntimeFactory, NetworkedClientTransport, NetworkedSyncplayClient, NetworkedSyncplayClientInputDelayOptions, NetworkedSyncplayClientNetStats, NetworkedSyncplayClientOptions, NetworkedSyncplayClientPacingOptions, NetworkedSyncplayPreparationOptions, NetworkedSyncplaySessionDescriptor, PreparedNetworkedSyncplayTransport, SyncplayPredictionMode } from './networked-client.js';
@@ -24,6 +24,20 @@ export interface DeterministicKccInput3D {
24
24
  readonly moveZ: number;
25
25
  readonly jumpPressed?: boolean;
26
26
  }
27
+ export interface DeterministicKccJumpTuning3D {
28
+ /** Upward velocity the jump sets, in units per frame. Default 10. */
29
+ readonly impulse?: number;
30
+ /**
31
+ * Immediate lift applied on the jump frame to unstick the capsule from the
32
+ * ground it is resting on. Defaults to `impulse / 10`, which keeps the
33
+ * historical 1-unit lift at the default impulse and scales with a retune.
34
+ */
35
+ readonly liftOff?: number;
36
+ /** Frames a jump stays available after leaving the ground. Default 4. */
37
+ readonly coyoteFrames?: number;
38
+ /** Frames an early jump press is held for. Minimum 1, default 4. */
39
+ readonly bufferFrames?: number;
40
+ }
27
41
  export interface DeterministicKccExternalImpulse3D {
28
42
  readonly id: string;
29
43
  readonly vx: number;
@@ -198,6 +212,14 @@ export declare function stepDeterministicKcc3D(body: DeterministicKccBody3D, inp
198
212
  * bit-identical checksums.
199
213
  */
200
214
  readonly groundOnStaticBodies?: boolean;
215
+ /**
216
+ * Jump tuning, in the KCC's frame units. Gravity retunes through a processor
217
+ * because processors run before `vy -= 1`, but the jump impulse is applied
218
+ * AFTER them, so it cannot be scaled the same way — without these a game that
219
+ * retunes gravity must own jump, coyote and buffer itself. Every default
220
+ * reproduces the previous hardcoded behaviour bit-for-bit.
221
+ */
222
+ readonly jump?: DeterministicKccJumpTuning3D;
201
223
  }): DeterministicKccStepResult3D;
202
224
  export interface DeterministicKccCarryState3D {
203
225
  readonly bodyId: string;
@@ -1,8 +1,39 @@
1
1
  import { defaultChecksum } from './canonical.js';
2
2
  import { createDeterministicPhysicsWorld3D, overlapDeterministicPhysics3D, } from './physics3d.js';
3
3
  import { heightfieldLocalSurfaceY } from './physics3d-heightfield.js';
4
- import { mulMat3Vec, quaternionFromQuarterTurns, quatMultiply, quatToMat3, rotatePointByQuat } from './physics3d-shared.js';
4
+ import { mulMat3Vec, quaternionFromQuarterTurns, quatMultiply, quatToMat3, rotatePointByQuat, solverSqrt } from './physics3d-shared.js';
5
+ function normalizeKccJumpTuning3D(tuning) {
6
+ const impulse = finiteKccJumpNumber(tuning?.impulse, 10, 'impulse');
7
+ return {
8
+ impulse,
9
+ liftOff: finiteKccJumpNumber(tuning?.liftOff, impulse / 10, 'liftOff'),
10
+ coyoteFrames: kccJumpFrameCount(tuning?.coyoteFrames, 'coyoteFrames', 0),
11
+ // A press arms the jump by writing this into jumpBufferFrames and the gate
12
+ // needs it > 0, so 0 would not mean "no buffering" — it would mean "never
13
+ // jump". 1 is the real floor: the press lives for the frame it happened on.
14
+ bufferFrames: kccJumpFrameCount(tuning?.bufferFrames, 'bufferFrames', 1),
15
+ };
16
+ }
17
+ function finiteKccJumpNumber(value, fallback, field) {
18
+ if (value === undefined) {
19
+ return fallback;
20
+ }
21
+ if (!Number.isFinite(value)) {
22
+ throw new Error(`KCC jump.${field} must be a finite number`);
23
+ }
24
+ return value;
25
+ }
26
+ function kccJumpFrameCount(value, field, minimum) {
27
+ if (value === undefined) {
28
+ return 4;
29
+ }
30
+ if (!Number.isInteger(value) || value < minimum) {
31
+ throw new Error(`KCC jump.${field} must be an integer >= ${minimum}, got ${value}`);
32
+ }
33
+ return value;
34
+ }
5
35
  export function stepDeterministicKcc3D(body, input, world, options) {
36
+ const jump = normalizeKccJumpTuning3D(options.jump);
6
37
  let next = applyProcessors(body, input, options.frame, options.processors ?? []);
7
38
  const events = [];
8
39
  let correctionCount = 0;
@@ -10,8 +41,8 @@ export function stepDeterministicKcc3D(body, input, world, options) {
10
41
  let collisionFilterCalls = 0;
11
42
  let collisionFilterRejectedHits = 0;
12
43
  const externalImpulse = combineExternalKccImpulses(options.externalImpulses ?? []);
13
- let coyoteFrames = next.grounded ? 4 : Math.max(0, next.coyoteFrames - 1);
14
- let jumpBufferFrames = input.jumpPressed === true ? 4 : Math.max(0, next.jumpBufferFrames - 1);
44
+ let coyoteFrames = next.grounded ? jump.coyoteFrames : Math.max(0, next.coyoteFrames - 1);
45
+ let jumpBufferFrames = input.jumpPressed === true ? jump.bufferFrames : Math.max(0, next.jumpBufferFrames - 1);
15
46
  let vx = input.moveX + externalImpulse.vx;
16
47
  let vz = input.moveZ + externalImpulse.vz;
17
48
  let vy = next.vy - 1 + externalImpulse.vy;
@@ -70,8 +101,8 @@ export function stepDeterministicKcc3D(body, input, world, options) {
70
101
  }
71
102
  }
72
103
  if (jumpBufferFrames > 0 && (next.grounded || next.coyoteFrames > 0 || grounded)) {
73
- vy = 10;
74
- y += 1;
104
+ vy = jump.impulse;
105
+ y += jump.liftOff;
75
106
  grounded = false;
76
107
  platformId = undefined;
77
108
  jumpBufferFrames = 0;
@@ -198,7 +229,7 @@ export function stepDeterministicKcc3D(body, input, world, options) {
198
229
  next = boundsCorrection.body;
199
230
  const finalBody = {
200
231
  ...next,
201
- coyoteFrames: next.grounded ? 4 : coyoteFrames,
232
+ coyoteFrames: next.grounded ? jump.coyoteFrames : coyoteFrames,
202
233
  jumpBufferFrames,
203
234
  };
204
235
  return {
@@ -361,6 +392,42 @@ function groundContactTopAt(body, x, z) {
361
392
  * skinWidth defaults to 0; a plain double is bit-identical on every peer.
362
393
  */
363
394
  const KCC_GROUND_CLEARANCE = 1e-4;
395
+ /**
396
+ * Extra vertical lift a round capsule bottom needs to rest ON a sloped surface
397
+ * instead of IN it.
398
+ *
399
+ * groundContactTopAt reports the height under the capsule CENTRE, but the
400
+ * bottom is a sphere, so on gradient `g` the contact sits uphill and a bare
401
+ * radius of lift buries the uphill flank by `radius * (sec θ - 1)`. Past ~1.7°
402
+ * that exceeds KCC_GROUND_CLEARANCE, every overlap probe (full, x-only AND
403
+ * z-only) hits, and resolvePhysicsSlide3D reverts the whole horizontal move.
404
+ *
405
+ * Sampled a bottom-sphere radius out: that is the footprint the sphere can
406
+ * touch, it reads a plane exactly at any angle, and it self-bounds the
407
+ * discontinuities groundContactTopAt has on some shapes (a compound's children
408
+ * can butt together at a height jump) — a jump of D reads as D/radius here
409
+ * instead of the skyward fling an epsilon probe would give. Per axis it takes
410
+ * the steeper of forward/backward so a sample straddling an edge cannot
411
+ * average its way to an under-lift and back into the freeze.
412
+ */
413
+ function kccSlopeClearance(body, x, z, top, geometry) {
414
+ const step = geometry.radius + geometry.skinWidth;
415
+ const axisGradient = (deltaX, deltaZ) => {
416
+ const ahead = groundContactTopAt(body, x + deltaX, z + deltaZ);
417
+ const behind = groundContactTopAt(body, x - deltaX, z - deltaZ);
418
+ const forward = ahead === undefined ? 0 : Math.abs(ahead - top) / step;
419
+ const backward = behind === undefined ? 0 : Math.abs(top - behind) / step;
420
+ return Math.max(forward, backward);
421
+ };
422
+ const gradientX = axisGradient(step, 0);
423
+ const gradientZ = axisGradient(0, step);
424
+ const gradientSq = gradientX * gradientX + gradientZ * gradientZ;
425
+ if (gradientSq <= 0) {
426
+ return 0;
427
+ }
428
+ // radius + skinWidth is the bottom-sphere radius physicsOverlapsKcc probes with.
429
+ return (geometry.radius + geometry.skinWidth) * (solverSqrt(1 + gradientSq) - 1);
430
+ }
364
431
  /**
365
432
  * Distance from the capsule's centre to the bottom of its collision shape.
366
433
  * A capsule's vertical extent is halfHeight + radius (see primitiveHalfExtent),
@@ -397,8 +464,15 @@ function staticGroundUnderKcc(physics, x, y, z, previousFeetY, geometry, ignored
397
464
  const grounds = physics.bodies
398
465
  .filter((body) => body.kind === 'static' && hitIds.has(body.id) && !ignoredBodyIds.has(body.id))
399
466
  .map((body) => ({ body, top: groundContactTopAt(body, x, z) }))
400
- .filter((candidate) => candidate.top !== undefined
401
- && feetY <= candidate.top + snapDistance
467
+ .filter((candidate) => candidate.top !== undefined)
468
+ .map((candidate) => ({
469
+ ...candidate,
470
+ lift: kccSlopeClearance(candidate.body, x, z, candidate.top, geometry),
471
+ }))
472
+ // The band is measured from where the capsule actually STANDS, so a slope
473
+ // lift bigger than snapDistance cannot drop it out of its own ground the
474
+ // next frame and leave it cycling between grounded and falling.
475
+ .filter((candidate) => feetY <= candidate.top + candidate.lift + snapDistance
402
476
  && previousFeetY >= candidate.top - snapDistance)
403
477
  .sort((left, right) => right.top - left.top || compareStrings(left.body.id, right.body.id));
404
478
  const ground = grounds[0];
@@ -407,7 +481,7 @@ function staticGroundUnderKcc(physics, x, y, z, previousFeetY, geometry, ignored
407
481
  }
408
482
  return {
409
483
  bodyId: ground.body.id,
410
- topY: ground.top + kccFeetOffset(geometry),
484
+ topY: ground.top + kccFeetOffset(geometry) + ground.lift,
411
485
  };
412
486
  }
413
487
  /** World half-extents (x,y,z) of a primitive shape under rotation matrix `m`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@series-inc/rundot-syncplay",
3
- "version": "5.26.0-beta.3",
3
+ "version": "5.26.0-beta.4",
4
4
  "description": "Kinetix orchestration, rollback, replay, late-join, and deterministic networking for RUN.game",
5
5
  "repository": {
6
6
  "type": "git",