@series-inc/rundot-syncplay 5.23.0-beta.7

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 (264) hide show
  1. package/README.md +610 -0
  2. package/dist/animation.d.ts +119 -0
  3. package/dist/animation.js +488 -0
  4. package/dist/asset-cooking.d.ts +82 -0
  5. package/dist/asset-cooking.js +364 -0
  6. package/dist/authority-room.d.ts +217 -0
  7. package/dist/authority-room.js +701 -0
  8. package/dist/bot-documents.d.ts +115 -0
  9. package/dist/bot-documents.js +549 -0
  10. package/dist/bot-primitives.d.ts +152 -0
  11. package/dist/bot-primitives.js +472 -0
  12. package/dist/browser.d.ts +57 -0
  13. package/dist/browser.js +53 -0
  14. package/dist/canonical.d.ts +12 -0
  15. package/dist/canonical.js +288 -0
  16. package/dist/certification.d.ts +455 -0
  17. package/dist/certification.js +1855 -0
  18. package/dist/chaos-certification.d.ts +56 -0
  19. package/dist/chaos-certification.js +1397 -0
  20. package/dist/cjs/animation.js +495 -0
  21. package/dist/cjs/asset-cooking.js +372 -0
  22. package/dist/cjs/authority-room.js +704 -0
  23. package/dist/cjs/bot-documents.js +556 -0
  24. package/dist/cjs/bot-primitives.js +482 -0
  25. package/dist/cjs/browser.js +96 -0
  26. package/dist/cjs/canonical.js +299 -0
  27. package/dist/cjs/certification.js +1860 -0
  28. package/dist/cjs/chaos-certification.js +1402 -0
  29. package/dist/cjs/collections.js +36 -0
  30. package/dist/cjs/collision.js +96 -0
  31. package/dist/cjs/commands.js +987 -0
  32. package/dist/cjs/config-bundle.js +491 -0
  33. package/dist/cjs/creator.js +30 -0
  34. package/dist/cjs/deterministic-primitives.js +134 -0
  35. package/dist/cjs/deterministic-session.js +1410 -0
  36. package/dist/cjs/ecs-lite.js +155 -0
  37. package/dist/cjs/effects-adapter.js +97 -0
  38. package/dist/cjs/engine-complete.js +2297 -0
  39. package/dist/cjs/engine-core-proof.js +70 -0
  40. package/dist/cjs/engine-parity-matrix.js +874 -0
  41. package/dist/cjs/engine-parity.js +3183 -0
  42. package/dist/cjs/engine-runtime.js +3141 -0
  43. package/dist/cjs/errors.js +22 -0
  44. package/dist/cjs/events.js +247 -0
  45. package/dist/cjs/examples/counter-duel.js +157 -0
  46. package/dist/cjs/examples/fixed-arena.js +368 -0
  47. package/dist/cjs/index.js +229 -0
  48. package/dist/cjs/input-authority.js +150 -0
  49. package/dist/cjs/input-button.js +256 -0
  50. package/dist/cjs/input-timeline.js +418 -0
  51. package/dist/cjs/instant-replay.js +66 -0
  52. package/dist/cjs/lag-compensation3d.js +837 -0
  53. package/dist/cjs/local-authority-server-runner.js +1121 -0
  54. package/dist/cjs/local-authority-wire.js +1497 -0
  55. package/dist/cjs/local-authority.js +817 -0
  56. package/dist/cjs/local-ws-adapter.js +150 -0
  57. package/dist/cjs/match-log.js +61 -0
  58. package/dist/cjs/math.js +157 -0
  59. package/dist/cjs/mock-network-adapter.js +26 -0
  60. package/dist/cjs/movement.js +703 -0
  61. package/dist/cjs/movement3d.js +1246 -0
  62. package/dist/cjs/multiclient-authority.js +308 -0
  63. package/dist/cjs/multiplayer-service.js +434 -0
  64. package/dist/cjs/navigation.js +347 -0
  65. package/dist/cjs/navmesh.js +1437 -0
  66. package/dist/cjs/networked-client.js +691 -0
  67. package/dist/cjs/networked-input-authority.js +443 -0
  68. package/dist/cjs/node.js +47 -0
  69. package/dist/cjs/number-lab.js +78 -0
  70. package/dist/cjs/offline-session.js +370 -0
  71. package/dist/cjs/package.json +1 -0
  72. package/dist/cjs/pause-resume.js +43 -0
  73. package/dist/cjs/physics-cert.js +426 -0
  74. package/dist/cjs/physics2d.js +751 -0
  75. package/dist/cjs/physics3d.js +1996 -0
  76. package/dist/cjs/prediction-culling.js +203 -0
  77. package/dist/cjs/protocol-lab.js +1299 -0
  78. package/dist/cjs/r3f-render-adapter.js +209 -0
  79. package/dist/cjs/random.js +59 -0
  80. package/dist/cjs/replay-bundle.js +1598 -0
  81. package/dist/cjs/replay-inspect.js +236 -0
  82. package/dist/cjs/replay-scoring.js +68 -0
  83. package/dist/cjs/replay.js +356 -0
  84. package/dist/cjs/reports.js +181 -0
  85. package/dist/cjs/rollback-history.js +251 -0
  86. package/dist/cjs/runtime-guards.js +111 -0
  87. package/dist/cjs/sample-lab.js +145 -0
  88. package/dist/cjs/sample-scenes.js +752 -0
  89. package/dist/cjs/schema-artifacts.js +359 -0
  90. package/dist/cjs/schema-codegen.js +544 -0
  91. package/dist/cjs/schema-dsl-binary.js +655 -0
  92. package/dist/cjs/schema.js +319 -0
  93. package/dist/cjs/sdk-offline.js +99 -0
  94. package/dist/cjs/session-promotion.js +74 -0
  95. package/dist/cjs/session-snapshot.js +265 -0
  96. package/dist/cjs/session-wire.js +216 -0
  97. package/dist/cjs/signals.js +103 -0
  98. package/dist/cjs/sparse-set.js +169 -0
  99. package/dist/cjs/synctest.js +136 -0
  100. package/dist/cjs/system-lifecycle.js +286 -0
  101. package/dist/cjs/timers.js +61 -0
  102. package/dist/cjs/tools/static-checker.js +1468 -0
  103. package/dist/cjs/types.js +2 -0
  104. package/dist/cjs/venus-mp-budget.js +125 -0
  105. package/dist/cjs/ws-frame.js +255 -0
  106. package/dist/cli.d.ts +2 -0
  107. package/dist/cli.js +11379 -0
  108. package/dist/collections.d.ts +9 -0
  109. package/dist/collections.js +30 -0
  110. package/dist/collision.d.ts +25 -0
  111. package/dist/collision.js +85 -0
  112. package/dist/commands.d.ts +137 -0
  113. package/dist/commands.js +977 -0
  114. package/dist/config-bundle.d.ts +89 -0
  115. package/dist/config-bundle.js +483 -0
  116. package/dist/creator.d.ts +20 -0
  117. package/dist/creator.js +11 -0
  118. package/dist/deterministic-primitives.d.ts +59 -0
  119. package/dist/deterministic-primitives.js +126 -0
  120. package/dist/deterministic-session.d.ts +197 -0
  121. package/dist/deterministic-session.js +1401 -0
  122. package/dist/ecs-lite.d.ts +26 -0
  123. package/dist/ecs-lite.js +152 -0
  124. package/dist/effects-adapter.d.ts +21 -0
  125. package/dist/effects-adapter.js +94 -0
  126. package/dist/engine-complete.d.ts +45 -0
  127. package/dist/engine-complete.js +2292 -0
  128. package/dist/engine-core-proof.d.ts +26 -0
  129. package/dist/engine-core-proof.js +66 -0
  130. package/dist/engine-parity-matrix.d.ts +300 -0
  131. package/dist/engine-parity-matrix.js +869 -0
  132. package/dist/engine-parity.d.ts +74 -0
  133. package/dist/engine-parity.js +3178 -0
  134. package/dist/engine-runtime.d.ts +351 -0
  135. package/dist/engine-runtime.js +3123 -0
  136. package/dist/errors.d.ts +11 -0
  137. package/dist/errors.js +17 -0
  138. package/dist/events.d.ts +45 -0
  139. package/dist/events.js +242 -0
  140. package/dist/examples/counter-duel.d.ts +43 -0
  141. package/dist/examples/counter-duel.js +150 -0
  142. package/dist/examples/fixed-arena.d.ts +110 -0
  143. package/dist/examples/fixed-arena.js +356 -0
  144. package/dist/index.d.ts +101 -0
  145. package/dist/index.js +66 -0
  146. package/dist/input-authority.d.ts +82 -0
  147. package/dist/input-authority.js +147 -0
  148. package/dist/input-button.d.ts +36 -0
  149. package/dist/input-button.js +249 -0
  150. package/dist/input-timeline.d.ts +65 -0
  151. package/dist/input-timeline.js +412 -0
  152. package/dist/instant-replay.d.ts +59 -0
  153. package/dist/instant-replay.js +62 -0
  154. package/dist/lag-compensation3d.d.ts +146 -0
  155. package/dist/lag-compensation3d.js +827 -0
  156. package/dist/local-authority-server-runner.d.ts +79 -0
  157. package/dist/local-authority-server-runner.js +1118 -0
  158. package/dist/local-authority-wire.d.ts +142 -0
  159. package/dist/local-authority-wire.js +1477 -0
  160. package/dist/local-authority.d.ts +160 -0
  161. package/dist/local-authority.js +810 -0
  162. package/dist/local-ws-adapter.d.ts +14 -0
  163. package/dist/local-ws-adapter.js +146 -0
  164. package/dist/match-log.d.ts +74 -0
  165. package/dist/match-log.js +57 -0
  166. package/dist/math.d.ts +41 -0
  167. package/dist/math.js +154 -0
  168. package/dist/mock-network-adapter.d.ts +13 -0
  169. package/dist/mock-network-adapter.js +23 -0
  170. package/dist/movement.d.ts +155 -0
  171. package/dist/movement.js +694 -0
  172. package/dist/movement3d.d.ts +195 -0
  173. package/dist/movement3d.js +1238 -0
  174. package/dist/multiclient-authority.d.ts +30 -0
  175. package/dist/multiclient-authority.js +305 -0
  176. package/dist/multiplayer-service.d.ts +153 -0
  177. package/dist/multiplayer-service.js +429 -0
  178. package/dist/navigation.d.ts +91 -0
  179. package/dist/navigation.js +338 -0
  180. package/dist/navmesh.d.ts +236 -0
  181. package/dist/navmesh.js +1423 -0
  182. package/dist/networked-client.d.ts +124 -0
  183. package/dist/networked-client.js +688 -0
  184. package/dist/networked-input-authority.d.ts +51 -0
  185. package/dist/networked-input-authority.js +439 -0
  186. package/dist/node.d.ts +22 -0
  187. package/dist/node.js +19 -0
  188. package/dist/number-lab.d.ts +15 -0
  189. package/dist/number-lab.js +75 -0
  190. package/dist/offline-session.d.ts +2 -0
  191. package/dist/offline-session.js +367 -0
  192. package/dist/pause-resume.d.ts +48 -0
  193. package/dist/pause-resume.js +40 -0
  194. package/dist/physics-cert.d.ts +40 -0
  195. package/dist/physics-cert.js +389 -0
  196. package/dist/physics2d.d.ts +142 -0
  197. package/dist/physics2d.js +741 -0
  198. package/dist/physics3d.d.ts +285 -0
  199. package/dist/physics3d.js +1979 -0
  200. package/dist/prediction-culling.d.ts +34 -0
  201. package/dist/prediction-culling.js +200 -0
  202. package/dist/protocol-lab.d.ts +115 -0
  203. package/dist/protocol-lab.js +1295 -0
  204. package/dist/r3f-render-adapter.d.ts +61 -0
  205. package/dist/r3f-render-adapter.js +203 -0
  206. package/dist/random.d.ts +13 -0
  207. package/dist/random.js +55 -0
  208. package/dist/replay-bundle.d.ts +150 -0
  209. package/dist/replay-bundle.js +1586 -0
  210. package/dist/replay-inspect.d.ts +60 -0
  211. package/dist/replay-inspect.js +228 -0
  212. package/dist/replay-scoring.d.ts +28 -0
  213. package/dist/replay-scoring.js +65 -0
  214. package/dist/replay.d.ts +17 -0
  215. package/dist/replay.js +351 -0
  216. package/dist/reports.d.ts +176 -0
  217. package/dist/reports.js +174 -0
  218. package/dist/rollback-history.d.ts +46 -0
  219. package/dist/rollback-history.js +247 -0
  220. package/dist/runtime-guards.d.ts +1 -0
  221. package/dist/runtime-guards.js +108 -0
  222. package/dist/sample-lab.d.ts +32 -0
  223. package/dist/sample-lab.js +134 -0
  224. package/dist/sample-scenes.d.ts +86 -0
  225. package/dist/sample-scenes.js +745 -0
  226. package/dist/schema-artifacts.d.ts +49 -0
  227. package/dist/schema-artifacts.js +356 -0
  228. package/dist/schema-codegen.d.ts +21 -0
  229. package/dist/schema-codegen.js +538 -0
  230. package/dist/schema-dsl-binary.d.ts +75 -0
  231. package/dist/schema-dsl-binary.js +648 -0
  232. package/dist/schema.d.ts +87 -0
  233. package/dist/schema.js +315 -0
  234. package/dist/sdk-offline.d.ts +34 -0
  235. package/dist/sdk-offline.js +96 -0
  236. package/dist/session-promotion.d.ts +26 -0
  237. package/dist/session-promotion.js +70 -0
  238. package/dist/session-snapshot.d.ts +95 -0
  239. package/dist/session-snapshot.js +255 -0
  240. package/dist/session-wire.d.ts +89 -0
  241. package/dist/session-wire.js +211 -0
  242. package/dist/signals.d.ts +31 -0
  243. package/dist/signals.js +99 -0
  244. package/dist/sparse-set.d.ts +34 -0
  245. package/dist/sparse-set.js +165 -0
  246. package/dist/synctest.d.ts +31 -0
  247. package/dist/synctest.js +133 -0
  248. package/dist/system-lifecycle.d.ts +72 -0
  249. package/dist/system-lifecycle.js +281 -0
  250. package/dist/timers.d.ts +17 -0
  251. package/dist/timers.js +53 -0
  252. package/dist/tools/index.d.ts +4 -0
  253. package/dist/tools/index.js +4 -0
  254. package/dist/tools/static-checker.d.ts +31 -0
  255. package/dist/tools/static-checker.js +1462 -0
  256. package/dist/tools/vite-plugin.d.ts +69 -0
  257. package/dist/tools/vite-plugin.js +203 -0
  258. package/dist/types.d.ts +148 -0
  259. package/dist/types.js +1 -0
  260. package/dist/venus-mp-budget.d.ts +29 -0
  261. package/dist/venus-mp-budget.js +122 -0
  262. package/dist/ws-frame.d.ts +49 -0
  263. package/dist/ws-frame.js +247 -0
  264. package/package.json +169 -0
