@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,265 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_MAX_SNAPSHOT_BYTES = exports.DEFAULT_SNAPSHOT_CHUNK_SIZE = void 0;
4
+ exports.deterministicRoomDescriptorHash = deterministicRoomDescriptorHash;
5
+ exports.encodeDeterministicStateSnapshot = encodeDeterministicStateSnapshot;
6
+ exports.decodeDeterministicStateSnapshot = decodeDeterministicStateSnapshot;
7
+ exports.bytesToBase64 = bytesToBase64;
8
+ exports.base64ToBytes = base64ToBytes;
9
+ exports.createSnapshotTransferFromBytes = createSnapshotTransferFromBytes;
10
+ exports.createSnapshotTransferCollector = createSnapshotTransferCollector;
11
+ const canonical_js_1 = require("./canonical.js");
12
+ /**
13
+ * Room-level snapshot identity, computable by the game-agnostic authority AND
14
+ * every client from the session-start parameters — the `descriptorHash` on
15
+ * snapshot wire envelopes. Game-build identity (`schemaHash`) is layered on
16
+ * top by clients, which know their schema ids; the authority passes it through
17
+ * opaquely.
18
+ */
19
+ function deterministicRoomDescriptorHash(params) {
20
+ return (0, canonical_js_1.defaultChecksum)({
21
+ kind: 'rdm-room-descriptor',
22
+ seed: params.seed,
23
+ playerCount: params.playerCount,
24
+ tickRateHz: params.tickRateHz,
25
+ hardToleranceTicks: params.hardToleranceTicks,
26
+ });
27
+ }
28
+ function encodeDeterministicStateSnapshot(snapshot) {
29
+ return (0, canonical_js_1.canonicalStringify)(snapshot);
30
+ }
31
+ function decodeDeterministicStateSnapshot(text) {
32
+ let parsed;
33
+ try {
34
+ parsed = JSON.parse(text);
35
+ }
36
+ catch {
37
+ return { ok: false, reason: 'invalid-json' };
38
+ }
39
+ if (!isRecord(parsed)) {
40
+ return { ok: false, reason: 'not-a-snapshot' };
41
+ }
42
+ if (parsed.v !== 1) {
43
+ return { ok: false, reason: 'unsupported-snapshot-version' };
44
+ }
45
+ if (!isNonNegativeInt(parsed.tick) ||
46
+ !isNonNegativeInt(parsed.sessionFrame) ||
47
+ parsed.sessionFrame !== parsed.tick + 1 ||
48
+ typeof parsed.stateBytes !== 'string' ||
49
+ !Number.isInteger(parsed.randomState) ||
50
+ typeof parsed.stateChecksum !== 'string' ||
51
+ parsed.stateChecksum.length === 0 ||
52
+ !Array.isArray(parsed.confirmedInputs)) {
53
+ return { ok: false, reason: 'malformed-snapshot' };
54
+ }
55
+ return {
56
+ ok: true,
57
+ snapshot: {
58
+ v: 1,
59
+ tick: parsed.tick,
60
+ sessionFrame: parsed.sessionFrame,
61
+ stateBytes: parsed.stateBytes,
62
+ randomState: parsed.randomState,
63
+ stateChecksum: parsed.stateChecksum,
64
+ confirmedInputs: (0, canonical_js_1.cloneCanonical)(parsed.confirmedInputs),
65
+ },
66
+ };
67
+ }
68
+ // ── base64 (browser-safe: no Buffer, no atob dependency) ──
69
+ const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
70
+ const BASE64_LOOKUP = new Map([...BASE64_ALPHABET].map((char, index) => [char, index]));
71
+ function bytesToBase64(bytes) {
72
+ let out = '';
73
+ for (let index = 0; index < bytes.length; index += 3) {
74
+ const b0 = bytes[index];
75
+ const b1 = index + 1 < bytes.length ? bytes[index + 1] : 0;
76
+ const b2 = index + 2 < bytes.length ? bytes[index + 2] : 0;
77
+ out += BASE64_ALPHABET[b0 >> 2];
78
+ out += BASE64_ALPHABET[((b0 & 0x03) << 4) | (b1 >> 4)];
79
+ out += index + 1 < bytes.length ? BASE64_ALPHABET[((b1 & 0x0f) << 2) | (b2 >> 6)] : '=';
80
+ out += index + 2 < bytes.length ? BASE64_ALPHABET[b2 & 0x3f] : '=';
81
+ }
82
+ return out;
83
+ }
84
+ function base64ToBytes(text) {
85
+ if (text.length % 4 !== 0) {
86
+ throw new Error('base64 length must be a multiple of 4');
87
+ }
88
+ const padding = text.endsWith('==') ? 2 : text.endsWith('=') ? 1 : 0;
89
+ const byteLength = (text.length / 4) * 3 - padding;
90
+ const bytes = new Uint8Array(byteLength);
91
+ let byteIndex = 0;
92
+ for (let index = 0; index < text.length; index += 4) {
93
+ const values = [];
94
+ for (let offset = 0; offset < 4; offset += 1) {
95
+ const char = text[index + offset];
96
+ if (char === '=') {
97
+ if (index + 4 < text.length || offset < 2) {
98
+ throw new Error('base64 padding is only valid at the end');
99
+ }
100
+ values.push(0);
101
+ continue;
102
+ }
103
+ const value = BASE64_LOOKUP.get(char);
104
+ if (value === undefined) {
105
+ throw new Error(`invalid base64 character ${JSON.stringify(char)}`);
106
+ }
107
+ values.push(value);
108
+ }
109
+ const triple = (values[0] << 18) | (values[1] << 12) | (values[2] << 6) | values[3];
110
+ if (byteIndex < byteLength)
111
+ bytes[byteIndex++] = (triple >> 16) & 0xff;
112
+ if (byteIndex < byteLength)
113
+ bytes[byteIndex++] = (triple >> 8) & 0xff;
114
+ if (byteIndex < byteLength)
115
+ bytes[byteIndex++] = triple & 0xff;
116
+ }
117
+ return bytes;
118
+ }
119
+ // ── transfer builder (raw bytes → frozen wire transfer shape) ──
120
+ exports.DEFAULT_SNAPSHOT_CHUNK_SIZE = 262_144;
121
+ exports.DEFAULT_MAX_SNAPSHOT_BYTES = 4_194_304;
122
+ function createSnapshotTransferFromBytes(snapshotBytes, options) {
123
+ const chunkSize = options.chunkSize ?? exports.DEFAULT_SNAPSHOT_CHUNK_SIZE;
124
+ if (!Number.isInteger(chunkSize) || chunkSize <= 0) {
125
+ throw new Error('Snapshot chunkSize must be a positive integer');
126
+ }
127
+ if (snapshotBytes.length === 0) {
128
+ throw new Error('Snapshot bytes must be non-empty');
129
+ }
130
+ const chunks = [];
131
+ for (let offset = 0; offset < snapshotBytes.length; offset += chunkSize) {
132
+ chunks.push(snapshotBytes.slice(offset, offset + chunkSize));
133
+ }
134
+ return {
135
+ transferId: options.transferId,
136
+ identity: (0, canonical_js_1.cloneCanonical)(options.identity),
137
+ frame: options.frame,
138
+ snapshotChecksum: (0, canonical_js_1.defaultChecksum)(snapshotBytes),
139
+ snapshotBytes,
140
+ chunkSize,
141
+ chunks,
142
+ chunkChecksums: chunks.map((chunk) => (0, canonical_js_1.defaultChecksum)(chunk)),
143
+ };
144
+ }
145
+ function createSnapshotTransferCollector(options = {}) {
146
+ const maxSnapshotBytes = options.maxSnapshotBytes ?? exports.DEFAULT_MAX_SNAPSHOT_BYTES;
147
+ const pending = new Map();
148
+ return {
149
+ offer(payload) {
150
+ const identity = parseIdentity(payload.identity);
151
+ if (typeof payload.transferId !== 'string' ||
152
+ payload.transferId.length === 0 ||
153
+ identity === undefined ||
154
+ !isNonNegativeInt(payload.frame) ||
155
+ typeof payload.snapshotChecksum !== 'string' ||
156
+ !isNonNegativeInt(payload.chunkCount) ||
157
+ payload.chunkCount < 1 ||
158
+ !isNonNegativeInt(payload.totalBytes) ||
159
+ payload.totalBytes < 1 ||
160
+ // Every chunk carries at least one character, so a chunkCount that
161
+ // exceeds totalBytes is self-contradictory (forged offer).
162
+ payload.chunkCount > payload.totalBytes ||
163
+ !isStringArray(payload.chunkChecksums) ||
164
+ payload.chunkChecksums.length !== payload.chunkCount) {
165
+ return { accepted: false, reason: 'malformed-snapshot-offer' };
166
+ }
167
+ if (payload.totalBytes > maxSnapshotBytes) {
168
+ return { accepted: false, reason: 'snapshot-too-large' };
169
+ }
170
+ pending.set(payload.transferId, {
171
+ transferId: payload.transferId,
172
+ identity,
173
+ frame: payload.frame,
174
+ snapshotChecksum: payload.snapshotChecksum,
175
+ chunkCount: payload.chunkCount,
176
+ totalBytes: payload.totalBytes,
177
+ chunkChecksums: [...payload.chunkChecksums],
178
+ chunks: new Map(),
179
+ });
180
+ return { accepted: true };
181
+ },
182
+ chunk(payload) {
183
+ if (typeof payload.transferId !== 'string') {
184
+ return { accepted: false, reason: 'malformed-snapshot-chunk' };
185
+ }
186
+ const transfer = pending.get(payload.transferId);
187
+ if (transfer === undefined) {
188
+ return { accepted: false, reason: 'unknown-snapshot-transfer' };
189
+ }
190
+ if (typeof payload.chunk !== 'string' || typeof payload.chunkChecksum !== 'string') {
191
+ return { accepted: false, reason: 'malformed-snapshot-chunk' };
192
+ }
193
+ const chunkIndex = payload.chunkIndex;
194
+ if (!isNonNegativeInt(chunkIndex) || chunkIndex >= transfer.chunkCount) {
195
+ return { accepted: false, reason: 'snapshot-chunk-index-out-of-range' };
196
+ }
197
+ if (transfer.chunks.has(chunkIndex)) {
198
+ return { accepted: false, reason: 'duplicate-snapshot-chunk' };
199
+ }
200
+ if (transfer.chunkChecksums[chunkIndex] !== payload.chunkChecksum ||
201
+ (0, canonical_js_1.defaultChecksum)(payload.chunk) !== payload.chunkChecksum) {
202
+ return { accepted: false, reason: 'snapshot-chunk-checksum-mismatch' };
203
+ }
204
+ transfer.chunks.set(chunkIndex, payload.chunk);
205
+ return { accepted: true };
206
+ },
207
+ complete(payload) {
208
+ if (typeof payload.transferId !== 'string') {
209
+ return { accepted: false, reason: 'malformed-snapshot-complete' };
210
+ }
211
+ const transfer = pending.get(payload.transferId);
212
+ if (transfer === undefined) {
213
+ return { accepted: false, reason: 'unknown-snapshot-transfer' };
214
+ }
215
+ if (transfer.chunks.size !== transfer.chunkCount) {
216
+ return { accepted: false, reason: 'missing-snapshot-chunk' };
217
+ }
218
+ const snapshotBytes = Array.from({ length: transfer.chunkCount }, (_, index) => transfer.chunks.get(index)).join('');
219
+ // A failed terminal check consumes the transfer either way — the bytes are bad.
220
+ pending.delete(payload.transferId);
221
+ if (snapshotBytes.length !== transfer.totalBytes) {
222
+ return { accepted: false, reason: 'snapshot-byte-length-mismatch' };
223
+ }
224
+ if ((0, canonical_js_1.defaultChecksum)(snapshotBytes) !== transfer.snapshotChecksum) {
225
+ return { accepted: false, reason: 'snapshot-transfer-checksum-mismatch' };
226
+ }
227
+ return {
228
+ accepted: true,
229
+ transfer: {
230
+ transferId: transfer.transferId,
231
+ identity: transfer.identity,
232
+ frame: transfer.frame,
233
+ snapshotChecksum: transfer.snapshotChecksum,
234
+ snapshotBytes,
235
+ },
236
+ };
237
+ },
238
+ abort(transferId) {
239
+ pending.delete(transferId);
240
+ },
241
+ get pendingTransferIds() {
242
+ return [...pending.keys()];
243
+ },
244
+ };
245
+ }
246
+ function parseIdentity(value) {
247
+ if (!isRecord(value) || typeof value.descriptorHash !== 'string' || value.descriptorHash.length === 0) {
248
+ return undefined;
249
+ }
250
+ return {
251
+ descriptorHash: value.descriptorHash,
252
+ ...(typeof value.schemaHash === 'string' ? { schemaHash: value.schemaHash } : {}),
253
+ ...(typeof value.assetDbHash === 'string' ? { assetDbHash: value.assetDbHash } : {}),
254
+ ...(typeof value.configHash === 'string' ? { configHash: value.configHash } : {}),
255
+ };
256
+ }
257
+ function isRecord(value) {
258
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
259
+ }
260
+ function isNonNegativeInt(value) {
261
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0;
262
+ }
263
+ function isStringArray(value) {
264
+ return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
265
+ }
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deterministicSessionWireVersion = void 0;
4
+ exports.encodeDeterministicSessionMessage = encodeDeterministicSessionMessage;
5
+ exports.decodeDeterministicSessionMessage = decodeDeterministicSessionMessage;
6
+ const canonical_js_1 = require("./canonical.js");
7
+ const local_authority_wire_js_1 = require("./local-authority-wire.js");
8
+ /**
9
+ * Frozen v1 session-control wire protocol for game-agnostic input-authority rooms (§8, M1).
10
+ *
11
+ * Covers the server/client roles for a live match: session-start (slot assignment + the
12
+ * server-owned RNG seed + tick params), clock/time-sync, input, confirmed-input (the verified
13
+ * tick), reconnect, spectator, and close/error. Snapshot request/offer/chunk/complete for
14
+ * late-join reuse the existing envelopes in `local-authority-wire.ts` (wired at M5).
15
+ *
16
+ * Both families share the `rdm-local-authority/v1` version namespace. Decoding is FAIL-CLOSED:
17
+ * unknown protocol, unknown version, unknown kind, malformed payload, or checksum mismatch are
18
+ * rejected with a reason — never silently coerced. This is a public forward-compat boundary; the
19
+ * discriminants below are frozen and must only be extended, never repurposed.
20
+ */
21
+ exports.deterministicSessionWireVersion = 1;
22
+ const sessionMessageKinds = new Set([
23
+ 'session-start',
24
+ 'time-sync-ping',
25
+ 'time-sync-pong',
26
+ 'input',
27
+ 'confirmed-input',
28
+ 'reconnect',
29
+ 'spectator-join',
30
+ 'checksum-report',
31
+ 'net-stats',
32
+ 'close',
33
+ 'error',
34
+ ]);
35
+ function encodeDeterministicSessionMessage(message) {
36
+ const { kind, ...rest } = message;
37
+ const payload = rest;
38
+ const envelope = {
39
+ protocol: local_authority_wire_js_1.deterministicLocalAuthorityWireProtocol,
40
+ v: exports.deterministicSessionWireVersion,
41
+ kind,
42
+ payload,
43
+ checksum: sessionChecksum(kind, payload),
44
+ };
45
+ return (0, canonical_js_1.canonicalStringify)(envelope);
46
+ }
47
+ function decodeDeterministicSessionMessage(text) {
48
+ let parsed;
49
+ try {
50
+ parsed = JSON.parse(text);
51
+ }
52
+ catch {
53
+ return { ok: false, reason: 'invalid-json' };
54
+ }
55
+ if (!isRecord(parsed)) {
56
+ return { ok: false, reason: 'not-an-envelope' };
57
+ }
58
+ if (parsed.protocol !== local_authority_wire_js_1.deterministicLocalAuthorityWireProtocol) {
59
+ return { ok: false, reason: 'unknown-protocol' };
60
+ }
61
+ if (parsed.v !== exports.deterministicSessionWireVersion) {
62
+ return { ok: false, reason: 'unknown-version' };
63
+ }
64
+ if (typeof parsed.kind !== 'string' || !sessionMessageKinds.has(parsed.kind)) {
65
+ return { ok: false, reason: 'unknown-kind' };
66
+ }
67
+ const kind = parsed.kind;
68
+ if (!isRecord(parsed.payload)) {
69
+ return { ok: false, reason: 'malformed-payload' };
70
+ }
71
+ // Symmetry with encode: the encoder checksummed the LIVE payload value and
72
+ // then canonical-encoded it, so sentinel values (-0, NaN, ±Infinity,
73
+ // undefined, bigint, Map/Set/typed arrays) sit on the wire as literal
74
+ // `{"$rdm":…}` objects. Decode them back to real values BEFORE recomputing
75
+ // the checksum — hashing the raw parse output would trip canonicalize's
76
+ // reserved-key guard and throw out of a decoder whose contract is
77
+ // fail-closed. Any malformed/hostile `$rdm` literal lands in the catch.
78
+ let payload;
79
+ let expectedChecksum;
80
+ try {
81
+ const decoded = (0, canonical_js_1.decodeCanonicalValue)(parsed.payload);
82
+ if (!isRecord(decoded)) {
83
+ return { ok: false, reason: 'malformed-payload' };
84
+ }
85
+ payload = decoded;
86
+ expectedChecksum = sessionChecksum(kind, payload);
87
+ }
88
+ catch {
89
+ return { ok: false, reason: 'malformed-payload' };
90
+ }
91
+ if (parsed.checksum !== expectedChecksum) {
92
+ return { ok: false, reason: 'checksum-mismatch' };
93
+ }
94
+ const message = parseSessionPayload(kind, payload);
95
+ if (message === undefined) {
96
+ return { ok: false, reason: 'malformed-payload' };
97
+ }
98
+ return { ok: true, message };
99
+ }
100
+ function parseSessionPayload(kind, payload) {
101
+ switch (kind) {
102
+ case 'session-start':
103
+ if (isInt(payload.slot) &&
104
+ isInt(payload.playerCount) &&
105
+ isInt(payload.seed) &&
106
+ isFiniteNumber(payload.tickRateHz) &&
107
+ isInt(payload.hardToleranceTicks) &&
108
+ isRole(payload.role)) {
109
+ return {
110
+ kind,
111
+ slot: payload.slot,
112
+ playerCount: payload.playerCount,
113
+ seed: payload.seed,
114
+ tickRateHz: payload.tickRateHz,
115
+ hardToleranceTicks: payload.hardToleranceTicks,
116
+ role: payload.role,
117
+ };
118
+ }
119
+ return undefined;
120
+ case 'time-sync-ping':
121
+ if (isInt(payload.clientSendTick) && isInt(payload.nonce)) {
122
+ return { kind, clientSendTick: payload.clientSendTick, nonce: payload.nonce };
123
+ }
124
+ return undefined;
125
+ case 'time-sync-pong':
126
+ if (isInt(payload.clientSendTick) && isInt(payload.nonce) && isInt(payload.authorityTick)) {
127
+ return { kind, clientSendTick: payload.clientSendTick, nonce: payload.nonce, authorityTick: payload.authorityTick };
128
+ }
129
+ return undefined;
130
+ case 'input':
131
+ if (isInt(payload.slot) && isInt(payload.tick) && isInt(payload.attempt) && payload.input !== undefined) {
132
+ return { kind, slot: payload.slot, tick: payload.tick, input: payload.input, attempt: payload.attempt };
133
+ }
134
+ return undefined;
135
+ case 'confirmed-input':
136
+ if (isInt(payload.authorityTick) && isConfirmedFrameArray(payload.frames)) {
137
+ return { kind, authorityTick: payload.authorityTick, frames: payload.frames };
138
+ }
139
+ return undefined;
140
+ case 'reconnect':
141
+ if (isInt(payload.slot) && isInt(payload.resumeFromTick)) {
142
+ return { kind, slot: payload.slot, resumeFromTick: payload.resumeFromTick };
143
+ }
144
+ return undefined;
145
+ case 'spectator-join':
146
+ return { kind };
147
+ case 'checksum-report':
148
+ if (isInt(payload.slot) && isInt(payload.tick) && typeof payload.checksum === 'string') {
149
+ return { kind, slot: payload.slot, tick: payload.tick, checksum: payload.checksum };
150
+ }
151
+ return undefined;
152
+ case 'net-stats':
153
+ if (isInt(payload.slot) &&
154
+ isInt(payload.tick) &&
155
+ isInt(payload.rttMs) &&
156
+ isInt(payload.rollbacks) &&
157
+ isInt(payload.maxRollbackDepth) &&
158
+ isInt(payload.timescalePermille) &&
159
+ isInt(payload.inputDelayTicks)) {
160
+ return {
161
+ kind,
162
+ slot: payload.slot,
163
+ tick: payload.tick,
164
+ rttMs: payload.rttMs,
165
+ rollbacks: payload.rollbacks,
166
+ maxRollbackDepth: payload.maxRollbackDepth,
167
+ timescalePermille: payload.timescalePermille,
168
+ inputDelayTicks: payload.inputDelayTicks,
169
+ };
170
+ }
171
+ return undefined;
172
+ case 'close':
173
+ if (typeof payload.code === 'string' && typeof payload.reason === 'string') {
174
+ return { kind, code: payload.code, reason: payload.reason };
175
+ }
176
+ return undefined;
177
+ case 'error':
178
+ if (typeof payload.code === 'string' && typeof payload.message === 'string') {
179
+ return { kind, code: payload.code, message: payload.message };
180
+ }
181
+ return undefined;
182
+ default:
183
+ return assertNever(kind);
184
+ }
185
+ }
186
+ function sessionChecksum(kind, payload) {
187
+ return String((0, canonical_js_1.defaultChecksum)({ protocol: local_authority_wire_js_1.deterministicLocalAuthorityWireProtocol, v: exports.deterministicSessionWireVersion, kind, payload }));
188
+ }
189
+ function isRecord(value) {
190
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
191
+ }
192
+ function isInt(value) {
193
+ return typeof value === 'number' && Number.isInteger(value);
194
+ }
195
+ function isFiniteNumber(value) {
196
+ return typeof value === 'number' && Number.isFinite(value);
197
+ }
198
+ function isRole(value) {
199
+ return value === 'player' || value === 'spectator';
200
+ }
201
+ function isConfirmedFrameArray(value) {
202
+ // The codec is stateless (it does not know playerCount), so it cannot check inputs.length — the
203
+ // consumer validates that against its own slot count. It CAN reject a frame whose substituted-slot
204
+ // indices are not integers (a string index survives JSON.parse and would corrupt slot handling).
205
+ return (Array.isArray(value) &&
206
+ value.every((frame) => isRecord(frame) &&
207
+ isInt(frame.tick) &&
208
+ Array.isArray(frame.inputs) &&
209
+ Array.isArray(frame.substitutedSlots) &&
210
+ frame.substitutedSlots.every((slot) => isInt(slot)) &&
211
+ typeof frame.checksum === 'string'));
212
+ }
213
+ function assertNever(value) {
214
+ void value;
215
+ return undefined;
216
+ }
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDeterministicSignalBus = createDeterministicSignalBus;
4
+ exports.runDeterministicSignalFixture = runDeterministicSignalFixture;
5
+ const canonical_js_1 = require("./canonical.js");
6
+ function createDeterministicSignalBus(subscriptions) {
7
+ const orderedSubscriptions = [...subscriptions].sort(compareSubscriptions);
8
+ const emitted = [];
9
+ const dispatched = [];
10
+ let sequence = 0;
11
+ return {
12
+ emit(signal) {
13
+ emitted.push({ sequence, signal: (0, canonical_js_1.cloneCanonical)(signal) });
14
+ sequence += 1;
15
+ },
16
+ dispatch(state, frame) {
17
+ let nextState = (0, canonical_js_1.cloneCanonical)(state);
18
+ const due = emitted
19
+ .filter((entry) => entry.signal.frame <= frame)
20
+ .sort(compareQueuedSignals);
21
+ for (const entry of due) {
22
+ for (const subscription of orderedSubscriptions) {
23
+ if (subscription.type === entry.signal.type) {
24
+ nextState = subscription.handle(nextState, entry.signal);
25
+ }
26
+ }
27
+ dispatched.push((0, canonical_js_1.cloneCanonical)(entry.signal));
28
+ }
29
+ const dueSequences = new Set(due.map((entry) => entry.sequence));
30
+ for (let index = emitted.length - 1; index >= 0; index -= 1) {
31
+ if (dueSequences.has(emitted[index].sequence)) {
32
+ emitted.splice(index, 1);
33
+ }
34
+ }
35
+ return nextState;
36
+ },
37
+ snapshot() {
38
+ const snapshot = {
39
+ emitted: emitted.sort(compareQueuedSignals).map((entry) => entry.signal),
40
+ dispatched,
41
+ sequence,
42
+ };
43
+ return (0, canonical_js_1.cloneCanonical)(snapshot);
44
+ },
45
+ restore(snapshot) {
46
+ emitted.length = 0;
47
+ snapshot.emitted.forEach((signal, index) => emitted.push({ sequence: index, signal: (0, canonical_js_1.cloneCanonical)(signal) }));
48
+ dispatched.length = 0;
49
+ snapshot.dispatched.forEach((signal) => dispatched.push((0, canonical_js_1.cloneCanonical)(signal)));
50
+ sequence = snapshot.sequence;
51
+ },
52
+ };
53
+ }
54
+ function runDeterministicSignalFixture() {
55
+ const subscriptions = [
56
+ {
57
+ id: 'score',
58
+ type: 'hit',
59
+ order: 20,
60
+ handle: (state, signal) => ({
61
+ ...state,
62
+ score: state.score + Number(signal.payload.points),
63
+ trace: [...state.trace, `score:${signal.payload.points}`],
64
+ }),
65
+ },
66
+ {
67
+ id: 'damage',
68
+ type: 'hit',
69
+ order: 10,
70
+ handle: (state, signal) => ({
71
+ ...state,
72
+ damage: state.damage + Number(signal.payload.damage),
73
+ trace: [...state.trace, `damage:${signal.payload.damage}`],
74
+ }),
75
+ },
76
+ ];
77
+ const bus = createDeterministicSignalBus(subscriptions);
78
+ const initial = { score: 0, damage: 0, trace: [] };
79
+ bus.emit({ frame: 2, type: 'hit', payload: { points: 3, damage: 1 } });
80
+ bus.emit({ frame: 1, type: 'hit', payload: { points: 5, damage: 2 } });
81
+ const snapshot = bus.snapshot();
82
+ const after = bus.dispatch(bus.dispatch(initial, 1), 2);
83
+ const checksum = (0, canonical_js_1.defaultChecksum)(after);
84
+ const restoredBus = createDeterministicSignalBus(subscriptions);
85
+ restoredBus.restore(snapshot);
86
+ const restored = restoredBus.dispatch(restoredBus.dispatch(initial, 1), 2);
87
+ return {
88
+ checksum,
89
+ restoredChecksum: (0, canonical_js_1.defaultChecksum)(restored),
90
+ dispatchedSignals: bus.snapshot().dispatched.length,
91
+ subscriptionOrderStable: after.trace.join('|') === 'damage:2|score:5|damage:1|score:3',
92
+ rollbackEquivalent: checksum === (0, canonical_js_1.defaultChecksum)(restored),
93
+ };
94
+ }
95
+ function compareSubscriptions(left, right) {
96
+ return left.order - right.order || compareStrings(left.id, right.id);
97
+ }
98
+ function compareQueuedSignals(left, right) {
99
+ return left.signal.frame - right.signal.frame || left.sequence - right.sequence;
100
+ }
101
+ function compareStrings(left, right) {
102
+ return left < right ? -1 : left > right ? 1 : 0;
103
+ }