@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,99 @@
1
+ import { cloneCanonical, defaultChecksum } from './canonical.js';
2
+ export function createDeterministicSignalBus(subscriptions) {
3
+ const orderedSubscriptions = [...subscriptions].sort(compareSubscriptions);
4
+ const emitted = [];
5
+ const dispatched = [];
6
+ let sequence = 0;
7
+ return {
8
+ emit(signal) {
9
+ emitted.push({ sequence, signal: cloneCanonical(signal) });
10
+ sequence += 1;
11
+ },
12
+ dispatch(state, frame) {
13
+ let nextState = cloneCanonical(state);
14
+ const due = emitted
15
+ .filter((entry) => entry.signal.frame <= frame)
16
+ .sort(compareQueuedSignals);
17
+ for (const entry of due) {
18
+ for (const subscription of orderedSubscriptions) {
19
+ if (subscription.type === entry.signal.type) {
20
+ nextState = subscription.handle(nextState, entry.signal);
21
+ }
22
+ }
23
+ dispatched.push(cloneCanonical(entry.signal));
24
+ }
25
+ const dueSequences = new Set(due.map((entry) => entry.sequence));
26
+ for (let index = emitted.length - 1; index >= 0; index -= 1) {
27
+ if (dueSequences.has(emitted[index].sequence)) {
28
+ emitted.splice(index, 1);
29
+ }
30
+ }
31
+ return nextState;
32
+ },
33
+ snapshot() {
34
+ const snapshot = {
35
+ emitted: emitted.sort(compareQueuedSignals).map((entry) => entry.signal),
36
+ dispatched,
37
+ sequence,
38
+ };
39
+ return cloneCanonical(snapshot);
40
+ },
41
+ restore(snapshot) {
42
+ emitted.length = 0;
43
+ snapshot.emitted.forEach((signal, index) => emitted.push({ sequence: index, signal: cloneCanonical(signal) }));
44
+ dispatched.length = 0;
45
+ snapshot.dispatched.forEach((signal) => dispatched.push(cloneCanonical(signal)));
46
+ sequence = snapshot.sequence;
47
+ },
48
+ };
49
+ }
50
+ export function runDeterministicSignalFixture() {
51
+ const subscriptions = [
52
+ {
53
+ id: 'score',
54
+ type: 'hit',
55
+ order: 20,
56
+ handle: (state, signal) => ({
57
+ ...state,
58
+ score: state.score + Number(signal.payload.points),
59
+ trace: [...state.trace, `score:${signal.payload.points}`],
60
+ }),
61
+ },
62
+ {
63
+ id: 'damage',
64
+ type: 'hit',
65
+ order: 10,
66
+ handle: (state, signal) => ({
67
+ ...state,
68
+ damage: state.damage + Number(signal.payload.damage),
69
+ trace: [...state.trace, `damage:${signal.payload.damage}`],
70
+ }),
71
+ },
72
+ ];
73
+ const bus = createDeterministicSignalBus(subscriptions);
74
+ const initial = { score: 0, damage: 0, trace: [] };
75
+ bus.emit({ frame: 2, type: 'hit', payload: { points: 3, damage: 1 } });
76
+ bus.emit({ frame: 1, type: 'hit', payload: { points: 5, damage: 2 } });
77
+ const snapshot = bus.snapshot();
78
+ const after = bus.dispatch(bus.dispatch(initial, 1), 2);
79
+ const checksum = defaultChecksum(after);
80
+ const restoredBus = createDeterministicSignalBus(subscriptions);
81
+ restoredBus.restore(snapshot);
82
+ const restored = restoredBus.dispatch(restoredBus.dispatch(initial, 1), 2);
83
+ return {
84
+ checksum,
85
+ restoredChecksum: defaultChecksum(restored),
86
+ dispatchedSignals: bus.snapshot().dispatched.length,
87
+ subscriptionOrderStable: after.trace.join('|') === 'damage:2|score:5|damage:1|score:3',
88
+ rollbackEquivalent: checksum === defaultChecksum(restored),
89
+ };
90
+ }
91
+ function compareSubscriptions(left, right) {
92
+ return left.order - right.order || compareStrings(left.id, right.id);
93
+ }
94
+ function compareQueuedSignals(left, right) {
95
+ return left.signal.frame - right.signal.frame || left.sequence - right.sequence;
96
+ }
97
+ function compareStrings(left, right) {
98
+ return left < right ? -1 : left > right ? 1 : 0;
99
+ }
@@ -0,0 +1,34 @@
1
+ export interface DeterministicSparseSetSnapshot<T> {
2
+ readonly capacity: number;
3
+ readonly dense: readonly number[];
4
+ readonly values: readonly T[];
5
+ readonly sparseEntries: readonly {
6
+ readonly id: number;
7
+ readonly denseIndex: number;
8
+ }[];
9
+ }
10
+ export interface DeterministicSparseSet<T> {
11
+ add(id: number, value: T): void;
12
+ remove(id: number): boolean;
13
+ has(id: number): boolean;
14
+ get(id: number): T | undefined;
15
+ set(id: number, value: T): void;
16
+ query(): readonly {
17
+ readonly id: number;
18
+ readonly value: T;
19
+ }[];
20
+ snapshot(): DeterministicSparseSetSnapshot<T>;
21
+ restore(snapshot: DeterministicSparseSetSnapshot<T>): void;
22
+ checksum(): string;
23
+ }
24
+ export interface DeterministicSparseSetFixtureResult {
25
+ readonly checksum: string;
26
+ readonly restoredChecksum: string;
27
+ readonly queryOrderStable: boolean;
28
+ readonly denseCount: number;
29
+ readonly rejectedDuplicate: boolean;
30
+ readonly rejectedMalformedSnapshot: boolean;
31
+ readonly removed: number;
32
+ }
33
+ export declare function createDeterministicSparseSet<T>(capacity?: number): DeterministicSparseSet<T>;
34
+ export declare function runDeterministicSparseSetFixture(): DeterministicSparseSetFixtureResult;
@@ -0,0 +1,165 @@
1
+ import { cloneCanonical, defaultChecksum } from './canonical.js';
2
+ export function createDeterministicSparseSet(capacity = 1024) {
3
+ const dense = [];
4
+ const values = [];
5
+ const sparse = new Map();
6
+ function assertId(id) {
7
+ if (!Number.isInteger(id) || id < 0 || id >= capacity) {
8
+ throw new Error(`Sparse-set id ${id} outside capacity ${capacity}`);
9
+ }
10
+ }
11
+ return {
12
+ add(id, value) {
13
+ assertId(id);
14
+ if (sparse.has(id)) {
15
+ throw new Error(`Sparse-set duplicate id ${id}`);
16
+ }
17
+ sparse.set(id, dense.length);
18
+ dense.push(id);
19
+ values.push(cloneCanonical(value));
20
+ },
21
+ remove(id) {
22
+ assertId(id);
23
+ const index = sparse.get(id);
24
+ if (index === undefined) {
25
+ return false;
26
+ }
27
+ const lastIndex = dense.length - 1;
28
+ const lastId = dense[lastIndex];
29
+ dense[index] = lastId;
30
+ values[index] = values[lastIndex];
31
+ sparse.set(lastId, index);
32
+ dense.pop();
33
+ values.pop();
34
+ sparse.delete(id);
35
+ return true;
36
+ },
37
+ has(id) {
38
+ assertId(id);
39
+ return sparse.has(id);
40
+ },
41
+ get(id) {
42
+ assertId(id);
43
+ const index = sparse.get(id);
44
+ return index === undefined ? undefined : cloneCanonical(values[index]);
45
+ },
46
+ set(id, value) {
47
+ assertId(id);
48
+ const index = sparse.get(id);
49
+ if (index === undefined) {
50
+ throw new Error(`Sparse-set missing id ${id}`);
51
+ }
52
+ values[index] = cloneCanonical(value);
53
+ },
54
+ query() {
55
+ return dense.map((id, index) => ({ id, value: cloneCanonical(values[index]) }))
56
+ .sort((left, right) => left.id - right.id);
57
+ },
58
+ snapshot() {
59
+ return cloneCanonical({
60
+ capacity,
61
+ dense,
62
+ values,
63
+ sparseEntries: [...sparse.entries()]
64
+ .map(([id, denseIndex]) => ({ id, denseIndex }))
65
+ .sort((left, right) => left.id - right.id),
66
+ });
67
+ },
68
+ restore(snapshot) {
69
+ if (snapshot.capacity !== capacity) {
70
+ throw new Error(`Sparse-set capacity mismatch ${snapshot.capacity} !== ${capacity}`);
71
+ }
72
+ validateSparseSetSnapshot(snapshot, capacity);
73
+ dense.length = 0;
74
+ values.length = 0;
75
+ sparse.clear();
76
+ snapshot.dense.forEach((id, index) => {
77
+ dense.push(id);
78
+ values.push(cloneCanonical(snapshot.values[index]));
79
+ sparse.set(id, index);
80
+ });
81
+ },
82
+ checksum() {
83
+ return defaultChecksum(this.snapshot());
84
+ },
85
+ };
86
+ }
87
+ export function runDeterministicSparseSetFixture() {
88
+ const set = createDeterministicSparseSet(512);
89
+ let rejectedDuplicate = false;
90
+ let removed = 0;
91
+ for (let index = 0; index < 256; index += 1) {
92
+ set.add((index * 37) % 512, { hp: 100 - (index % 17), team: index % 4 });
93
+ }
94
+ try {
95
+ set.add(0, { hp: 1, team: 1 });
96
+ }
97
+ catch {
98
+ rejectedDuplicate = true;
99
+ }
100
+ for (let id = 0; id < 512; id += 5) {
101
+ removed += set.remove(id) ? 1 : 0;
102
+ }
103
+ for (const entry of set.query()) {
104
+ set.set(entry.id, { hp: entry.value.hp + 1, team: entry.value.team });
105
+ }
106
+ const checksum = set.checksum();
107
+ const firstQuery = set.query().map((entry) => entry.id);
108
+ const snapshot = set.snapshot();
109
+ const restored = createDeterministicSparseSet(512);
110
+ restored.restore(snapshot);
111
+ let rejectedMalformedSnapshot = false;
112
+ try {
113
+ restored.restore({ ...snapshot, sparseEntries: snapshot.sparseEntries.slice(1) });
114
+ }
115
+ catch {
116
+ rejectedMalformedSnapshot = true;
117
+ }
118
+ return {
119
+ checksum,
120
+ restoredChecksum: restored.checksum(),
121
+ queryOrderStable: defaultChecksum(firstQuery) === defaultChecksum(restored.query().map((entry) => entry.id)),
122
+ denseCount: snapshot.dense.length,
123
+ rejectedDuplicate,
124
+ rejectedMalformedSnapshot,
125
+ removed,
126
+ };
127
+ }
128
+ function validateSparseSetSnapshot(snapshot, capacity) {
129
+ if (snapshot.dense.length !== snapshot.values.length) {
130
+ throw new Error(`Sparse-set snapshot dense/value length mismatch ${snapshot.dense.length} !== ${snapshot.values.length}`);
131
+ }
132
+ if (snapshot.sparseEntries.length !== snapshot.dense.length) {
133
+ throw new Error(`Sparse-set snapshot sparse/dense length mismatch ${snapshot.sparseEntries.length} !== ${snapshot.dense.length}`);
134
+ }
135
+ const denseIds = new Set();
136
+ for (const id of snapshot.dense) {
137
+ if (!Number.isInteger(id) || id < 0 || id >= capacity) {
138
+ throw new Error(`Sparse-set snapshot id ${id} outside capacity ${capacity}`);
139
+ }
140
+ if (denseIds.has(id)) {
141
+ throw new Error(`Sparse-set snapshot duplicate dense id ${id}`);
142
+ }
143
+ denseIds.add(id);
144
+ }
145
+ const sparseIds = new Set();
146
+ const sparseById = new Map();
147
+ for (const entry of snapshot.sparseEntries) {
148
+ if (!Number.isInteger(entry.id) || entry.id < 0 || entry.id >= capacity) {
149
+ throw new Error(`Sparse-set snapshot sparse id ${entry.id} outside capacity ${capacity}`);
150
+ }
151
+ if (!Number.isInteger(entry.denseIndex) || entry.denseIndex < 0 || entry.denseIndex >= snapshot.dense.length) {
152
+ throw new Error(`Sparse-set snapshot dense index ${entry.denseIndex} outside dense length ${snapshot.dense.length}`);
153
+ }
154
+ if (sparseIds.has(entry.id)) {
155
+ throw new Error(`Sparse-set snapshot duplicate sparse id ${entry.id}`);
156
+ }
157
+ sparseIds.add(entry.id);
158
+ sparseById.set(entry.id, entry.denseIndex);
159
+ }
160
+ for (const [index, id] of snapshot.dense.entries()) {
161
+ if (sparseById.get(id) !== index) {
162
+ throw new Error(`Sparse-set snapshot sparse entry mismatch for id ${id}`);
163
+ }
164
+ }
165
+ }
@@ -0,0 +1,31 @@
1
+ import { DeterministicRandom } from './random.js';
2
+ import type { DeterministicSessionConfig } from './types.js';
3
+ export type SyncplaySynctestPhase = 'straight' | 'rollback' | 'replay';
4
+ export type SyncplaySynctestInputFuzz<Input> = (random: DeterministicRandom, slot: number, frame: number) => Input;
5
+ export interface SyncplaySynctestConfig<State, Input> extends DeterministicSessionConfig<State, Input> {
6
+ readonly frames?: number;
7
+ readonly seeds?: readonly number[];
8
+ readonly inputFuzz?: SyncplaySynctestInputFuzz<Input>;
9
+ }
10
+ export interface SyncplaySynctestDivergence {
11
+ readonly seed: number;
12
+ readonly frame: number;
13
+ readonly phase: SyncplaySynctestPhase;
14
+ readonly straightChecksum: string;
15
+ readonly rollbackChecksum: string;
16
+ }
17
+ export interface SyncplaySynctestResult {
18
+ readonly ok: boolean;
19
+ readonly seedsRun: number;
20
+ readonly framesChecked: number;
21
+ readonly firstDivergence?: SyncplaySynctestDivergence;
22
+ }
23
+ /**
24
+ * GGPO-style offline synctest: for every seed, runs the game straight through
25
+ * and again with a rollback (snapshot → step → restore → re-step) on EVERY
26
+ * frame, comparing per-frame checksums, then re-verifies the exported replay
27
+ * from scratch. Catches logic nondeterminism (stray Math.random, iteration
28
+ * order dependence, restore-sensitive state) with no networking involved.
29
+ * Divergence is reported in the result, never thrown; config errors throw.
30
+ */
31
+ export declare function runSyncplaySynctest<State, Input>(config: SyncplaySynctestConfig<State, Input>): SyncplaySynctestResult;
@@ -0,0 +1,133 @@
1
+ import { stableSortedKeys } from './collections.js';
2
+ import { SyncplayError, invariant } from './errors.js';
3
+ import { createOfflineSession } from './offline-session.js';
4
+ import { DeterministicRandom } from './random.js';
5
+ import { verifyReplay } from './replay.js';
6
+ /**
7
+ * GGPO-style offline synctest: for every seed, runs the game straight through
8
+ * and again with a rollback (snapshot → step → restore → re-step) on EVERY
9
+ * frame, comparing per-frame checksums, then re-verifies the exported replay
10
+ * from scratch. Catches logic nondeterminism (stray Math.random, iteration
11
+ * order dependence, restore-sensitive state) with no networking involved.
12
+ * Divergence is reported in the result, never thrown; config errors throw.
13
+ */
14
+ export function runSyncplaySynctest(config) {
15
+ const { frames: framesOption, seeds: seedsOption, inputFuzz: inputFuzzOption, ...sessionBase } = config;
16
+ const frames = framesOption ?? 300;
17
+ const seeds = seedsOption ?? [1, 2, 3];
18
+ invariant(Number.isInteger(frames) && frames >= 1, 'synctest frames must be a positive integer', 'synctest.frames');
19
+ invariant(seeds.length > 0 && seeds.every((seed) => Number.isInteger(seed)), 'synctest seeds must be a non-empty list of integers', 'synctest.seeds');
20
+ const inputFuzz = inputFuzzOption ?? createDefaultInputFuzz(config.defaultInput);
21
+ let seedsRun = 0;
22
+ let framesChecked = 0;
23
+ for (const seed of seeds) {
24
+ seedsRun += 1;
25
+ let lastChecksum;
26
+ const sessionConfig = {
27
+ ...sessionBase,
28
+ randomSeed: seed,
29
+ checksumIntervalFrames: 1,
30
+ replayRecordingMode: 'memory',
31
+ onChecksumRecorded(checksum) {
32
+ lastChecksum = checksum;
33
+ sessionBase.onChecksumRecorded?.(checksum);
34
+ },
35
+ };
36
+ const straight = createOfflineSession(sessionConfig);
37
+ const schedule = buildInputSchedule(config.playerCount, frames, seed, inputFuzz);
38
+ const straightChecksums = [];
39
+ for (let frame = 0; frame < frames; frame += 1) {
40
+ applyScheduledInputs(straight, schedule[frame]);
41
+ straight.stepFrames(1);
42
+ straightChecksums.push(takeChecksum(lastChecksum, straight.currentFrame));
43
+ }
44
+ const replay = straight.exportReplay();
45
+ const rollback = createOfflineSession(sessionConfig);
46
+ for (let frame = 0; frame < frames; frame += 1) {
47
+ applyScheduledInputs(rollback, schedule[frame]);
48
+ rollback.stepFrames(1);
49
+ const firstPass = takeChecksum(lastChecksum, rollback.currentFrame);
50
+ rollback.restoreToFrame(frame);
51
+ applyScheduledInputs(rollback, schedule[frame]);
52
+ rollback.stepFrames(1);
53
+ const secondPass = takeChecksum(lastChecksum, rollback.currentFrame);
54
+ framesChecked += 1;
55
+ const straightChecksum = straightChecksums[frame];
56
+ if (firstPass !== straightChecksum) {
57
+ return diverged(seedsRun, framesChecked, {
58
+ seed,
59
+ frame: frame + 1,
60
+ phase: 'straight',
61
+ straightChecksum,
62
+ rollbackChecksum: firstPass,
63
+ });
64
+ }
65
+ if (secondPass !== straightChecksum) {
66
+ return diverged(seedsRun, framesChecked, {
67
+ seed,
68
+ frame: frame + 1,
69
+ phase: 'rollback',
70
+ straightChecksum,
71
+ rollbackChecksum: secondPass,
72
+ });
73
+ }
74
+ }
75
+ const replayResult = verifyReplay({ ...sessionBase, replay, throwOnMismatch: false });
76
+ if (replayResult.firstMismatch !== undefined) {
77
+ return diverged(seedsRun, framesChecked, {
78
+ seed,
79
+ frame: replayResult.firstMismatch.frame,
80
+ phase: 'replay',
81
+ straightChecksum: replayResult.firstMismatch.expectedChecksum,
82
+ rollbackChecksum: replayResult.firstMismatch.actualChecksum,
83
+ });
84
+ }
85
+ }
86
+ return { ok: true, seedsRun, framesChecked };
87
+ }
88
+ function diverged(seedsRun, framesChecked, firstDivergence) {
89
+ return { ok: false, seedsRun, framesChecked, firstDivergence };
90
+ }
91
+ function takeChecksum(lastChecksum, frame) {
92
+ if (lastChecksum === undefined || lastChecksum.frame !== frame) {
93
+ throw new SyncplayError(`Synctest expected a checksum record for frame ${frame}`, {
94
+ code: 'synctest.missing-checksum',
95
+ frame,
96
+ });
97
+ }
98
+ return lastChecksum.checksum;
99
+ }
100
+ function buildInputSchedule(playerCount, frames, seed, inputFuzz) {
101
+ const random = new DeterministicRandom(seed >>> 0).fork('synctest.input-fuzz');
102
+ const schedule = [];
103
+ for (let frame = 0; frame < frames; frame += 1) {
104
+ schedule.push(Array.from({ length: playerCount }, (_, slot) => inputFuzz(random, slot, frame)));
105
+ }
106
+ return schedule;
107
+ }
108
+ function applyScheduledInputs(session, inputs) {
109
+ inputs.forEach((input, slot) => session.setInput(slot, input));
110
+ }
111
+ function createDefaultInputFuzz(defaultInput) {
112
+ return (random) => fuzzFromTemplate(defaultInput, random);
113
+ }
114
+ function fuzzFromTemplate(template, random) {
115
+ if (typeof template === 'boolean') {
116
+ return random.nextInt(0, 1) === 1;
117
+ }
118
+ if (typeof template === 'number') {
119
+ return random.nextInt(-1, 1);
120
+ }
121
+ if (Array.isArray(template)) {
122
+ return template.map((item) => fuzzFromTemplate(item, random));
123
+ }
124
+ if (template !== null && typeof template === 'object' && Object.getPrototypeOf(template) === Object.prototype) {
125
+ const record = template;
126
+ const fuzzed = {};
127
+ for (const key of stableSortedKeys(record)) {
128
+ fuzzed[key] = fuzzFromTemplate(record[key], random);
129
+ }
130
+ return fuzzed;
131
+ }
132
+ return template;
133
+ }
@@ -0,0 +1,72 @@
1
+ import { type CanonicalValue } from './canonical.js';
2
+ export interface DeterministicLifecycleSystemDescriptor {
3
+ readonly id: string;
4
+ readonly order: number;
5
+ readonly startEnabled?: boolean;
6
+ readonly parentId?: string;
7
+ readonly signalsOnly?: boolean;
8
+ }
9
+ export interface DeterministicLifecycleSystemHooks {
10
+ readonly onInit?: (ctx: DeterministicLifecycleSystemContext) => void;
11
+ readonly onEnabled?: (ctx: DeterministicLifecycleSystemContext) => void;
12
+ readonly onDisabled?: (ctx: DeterministicLifecycleSystemContext) => void;
13
+ readonly update?: (ctx: DeterministicLifecycleSystemContext) => void;
14
+ readonly signal?: (ctx: DeterministicLifecycleSystemContext, signal: DeterministicLifecycleSignal) => void;
15
+ }
16
+ export interface DeterministicLifecycleSignal {
17
+ readonly id: string;
18
+ readonly payload: CanonicalValue;
19
+ }
20
+ export interface DeterministicLifecycleSystemContext {
21
+ readonly frame: number;
22
+ readonly systemId: string;
23
+ emit(event: string): void;
24
+ increment(key: string, amount?: number): void;
25
+ signal(signal: DeterministicLifecycleSignal): void;
26
+ }
27
+ export interface DeterministicLifecycleSnapshot {
28
+ readonly frame: number;
29
+ readonly initialized: readonly string[];
30
+ readonly enabled: readonly string[];
31
+ readonly counters: Readonly<Record<string, number>>;
32
+ readonly pendingSignals: readonly DeterministicLifecycleSignal[];
33
+ readonly log: readonly string[];
34
+ readonly checksum: string;
35
+ }
36
+ export interface DeterministicLifecycleRunner {
37
+ readonly frame: number;
38
+ enable(systemId: string): void;
39
+ disable(systemId: string): void;
40
+ signal(signal: DeterministicLifecycleSignal): void;
41
+ step(): void;
42
+ snapshot(): DeterministicLifecycleSnapshot;
43
+ restore(snapshot: DeterministicLifecycleSnapshot): void;
44
+ }
45
+ export interface DeterministicLifecycleFixtureResult {
46
+ readonly checksum: string;
47
+ readonly restoredChecksum: string;
48
+ readonly rollbackEquivalent: boolean;
49
+ readonly initCalls: number;
50
+ readonly updateCalls: number;
51
+ readonly enabledCalls: number;
52
+ readonly disabledCalls: number;
53
+ readonly signalCalls: number;
54
+ readonly hierarchySkippedUpdates: number;
55
+ readonly startDisabledCovered: boolean;
56
+ readonly signalOnlySkippedUpdate: boolean;
57
+ readonly orderStable: boolean;
58
+ }
59
+ export interface DeterministicLifecycleCounterSummary {
60
+ readonly initCalls: number;
61
+ readonly updateCalls: number;
62
+ readonly enabledCalls: number;
63
+ readonly disabledCalls: number;
64
+ readonly signalCalls: number;
65
+ readonly hierarchySkippedUpdates: number;
66
+ readonly startDisabledCovered: boolean;
67
+ readonly signalOnlySkippedUpdate: boolean;
68
+ readonly orderStable: boolean;
69
+ }
70
+ export declare function createDeterministicLifecycleRunner(descriptors: readonly DeterministicLifecycleSystemDescriptor[], hooks: Readonly<Record<string, DeterministicLifecycleSystemHooks>>): DeterministicLifecycleRunner;
71
+ export declare function runDeterministicLifecycleFixture(frames?: number): DeterministicLifecycleFixtureResult;
72
+ export declare function summarizeDeterministicLifecycleCounters(counters: Readonly<Record<string, number>>, log: readonly string[]): DeterministicLifecycleCounterSummary;