@@ -0,0 +1,95 @@
1
+ import type { EncodedInput } from './input-authority.js';
2
+ import type { DeterministicLocalAuthoritySnapshotIdentity, DeterministicLocalAuthoritySnapshotTransfer } from './local-authority-wire.js';
3
+ /**
4
+ * Session-state snapshot payload + transfer plumbing for snapshot late-join (§6 M5.5).
5
+ *
6
+ * The frozen `rdm-local-authority/v1` wire reserved `snapshot-request/offer/chunk/complete`
7
+ * envelope kinds whose `snapshotBytes` field is an opaque string. This module defines what
8
+ * Syncplay puts INSIDE that string — a versioned envelope around the game's opaque serialized
9
+ * state — plus a transfer builder/collector shared by the authority room (which stores,
10
+ * validates, and forwards snapshots without ever running game code) and the networked client
11
+ * (donor and joiner roles). No changes to the frozen wire files: transfers are built as plain
12
+ * `DeterministicLocalAuthoritySnapshotTransfer` data and ride the existing envelope creators.
13
+ */
14
+ export interface DeterministicStateSnapshot {
15
+ readonly v: 1;
16
+ /** The confirmed tick this snapshot proves (checksum-report tick convention). */
17
+ readonly tick: number;
18
+ /** The session frame holding the state AFTER stepping `tick` — always tick + 1. */
19
+ readonly sessionFrame: number;
20
+ /** base64 of the game's opaque serialized state (its serializeState seam). */
21
+ readonly stateBytes: string;
22
+ readonly randomState: number;
23
+ /** defaultChecksum of the state at `sessionFrame` — comparable to checksum-reports at `tick`. */
24
+ readonly stateChecksum: string;
25
+ /** Confirmed inputs at `tick` — seeds the joiner's last-confirmed prediction inputs. */
26
+ readonly confirmedInputs: readonly EncodedInput[];
27
+ }
28
+ /**
29
+ * Room-level snapshot identity, computable by the game-agnostic authority AND
30
+ * every client from the session-start parameters — the `descriptorHash` on
31
+ * snapshot wire envelopes. Game-build identity (`schemaHash`) is layered on
32
+ * top by clients, which know their schema ids; the authority passes it through
33
+ * opaquely.
34
+ */
35
+ export declare function deterministicRoomDescriptorHash(params: {
36
+ readonly seed: number;
37
+ readonly playerCount: number;
38
+ readonly tickRateHz: number;
39
+ readonly hardToleranceTicks: number;
40
+ }): string;
41
+ export type DeterministicStateSnapshotDecodeRejection = 'invalid-json' | 'not-a-snapshot' | 'unsupported-snapshot-version' | 'malformed-snapshot';
42
+ export type DeterministicStateSnapshotDecodeResult = {
43
+ readonly ok: true;
44
+ readonly snapshot: DeterministicStateSnapshot;
45
+ } | {
46
+ readonly ok: false;
47
+ readonly reason: DeterministicStateSnapshotDecodeRejection;
48
+ };
49
+ export declare function encodeDeterministicStateSnapshot(snapshot: DeterministicStateSnapshot): string;
50
+ export declare function decodeDeterministicStateSnapshot(text: string): DeterministicStateSnapshotDecodeResult;
51
+ export declare function bytesToBase64(bytes: Uint8Array): string;
52
+ export declare function base64ToBytes(text: string): Uint8Array;
53
+ export declare const DEFAULT_SNAPSHOT_CHUNK_SIZE = 262144;
54
+ export declare const DEFAULT_MAX_SNAPSHOT_BYTES = 4194304;
55
+ export interface CreateSnapshotTransferOptions {
56
+ readonly identity: DeterministicLocalAuthoritySnapshotIdentity;
57
+ /** Session frame of the snapshot (tick + 1) — the wire's meaning of transfer `frame`. */
58
+ readonly frame: number;
59
+ readonly transferId: string;
60
+ readonly chunkSize?: number;
61
+ }
62
+ export declare function createSnapshotTransferFromBytes(snapshotBytes: string, options: CreateSnapshotTransferOptions): DeterministicLocalAuthoritySnapshotTransfer;
63
+ export interface CollectedSnapshotTransfer {
64
+ readonly transferId: string;
65
+ readonly identity: DeterministicLocalAuthoritySnapshotIdentity;
66
+ /** Session frame (tick + 1) as offered on the wire. */
67
+ readonly frame: number;
68
+ readonly snapshotChecksum: string;
69
+ readonly snapshotBytes: string;
70
+ }
71
+ export type SnapshotTransferStepResult = {
72
+ readonly accepted: true;
73
+ } | {
74
+ readonly accepted: false;
75
+ readonly reason: SnapshotTransferRejection;
76
+ };
77
+ export type SnapshotTransferCompleteResult = {
78
+ readonly accepted: true;
79
+ readonly transfer: CollectedSnapshotTransfer;
80
+ } | {
81
+ readonly accepted: false;
82
+ readonly reason: SnapshotTransferRejection;
83
+ };
84
+ export type SnapshotTransferRejection = 'malformed-snapshot-offer' | 'snapshot-too-large' | 'unknown-snapshot-transfer' | 'snapshot-chunk-index-out-of-range' | 'duplicate-snapshot-chunk' | 'snapshot-chunk-checksum-mismatch' | 'malformed-snapshot-chunk' | 'malformed-snapshot-complete' | 'missing-snapshot-chunk' | 'snapshot-byte-length-mismatch' | 'snapshot-transfer-checksum-mismatch';
85
+ export interface SnapshotTransferCollector {
86
+ offer(payload: Readonly<Record<string, unknown>>): SnapshotTransferStepResult;
87
+ chunk(payload: Readonly<Record<string, unknown>>): SnapshotTransferStepResult;
88
+ complete(payload: Readonly<Record<string, unknown>>): SnapshotTransferCompleteResult;
89
+ abort(transferId: string): void;
90
+ readonly pendingTransferIds: readonly string[];
91
+ }
92
+ export interface SnapshotTransferCollectorOptions {
93
+ readonly maxSnapshotBytes?: number;
94
+ }
95
+ export declare function createSnapshotTransferCollector(options?: SnapshotTransferCollectorOptions): SnapshotTransferCollector;
@@ -0,0 +1,255 @@
1
+ import { canonicalStringify, cloneCanonical, defaultChecksum } from './canonical.js';
2
+ /**
3
+ * Room-level snapshot identity, computable by the game-agnostic authority AND
4
+ * every client from the session-start parameters — the `descriptorHash` on
5
+ * snapshot wire envelopes. Game-build identity (`schemaHash`) is layered on
6
+ * top by clients, which know their schema ids; the authority passes it through
7
+ * opaquely.
8
+ */
9
+ export function deterministicRoomDescriptorHash(params) {
10
+ return defaultChecksum({
11
+ kind: 'rdm-room-descriptor',
12
+ seed: params.seed,
13
+ playerCount: params.playerCount,
14
+ tickRateHz: params.tickRateHz,
15
+ hardToleranceTicks: params.hardToleranceTicks,
16
+ });
17
+ }
18
+ export function encodeDeterministicStateSnapshot(snapshot) {
19
+ return canonicalStringify(snapshot);
20
+ }
21
+ export function decodeDeterministicStateSnapshot(text) {
22
+ let parsed;
23
+ try {
24
+ parsed = JSON.parse(text);
25
+ }
26
+ catch {
27
+ return { ok: false, reason: 'invalid-json' };
28
+ }
29
+ if (!isRecord(parsed)) {
30
+ return { ok: false, reason: 'not-a-snapshot' };
31
+ }
32
+ if (parsed.v !== 1) {
33
+ return { ok: false, reason: 'unsupported-snapshot-version' };
34
+ }
35
+ if (!isNonNegativeInt(parsed.tick) ||
36
+ !isNonNegativeInt(parsed.sessionFrame) ||
37
+ parsed.sessionFrame !== parsed.tick + 1 ||
38
+ typeof parsed.stateBytes !== 'string' ||
39
+ !Number.isInteger(parsed.randomState) ||
40
+ typeof parsed.stateChecksum !== 'string' ||
41
+ parsed.stateChecksum.length === 0 ||
42
+ !Array.isArray(parsed.confirmedInputs)) {
43
+ return { ok: false, reason: 'malformed-snapshot' };
44
+ }
45
+ return {
46
+ ok: true,
47
+ snapshot: {
48
+ v: 1,
49
+ tick: parsed.tick,
50
+ sessionFrame: parsed.sessionFrame,
51
+ stateBytes: parsed.stateBytes,
52
+ randomState: parsed.randomState,
53
+ stateChecksum: parsed.stateChecksum,
54
+ confirmedInputs: cloneCanonical(parsed.confirmedInputs),
55
+ },
56
+ };
57
+ }
58
+ // ── base64 (browser-safe: no Buffer, no atob dependency) ──
59
+ const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
60
+ const BASE64_LOOKUP = new Map([...BASE64_ALPHABET].map((char, index) => [char, index]));
61
+ export function bytesToBase64(bytes) {
62
+ let out = '';
63
+ for (let index = 0; index < bytes.length; index += 3) {
64
+ const b0 = bytes[index];
65
+ const b1 = index + 1 < bytes.length ? bytes[index + 1] : 0;
66
+ const b2 = index + 2 < bytes.length ? bytes[index + 2] : 0;
67
+ out += BASE64_ALPHABET[b0 >> 2];
68
+ out += BASE64_ALPHABET[((b0 & 0x03) << 4) | (b1 >> 4)];
69
+ out += index + 1 < bytes.length ? BASE64_ALPHABET[((b1 & 0x0f) << 2) | (b2 >> 6)] : '=';
70
+ out += index + 2 < bytes.length ? BASE64_ALPHABET[b2 & 0x3f] : '=';
71
+ }
72
+ return out;
73
+ }
74
+ export function base64ToBytes(text) {
75
+ if (text.length % 4 !== 0) {
76
+ throw new Error('base64 length must be a multiple of 4');
77
+ }
78
+ const padding = text.endsWith('==') ? 2 : text.endsWith('=') ? 1 : 0;
79
+ const byteLength = (text.length / 4) * 3 - padding;
80
+ const bytes = new Uint8Array(byteLength);
81
+ let byteIndex = 0;
82
+ for (let index = 0; index < text.length; index += 4) {
83
+ const values = [];
84
+ for (let offset = 0; offset < 4; offset += 1) {
85
+ const char = text[index + offset];
86
+ if (char === '=') {
87
+ if (index + 4 < text.length || offset < 2) {
88
+ throw new Error('base64 padding is only valid at the end');
89
+ }
90
+ values.push(0);
91
+ continue;
92
+ }
93
+ const value = BASE64_LOOKUP.get(char);
94
+ if (value === undefined) {
95
+ throw new Error(`invalid base64 character ${JSON.stringify(char)}`);
96
+ }
97
+ values.push(value);
98
+ }
99
+ const triple = (values[0] << 18) | (values[1] << 12) | (values[2] << 6) | values[3];
100
+ if (byteIndex < byteLength)
101
+ bytes[byteIndex++] = (triple >> 16) & 0xff;
102
+ if (byteIndex < byteLength)
103
+ bytes[byteIndex++] = (triple >> 8) & 0xff;
104
+ if (byteIndex < byteLength)
105
+ bytes[byteIndex++] = triple & 0xff;
106
+ }
107
+ return bytes;
108
+ }
109
+ // ── transfer builder (raw bytes → frozen wire transfer shape) ──
110
+ export const DEFAULT_SNAPSHOT_CHUNK_SIZE = 262_144;
111
+ export const DEFAULT_MAX_SNAPSHOT_BYTES = 4_194_304;
112
+ export function createSnapshotTransferFromBytes(snapshotBytes, options) {
113
+ const chunkSize = options.chunkSize ?? DEFAULT_SNAPSHOT_CHUNK_SIZE;
114
+ if (!Number.isInteger(chunkSize) || chunkSize <= 0) {
115
+ throw new Error('Snapshot chunkSize must be a positive integer');
116
+ }
117
+ if (snapshotBytes.length === 0) {
118
+ throw new Error('Snapshot bytes must be non-empty');
119
+ }
120
+ const chunks = [];
121
+ for (let offset = 0; offset < snapshotBytes.length; offset += chunkSize) {
122
+ chunks.push(snapshotBytes.slice(offset, offset + chunkSize));
123
+ }
124
+ return {
125
+ transferId: options.transferId,
126
+ identity: cloneCanonical(options.identity),
127
+ frame: options.frame,
128
+ snapshotChecksum: defaultChecksum(snapshotBytes),
129
+ snapshotBytes,
130
+ chunkSize,
131
+ chunks,
132
+ chunkChecksums: chunks.map((chunk) => defaultChecksum(chunk)),
133
+ };
134
+ }
135
+ export function createSnapshotTransferCollector(options = {}) {
136
+ const maxSnapshotBytes = options.maxSnapshotBytes ?? DEFAULT_MAX_SNAPSHOT_BYTES;
137
+ const pending = new Map();
138
+ return {
139
+ offer(payload) {
140
+ const identity = parseIdentity(payload.identity);
141
+ if (typeof payload.transferId !== 'string' ||
142
+ payload.transferId.length === 0 ||
143
+ identity === undefined ||
144
+ !isNonNegativeInt(payload.frame) ||
145
+ typeof payload.snapshotChecksum !== 'string' ||
146
+ !isNonNegativeInt(payload.chunkCount) ||
147
+ payload.chunkCount < 1 ||
148
+ !isNonNegativeInt(payload.totalBytes) ||
149
+ payload.totalBytes < 1 ||
150
+ // Every chunk carries at least one character, so a chunkCount that
151
+ // exceeds totalBytes is self-contradictory (forged offer).
152
+ payload.chunkCount > payload.totalBytes ||
153
+ !isStringArray(payload.chunkChecksums) ||
154
+ payload.chunkChecksums.length !== payload.chunkCount) {
155
+ return { accepted: false, reason: 'malformed-snapshot-offer' };
156
+ }
157
+ if (payload.totalBytes > maxSnapshotBytes) {
158
+ return { accepted: false, reason: 'snapshot-too-large' };
159
+ }
160
+ pending.set(payload.transferId, {
161
+ transferId: payload.transferId,
162
+ identity,
163
+ frame: payload.frame,
164
+ snapshotChecksum: payload.snapshotChecksum,
165
+ chunkCount: payload.chunkCount,
166
+ totalBytes: payload.totalBytes,
167
+ chunkChecksums: [...payload.chunkChecksums],
168
+ chunks: new Map(),
169
+ });
170
+ return { accepted: true };
171
+ },
172
+ chunk(payload) {
173
+ if (typeof payload.transferId !== 'string') {
174
+ return { accepted: false, reason: 'malformed-snapshot-chunk' };
175
+ }
176
+ const transfer = pending.get(payload.transferId);
177
+ if (transfer === undefined) {
178
+ return { accepted: false, reason: 'unknown-snapshot-transfer' };
179
+ }
180
+ if (typeof payload.chunk !== 'string' || typeof payload.chunkChecksum !== 'string') {
181
+ return { accepted: false, reason: 'malformed-snapshot-chunk' };
182
+ }
183
+ const chunkIndex = payload.chunkIndex;
184
+ if (!isNonNegativeInt(chunkIndex) || chunkIndex >= transfer.chunkCount) {
185
+ return { accepted: false, reason: 'snapshot-chunk-index-out-of-range' };
186
+ }
187
+ if (transfer.chunks.has(chunkIndex)) {
188
+ return { accepted: false, reason: 'duplicate-snapshot-chunk' };
189
+ }
190
+ if (transfer.chunkChecksums[chunkIndex] !== payload.chunkChecksum ||
191
+ defaultChecksum(payload.chunk) !== payload.chunkChecksum) {
192
+ return { accepted: false, reason: 'snapshot-chunk-checksum-mismatch' };
193
+ }
194
+ transfer.chunks.set(chunkIndex, payload.chunk);
195
+ return { accepted: true };
196
+ },
197
+ complete(payload) {
198
+ if (typeof payload.transferId !== 'string') {
199
+ return { accepted: false, reason: 'malformed-snapshot-complete' };
200
+ }
201
+ const transfer = pending.get(payload.transferId);
202
+ if (transfer === undefined) {
203
+ return { accepted: false, reason: 'unknown-snapshot-transfer' };
204
+ }
205
+ if (transfer.chunks.size !== transfer.chunkCount) {
206
+ return { accepted: false, reason: 'missing-snapshot-chunk' };
207
+ }
208
+ const snapshotBytes = Array.from({ length: transfer.chunkCount }, (_, index) => transfer.chunks.get(index)).join('');
209
+ // A failed terminal check consumes the transfer either way — the bytes are bad.
210
+ pending.delete(payload.transferId);
211
+ if (snapshotBytes.length !== transfer.totalBytes) {
212
+ return { accepted: false, reason: 'snapshot-byte-length-mismatch' };
213
+ }
214
+ if (defaultChecksum(snapshotBytes) !== transfer.snapshotChecksum) {
215
+ return { accepted: false, reason: 'snapshot-transfer-checksum-mismatch' };
216
+ }
217
+ return {
218
+ accepted: true,
219
+ transfer: {
220
+ transferId: transfer.transferId,
221
+ identity: transfer.identity,
222
+ frame: transfer.frame,
223
+ snapshotChecksum: transfer.snapshotChecksum,
224
+ snapshotBytes,
225
+ },
226
+ };
227
+ },
228
+ abort(transferId) {
229
+ pending.delete(transferId);
230
+ },
231
+ get pendingTransferIds() {
232
+ return [...pending.keys()];
233
+ },
234
+ };
235
+ }
236
+ function parseIdentity(value) {
237
+ if (!isRecord(value) || typeof value.descriptorHash !== 'string' || value.descriptorHash.length === 0) {
238
+ return undefined;
239
+ }
240
+ return {
241
+ descriptorHash: value.descriptorHash,
242
+ ...(typeof value.schemaHash === 'string' ? { schemaHash: value.schemaHash } : {}),
243
+ ...(typeof value.assetDbHash === 'string' ? { assetDbHash: value.assetDbHash } : {}),
244
+ ...(typeof value.configHash === 'string' ? { configHash: value.configHash } : {}),
245
+ };
246
+ }
247
+ function isRecord(value) {
248
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
249
+ }
250
+ function isNonNegativeInt(value) {
251
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0;
252
+ }
253
+ function isStringArray(value) {
254
+ return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
255
+ }
@@ -0,0 +1,89 @@
1
+ import type { ConfirmedInputFrame, EncodedInput } from './input-authority.js';
2
+ /**
3
+ * Frozen v1 session-control wire protocol for game-agnostic input-authority rooms (§8, M1).
4
+ *
5
+ * Covers the server/client roles for a live match: session-start (slot assignment + the
6
+ * server-owned RNG seed + tick params), clock/time-sync, input, confirmed-input (the verified
7
+ * tick), reconnect, spectator, and close/error. Snapshot request/offer/chunk/complete for
8
+ * late-join reuse the existing envelopes in `local-authority-wire.ts` (wired at M5).
9
+ *
10
+ * Both families share the `rdm-local-authority/v1` version namespace. Decoding is FAIL-CLOSED:
11
+ * unknown protocol, unknown version, unknown kind, malformed payload, or checksum mismatch are
12
+ * rejected with a reason — never silently coerced. This is a public forward-compat boundary; the
13
+ * discriminants below are frozen and must only be extended, never repurposed.
14
+ */
15
+ export declare const deterministicSessionWireVersion = 1;
16
+ export type DeterministicSessionRole = 'player' | 'spectator';
17
+ export type DeterministicSessionMessage = {
18
+ readonly kind: 'session-start';
19
+ readonly slot: number;
20
+ readonly playerCount: number;
21
+ readonly seed: number;
22
+ readonly tickRateHz: number;
23
+ readonly hardToleranceTicks: number;
24
+ readonly role: DeterministicSessionRole;
25
+ } | {
26
+ readonly kind: 'time-sync-ping';
27
+ readonly clientSendTick: number;
28
+ readonly nonce: number;
29
+ } | {
30
+ readonly kind: 'time-sync-pong';
31
+ readonly clientSendTick: number;
32
+ readonly nonce: number;
33
+ readonly authorityTick: number;
34
+ } | {
35
+ readonly kind: 'input';
36
+ readonly slot: number;
37
+ readonly tick: number;
38
+ readonly input: EncodedInput;
39
+ readonly attempt: number;
40
+ } | {
41
+ readonly kind: 'confirmed-input';
42
+ readonly authorityTick: number;
43
+ readonly frames: readonly ConfirmedInputFrame[];
44
+ } | {
45
+ readonly kind: 'reconnect';
46
+ readonly slot: number;
47
+ readonly resumeFromTick: number;
48
+ } | {
49
+ readonly kind: 'spectator-join';
50
+ } | {
51
+ readonly kind: 'checksum-report';
52
+ readonly slot: number;
53
+ readonly tick: number;
54
+ readonly checksum: string;
55
+ } | {
56
+ /**
57
+ * Client → authority netcode telemetry (G8). Additive v1 extension: unknown
58
+ * kinds are dropped fail-closed by older decoders, so this never breaks an
59
+ * old authority. All fields are integers (rttMs rounded, timescale in
60
+ * permille) so the payload stays canonical-checksum friendly.
61
+ */
62
+ readonly kind: 'net-stats';
63
+ readonly slot: number;
64
+ readonly tick: number;
65
+ readonly rttMs: number;
66
+ readonly rollbacks: number;
67
+ readonly maxRollbackDepth: number;
68
+ readonly timescalePermille: number;
69
+ readonly inputDelayTicks: number;
70
+ } | {
71
+ readonly kind: 'close';
72
+ readonly code: string;
73
+ readonly reason: string;
74
+ } | {
75
+ readonly kind: 'error';
76
+ readonly code: string;
77
+ readonly message: string;
78
+ };
79
+ export type DeterministicSessionMessageKind = DeterministicSessionMessage['kind'];
80
+ export type DeterministicSessionDecodeRejection = 'invalid-json' | 'not-an-envelope' | 'unknown-protocol' | 'unknown-version' | 'unknown-kind' | 'checksum-mismatch' | 'malformed-payload';
81
+ export type DeterministicSessionDecodeResult = {
82
+ readonly ok: true;
83
+ readonly message: DeterministicSessionMessage;
84
+ } | {
85
+ readonly ok: false;
86
+ readonly reason: DeterministicSessionDecodeRejection;
87
+ };
88
+ export declare function encodeDeterministicSessionMessage(message: DeterministicSessionMessage): string;
89
+ export declare function decodeDeterministicSessionMessage(text: string): DeterministicSessionDecodeResult;
@@ -0,0 +1,211 @@
1
+ import { canonicalStringify, decodeCanonicalValue, defaultChecksum } from './canonical.js';
2
+ import { deterministicLocalAuthorityWireProtocol } from './local-authority-wire.js';
3
+ /**
4
+ * Frozen v1 session-control wire protocol for game-agnostic input-authority rooms (§8, M1).
5
+ *
6
+ * Covers the server/client roles for a live match: session-start (slot assignment + the
7
+ * server-owned RNG seed + tick params), clock/time-sync, input, confirmed-input (the verified
8
+ * tick), reconnect, spectator, and close/error. Snapshot request/offer/chunk/complete for
9
+ * late-join reuse the existing envelopes in `local-authority-wire.ts` (wired at M5).
10
+ *
11
+ * Both families share the `rdm-local-authority/v1` version namespace. Decoding is FAIL-CLOSED:
12
+ * unknown protocol, unknown version, unknown kind, malformed payload, or checksum mismatch are
13
+ * rejected with a reason — never silently coerced. This is a public forward-compat boundary; the
14
+ * discriminants below are frozen and must only be extended, never repurposed.
15
+ */
16
+ export const deterministicSessionWireVersion = 1;
17
+ const sessionMessageKinds = new Set([
18
+ 'session-start',
19
+ 'time-sync-ping',
20
+ 'time-sync-pong',
21
+ 'input',
22
+ 'confirmed-input',
23
+ 'reconnect',
24
+ 'spectator-join',
25
+ 'checksum-report',
26
+ 'net-stats',
27
+ 'close',
28
+ 'error',
29
+ ]);
30
+ export function encodeDeterministicSessionMessage(message) {
31
+ const { kind, ...rest } = message;
32
+ const payload = rest;
33
+ const envelope = {
34
+ protocol: deterministicLocalAuthorityWireProtocol,
35
+ v: deterministicSessionWireVersion,
36
+ kind,
37
+ payload,
38
+ checksum: sessionChecksum(kind, payload),
39
+ };
40
+ return canonicalStringify(envelope);
41
+ }
42
+ export function decodeDeterministicSessionMessage(text) {
43
+ let parsed;
44
+ try {
45
+ parsed = JSON.parse(text);
46
+ }
47
+ catch {
48
+ return { ok: false, reason: 'invalid-json' };
49
+ }
50
+ if (!isRecord(parsed)) {
51
+ return { ok: false, reason: 'not-an-envelope' };
52
+ }
53
+ if (parsed.protocol !== deterministicLocalAuthorityWireProtocol) {
54
+ return { ok: false, reason: 'unknown-protocol' };
55
+ }
56
+ if (parsed.v !== deterministicSessionWireVersion) {
57
+ return { ok: false, reason: 'unknown-version' };
58
+ }
59
+ if (typeof parsed.kind !== 'string' || !sessionMessageKinds.has(parsed.kind)) {
60
+ return { ok: false, reason: 'unknown-kind' };
61
+ }
62
+ const kind = parsed.kind;
63
+ if (!isRecord(parsed.payload)) {
64
+ return { ok: false, reason: 'malformed-payload' };
65
+ }
66
+ // Symmetry with encode: the encoder checksummed the LIVE payload value and
67
+ // then canonical-encoded it, so sentinel values (-0, NaN, ±Infinity,
68
+ // undefined, bigint, Map/Set/typed arrays) sit on the wire as literal
69
+ // `{"$rdm":…}` objects. Decode them back to real values BEFORE recomputing
70
+ // the checksum — hashing the raw parse output would trip canonicalize's
71
+ // reserved-key guard and throw out of a decoder whose contract is
72
+ // fail-closed. Any malformed/hostile `$rdm` literal lands in the catch.
73
+ let payload;
74
+ let expectedChecksum;
75
+ try {
76
+ const decoded = decodeCanonicalValue(parsed.payload);
77
+ if (!isRecord(decoded)) {
78
+ return { ok: false, reason: 'malformed-payload' };
79
+ }
80
+ payload = decoded;
81
+ expectedChecksum = sessionChecksum(kind, payload);
82
+ }
83
+ catch {
84
+ return { ok: false, reason: 'malformed-payload' };
85
+ }
86
+ if (parsed.checksum !== expectedChecksum) {
87
+ return { ok: false, reason: 'checksum-mismatch' };
88
+ }
89
+ const message = parseSessionPayload(kind, payload);
90
+ if (message === undefined) {
91
+ return { ok: false, reason: 'malformed-payload' };
92
+ }
93
+ return { ok: true, message };
94
+ }
95
+ function parseSessionPayload(kind, payload) {
96
+ switch (kind) {
97
+ case 'session-start':
98
+ if (isInt(payload.slot) &&
99
+ isInt(payload.playerCount) &&
100
+ isInt(payload.seed) &&
101
+ isFiniteNumber(payload.tickRateHz) &&
102
+ isInt(payload.hardToleranceTicks) &&
103
+ isRole(payload.role)) {
104
+ return {
105
+ kind,
106
+ slot: payload.slot,
107
+ playerCount: payload.playerCount,
108
+ seed: payload.seed,
109
+ tickRateHz: payload.tickRateHz,
110
+ hardToleranceTicks: payload.hardToleranceTicks,
111
+ role: payload.role,
112
+ };
113
+ }
114
+ return undefined;
115
+ case 'time-sync-ping':
116
+ if (isInt(payload.clientSendTick) && isInt(payload.nonce)) {
117
+ return { kind, clientSendTick: payload.clientSendTick, nonce: payload.nonce };
118
+ }
119
+ return undefined;
120
+ case 'time-sync-pong':
121
+ if (isInt(payload.clientSendTick) && isInt(payload.nonce) && isInt(payload.authorityTick)) {
122
+ return { kind, clientSendTick: payload.clientSendTick, nonce: payload.nonce, authorityTick: payload.authorityTick };
123
+ }
124
+ return undefined;
125
+ case 'input':
126
+ if (isInt(payload.slot) && isInt(payload.tick) && isInt(payload.attempt) && payload.input !== undefined) {
127
+ return { kind, slot: payload.slot, tick: payload.tick, input: payload.input, attempt: payload.attempt };
128
+ }
129
+ return undefined;
130
+ case 'confirmed-input':
131
+ if (isInt(payload.authorityTick) && isConfirmedFrameArray(payload.frames)) {
132
+ return { kind, authorityTick: payload.authorityTick, frames: payload.frames };
133
+ }
134
+ return undefined;
135
+ case 'reconnect':
136
+ if (isInt(payload.slot) && isInt(payload.resumeFromTick)) {
137
+ return { kind, slot: payload.slot, resumeFromTick: payload.resumeFromTick };
138
+ }
139
+ return undefined;
140
+ case 'spectator-join':
141
+ return { kind };
142
+ case 'checksum-report':
143
+ if (isInt(payload.slot) && isInt(payload.tick) && typeof payload.checksum === 'string') {
144
+ return { kind, slot: payload.slot, tick: payload.tick, checksum: payload.checksum };
145
+ }
146
+ return undefined;
147
+ case 'net-stats':
148
+ if (isInt(payload.slot) &&
149
+ isInt(payload.tick) &&
150
+ isInt(payload.rttMs) &&
151
+ isInt(payload.rollbacks) &&
152
+ isInt(payload.maxRollbackDepth) &&
153
+ isInt(payload.timescalePermille) &&
154
+ isInt(payload.inputDelayTicks)) {
155
+ return {
156
+ kind,
157
+ slot: payload.slot,
158
+ tick: payload.tick,
159
+ rttMs: payload.rttMs,
160
+ rollbacks: payload.rollbacks,
161
+ maxRollbackDepth: payload.maxRollbackDepth,
162
+ timescalePermille: payload.timescalePermille,
163
+ inputDelayTicks: payload.inputDelayTicks,
164
+ };
165
+ }
166
+ return undefined;
167
+ case 'close':
168
+ if (typeof payload.code === 'string' && typeof payload.reason === 'string') {
169
+ return { kind, code: payload.code, reason: payload.reason };
170
+ }
171
+ return undefined;
172
+ case 'error':
173
+ if (typeof payload.code === 'string' && typeof payload.message === 'string') {
174
+ return { kind, code: payload.code, message: payload.message };
175
+ }
176
+ return undefined;
177
+ default:
178
+ return assertNever(kind);
179
+ }
180
+ }
181
+ function sessionChecksum(kind, payload) {
182
+ return String(defaultChecksum({ protocol: deterministicLocalAuthorityWireProtocol, v: deterministicSessionWireVersion, kind, payload }));
183
+ }
184
+ function isRecord(value) {
185
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
186
+ }
187
+ function isInt(value) {
188
+ return typeof value === 'number' && Number.isInteger(value);
189
+ }
190
+ function isFiniteNumber(value) {
191
+ return typeof value === 'number' && Number.isFinite(value);
192
+ }
193
+ function isRole(value) {
194
+ return value === 'player' || value === 'spectator';
195
+ }
196
+ function isConfirmedFrameArray(value) {
197
+ // The codec is stateless (it does not know playerCount), so it cannot check inputs.length — the
198
+ // consumer validates that against its own slot count. It CAN reject a frame whose substituted-slot
199
+ // indices are not integers (a string index survives JSON.parse and would corrupt slot handling).
200
+ return (Array.isArray(value) &&
201
+ value.every((frame) => isRecord(frame) &&
202
+ isInt(frame.tick) &&
203
+ Array.isArray(frame.inputs) &&
204
+ Array.isArray(frame.substitutedSlots) &&
205
+ frame.substitutedSlots.every((slot) => isInt(slot)) &&
206
+ typeof frame.checksum === 'string'));
207
+ }
208
+ function assertNever(value) {
209
+ void value;
210
+ return undefined;
211
+ }
@@ -0,0 +1,31 @@
1
+ export interface DeterministicSignal<TPayload = Readonly<Record<string, boolean | number | string>>> {
2
+ readonly frame: number;
3
+ readonly type: string;
4
+ readonly payload: TPayload;
5
+ }
6
+ export interface DeterministicSignalSubscription<TState, TPayload = Readonly<Record<string, boolean | number | string>>> {
7
+ readonly id: string;
8
+ readonly type: string;
9
+ readonly order: number;
10
+ handle(state: Readonly<TState>, signal: DeterministicSignal<TPayload>): TState;
11
+ }
12
+ export interface DeterministicSignalBusSnapshot<TPayload = Readonly<Record<string, boolean | number | string>>> {
13
+ readonly emitted: readonly DeterministicSignal<TPayload>[];
14
+ readonly dispatched: readonly DeterministicSignal<TPayload>[];
15
+ readonly sequence: number;
16
+ }
17
+ export interface DeterministicSignalBus<TState, TPayload = Readonly<Record<string, boolean | number | string>>> {
18
+ emit(signal: DeterministicSignal<TPayload>): void;
19
+ dispatch(state: Readonly<TState>, frame: number): TState;
20
+ snapshot(): DeterministicSignalBusSnapshot<TPayload>;
21
+ restore(snapshot: DeterministicSignalBusSnapshot<TPayload>): void;
22
+ }
23
+ export interface DeterministicSignalFixtureResult {
24
+ readonly checksum: string;
25
+ readonly restoredChecksum: string;
26
+ readonly dispatchedSignals: number;
27
+ readonly subscriptionOrderStable: boolean;
28
+ readonly rollbackEquivalent: boolean;
29
+ }
30
+ export declare function createDeterministicSignalBus<TState, TPayload = Readonly<Record<string, boolean | number | string>>>(subscriptions: readonly DeterministicSignalSubscription<TState, TPayload>[]): DeterministicSignalBus<TState, TPayload>;
31
+ export declare function runDeterministicSignalFixture(): DeterministicSignalFixtureResult;