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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +33 -12
  2. package/dist/authority-room.d.ts +11 -0
  3. package/dist/authority-room.js +43 -0
  4. package/dist/browser.d.ts +7 -0
  5. package/dist/browser.js +4 -0
  6. package/dist/cjs/authority-room.js +43 -0
  7. package/dist/cjs/browser.js +15 -1
  8. package/dist/cjs/engine-complete.js +1 -4
  9. package/dist/cjs/engine-parity-matrix.js +11 -2
  10. package/dist/cjs/engine-parity.js +1 -1
  11. package/dist/cjs/index.js +17 -1
  12. package/dist/cjs/input-authority.js +34 -2
  13. package/dist/cjs/local-authority-server-runner.js +3 -1
  14. package/dist/cjs/local-authority-wire.js +20 -1
  15. package/dist/cjs/match-log.js +27 -0
  16. package/dist/cjs/networked-client.js +59 -11
  17. package/dist/cjs/node.js +19 -1
  18. package/dist/cjs/physics-cert.js +1 -1
  19. package/dist/cjs/physics3d-heightfield.js +51 -0
  20. package/dist/cjs/physics3d-joints.js +92 -18
  21. package/dist/cjs/physics3d-mass.js +131 -0
  22. package/dist/cjs/physics3d-shared.js +5 -1
  23. package/dist/cjs/physics3d-solver.js +109 -19
  24. package/dist/cjs/physics3d-toi.js +23 -0
  25. package/dist/cjs/physics3d.js +206 -24
  26. package/dist/cjs/replay.js +25 -1
  27. package/dist/cjs/sdk-offline.js +9 -1
  28. package/dist/cjs/sdk-session.js +8 -0
  29. package/dist/cjs/secret-authority.js +647 -0
  30. package/dist/cjs/secret-client.js +615 -0
  31. package/dist/cjs/secret-command-classification.js +8 -0
  32. package/dist/cjs/secret-config.js +211 -0
  33. package/dist/cjs/secret-crypto.js +208 -0
  34. package/dist/cjs/secret-protocol.js +238 -0
  35. package/dist/cjs/session-build.js +7 -4
  36. package/dist/cjs/tools/static-checker.js +61 -13
  37. package/dist/cli.js +30 -7
  38. package/dist/deterministic-primitives.d.ts +1 -0
  39. package/dist/engine-complete.js +1 -4
  40. package/dist/engine-parity-matrix.js +11 -2
  41. package/dist/engine-parity.js +1 -1
  42. package/dist/index.d.ts +9 -0
  43. package/dist/index.js +5 -0
  44. package/dist/input-authority.d.ts +3 -0
  45. package/dist/input-authority.js +35 -3
  46. package/dist/local-authority-server-runner.d.ts +1 -0
  47. package/dist/local-authority-server-runner.js +2 -1
  48. package/dist/local-authority-wire.js +20 -1
  49. package/dist/match-log.d.ts +4 -1
  50. package/dist/match-log.js +27 -0
  51. package/dist/networked-client.d.ts +8 -0
  52. package/dist/networked-client.js +59 -11
  53. package/dist/node.d.ts +5 -0
  54. package/dist/node.js +4 -0
  55. package/dist/physics-cert.js +1 -1
  56. package/dist/physics3d-heightfield.d.ts +17 -0
  57. package/dist/physics3d-heightfield.js +47 -0
  58. package/dist/physics3d-joints.d.ts +6 -1
  59. package/dist/physics3d-joints.js +93 -19
  60. package/dist/physics3d-mass.d.ts +11 -0
  61. package/dist/physics3d-mass.js +126 -0
  62. package/dist/physics3d-shared.d.ts +1 -0
  63. package/dist/physics3d-shared.js +4 -1
  64. package/dist/physics3d-solver.d.ts +15 -3
  65. package/dist/physics3d-solver.js +108 -19
  66. package/dist/physics3d-toi.d.ts +9 -0
  67. package/dist/physics3d-toi.js +22 -0
  68. package/dist/physics3d.d.ts +8 -1
  69. package/dist/physics3d.js +209 -27
  70. package/dist/replay.d.ts +5 -0
  71. package/dist/replay.js +25 -1
  72. package/dist/reports.d.ts +6 -1
  73. package/dist/sdk-offline.d.ts +4 -0
  74. package/dist/sdk-offline.js +9 -1
  75. package/dist/sdk-session.d.ts +4 -0
  76. package/dist/sdk-session.js +8 -0
  77. package/dist/secret-authority.d.ts +90 -0
  78. package/dist/secret-authority.js +643 -0
  79. package/dist/secret-client.d.ts +89 -0
  80. package/dist/secret-client.js +611 -0
  81. package/dist/secret-command-classification.d.ts +1 -0
  82. package/dist/secret-command-classification.js +5 -0
  83. package/dist/secret-config.d.ts +45 -0
  84. package/dist/secret-config.js +206 -0
  85. package/dist/secret-crypto.d.ts +29 -0
  86. package/dist/secret-crypto.js +190 -0
  87. package/dist/secret-protocol.d.ts +181 -0
  88. package/dist/secret-protocol.js +230 -0
  89. package/dist/session-build.d.ts +16 -0
  90. package/dist/session-build.js +7 -4
  91. package/dist/tools/static-checker.d.ts +11 -0
  92. package/dist/tools/static-checker.js +60 -13
  93. package/package.json +2 -2
package/README.md CHANGED
@@ -8,18 +8,29 @@ rollback — with no game-specific server logic.
8
8
  This package is intentionally standalone. It does not depend on Venus MP,
9
9
  Cloud Run, React Three Fiber, Rapier, Firebase, or the SDK bridge.
10
10
 
11
- ## Preview Status
12
-
13
- This is a headless engine and certification package. It is designed to prove
14
- deterministic simulation behavior from the CLI before renderer integration is
15
- added, and to hand deploy-check artifacts to Venus backend upload/room-server
16
- validation when a room bundle opts in.
17
-
18
- Production matchmaking, ticket UX, client connection flows, and R3F renderer
19
- proof are not part of this package yet. The current production-facing Venus MP
20
- changes validate uploaded deterministic room proof metadata and pass normalized
21
- proof into room startup; they do not add new matchmaking or client connection
22
- semantics.
11
+ ## Public and private projections
12
+
13
+ The deterministic session remains a shared public projection: every client
14
+ applies the same confirmed inputs and authority commands. Optional secret
15
+ systems add a separate authority-owned projection for simultaneous choices,
16
+ cryptographic random draws, decks, bags, and roles. Private values go only to
17
+ their owners; signed public consequences join exactly one confirmed frame and
18
+ therefore survive rollback, replay, reconnect, and match-log verification.
19
+
20
+ The RUN room authority is trusted and can see secret values. SHA-256
21
+ commitments, hash chains, Merkle proofs, and Ed25519 receipts prevent peers or
22
+ tampered logs from forging outcomes; they do not hide a player's own values
23
+ from that player's browser or make the authority zero-knowledge. Public donated
24
+ snapshots remain secret-free, while server-only secret state restores against a
25
+ cryptographic configuration hash.
26
+
27
+ ## BETA Status
28
+
29
+ The standalone engine, certification CLI, curated public entrypoints, SDK bridge,
30
+ generic input authority, replay verification, and hosted multiplayer path are in
31
+ active development. Syncplay has not gone live. Package publication, production
32
+ rollout, and 1.0 graduation are separate release decisions; this build remains
33
+ explicitly BETA.
23
34
 
24
35
  ## State Ownership Contract
25
36
 
@@ -557,6 +568,16 @@ remediation text. Valid suppressions must use
557
568
  `// rdm-ignore-next-line <rule-id>: <reason>` and are reported with file, line,
558
569
  rule, and reason; invalid suppressions are errors.
559
570
 
571
+ `static:determinism` with explicit paths (`--entry`, `--paths`) follows the
572
+ import graph transitively by default (`--no-follow-imports` opts out); the
573
+ JSON artifact records `requestedPaths` (CLI inputs), `checkedPaths` (files
574
+ actually checked, post-traversal), `followImports`, and `unresolvedImports`
575
+ (relative imports that resolve to no file — unchecked holes). Bare package
576
+ specifiers are never followed; if your simulation uses tsconfig path aliases,
577
+ pass `--project tsconfig.json` so aliased imports resolve and get checked —
578
+ without it they are skipped like package imports. Package self-certification
579
+ (no explicit paths) checks its curated source list without following imports.
580
+
560
581
  The checker covers host random/time, timers, async simulation work, I/O,
561
582
  unordered object iteration, random sort comparators, render-object mutation,
562
583
  uncertified physics stepping, and descriptor-declared state-field access.
@@ -1,5 +1,8 @@
1
1
  import { type ConfirmedInputFrame, type EncodedInput } from './input-authority.js';
2
2
  import type { SyncplayMatchLog } from './match-log.js';
3
+ import { type SyncplaySecretAuthoritySnapshot } from './secret-authority.js';
4
+ import type { SyncplaySecretAuthorityCrypto } from './secret-crypto.js';
5
+ import type { SyncplaySecretSystemsConfig } from './secret-config.js';
3
6
  /**
4
7
  * Transport-agnostic, game-agnostic deterministic authority room (§6 M2).
5
8
  *
@@ -89,6 +92,10 @@ export interface DeterministicAuthorityRoomConfig {
89
92
  * MUST call `resyncSeatedPlayers()` so surviving clients re-seat.
90
93
  */
91
94
  readonly restoreFrom?: DeterministicAuthorityRoomRestoreState;
95
+ readonly authorityId?: string;
96
+ readonly secrets?: SyncplaySecretSystemsConfig;
97
+ readonly secretCrypto?: SyncplaySecretAuthorityCrypto;
98
+ readonly secretMsgType?: string;
92
99
  }
93
100
  export interface DeterministicAuthorityRoomRestoreState {
94
101
  readonly now: number;
@@ -98,6 +105,7 @@ export interface DeterministicAuthorityRoomRestoreState {
98
105
  readonly tail: readonly ConfirmedInputFrame[];
99
106
  readonly snapshotBytes?: string;
100
107
  readonly schemaHash?: string;
108
+ readonly secretState?: SyncplaySecretAuthoritySnapshot;
101
109
  }
102
110
  export interface AuthorityRoomCatchUpEvent {
103
111
  readonly playerId: string;
@@ -178,6 +186,7 @@ export interface DeterministicAuthorityRoom {
178
186
  /** Seat a read-only spectator (G11): role 'spectator', no slot, full catch-up + live follow. */
179
187
  onSpectatorJoin(playerId: string): boolean;
180
188
  onPlayerMessage(playerId: string, envelope: string): void;
189
+ onPlayerSecretMessage(playerId: string, envelope: string): void;
181
190
  onPlayerLeave(playerId: string): void;
182
191
  /**
183
192
  * A player's transport re-attached inside the host's grace window (socket
@@ -213,5 +222,7 @@ export interface DeterministicAuthorityRoom {
213
222
  * `preserveFullHistory`, or the match outlived `historyRetentionTicks`).
214
223
  */
215
224
  matchLog(): SyncplayMatchLog | undefined;
225
+ /** Server-only secret state. Never include this value in public snapshots or match logs. */
226
+ secretSnapshot(): SyncplaySecretAuthoritySnapshot | undefined;
216
227
  }
217
228
  export declare function createDeterministicAuthorityRoom(transport: AuthorityRoomTransport, config: DeterministicAuthorityRoomConfig): DeterministicAuthorityRoom;
@@ -4,7 +4,9 @@ import { createInputAuthority } from './input-authority.js';
4
4
  import { createDeterministicLocalAuthoritySnapshotChunkEnvelope, createDeterministicLocalAuthoritySnapshotCompleteEnvelope, createDeterministicLocalAuthoritySnapshotOfferEnvelope, createDeterministicLocalAuthoritySnapshotRequestEnvelope, decodeDeterministicLocalAuthorityWireEnvelope, } from './local-authority-wire.js';
5
5
  import { DEFAULT_SNAPSHOT_CHUNK_SIZE, createSnapshotTransferCollector, createSnapshotTransferFromBytes, decodeDeterministicStateSnapshot, deterministicRoomDescriptorHash, } from './session-snapshot.js';
6
6
  import { decodeDeterministicSessionMessage, encodeDeterministicSessionMessage } from './session-wire.js';
7
+ import { createSyncplaySecretAuthority, } from './secret-authority.js';
7
8
  const DEFAULT_SESSION_MSG_TYPE = 'rdm';
9
+ const DEFAULT_SECRET_MSG_TYPE = 'rdm-secret';
8
10
  /** One hour at 60 Hz — see DeterministicAuthorityRoomConfig.historyRetentionTicks. */
9
11
  const DEFAULT_HISTORY_RETENTION_TICKS = 216_000;
10
12
  /** Frames per history-replay unicast. Bounds message size during catch-up. */
@@ -48,6 +50,18 @@ export function createDeterministicAuthorityRoom(transport, config) {
48
50
  // colliding with a still-present player and orphaning the vacated slot.
49
51
  let assignedSlots = 0;
50
52
  let now = -1;
53
+ const secretAuthority = config.secrets === undefined
54
+ ? undefined
55
+ : createSyncplaySecretAuthority({
56
+ authorityId: requireSecretAuthorityId(config.authorityId),
57
+ playerCount: config.playerCount,
58
+ config: config.secrets,
59
+ crypto: requireSecretCrypto(config.secretCrypto),
60
+ sendPrivate(playerId, encoded) {
61
+ transport.sendTo(playerId, config.secretMsgType ?? DEFAULT_SECRET_MSG_TYPE, encoded);
62
+ },
63
+ ...(config.restoreFrom?.secretState === undefined ? {} : { restoreFrom: config.restoreFrom.secretState }),
64
+ });
51
65
  // M5.5 snapshot acquisition state. The authority stores at most one validated
52
66
  // snapshot and runs at most one donor transfer at a time.
53
67
  const snapshotCadenceTicks = config.snapshotCadenceTicks ?? DEFAULT_SNAPSHOT_CADENCE_TICKS;
@@ -227,6 +241,7 @@ export function createDeterministicAuthorityRoom(transport, config) {
227
241
  const plan = catchUpPlan();
228
242
  invariant(plan !== 'unavailable', 'a restored room must have a catch-up plan for seated players', 'authority-room.restore-catchup');
229
243
  sendSessionStart(playerId, slot);
244
+ secretAuthority?.onPlayerReconnected(playerId, slot);
230
245
  sendCatchUpTo(playerId, plan, 'resync');
231
246
  }
232
247
  }
@@ -246,6 +261,7 @@ export function createDeterministicAuthorityRoom(transport, config) {
246
261
  }
247
262
  slotByPlayer.set(playerId, formerSlot);
248
263
  sendSessionStart(playerId, formerSlot);
264
+ secretAuthority?.onPlayerReconnected(playerId, formerSlot);
249
265
  sendCatchUpTo(playerId, rejoinPlan, 'rejoin');
250
266
  return { accepted: true, slot: formerSlot, rejoined: true };
251
267
  }
@@ -264,6 +280,7 @@ export function createDeterministicAuthorityRoom(transport, config) {
264
280
  slotByPlayer.set(playerId, slot);
265
281
  everSlotByPlayer.set(playerId, slot);
266
282
  sendSessionStart(playerId, slot);
283
+ secretAuthority?.onPlayerJoined(playerId, slot);
267
284
  sendCatchUpTo(playerId, plan, 'join');
268
285
  return { accepted: true, slot };
269
286
  }
@@ -283,6 +300,7 @@ export function createDeterministicAuthorityRoom(transport, config) {
283
300
  return false;
284
301
  spectators.add(playerId);
285
302
  sendSessionStart(playerId, -1, 'spectator');
303
+ secretAuthority?.onSpectatorJoined(playerId);
286
304
  sendCatchUpTo(playerId, plan, 'join');
287
305
  return true;
288
306
  }
@@ -309,8 +327,14 @@ export function createDeterministicAuthorityRoom(transport, config) {
309
327
  // client that fell behind the snapshot tick, so the stored snapshot rides
310
328
  // along — the client applies it only when it is ahead of its own state.
311
329
  const reconnectPlan = !historyReachesTickZero() && storedSnapshot !== undefined ? 'snapshot' : 'history';
330
+ secretAuthority?.onPlayerReconnected(playerId, slot);
312
331
  sendCatchUpTo(playerId, reconnectPlan, 'reconnect');
313
332
  }
333
+ function onPlayerSecretMessage(playerId, envelope) {
334
+ if (secretAuthority === undefined)
335
+ return;
336
+ secretAuthority.onPlayerRequest(playerId, slotByPlayer.get(playerId) ?? -1, envelope);
337
+ }
314
338
  function onPlayerMessage(playerId, envelope) {
315
339
  // Fail closed: unknown/malformed messages are dropped. Snapshot transfer
316
340
  // envelopes are the OTHER frozen wire family (schemaVersion vs v field) —
@@ -571,7 +595,10 @@ export function createDeterministicAuthorityRoom(transport, config) {
571
595
  // Fixed roster for v1 (late-join/reconnect is M5). Dropping the mapping stops accepting the
572
596
  // departed player's input; their slot stays reserved because `assignedSlots` never decreases,
573
597
  // so the authority substitutes for it rather than handing the number to a new joiner.
598
+ const slot = slotByPlayer.get(playerId);
574
599
  slotByPlayer.delete(playerId);
600
+ if (slot !== undefined)
601
+ secretAuthority?.onPlayerLeft(playerId, slot);
575
602
  if (pendingTransfer?.donorId === playerId) {
576
603
  failPendingTransfer('snapshot-donor-left');
577
604
  }
@@ -586,6 +613,9 @@ export function createDeterministicAuthorityRoom(transport, config) {
586
613
  return;
587
614
  }
588
615
  now += 1;
616
+ for (const command of secretAuthority?.onTick(now, [...slotByPlayer.values()]) ?? []) {
617
+ authority.queueCommand(now, command);
618
+ }
589
619
  authority.advance(now);
590
620
  maintainSnapshotAcquisition();
591
621
  if (authority.confirmedThrough < 0) {
@@ -636,6 +666,7 @@ export function createDeterministicAuthorityRoom(transport, config) {
636
666
  onPlayerJoin,
637
667
  onSpectatorJoin,
638
668
  onPlayerMessage,
669
+ onPlayerSecretMessage,
639
670
  onPlayerLeave,
640
671
  onPlayerReconnected,
641
672
  onTick,
@@ -672,6 +703,7 @@ export function createDeterministicAuthorityRoom(transport, config) {
672
703
  if (plan === 'unavailable')
673
704
  return false;
674
705
  sendSessionStart(playerId, slot);
706
+ secretAuthority?.onPlayerReconnected(playerId, slot);
675
707
  sendCatchUpTo(playerId, plan, 'resync');
676
708
  return true;
677
709
  },
@@ -697,5 +729,16 @@ export function createDeterministicAuthorityRoom(transport, config) {
697
729
  frames: authority.confirmedFrameRange(0, authority.confirmedThrough),
698
730
  };
699
731
  },
732
+ secretSnapshot() {
733
+ return secretAuthority?.snapshot();
734
+ },
700
735
  };
701
736
  }
737
+ function requireSecretAuthorityId(authorityId) {
738
+ invariant(typeof authorityId === 'string' && authorityId.length > 0, 'secret authorityId is required', 'authority-room.secret-authority-id');
739
+ return authorityId;
740
+ }
741
+ function requireSecretCrypto(crypto) {
742
+ invariant(crypto !== undefined, 'secret signing is unavailable', 'authority-room.secret-signing');
743
+ return crypto;
744
+ }
package/dist/browser.d.ts CHANGED
@@ -90,3 +90,10 @@ export type { DeterministicSignal, DeterministicSignalBus, DeterministicSignalBu
90
90
  export type { DeterministicSparseSet, DeterministicSparseSetSnapshot } from './sparse-set.js';
91
91
  export type { DeterministicLifecycleCounterSummary, DeterministicLifecycleRunner, DeterministicLifecycleSignal, DeterministicLifecycleSnapshot, DeterministicLifecycleSystemContext, DeterministicLifecycleSystemDescriptor, DeterministicLifecycleSystemHooks } from './system-lifecycle.js';
92
92
  export type { DeterministicStep, ReplayFile, ReplayVerificationResult } from './types.js';
93
+ export { syncplaySecretConfigHash, syncplaySecretLimits, validateSyncplaySecretSystemsConfig } from './secret-config.js';
94
+ export { decodeSyncplaySecretPrivateMessage, isSyncplaySecretCommand, syncplaySecretProtocol } from './secret-protocol.js';
95
+ export { createBrowserSyncplaySecretCrypto, verifySyncplaySignedReceipt } from './secret-crypto.js';
96
+ export { createSyncplaySecretEventReader, createSyncplaySecretsClient } from './secret-client.js';
97
+ export type { SyncplaySecretSystemDefinition, SyncplaySecretSystemsConfig } from './secret-config.js';
98
+ export type { SyncplayOwnedSecretToken, SyncplaySecretCommand, SyncplaySecretErrorCode, SyncplaySecretPrivateMessage, SyncplaySignedReceipt } from './secret-protocol.js';
99
+ export type { SyncplayChoiceClient, SyncplayChoiceResolvedEvent, SyncplayCollectionClient, SyncplayCollectionRevealedEvent, SyncplayOwnedSecret, SyncplayRandomClient, SyncplayRandomDrawnEvent, SyncplayRoleClient, SyncplayRoleRevealedEvent, SyncplaySecretEventReader, SyncplaySecretsClient } from './secret-client.js';
package/dist/browser.js CHANGED
@@ -47,3 +47,7 @@ export { decodeDeterministicSessionMessage, deterministicSessionWireVersion, enc
47
47
  export { createDeterministicSignalBus } from './signals.js';
48
48
  export { createDeterministicSparseSet } from './sparse-set.js';
49
49
  export { createDeterministicLifecycleRunner, summarizeDeterministicLifecycleCounters } from './system-lifecycle.js';
50
+ export { syncplaySecretConfigHash, syncplaySecretLimits, validateSyncplaySecretSystemsConfig } from './secret-config.js';
51
+ export { decodeSyncplaySecretPrivateMessage, isSyncplaySecretCommand, syncplaySecretProtocol } from './secret-protocol.js';
52
+ export { createBrowserSyncplaySecretCrypto, verifySyncplaySignedReceipt } from './secret-crypto.js';
53
+ export { createSyncplaySecretEventReader, createSyncplaySecretsClient } from './secret-client.js';
@@ -7,7 +7,9 @@ const input_authority_js_1 = require("./input-authority.js");
7
7
  const local_authority_wire_js_1 = require("./local-authority-wire.js");
8
8
  const session_snapshot_js_1 = require("./session-snapshot.js");
9
9
  const session_wire_js_1 = require("./session-wire.js");
10
+ const secret_authority_js_1 = require("./secret-authority.js");
10
11
  const DEFAULT_SESSION_MSG_TYPE = 'rdm';
12
+ const DEFAULT_SECRET_MSG_TYPE = 'rdm-secret';
11
13
  /** One hour at 60 Hz — see DeterministicAuthorityRoomConfig.historyRetentionTicks. */
12
14
  const DEFAULT_HISTORY_RETENTION_TICKS = 216_000;
13
15
  /** Frames per history-replay unicast. Bounds message size during catch-up. */
@@ -51,6 +53,18 @@ function createDeterministicAuthorityRoom(transport, config) {
51
53
  // colliding with a still-present player and orphaning the vacated slot.
52
54
  let assignedSlots = 0;
53
55
  let now = -1;
56
+ const secretAuthority = config.secrets === undefined
57
+ ? undefined
58
+ : (0, secret_authority_js_1.createSyncplaySecretAuthority)({
59
+ authorityId: requireSecretAuthorityId(config.authorityId),
60
+ playerCount: config.playerCount,
61
+ config: config.secrets,
62
+ crypto: requireSecretCrypto(config.secretCrypto),
63
+ sendPrivate(playerId, encoded) {
64
+ transport.sendTo(playerId, config.secretMsgType ?? DEFAULT_SECRET_MSG_TYPE, encoded);
65
+ },
66
+ ...(config.restoreFrom?.secretState === undefined ? {} : { restoreFrom: config.restoreFrom.secretState }),
67
+ });
54
68
  // M5.5 snapshot acquisition state. The authority stores at most one validated
55
69
  // snapshot and runs at most one donor transfer at a time.
56
70
  const snapshotCadenceTicks = config.snapshotCadenceTicks ?? DEFAULT_SNAPSHOT_CADENCE_TICKS;
@@ -230,6 +244,7 @@ function createDeterministicAuthorityRoom(transport, config) {
230
244
  const plan = catchUpPlan();
231
245
  (0, errors_js_1.invariant)(plan !== 'unavailable', 'a restored room must have a catch-up plan for seated players', 'authority-room.restore-catchup');
232
246
  sendSessionStart(playerId, slot);
247
+ secretAuthority?.onPlayerReconnected(playerId, slot);
233
248
  sendCatchUpTo(playerId, plan, 'resync');
234
249
  }
235
250
  }
@@ -249,6 +264,7 @@ function createDeterministicAuthorityRoom(transport, config) {
249
264
  }
250
265
  slotByPlayer.set(playerId, formerSlot);
251
266
  sendSessionStart(playerId, formerSlot);
267
+ secretAuthority?.onPlayerReconnected(playerId, formerSlot);
252
268
  sendCatchUpTo(playerId, rejoinPlan, 'rejoin');
253
269
  return { accepted: true, slot: formerSlot, rejoined: true };
254
270
  }
@@ -267,6 +283,7 @@ function createDeterministicAuthorityRoom(transport, config) {
267
283
  slotByPlayer.set(playerId, slot);
268
284
  everSlotByPlayer.set(playerId, slot);
269
285
  sendSessionStart(playerId, slot);
286
+ secretAuthority?.onPlayerJoined(playerId, slot);
270
287
  sendCatchUpTo(playerId, plan, 'join');
271
288
  return { accepted: true, slot };
272
289
  }
@@ -286,6 +303,7 @@ function createDeterministicAuthorityRoom(transport, config) {
286
303
  return false;
287
304
  spectators.add(playerId);
288
305
  sendSessionStart(playerId, -1, 'spectator');
306
+ secretAuthority?.onSpectatorJoined(playerId);
289
307
  sendCatchUpTo(playerId, plan, 'join');
290
308
  return true;
291
309
  }
@@ -312,8 +330,14 @@ function createDeterministicAuthorityRoom(transport, config) {
312
330
  // client that fell behind the snapshot tick, so the stored snapshot rides
313
331
  // along — the client applies it only when it is ahead of its own state.
314
332
  const reconnectPlan = !historyReachesTickZero() && storedSnapshot !== undefined ? 'snapshot' : 'history';
333
+ secretAuthority?.onPlayerReconnected(playerId, slot);
315
334
  sendCatchUpTo(playerId, reconnectPlan, 'reconnect');
316
335
  }
336
+ function onPlayerSecretMessage(playerId, envelope) {
337
+ if (secretAuthority === undefined)
338
+ return;
339
+ secretAuthority.onPlayerRequest(playerId, slotByPlayer.get(playerId) ?? -1, envelope);
340
+ }
317
341
  function onPlayerMessage(playerId, envelope) {
318
342
  // Fail closed: unknown/malformed messages are dropped. Snapshot transfer
319
343
  // envelopes are the OTHER frozen wire family (schemaVersion vs v field) —
@@ -574,7 +598,10 @@ function createDeterministicAuthorityRoom(transport, config) {
574
598
  // Fixed roster for v1 (late-join/reconnect is M5). Dropping the mapping stops accepting the
575
599
  // departed player's input; their slot stays reserved because `assignedSlots` never decreases,
576
600
  // so the authority substitutes for it rather than handing the number to a new joiner.
601
+ const slot = slotByPlayer.get(playerId);
577
602
  slotByPlayer.delete(playerId);
603
+ if (slot !== undefined)
604
+ secretAuthority?.onPlayerLeft(playerId, slot);
578
605
  if (pendingTransfer?.donorId === playerId) {
579
606
  failPendingTransfer('snapshot-donor-left');
580
607
  }
@@ -589,6 +616,9 @@ function createDeterministicAuthorityRoom(transport, config) {
589
616
  return;
590
617
  }
591
618
  now += 1;
619
+ for (const command of secretAuthority?.onTick(now, [...slotByPlayer.values()]) ?? []) {
620
+ authority.queueCommand(now, command);
621
+ }
592
622
  authority.advance(now);
593
623
  maintainSnapshotAcquisition();
594
624
  if (authority.confirmedThrough < 0) {
@@ -639,6 +669,7 @@ function createDeterministicAuthorityRoom(transport, config) {
639
669
  onPlayerJoin,
640
670
  onSpectatorJoin,
641
671
  onPlayerMessage,
672
+ onPlayerSecretMessage,
642
673
  onPlayerLeave,
643
674
  onPlayerReconnected,
644
675
  onTick,
@@ -675,6 +706,7 @@ function createDeterministicAuthorityRoom(transport, config) {
675
706
  if (plan === 'unavailable')
676
707
  return false;
677
708
  sendSessionStart(playerId, slot);
709
+ secretAuthority?.onPlayerReconnected(playerId, slot);
678
710
  sendCatchUpTo(playerId, plan, 'resync');
679
711
  return true;
680
712
  },
@@ -700,5 +732,16 @@ function createDeterministicAuthorityRoom(transport, config) {
700
732
  frames: authority.confirmedFrameRange(0, authority.confirmedThrough),
701
733
  };
702
734
  },
735
+ secretSnapshot() {
736
+ return secretAuthority?.snapshot();
737
+ },
703
738
  };
704
739
  }
740
+ function requireSecretAuthorityId(authorityId) {
741
+ (0, errors_js_1.invariant)(typeof authorityId === 'string' && authorityId.length > 0, 'secret authorityId is required', 'authority-room.secret-authority-id');
742
+ return authorityId;
743
+ }
744
+ function requireSecretCrypto(crypto) {
745
+ (0, errors_js_1.invariant)(crypto !== undefined, 'secret signing is unavailable', 'authority-room.secret-signing');
746
+ return crypto;
747
+ }
@@ -5,7 +5,7 @@ exports.sortAabbsByStableId = exports.segmentsIntersect = exports.segmentSegment
5
5
  exports.createDeterministicLocalAuthoritySnapshotOfferEnvelope = exports.createDeterministicLocalAuthoritySnapshotEnvelope = exports.createDeterministicLocalAuthoritySnapshotCompleteEnvelope = exports.createDeterministicLocalAuthoritySnapshotChunkEnvelope = exports.createDeterministicLocalAuthorityInputBundleEnvelope = exports.createDeterministicLocalAuthorityCorrectionEnvelope = exports.createDeterministicLocalAuthorityCommandBundleEnvelope = exports.createDeterministicLocalAuthorityAckEnvelope = exports.applyDeterministicLocalAuthorityInputBundleEnvelope = exports.applyDeterministicLocalAuthorityCommandBundleEnvelope = exports.createDeterministicLocalAuthorityRuntime = exports.validateDeterministicLagCompensatedShot3D = exports.validateDeterministicAuthoritativeLagCompensatedShot3D = exports.rewindDeterministicLagCompensatedTargets3D = exports.createDeterministicLagCompensation3DHistory = exports.buildDeterministicLagCompensatedHitboxProxies3D = exports.runInstantReplay = exports.createInstantReplayRunner = exports.sortDeterministicInputTimelineFlags = exports.evaluateDeterministicInputTimelineResult = exports.accumulateDeterministicInputTimelineState = exports.createInputAuthority = exports.filterDeterministicEventsForPlayer = exports.createDeterministicEventTimeline = exports.validateDeterministicFrameSystemsStateless = exports.runDeterministicFrameRuntimeMutationCorpus = exports.isDeterministicFrameHeapStaleHandleError = exports.generateDeterministicFrameRuntimeSource = exports.createDeterministicSparseSetFrameRuntime = exports.createDeterministicSlotMap = exports.createDeterministicFrameRuntimeAccessors = exports.createDeterministicFrameRuntime = exports.createDeterministicBitset = exports.compileDeterministicDslRuntimeDescriptor = exports.assertDeterministicFrameSystemsStateless = exports.assertDeterministicFrameHeapStaleHandleError = exports.createDeterministicEventEffectsAdapter = exports.verifyDeterministicSessionRuntimeReplay = exports.createDeterministicSessionReplayBundle = exports.createDeterministicSessionCore = exports.verifyDeterministicConfigBundle = exports.summarizeDeterministicConfigBundleFailureReasons = exports.evaluateDeterministicConfigBundleParityResult = exports.cookDeterministicConfigBundle = exports.evaluateDeterministicCommandTimelineMigrationResult = exports.deterministicPayloadMatchesFields = exports.createDeterministicCommandTimelineSnapshot = exports.createDeterministicCommandTimeline = exports.assertDeterministicPayloadFields = exports.translateAabb = void 0;
6
6
  exports.stepDeterministicPhysicsWorld2D = exports.raycastDeterministicPhysics2D = exports.raycastAllDeterministicPhysics2D = exports.queryDeterministicPhysicsShapeOverlap2D = exports.queryDeterministicPhysicsAabb2D = exports.nearestDeterministicPhysicsHit2D = exports.createDeterministicPhysicsWorld2D = exports.circleCastDeterministicPhysics2D = exports.createPauseController = exports.createOfflineSession = exports.valueNoise2D = exports.valueNoise1D = exports.hashUint32 = exports.hash2 = exports.fbm2D = exports.createNetworkedSyncplayClient = exports.stepDeterministicNavmeshOffMeshTraversal = exports.stepDeterministicNavmeshAgents = exports.smoothDeterministicNavmeshPath = exports.runDeterministicNavmeshStress = exports.locateDeterministicNavmeshPolygon = exports.findDeterministicNavmeshPointPath = exports.findDeterministicNavmeshPath = exports.cookImportedDeterministicNavmesh = exports.cookDeterministicNavmesh = exports.stepDeterministicNavigationAgents = exports.smoothDeterministicPath = exports.findDeterministicPath = exports.createDeterministicFlowField = exports.cookTilemapNavigationGraph = exports.cookDeterministicNavigationGraph = exports.applyDeterministicReciprocalAvoidance = exports.createSyncplayClient = exports.stepDeterministicKcc3D = exports.stepDeterministicCarry3D = exports.validateDeterministicLagCompensatedHit = exports.stepDeterministicTopDownMover = exports.stepDeterministicKcc2D = exports.stepDeterministicFpsAim = exports.runDeterministicMovementSimulation = exports.runDeterministicMovementCrowdSimulation = exports.rewindDeterministicTargets = exports.createDeterministicMath = exports.encodeDeterministicLocalAuthorityWireEnvelope = exports.deterministicLocalAuthorityWireProtocol = exports.decodeDeterministicLocalAuthorityWireEnvelope = exports.createDeterministicLocalAuthorityWireReceiver = exports.createDeterministicLocalAuthorityWireAuth = exports.createDeterministicLocalAuthoritySnapshotTransfer = exports.createDeterministicLocalAuthoritySnapshotRequestEnvelope = void 0;
7
7
  exports.bytesToBase64 = exports.base64ToBytes = exports.exportPromotionSnapshot = exports.createSessionFromPromotionSnapshot = exports.createSyncplaySession = exports.createSyncplayOfflineRuntime = exports.createSyncplayGameRuntime = exports.stableDeterministicDslSchemaJson = exports.parseDeterministicSchemaDsl = exports.deterministicDslMaxPlayers = exports.createDeterministicBinarySerializer = exports.proveSchemaBinaryRoundTrip = exports.generateSchemaTypes = exports.generateSchemaBinarySource = exports.createDeterministicDescriptorBinarySchema = exports.createSchemaArtifacts = exports.stableDescriptorId = exports.defineSyncplayGame = exports.verifyReplayScoreAgainstTrustedRuntime = exports.verifyDeterministicSelfContainedFrameRuntimeReplayBundle = exports.verifyDeterministicReplayBundle = exports.verifyDeterministicFrameRuntimeReplayBundle = exports.sliceDeterministicReplayBundle = exports.minimizeDeterministicReplayDesyncBundle = exports.minimizeDeterministicReplayDesync = exports.createDeterministicReplayRuntimeManifest = exports.createDeterministicReplayDebugTables = exports.createDeterministicReplayBundle = exports.verifyReplay = exports.verifyDeterministicReplay = exports.interpolateDeterministicRenderVec2 = exports.interpolateDeterministicRenderNumber = exports.createDeterministicR3fRenderAdapter = exports.createDeterministicR3fAdapter = exports.stepDeterministicVehicle3D = exports.stepDeterministicPhysicsWorld3D = exports.shapeCastDeterministicPhysics3DDetailed = exports.shapeCastDeterministicPhysics3D = exports.setDeterministicBodyVelocity3D = exports.runDeterministicPhysics3DStress = exports.raycastDeterministicPhysics3DDetailed = exports.raycastDeterministicPhysics3D = exports.overlapDeterministicPhysics3D = exports.deterministicPhysicsCallback3D = exports.deterministicPhysicsBodyById3D = exports.deriveInverseInertia = exports.createDeterministicPhysicsWorld3D = exports.contactDetailsDeterministicPhysics3D = exports.applyDeterministicImpulses3D = exports.applyDeterministicImpulse3D = void 0;
8
- exports.summarizeDeterministicLifecycleCounters = exports.createDeterministicLifecycleRunner = exports.createDeterministicSparseSet = exports.createDeterministicSignalBus = exports.encodeDeterministicSessionMessage = exports.deterministicSessionWireVersion = exports.decodeDeterministicSessionMessage = exports.encodeDeterministicStateSnapshot = exports.deterministicRoomDescriptorHash = exports.DEFAULT_SNAPSHOT_CHUNK_SIZE = exports.DEFAULT_MAX_SNAPSHOT_BYTES = exports.decodeDeterministicStateSnapshot = exports.createSnapshotTransferFromBytes = exports.createSnapshotTransferCollector = void 0;
8
+ exports.createSyncplaySecretsClient = exports.createSyncplaySecretEventReader = exports.verifySyncplaySignedReceipt = exports.createBrowserSyncplaySecretCrypto = exports.syncplaySecretProtocol = exports.isSyncplaySecretCommand = exports.decodeSyncplaySecretPrivateMessage = exports.validateSyncplaySecretSystemsConfig = exports.syncplaySecretLimits = exports.syncplaySecretConfigHash = exports.summarizeDeterministicLifecycleCounters = exports.createDeterministicLifecycleRunner = exports.createDeterministicSparseSet = exports.createDeterministicSignalBus = exports.encodeDeterministicSessionMessage = exports.deterministicSessionWireVersion = exports.decodeDeterministicSessionMessage = exports.encodeDeterministicStateSnapshot = exports.deterministicRoomDescriptorHash = exports.DEFAULT_SNAPSHOT_CHUNK_SIZE = exports.DEFAULT_MAX_SNAPSHOT_BYTES = exports.decodeDeterministicStateSnapshot = exports.createSnapshotTransferFromBytes = exports.createSnapshotTransferCollector = void 0;
9
9
  var animation_js_1 = require("./animation.js");
10
10
  Object.defineProperty(exports, "cookDeterministicAnimation", { enumerable: true, get: function () { return animation_js_1.cookDeterministicAnimation; } });
11
11
  Object.defineProperty(exports, "createDeterministicAnimationRuntimeState", { enumerable: true, get: function () { return animation_js_1.createDeterministicAnimationRuntimeState; } });
@@ -268,3 +268,17 @@ Object.defineProperty(exports, "createDeterministicSparseSet", { enumerable: tru
268
268
  var system_lifecycle_js_1 = require("./system-lifecycle.js");
269
269
  Object.defineProperty(exports, "createDeterministicLifecycleRunner", { enumerable: true, get: function () { return system_lifecycle_js_1.createDeterministicLifecycleRunner; } });
270
270
  Object.defineProperty(exports, "summarizeDeterministicLifecycleCounters", { enumerable: true, get: function () { return system_lifecycle_js_1.summarizeDeterministicLifecycleCounters; } });
271
+ var secret_config_js_1 = require("./secret-config.js");
272
+ Object.defineProperty(exports, "syncplaySecretConfigHash", { enumerable: true, get: function () { return secret_config_js_1.syncplaySecretConfigHash; } });
273
+ Object.defineProperty(exports, "syncplaySecretLimits", { enumerable: true, get: function () { return secret_config_js_1.syncplaySecretLimits; } });
274
+ Object.defineProperty(exports, "validateSyncplaySecretSystemsConfig", { enumerable: true, get: function () { return secret_config_js_1.validateSyncplaySecretSystemsConfig; } });
275
+ var secret_protocol_js_1 = require("./secret-protocol.js");
276
+ Object.defineProperty(exports, "decodeSyncplaySecretPrivateMessage", { enumerable: true, get: function () { return secret_protocol_js_1.decodeSyncplaySecretPrivateMessage; } });
277
+ Object.defineProperty(exports, "isSyncplaySecretCommand", { enumerable: true, get: function () { return secret_protocol_js_1.isSyncplaySecretCommand; } });
278
+ Object.defineProperty(exports, "syncplaySecretProtocol", { enumerable: true, get: function () { return secret_protocol_js_1.syncplaySecretProtocol; } });
279
+ var secret_crypto_js_1 = require("./secret-crypto.js");
280
+ Object.defineProperty(exports, "createBrowserSyncplaySecretCrypto", { enumerable: true, get: function () { return secret_crypto_js_1.createBrowserSyncplaySecretCrypto; } });
281
+ Object.defineProperty(exports, "verifySyncplaySignedReceipt", { enumerable: true, get: function () { return secret_crypto_js_1.verifySyncplaySignedReceipt; } });
282
+ var secret_client_js_1 = require("./secret-client.js");
283
+ Object.defineProperty(exports, "createSyncplaySecretEventReader", { enumerable: true, get: function () { return secret_client_js_1.createSyncplaySecretEventReader; } });
284
+ Object.defineProperty(exports, "createSyncplaySecretsClient", { enumerable: true, get: function () { return secret_client_js_1.createSyncplaySecretsClient; } });
@@ -1276,10 +1276,7 @@ function physicsCategory(bodies, requestedFrames, executedFrames, enforceThresho
1276
1276
  && stack.settledDynamicBodies === stack.stackLayers,
1277
1277
  stackRollbackEquivalent: stack.rollbackEquivalent,
1278
1278
  stackAuthoringOrderStable: stack.stableUnderAuthoringOrderPermutation,
1279
- // Rigid-solver material response (impulse-based) — combine semantics unchanged
1280
- // (floor-average friction, max restitution); the bounce/friction velocity
1281
- // magnitudes come from the sequential-impulse solver, not the legacy pusher.
1282
- materialResponseCovered: materials.combinedFriction === 3
1279
+ materialResponseCovered: Math.abs(materials.combinedFriction - 0.28284271) < 1e-6
1283
1280
  && materials.combinedRestitution === 7
1284
1281
  && materials.normalVelocityAfterBounce > 0
1285
1282
  && materials.tangentVelocityAfterFrictionY < 10
@@ -625,11 +625,18 @@ const engineParityMatrixEntries = [
625
625
  'packages/syncplay/src/physics3d-solver.ts',
626
626
  'packages/syncplay/src/physics3d-toi.ts',
627
627
  'packages/syncplay/src/physics3d-joints.ts',
628
+ 'packages/syncplay/src/physics3d-mass.ts',
629
+ 'packages/syncplay/src/physics3d-heightfield.ts',
628
630
  'packages/syncplay/tests/physics2d.test.ts',
629
631
  'packages/syncplay/tests/physics3d-solver.test.ts',
630
632
  'packages/syncplay/tests/physics3d-joints.test.ts',
631
633
  'packages/syncplay/tests/physics3d-ccd.test.ts',
632
634
  'packages/syncplay/tests/physics3d-toi.test.ts',
635
+ 'packages/syncplay/tests/physics3d-mass.test.ts',
636
+ 'packages/syncplay/tests/physics3d-materials.test.ts',
637
+ 'packages/syncplay/tests/physics3d-heightfield.test.ts',
638
+ 'packages/syncplay/tests/physics3d-rotation-convention.test.ts',
639
+ 'packages/syncplay/tests/physics3d-mesh-onesided.test.ts',
633
640
  'packages/syncplay/tests/engine-complete.test.ts',
634
641
  'packages/syncplay/src/physics3d-vehicle.ts',
635
642
  'packages/syncplay/tests/physics3d-vehicle.test.ts',
@@ -640,16 +647,17 @@ const engineParityMatrixEntries = [
640
647
  'packages/syncplay/src/physics3d-hull.ts',
641
648
  'packages/syncplay/tests/physics3d-hull.test.ts',
642
649
  'TGS-Soft (sub-stepped soft-constraint) rigid solver: box3d-class stable stacking, tumbling, sleeping, axis locks, joints (ball/hinge/weld/rope/prismatic/spring/grab), opt-in TOI-bound CCD, compound colliders, orientation-aware queries',
643
- 'extreme-speed CCD (box3d model, src/physics3d-toi.ts): an opt-in ccd body whose per-tick motion exceeds half its smallest extent has its exact first time-of-impact found by GJK conservative advancement; it is advanced there and the impact resolved as a perfectly-inelastic collision (momentum transfers to the struck body — a fast bullet punches through and knocks targets loose, no non-physical stop-dead) with its position TOI-clamped so it never tunnels. A bullet at 60 units/tick into a resting stack or a frame+door cluster stays finite and bounded (fixes the #2821 relax divergence via witness-point contact anchors + a small speculative margin + shared multi-body seam impulse). Linear-sweep + stationary-target scope; glancing bodies slide (not frozen); non-ccd bodies still tunnel (opt-in). physics3d-ccd.test.ts covers momentum-transfer / no-freeze / finite-at-vx60 / rollback + permutation equivalence',
650
+ 'extreme-speed CCD (box3d model, src/physics3d-toi.ts): an opt-in ccd body whose per-tick motion exceeds half its smallest extent has its exact first time-of-impact found by GJK conservative advancement against convex shapes and culled heightfield cell triangles; it is advanced there and the impact resolved as a perfectly-inelastic collision with momentum transfer and a TOI position clamp. physics3d-ccd.test.ts and physics3d-heightfield.test.ts cover finite-at-vx60, heightfield-cliff no-tunnel, rollback, and permutation equivalence',
644
651
  'stacking stability gated in CI: tower-15 (<1° tilt), 50:1 mass-ratio column, pyramid, kick-recovery, and a 20-layer stack all meet drift/penetration/up-alignment thresholds with rollback + authoring-order-permutation equivalence',
645
652
  'convex-hull collision shape (4–16 verts): deterministic supporting-plane preprocessing, SAT+clip hull-vs-hull/box, closest-point hull-vs-sphere/capsule, support-sample hull-vs-static-mesh, exact ray/overlap/shape-cast queries; golden hull-pair battery pinned',
646
653
  'cone-twist ball joint (swing cone + twist limits via fixed-point cosine) drives a 6-body ragdoll that settles kinetically and stays articulated; rigid-wheel vehicle recipe (sphere wheels + hinge motors) certified alongside the raycast controller',
647
654
  'builtin-rigid physics-cert lane passes all 4 determinism gates; contact/joint caches are checksummed rollback state',
655
+ 'builtin-rigid@2 certifies true hull/compound inertia and center-of-mass integration, geometric-mean friction, rolling resistance, static heightfields with CCD/query parity, and active spherical motor/orientation-spring rows',
648
656
  'raycast-wheel vehicle controller (stepDeterministicVehicle3D): spring-damper suspension, friction-circle drive/steer/brake, fixed-point steering trig, moving-ground reaction (slip relative to the contact body; equal-and-opposite reaction on dynamic ground)',
649
657
  'per-world static AABB-tree query index: raycast/overlap/shape-cast are O(log n + hits) with bit-identical results; batched applyDeterministicImpulses3D applies a frame of controller impulses in one canonical rebuild',
650
658
  'pooled narrow-phase/constraint scratch (scalarized box face-clip over ping-pong clip slots, step-scoped constraint pool); prismatic/breakable/collision-suppressed joints, a soft frequencyHz/dampingRatio spring joint (breakable/disable-collision), and per-body gravityScale — joint breaks are sticky checksummed cache state',
651
659
  'kinematic bodies integrate their authored velocity (linear + angular) inside the solver, drive the dynamic bodies they touch through the contact relative-velocity term, absorb the equal-and-opposite reaction (zero inverse mass), and wake the sleeping piles they push; a kinematic mesh is therefore a moving platform / conveyor collider. A dynamic mesh is rejected at world creation',
652
- 'one-sided mesh contacts resolve penetration for zero-radius shapes: a support sample projecting onto a triangle face uses its signed plane distance (capped at the sample shape\'s diameter), so a box or hull rests on and is pushed by a mesh instead of falling through it — previously only sphere/capsule (radius-bearing) samples could generate a resolvable mesh contact',
660
+ 'one-sided mesh and heightfield contacts resolve penetration for zero-radius shapes while rejecting genuine back-face approaches; a shallow solver-creep recovery band keeps boxes/hulls resting without ejecting bodies from below',
653
661
  'compound colliders accept hull children (packages/syncplay/src/collider-cooking.ts): an offline V-HACD-shaped cooker voxelizes a concave triangle soup, splits it on the axis-aligned plane that best reduces concavity, and emits a compound of ≤16-vert convex hulls that the runtime validates with buildSolverHull. The cook is order-invariant and byte-reproducible (exact integer-lattice convex hull, lexicographic tie-breaks); no decomposition code runs in a simulation step',
654
662
  ],
655
663
  cliProofs: [
@@ -663,6 +671,7 @@ const engineParityMatrixEntries = [
663
671
  gaps: [
664
672
  'Mesh-vs-mesh collision remains unsupported by design: a mesh collider is static or kinematic, and a dynamic concave body must be cooked to a compound of hulls (collider:cook).',
665
673
  'Kinematic bodies resolve contacts discretely — a kinematic mesh driven faster than its own extent per tick can pass through a thin body (no CCD for kinematic; ccd is a dynamic-body opt-in).',
674
+ 'Dynamic ccd sweeps box, sphere, capsule, hull, and heightfield targets; triangle-soup mesh and compound targets remain discrete-only.',
666
675
  'Convex decomposition is offline only: the cooked hull set is conservative (it contains the source, inflated by up to one voxel of the chosen resolution) and hulls stay capped at 16 verts.',
667
676
  ],
668
677
  nextCliWork: [
@@ -767,7 +767,7 @@ function runPhysicsCategory(config) {
767
767
  && physics3DQueries.overlapCovered
768
768
  && physics3DQueries.shapeCastCovered,
769
769
  physics3DMaterialResponse: physics3DMaterials.rollbackEquivalent
770
- && physics3DMaterials.combinedFriction === 3
770
+ && Math.abs(physics3DMaterials.combinedFriction - 0.28284271) < 1e-6
771
771
  && physics3DMaterials.combinedRestitution === 7
772
772
  && physics3DMaterials.normalVelocityAfterBounce < 0,
773
773
  physics3DDetailedQueriesAndContacts: physics3DDetails.rollbackEquivalent
package/dist/cjs/index.js CHANGED
@@ -5,7 +5,7 @@ exports.createDeterministicCommandTimelineSnapshot = exports.createDeterministic
5
5
  exports.createDeterministicLocalAuthorityCorrectionEnvelope = exports.createDeterministicLocalAuthorityCommandBundleEnvelope = exports.createDeterministicLocalAuthorityAckEnvelope = exports.applyDeterministicLocalAuthorityInputBundleEnvelope = exports.applyDeterministicLocalAuthorityCommandBundleEnvelope = exports.createDeterministicLocalAuthorityRuntime = exports.validateDeterministicLagCompensatedShot3D = exports.validateDeterministicAuthoritativeLagCompensatedShot3D = exports.rewindDeterministicLagCompensatedTargets3D = exports.createDeterministicLagCompensation3DHistory = exports.buildDeterministicLagCompensatedHitboxProxies3D = exports.sortDeterministicInputTimelineFlags = exports.evaluateDeterministicInputTimelineResult = exports.accumulateDeterministicInputTimelineState = exports.createInputAuthority = exports.filterDeterministicEventsForPlayer = exports.createDeterministicEventTimeline = exports.SyncplayError = exports.validateDeterministicFrameSystemsStateless = exports.runDeterministicFrameRuntimeMutationCorpus = exports.isDeterministicFrameHeapStaleHandleError = exports.generateDeterministicFrameRuntimeSource = exports.createDeterministicSparseSetFrameRuntime = exports.createDeterministicSlotMap = exports.createDeterministicFrameRuntimeAccessors = exports.createDeterministicFrameRuntime = exports.createDeterministicBitset = exports.compileDeterministicDslRuntimeDescriptor = exports.assertDeterministicFrameSystemsStateless = exports.assertDeterministicFrameHeapStaleHandleError = exports.stableDeterministicEngineCompleteJson = exports.runDeterministicEngineCompleteSuite = exports.engineCompleteExecutionProofHash = exports.createDeterministicEventEffectsAdapter = exports.createEcsLiteWorld = exports.verifyDeterministicSessionRuntimeReplay = exports.createDeterministicSessionReplayBundle = exports.createDeterministicSessionCore = exports.stepTopDownBody = exports.stepKccLiteBody = exports.quantizeFpsAim = exports.detectVerticalCcdHit = exports.cookStaticColliders = exports.combineDeterministicPhysicsMaterial = exports.verifyDeterministicConfigBundle = exports.summarizeDeterministicConfigBundleFailureReasons = exports.evaluateDeterministicConfigBundleParityResult = exports.cookDeterministicConfigBundle = exports.evaluateDeterministicCommandTimelineMigrationResult = exports.deterministicPayloadMatchesFields = void 0;
6
6
  exports.queryDeterministicPhysicsShapeOverlap2D = exports.queryDeterministicPhysicsAabb2D = exports.nearestDeterministicPhysicsHit2D = exports.createDeterministicPhysicsWorld2D = exports.circleCastDeterministicPhysics2D = exports.createOfflineSession = exports.valueNoise2D = exports.valueNoise1D = exports.hashUint32 = exports.hash2 = exports.fbm2D = exports.createNetworkedSyncplayClient = exports.stepDeterministicNavmeshOffMeshTraversal = exports.stepDeterministicNavmeshAgents = exports.smoothDeterministicNavmeshPath = exports.runDeterministicNavmeshStress = exports.locateDeterministicNavmeshPolygon = exports.findDeterministicNavmeshPointPath = exports.findDeterministicNavmeshPath = exports.cookImportedDeterministicNavmesh = exports.cookDeterministicNavmesh = exports.stepDeterministicNavigationAgents = exports.smoothDeterministicPath = exports.findDeterministicPath = exports.createDeterministicFlowField = exports.cookTilemapNavigationGraph = exports.cookDeterministicNavigationGraph = exports.applyDeterministicReciprocalAvoidance = exports.stepDeterministicKcc3D = exports.stepDeterministicCarry3D = exports.validateDeterministicLagCompensatedHit = exports.stepDeterministicTopDownMover = exports.stepDeterministicKcc2D = exports.stepDeterministicFpsAim = exports.runDeterministicMovementSimulation = exports.runDeterministicMovementCrowdSimulation = exports.rewindDeterministicTargets = exports.createDeterministicMath = exports.encodeDeterministicLocalAuthorityWireEnvelope = exports.deterministicLocalAuthorityWireProtocol = exports.decodeDeterministicLocalAuthorityWireEnvelope = exports.createDeterministicLocalAuthorityWireReceiver = exports.createDeterministicLocalAuthorityWireAuth = exports.createDeterministicLocalAuthoritySnapshotTransfer = exports.createDeterministicLocalAuthoritySnapshotRequestEnvelope = exports.createDeterministicLocalAuthoritySnapshotOfferEnvelope = exports.createDeterministicLocalAuthoritySnapshotEnvelope = exports.createDeterministicLocalAuthoritySnapshotCompleteEnvelope = exports.createDeterministicLocalAuthoritySnapshotChunkEnvelope = exports.createDeterministicLocalAuthorityInputBundleEnvelope = void 0;
7
7
  exports.diffDeterministicReports = exports.createProtocolRollbackReport = exports.verifyScoredReplay = exports.stableFrameDumpJson = exports.stableFrameDiffJson = exports.inspectReplayFrameWithDescriptor = exports.inspectReplayFrame = exports.explainReplayChecksum = exports.diffReplayFrames = exports.verifyReplayScoreAgainstTrustedRuntime = exports.verifyDeterministicSelfContainedFrameRuntimeReplayBundle = exports.verifyDeterministicReplayBundle = exports.verifyDeterministicFrameRuntimeReplayBundle = exports.sliceDeterministicReplayBundle = exports.minimizeDeterministicReplayDesyncBundle = exports.minimizeDeterministicReplayDesync = exports.createDeterministicReplayRuntimeManifest = exports.createDeterministicReplayDebugTables = exports.createDeterministicReplayBundle = exports.verifyReplayWithDescriptor = exports.verifyReplay = exports.DeterministicRandom = exports.interpolateDeterministicRenderVec2 = exports.interpolateDeterministicRenderNumber = exports.createDeterministicR3fRenderAdapter = exports.createDeterministicR3fDebugOverlay = exports.stepDeterministicVehicle3D = exports.runDeterministicTower3DScenario = exports.runDeterministicStackingSuite3D = exports.runDeterministicPyramid3DScenario = exports.runDeterministicMassRatio3DScenario = exports.runDeterministicKickRecovery3DScenario = exports.stepDeterministicPhysicsWorld3D = exports.shapeCastDeterministicPhysics3DDetailed = exports.shapeCastDeterministicPhysics3D = exports.setDeterministicBodyVelocity3D = exports.runDeterministicPhysics3DStress = exports.raycastDeterministicPhysics3DDetailed = exports.raycastDeterministicPhysics3D = exports.overlapDeterministicPhysics3D = exports.deterministicPhysicsCallback3D = exports.deterministicPhysicsBodyById3D = exports.deriveInverseInertia = exports.createDeterministicPhysicsWorld3D = exports.contactDetailsDeterministicPhysics3D = exports.applyDeterministicImpulses3D = exports.applyDeterministicImpulse3D = exports.stepDeterministicPhysicsWorld2D = exports.raycastDeterministicPhysics2D = exports.raycastAllDeterministicPhysics2D = void 0;
8
- exports.tickCooldown = exports.scheduleFrameEvent = exports.popDueFrameEvents = exports.millisecondsToFrames = exports.framesToMilliseconds = exports.createCooldown = exports.summarizeDeterministicLifecycleCounters = exports.createDeterministicLifecycleRunner = exports.createDeterministicSparseSet = exports.createDeterministicSignalBus = exports.encodeDeterministicSessionMessage = exports.deterministicSessionWireVersion = exports.decodeDeterministicSessionMessage = exports.encodeDeterministicStateSnapshot = exports.deterministicRoomDescriptorHash = exports.DEFAULT_SNAPSHOT_CHUNK_SIZE = exports.DEFAULT_MAX_SNAPSHOT_BYTES = exports.decodeDeterministicStateSnapshot = exports.createSnapshotTransferFromBytes = exports.createSnapshotTransferCollector = exports.bytesToBase64 = exports.base64ToBytes = exports.createSyncplayOfflineRuntime = exports.stableDeterministicDslSchemaJson = exports.parseDeterministicSchemaDsl = exports.deterministicDslMaxPlayers = exports.createDeterministicBinarySerializer = exports.proveSchemaBinaryRoundTrip = exports.generateSchemaTypes = exports.generateSchemaBinarySource = exports.createDeterministicDescriptorBinarySchema = exports.createSchemaArtifacts = exports.stableDescriptorId = exports.defineSyncplayGame = exports.withRuntimeDeterminismGuards = exports.createDeterministicRollbackHistory = exports.stableReportJson = exports.stableReportDiffJson = exports.renderDeterministicReportHtml = void 0;
8
+ exports.createSyncplaySecretsClient = exports.createSyncplaySecretEventReader = exports.createSyncplaySecretAuthority = exports.verifySyncplaySignedReceipt = exports.createBrowserSyncplaySecretCrypto = exports.syncplaySecretProtocol = exports.isSyncplaySecretCommand = exports.decodeSyncplaySecretPrivateMessage = exports.validateSyncplaySecretSystemsConfig = exports.syncplaySecretLimits = exports.syncplaySecretConfigHash = exports.tickCooldown = exports.scheduleFrameEvent = exports.popDueFrameEvents = exports.millisecondsToFrames = exports.framesToMilliseconds = exports.createCooldown = exports.summarizeDeterministicLifecycleCounters = exports.createDeterministicLifecycleRunner = exports.createDeterministicSparseSet = exports.createDeterministicSignalBus = exports.encodeDeterministicSessionMessage = exports.deterministicSessionWireVersion = exports.decodeDeterministicSessionMessage = exports.encodeDeterministicStateSnapshot = exports.deterministicRoomDescriptorHash = exports.DEFAULT_SNAPSHOT_CHUNK_SIZE = exports.DEFAULT_MAX_SNAPSHOT_BYTES = exports.decodeDeterministicStateSnapshot = exports.createSnapshotTransferFromBytes = exports.createSnapshotTransferCollector = exports.bytesToBase64 = exports.base64ToBytes = exports.createSyncplayOfflineRuntime = exports.stableDeterministicDslSchemaJson = exports.parseDeterministicSchemaDsl = exports.deterministicDslMaxPlayers = exports.createDeterministicBinarySerializer = exports.proveSchemaBinaryRoundTrip = exports.generateSchemaTypes = exports.generateSchemaBinarySource = exports.createDeterministicDescriptorBinarySchema = exports.createSchemaArtifacts = exports.stableDescriptorId = exports.defineSyncplayGame = exports.withRuntimeDeterminismGuards = exports.createDeterministicRollbackHistory = exports.stableReportJson = exports.stableReportDiffJson = exports.renderDeterministicReportHtml = void 0;
9
9
  var animation_js_1 = require("./animation.js");
10
10
  Object.defineProperty(exports, "cookDeterministicAnimation", { enumerable: true, get: function () { return animation_js_1.cookDeterministicAnimation; } });
11
11
  Object.defineProperty(exports, "createDeterministicAnimationRuntimeState", { enumerable: true, get: function () { return animation_js_1.createDeterministicAnimationRuntimeState; } });
@@ -300,3 +300,19 @@ Object.defineProperty(exports, "millisecondsToFrames", { enumerable: true, get:
300
300
  Object.defineProperty(exports, "popDueFrameEvents", { enumerable: true, get: function () { return timers_js_1.popDueFrameEvents; } });
301
301
  Object.defineProperty(exports, "scheduleFrameEvent", { enumerable: true, get: function () { return timers_js_1.scheduleFrameEvent; } });
302
302
  Object.defineProperty(exports, "tickCooldown", { enumerable: true, get: function () { return timers_js_1.tickCooldown; } });
303
+ var secret_config_js_1 = require("./secret-config.js");
304
+ Object.defineProperty(exports, "syncplaySecretConfigHash", { enumerable: true, get: function () { return secret_config_js_1.syncplaySecretConfigHash; } });
305
+ Object.defineProperty(exports, "syncplaySecretLimits", { enumerable: true, get: function () { return secret_config_js_1.syncplaySecretLimits; } });
306
+ Object.defineProperty(exports, "validateSyncplaySecretSystemsConfig", { enumerable: true, get: function () { return secret_config_js_1.validateSyncplaySecretSystemsConfig; } });
307
+ var secret_protocol_js_1 = require("./secret-protocol.js");
308
+ Object.defineProperty(exports, "decodeSyncplaySecretPrivateMessage", { enumerable: true, get: function () { return secret_protocol_js_1.decodeSyncplaySecretPrivateMessage; } });
309
+ Object.defineProperty(exports, "isSyncplaySecretCommand", { enumerable: true, get: function () { return secret_protocol_js_1.isSyncplaySecretCommand; } });
310
+ Object.defineProperty(exports, "syncplaySecretProtocol", { enumerable: true, get: function () { return secret_protocol_js_1.syncplaySecretProtocol; } });
311
+ var secret_crypto_js_1 = require("./secret-crypto.js");
312
+ Object.defineProperty(exports, "createBrowserSyncplaySecretCrypto", { enumerable: true, get: function () { return secret_crypto_js_1.createBrowserSyncplaySecretCrypto; } });
313
+ Object.defineProperty(exports, "verifySyncplaySignedReceipt", { enumerable: true, get: function () { return secret_crypto_js_1.verifySyncplaySignedReceipt; } });
314
+ var secret_authority_js_1 = require("./secret-authority.js");
315
+ Object.defineProperty(exports, "createSyncplaySecretAuthority", { enumerable: true, get: function () { return secret_authority_js_1.createSyncplaySecretAuthority; } });
316
+ var secret_client_js_1 = require("./secret-client.js");
317
+ Object.defineProperty(exports, "createSyncplaySecretEventReader", { enumerable: true, get: function () { return secret_client_js_1.createSyncplaySecretEventReader; } });
318
+ Object.defineProperty(exports, "createSyncplaySecretsClient", { enumerable: true, get: function () { return secret_client_js_1.createSyncplaySecretsClient; } });
@@ -10,6 +10,7 @@ function createInputAuthority(config) {
10
10
  const maxFutureTicks = config.maxFutureTicks ?? Number.POSITIVE_INFINITY;
11
11
  const retainedConfirmedFrames = config.retainedConfirmedFrames ?? 512;
12
12
  const pending = new Map();
13
+ const pendingCommands = new Map();
13
14
  const lastConfirmedInput = Array.from({ length: playerCount }, () => config.neutralInput);
14
15
  const retained = new Map();
15
16
  let confirmedThrough = -1;
@@ -22,7 +23,7 @@ function createInputAuthority(config) {
22
23
  const frame = frames[index];
23
24
  (0, errors_js_1.invariant)(Number.isInteger(frame.tick) && (index === 0 || frame.tick === frames[index - 1].tick + 1), 'restoreFrom frames must be tick-contiguous', 'input-authority.restore-gap');
24
25
  (0, errors_js_1.invariant)(frame.inputs.length === playerCount, 'restoreFrom frame inputs must cover every player slot', 'input-authority.restore-inputs');
25
- retained.set(frame.tick, frame);
26
+ retained.set(frame.tick, (0, canonical_js_1.cloneCanonical)(frame));
26
27
  }
27
28
  (0, errors_js_1.invariant)(frames[frames.length - 1].tick === restoredThrough, 'restoreFrom frames must end at the restored frontier', 'input-authority.restore-frontier');
28
29
  earliestRetained = frames[0].tick;
@@ -75,11 +76,16 @@ function createInputAuthority(config) {
75
76
  }
76
77
  }
77
78
  pending.delete(tick);
79
+ const commands = (pendingCommands.get(tick) ?? [])
80
+ .map((command) => (0, canonical_js_1.cloneCanonical)(command))
81
+ .sort(compareAuthorityCommands);
82
+ pendingCommands.delete(tick);
78
83
  const frame = {
79
84
  tick,
80
85
  inputs,
81
86
  substitutedSlots,
82
- checksum: String((0, canonical_js_1.defaultChecksum)({ tick, inputs })),
87
+ ...(commands.length === 0 ? {} : { commands }),
88
+ checksum: String((0, canonical_js_1.defaultChecksum)(commands.length === 0 ? { tick, inputs } : { tick, inputs, commands })),
83
89
  };
84
90
  retained.set(tick, frame);
85
91
  if (earliestRetained === -1) {
@@ -116,6 +122,13 @@ function createInputAuthority(config) {
116
122
  return confirmedThrough;
117
123
  },
118
124
  receiveInput,
125
+ queueCommand(tick, command) {
126
+ (0, errors_js_1.invariant)(Number.isInteger(tick) && tick >= 0, 'command tick must be a non-negative integer', 'input-authority.command-tick');
127
+ (0, errors_js_1.invariant)(tick > confirmedThrough, `command tick ${tick} is already confirmed`, 'input-authority.command-confirmed');
128
+ const commands = pendingCommands.get(tick) ?? [];
129
+ commands.push((0, canonical_js_1.cloneCanonical)(command));
130
+ pendingCommands.set(tick, commands);
131
+ },
119
132
  advance,
120
133
  get earliestRetainedTick() {
121
134
  return earliestRetained;
@@ -148,3 +161,22 @@ function createInputAuthority(config) {
148
161
  },
149
162
  };
150
163
  }
164
+ function compareAuthorityCommands(left, right) {
165
+ const leftRecord = isCanonicalRecord(left) ? left : undefined;
166
+ const rightRecord = isCanonicalRecord(right) ? right : undefined;
167
+ const leftSystem = typeof leftRecord?.systemId === 'string' ? leftRecord.systemId : '';
168
+ const rightSystem = typeof rightRecord?.systemId === 'string' ? rightRecord.systemId : '';
169
+ const systemOrder = compareCanonicalText(leftSystem, rightSystem);
170
+ if (systemOrder !== 0)
171
+ return systemOrder;
172
+ const leftRequest = typeof leftRecord?.requestId === 'string' ? leftRecord.requestId : '';
173
+ const rightRequest = typeof rightRecord?.requestId === 'string' ? rightRecord.requestId : '';
174
+ const requestOrder = compareCanonicalText(leftRequest, rightRequest);
175
+ return requestOrder !== 0 ? requestOrder : compareCanonicalText((0, canonical_js_1.canonicalStringify)(left), (0, canonical_js_1.canonicalStringify)(right));
176
+ }
177
+ function compareCanonicalText(left, right) {
178
+ return left < right ? -1 : left > right ? 1 : 0;
179
+ }
180
+ function isCanonicalRecord(value) {
181
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
182
+ }