@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,169 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDeterministicSparseSet = createDeterministicSparseSet;
4
+ exports.runDeterministicSparseSetFixture = runDeterministicSparseSetFixture;
5
+ const canonical_js_1 = require("./canonical.js");
6
+ function createDeterministicSparseSet(capacity = 1024) {
7
+ const dense = [];
8
+ const values = [];
9
+ const sparse = new Map();
10
+ function assertId(id) {
11
+ if (!Number.isInteger(id) || id < 0 || id >= capacity) {
12
+ throw new Error(`Sparse-set id ${id} outside capacity ${capacity}`);
13
+ }
14
+ }
15
+ return {
16
+ add(id, value) {
17
+ assertId(id);
18
+ if (sparse.has(id)) {
19
+ throw new Error(`Sparse-set duplicate id ${id}`);
20
+ }
21
+ sparse.set(id, dense.length);
22
+ dense.push(id);
23
+ values.push((0, canonical_js_1.cloneCanonical)(value));
24
+ },
25
+ remove(id) {
26
+ assertId(id);
27
+ const index = sparse.get(id);
28
+ if (index === undefined) {
29
+ return false;
30
+ }
31
+ const lastIndex = dense.length - 1;
32
+ const lastId = dense[lastIndex];
33
+ dense[index] = lastId;
34
+ values[index] = values[lastIndex];
35
+ sparse.set(lastId, index);
36
+ dense.pop();
37
+ values.pop();
38
+ sparse.delete(id);
39
+ return true;
40
+ },
41
+ has(id) {
42
+ assertId(id);
43
+ return sparse.has(id);
44
+ },
45
+ get(id) {
46
+ assertId(id);
47
+ const index = sparse.get(id);
48
+ return index === undefined ? undefined : (0, canonical_js_1.cloneCanonical)(values[index]);
49
+ },
50
+ set(id, value) {
51
+ assertId(id);
52
+ const index = sparse.get(id);
53
+ if (index === undefined) {
54
+ throw new Error(`Sparse-set missing id ${id}`);
55
+ }
56
+ values[index] = (0, canonical_js_1.cloneCanonical)(value);
57
+ },
58
+ query() {
59
+ return dense.map((id, index) => ({ id, value: (0, canonical_js_1.cloneCanonical)(values[index]) }))
60
+ .sort((left, right) => left.id - right.id);
61
+ },
62
+ snapshot() {
63
+ return (0, canonical_js_1.cloneCanonical)({
64
+ capacity,
65
+ dense,
66
+ values,
67
+ sparseEntries: [...sparse.entries()]
68
+ .map(([id, denseIndex]) => ({ id, denseIndex }))
69
+ .sort((left, right) => left.id - right.id),
70
+ });
71
+ },
72
+ restore(snapshot) {
73
+ if (snapshot.capacity !== capacity) {
74
+ throw new Error(`Sparse-set capacity mismatch ${snapshot.capacity} !== ${capacity}`);
75
+ }
76
+ validateSparseSetSnapshot(snapshot, capacity);
77
+ dense.length = 0;
78
+ values.length = 0;
79
+ sparse.clear();
80
+ snapshot.dense.forEach((id, index) => {
81
+ dense.push(id);
82
+ values.push((0, canonical_js_1.cloneCanonical)(snapshot.values[index]));
83
+ sparse.set(id, index);
84
+ });
85
+ },
86
+ checksum() {
87
+ return (0, canonical_js_1.defaultChecksum)(this.snapshot());
88
+ },
89
+ };
90
+ }
91
+ function runDeterministicSparseSetFixture() {
92
+ const set = createDeterministicSparseSet(512);
93
+ let rejectedDuplicate = false;
94
+ let removed = 0;
95
+ for (let index = 0; index < 256; index += 1) {
96
+ set.add((index * 37) % 512, { hp: 100 - (index % 17), team: index % 4 });
97
+ }
98
+ try {
99
+ set.add(0, { hp: 1, team: 1 });
100
+ }
101
+ catch {
102
+ rejectedDuplicate = true;
103
+ }
104
+ for (let id = 0; id < 512; id += 5) {
105
+ removed += set.remove(id) ? 1 : 0;
106
+ }
107
+ for (const entry of set.query()) {
108
+ set.set(entry.id, { hp: entry.value.hp + 1, team: entry.value.team });
109
+ }
110
+ const checksum = set.checksum();
111
+ const firstQuery = set.query().map((entry) => entry.id);
112
+ const snapshot = set.snapshot();
113
+ const restored = createDeterministicSparseSet(512);
114
+ restored.restore(snapshot);
115
+ let rejectedMalformedSnapshot = false;
116
+ try {
117
+ restored.restore({ ...snapshot, sparseEntries: snapshot.sparseEntries.slice(1) });
118
+ }
119
+ catch {
120
+ rejectedMalformedSnapshot = true;
121
+ }
122
+ return {
123
+ checksum,
124
+ restoredChecksum: restored.checksum(),
125
+ queryOrderStable: (0, canonical_js_1.defaultChecksum)(firstQuery) === (0, canonical_js_1.defaultChecksum)(restored.query().map((entry) => entry.id)),
126
+ denseCount: snapshot.dense.length,
127
+ rejectedDuplicate,
128
+ rejectedMalformedSnapshot,
129
+ removed,
130
+ };
131
+ }
132
+ function validateSparseSetSnapshot(snapshot, capacity) {
133
+ if (snapshot.dense.length !== snapshot.values.length) {
134
+ throw new Error(`Sparse-set snapshot dense/value length mismatch ${snapshot.dense.length} !== ${snapshot.values.length}`);
135
+ }
136
+ if (snapshot.sparseEntries.length !== snapshot.dense.length) {
137
+ throw new Error(`Sparse-set snapshot sparse/dense length mismatch ${snapshot.sparseEntries.length} !== ${snapshot.dense.length}`);
138
+ }
139
+ const denseIds = new Set();
140
+ for (const id of snapshot.dense) {
141
+ if (!Number.isInteger(id) || id < 0 || id >= capacity) {
142
+ throw new Error(`Sparse-set snapshot id ${id} outside capacity ${capacity}`);
143
+ }
144
+ if (denseIds.has(id)) {
145
+ throw new Error(`Sparse-set snapshot duplicate dense id ${id}`);
146
+ }
147
+ denseIds.add(id);
148
+ }
149
+ const sparseIds = new Set();
150
+ const sparseById = new Map();
151
+ for (const entry of snapshot.sparseEntries) {
152
+ if (!Number.isInteger(entry.id) || entry.id < 0 || entry.id >= capacity) {
153
+ throw new Error(`Sparse-set snapshot sparse id ${entry.id} outside capacity ${capacity}`);
154
+ }
155
+ if (!Number.isInteger(entry.denseIndex) || entry.denseIndex < 0 || entry.denseIndex >= snapshot.dense.length) {
156
+ throw new Error(`Sparse-set snapshot dense index ${entry.denseIndex} outside dense length ${snapshot.dense.length}`);
157
+ }
158
+ if (sparseIds.has(entry.id)) {
159
+ throw new Error(`Sparse-set snapshot duplicate sparse id ${entry.id}`);
160
+ }
161
+ sparseIds.add(entry.id);
162
+ sparseById.set(entry.id, entry.denseIndex);
163
+ }
164
+ for (const [index, id] of snapshot.dense.entries()) {
165
+ if (sparseById.get(id) !== index) {
166
+ throw new Error(`Sparse-set snapshot sparse entry mismatch for id ${id}`);
167
+ }
168
+ }
169
+ }
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runSyncplaySynctest = runSyncplaySynctest;
4
+ const collections_js_1 = require("./collections.js");
5
+ const errors_js_1 = require("./errors.js");
6
+ const offline_session_js_1 = require("./offline-session.js");
7
+ const random_js_1 = require("./random.js");
8
+ const replay_js_1 = require("./replay.js");
9
+ /**
10
+ * GGPO-style offline synctest: for every seed, runs the game straight through
11
+ * and again with a rollback (snapshot → step → restore → re-step) on EVERY
12
+ * frame, comparing per-frame checksums, then re-verifies the exported replay
13
+ * from scratch. Catches logic nondeterminism (stray Math.random, iteration
14
+ * order dependence, restore-sensitive state) with no networking involved.
15
+ * Divergence is reported in the result, never thrown; config errors throw.
16
+ */
17
+ function runSyncplaySynctest(config) {
18
+ const { frames: framesOption, seeds: seedsOption, inputFuzz: inputFuzzOption, ...sessionBase } = config;
19
+ const frames = framesOption ?? 300;
20
+ const seeds = seedsOption ?? [1, 2, 3];
21
+ (0, errors_js_1.invariant)(Number.isInteger(frames) && frames >= 1, 'synctest frames must be a positive integer', 'synctest.frames');
22
+ (0, errors_js_1.invariant)(seeds.length > 0 && seeds.every((seed) => Number.isInteger(seed)), 'synctest seeds must be a non-empty list of integers', 'synctest.seeds');
23
+ const inputFuzz = inputFuzzOption ?? createDefaultInputFuzz(config.defaultInput);
24
+ let seedsRun = 0;
25
+ let framesChecked = 0;
26
+ for (const seed of seeds) {
27
+ seedsRun += 1;
28
+ let lastChecksum;
29
+ const sessionConfig = {
30
+ ...sessionBase,
31
+ randomSeed: seed,
32
+ checksumIntervalFrames: 1,
33
+ replayRecordingMode: 'memory',
34
+ onChecksumRecorded(checksum) {
35
+ lastChecksum = checksum;
36
+ sessionBase.onChecksumRecorded?.(checksum);
37
+ },
38
+ };
39
+ const straight = (0, offline_session_js_1.createOfflineSession)(sessionConfig);
40
+ const schedule = buildInputSchedule(config.playerCount, frames, seed, inputFuzz);
41
+ const straightChecksums = [];
42
+ for (let frame = 0; frame < frames; frame += 1) {
43
+ applyScheduledInputs(straight, schedule[frame]);
44
+ straight.stepFrames(1);
45
+ straightChecksums.push(takeChecksum(lastChecksum, straight.currentFrame));
46
+ }
47
+ const replay = straight.exportReplay();
48
+ const rollback = (0, offline_session_js_1.createOfflineSession)(sessionConfig);
49
+ for (let frame = 0; frame < frames; frame += 1) {
50
+ applyScheduledInputs(rollback, schedule[frame]);
51
+ rollback.stepFrames(1);
52
+ const firstPass = takeChecksum(lastChecksum, rollback.currentFrame);
53
+ rollback.restoreToFrame(frame);
54
+ applyScheduledInputs(rollback, schedule[frame]);
55
+ rollback.stepFrames(1);
56
+ const secondPass = takeChecksum(lastChecksum, rollback.currentFrame);
57
+ framesChecked += 1;
58
+ const straightChecksum = straightChecksums[frame];
59
+ if (firstPass !== straightChecksum) {
60
+ return diverged(seedsRun, framesChecked, {
61
+ seed,
62
+ frame: frame + 1,
63
+ phase: 'straight',
64
+ straightChecksum,
65
+ rollbackChecksum: firstPass,
66
+ });
67
+ }
68
+ if (secondPass !== straightChecksum) {
69
+ return diverged(seedsRun, framesChecked, {
70
+ seed,
71
+ frame: frame + 1,
72
+ phase: 'rollback',
73
+ straightChecksum,
74
+ rollbackChecksum: secondPass,
75
+ });
76
+ }
77
+ }
78
+ const replayResult = (0, replay_js_1.verifyReplay)({ ...sessionBase, replay, throwOnMismatch: false });
79
+ if (replayResult.firstMismatch !== undefined) {
80
+ return diverged(seedsRun, framesChecked, {
81
+ seed,
82
+ frame: replayResult.firstMismatch.frame,
83
+ phase: 'replay',
84
+ straightChecksum: replayResult.firstMismatch.expectedChecksum,
85
+ rollbackChecksum: replayResult.firstMismatch.actualChecksum,
86
+ });
87
+ }
88
+ }
89
+ return { ok: true, seedsRun, framesChecked };
90
+ }
91
+ function diverged(seedsRun, framesChecked, firstDivergence) {
92
+ return { ok: false, seedsRun, framesChecked, firstDivergence };
93
+ }
94
+ function takeChecksum(lastChecksum, frame) {
95
+ if (lastChecksum === undefined || lastChecksum.frame !== frame) {
96
+ throw new errors_js_1.SyncplayError(`Synctest expected a checksum record for frame ${frame}`, {
97
+ code: 'synctest.missing-checksum',
98
+ frame,
99
+ });
100
+ }
101
+ return lastChecksum.checksum;
102
+ }
103
+ function buildInputSchedule(playerCount, frames, seed, inputFuzz) {
104
+ const random = new random_js_1.DeterministicRandom(seed >>> 0).fork('synctest.input-fuzz');
105
+ const schedule = [];
106
+ for (let frame = 0; frame < frames; frame += 1) {
107
+ schedule.push(Array.from({ length: playerCount }, (_, slot) => inputFuzz(random, slot, frame)));
108
+ }
109
+ return schedule;
110
+ }
111
+ function applyScheduledInputs(session, inputs) {
112
+ inputs.forEach((input, slot) => session.setInput(slot, input));
113
+ }
114
+ function createDefaultInputFuzz(defaultInput) {
115
+ return (random) => fuzzFromTemplate(defaultInput, random);
116
+ }
117
+ function fuzzFromTemplate(template, random) {
118
+ if (typeof template === 'boolean') {
119
+ return random.nextInt(0, 1) === 1;
120
+ }
121
+ if (typeof template === 'number') {
122
+ return random.nextInt(-1, 1);
123
+ }
124
+ if (Array.isArray(template)) {
125
+ return template.map((item) => fuzzFromTemplate(item, random));
126
+ }
127
+ if (template !== null && typeof template === 'object' && Object.getPrototypeOf(template) === Object.prototype) {
128
+ const record = template;
129
+ const fuzzed = {};
130
+ for (const key of (0, collections_js_1.stableSortedKeys)(record)) {
131
+ fuzzed[key] = fuzzFromTemplate(record[key], random);
132
+ }
133
+ return fuzzed;
134
+ }
135
+ return template;
136
+ }
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDeterministicLifecycleRunner = createDeterministicLifecycleRunner;
4
+ exports.runDeterministicLifecycleFixture = runDeterministicLifecycleFixture;
5
+ exports.summarizeDeterministicLifecycleCounters = summarizeDeterministicLifecycleCounters;
6
+ const canonical_js_1 = require("./canonical.js");
7
+ function createDeterministicLifecycleRunner(descriptors, hooks) {
8
+ validateLifecycleDescriptors(descriptors, hooks);
9
+ const ordered = [...descriptors].sort(compareLifecycleDescriptors);
10
+ const descriptorById = new Map(ordered.map((descriptor) => [descriptor.id, descriptor]));
11
+ let frame = 0;
12
+ let initialized = new Set();
13
+ let enabled = new Set(ordered.filter((descriptor) => descriptor.startEnabled !== false).map((descriptor) => descriptor.id));
14
+ let pendingSignals = [];
15
+ let counters = new Map();
16
+ let log = [];
17
+ function context(systemId) {
18
+ return {
19
+ frame,
20
+ systemId,
21
+ emit(event) {
22
+ log.push(`${frame}:${systemId}:${event}`);
23
+ },
24
+ increment(key, amount = 1) {
25
+ counters.set(key, (counters.get(key) ?? 0) + amount);
26
+ },
27
+ signal(signal) {
28
+ pendingSignals.push((0, canonical_js_1.cloneCanonical)(signal));
29
+ },
30
+ };
31
+ }
32
+ function isEffectivelyEnabled(systemId) {
33
+ const descriptor = descriptorById.get(systemId);
34
+ if (descriptor === undefined || !enabled.has(systemId)) {
35
+ return false;
36
+ }
37
+ return descriptor.parentId === undefined || isEffectivelyEnabled(descriptor.parentId);
38
+ }
39
+ return {
40
+ get frame() {
41
+ return frame;
42
+ },
43
+ enable(systemId) {
44
+ assertLifecycleSystem(descriptorById, systemId);
45
+ if (!enabled.has(systemId)) {
46
+ enabled.add(systemId);
47
+ hooks[systemId]?.onEnabled?.(context(systemId));
48
+ }
49
+ },
50
+ disable(systemId) {
51
+ assertLifecycleSystem(descriptorById, systemId);
52
+ if (enabled.has(systemId)) {
53
+ enabled.delete(systemId);
54
+ hooks[systemId]?.onDisabled?.(context(systemId));
55
+ }
56
+ },
57
+ signal(signal) {
58
+ pendingSignals.push((0, canonical_js_1.cloneCanonical)(signal));
59
+ },
60
+ step() {
61
+ for (const descriptor of ordered) {
62
+ if (!initialized.has(descriptor.id)) {
63
+ initialized.add(descriptor.id);
64
+ hooks[descriptor.id]?.onInit?.(context(descriptor.id));
65
+ }
66
+ }
67
+ const signals = pendingSignals.sort(compareLifecycleSignals);
68
+ pendingSignals = [];
69
+ for (const descriptor of ordered) {
70
+ if (!isEffectivelyEnabled(descriptor.id)) {
71
+ continue;
72
+ }
73
+ const systemHooks = hooks[descriptor.id];
74
+ for (const signal of signals) {
75
+ systemHooks?.signal?.(context(descriptor.id), signal);
76
+ }
77
+ if (descriptor.signalsOnly !== true) {
78
+ systemHooks?.update?.(context(descriptor.id));
79
+ }
80
+ }
81
+ frame += 1;
82
+ },
83
+ snapshot() {
84
+ return lifecycleSnapshot(frame, initialized, enabled, counters, pendingSignals, log);
85
+ },
86
+ restore(snapshot) {
87
+ validateLifecycleSnapshot(snapshot, descriptorById);
88
+ frame = snapshot.frame;
89
+ initialized = new Set(snapshot.initialized);
90
+ enabled = new Set(snapshot.enabled);
91
+ counters = new Map(stableRecordKeys(snapshot.counters).map((key) => [key, snapshot.counters[key]]));
92
+ pendingSignals = snapshot.pendingSignals.map((signal) => (0, canonical_js_1.cloneCanonical)(signal));
93
+ log = [...snapshot.log];
94
+ },
95
+ };
96
+ }
97
+ function runDeterministicLifecycleFixture(frames = 128) {
98
+ const descriptors = [
99
+ { id: 'root', order: 0 },
100
+ { id: 'child', order: 1, parentId: 'root' },
101
+ { id: 'late', order: 2, startEnabled: false },
102
+ { id: 'signals', order: 3, signalsOnly: true },
103
+ ];
104
+ const hooks = Object.fromEntries(descriptors.map((descriptor) => [descriptor.id, lifecycleCountingHooks(descriptor.id)]));
105
+ const runner = createDeterministicLifecycleRunner(descriptors, hooks);
106
+ let snapshot;
107
+ for (let frame = 0; frame < frames; frame += 1) {
108
+ if (frame === 4)
109
+ runner.enable('late');
110
+ if (frame === 8)
111
+ runner.disable('root');
112
+ if (frame === 12)
113
+ runner.enable('root');
114
+ if (frame % 5 === 0)
115
+ runner.signal({ id: 'tick', payload: { frame } });
116
+ if (frame === Math.floor(frames / 2))
117
+ snapshot = runner.snapshot();
118
+ runner.step();
119
+ }
120
+ const checksum = runner.snapshot().checksum;
121
+ if (snapshot === undefined) {
122
+ throw new Error('Lifecycle fixture did not capture snapshot');
123
+ }
124
+ const restored = createDeterministicLifecycleRunner(descriptors, hooks);
125
+ restored.restore(snapshot);
126
+ for (let frame = snapshot.frame; frame < frames; frame += 1) {
127
+ if (frame === 4)
128
+ restored.enable('late');
129
+ if (frame === 8)
130
+ restored.disable('root');
131
+ if (frame === 12)
132
+ restored.enable('root');
133
+ if (frame % 5 === 0)
134
+ restored.signal({ id: 'tick', payload: { frame } });
135
+ restored.step();
136
+ }
137
+ const restoredSnapshot = restored.snapshot();
138
+ return {
139
+ checksum,
140
+ restoredChecksum: restoredSnapshot.checksum,
141
+ rollbackEquivalent: checksum === restoredSnapshot.checksum,
142
+ ...summarizeDeterministicLifecycleCounters(restoredSnapshot.counters, restoredSnapshot.log),
143
+ };
144
+ }
145
+ function summarizeDeterministicLifecycleCounters(counters, log) {
146
+ return {
147
+ initCalls: counters.init ?? 0,
148
+ updateCalls: counters.update ?? 0,
149
+ enabledCalls: counters.enabled ?? 0,
150
+ disabledCalls: counters.disabled ?? 0,
151
+ signalCalls: counters.signal ?? 0,
152
+ hierarchySkippedUpdates: countMissingChildUpdates(log, 8, 12),
153
+ startDisabledCovered: log.some((entry) => entry.endsWith('late:enabled')),
154
+ signalOnlySkippedUpdate: log.length > 0 && log.every((entry) => !entry.includes(':signals:update')),
155
+ orderStable: firstFrameOrder(log).join(',') === 'root,child',
156
+ };
157
+ }
158
+ function lifecycleCountingHooks(systemId) {
159
+ return {
160
+ onInit(ctx) {
161
+ ctx.increment('init');
162
+ ctx.emit('init');
163
+ },
164
+ onEnabled(ctx) {
165
+ ctx.increment('enabled');
166
+ ctx.emit('enabled');
167
+ },
168
+ onDisabled(ctx) {
169
+ ctx.increment('disabled');
170
+ ctx.emit('disabled');
171
+ },
172
+ update(ctx) {
173
+ ctx.increment('update');
174
+ ctx.emit('update');
175
+ },
176
+ signal(ctx, signal) {
177
+ ctx.increment('signal');
178
+ ctx.emit(`signal:${signal.id}:${systemId}`);
179
+ },
180
+ };
181
+ }
182
+ function lifecycleSnapshot(frame, initialized, enabled, counters, pendingSignals, log) {
183
+ const body = {
184
+ frame,
185
+ initialized: [...initialized].sort(compareStrings),
186
+ enabled: [...enabled].sort(compareStrings),
187
+ counters: Object.fromEntries([...counters.entries()].sort((left, right) => compareStrings(left[0], right[0]))),
188
+ pendingSignals: [...pendingSignals].sort(compareLifecycleSignals).map((signal) => (0, canonical_js_1.cloneCanonical)(signal)),
189
+ log: [...log],
190
+ };
191
+ return { ...body, checksum: (0, canonical_js_1.defaultChecksum)(body) };
192
+ }
193
+ function validateLifecycleDescriptors(descriptors, hooks) {
194
+ const ids = new Set();
195
+ for (const descriptor of descriptors) {
196
+ if (typeof descriptor.id !== 'string' || !/^[A-Za-z][A-Za-z0-9_-]*$/.test(descriptor.id)) {
197
+ throw new Error(`Invalid lifecycle system id ${descriptor.id}`);
198
+ }
199
+ if (ids.has(descriptor.id)) {
200
+ throw new Error(`Duplicate lifecycle system id ${descriptor.id}`);
201
+ }
202
+ ids.add(descriptor.id);
203
+ if (!Number.isInteger(descriptor.order)) {
204
+ throw new Error(`Lifecycle system ${descriptor.id} order must be an integer`);
205
+ }
206
+ }
207
+ for (const descriptor of descriptors) {
208
+ if (descriptor.parentId !== undefined && typeof descriptor.parentId !== 'string') {
209
+ throw new Error(`Lifecycle system ${descriptor.id} references invalid parent ${descriptor.parentId}`);
210
+ }
211
+ if (descriptor.parentId !== undefined && !ids.has(descriptor.parentId)) {
212
+ throw new Error(`Lifecycle system ${descriptor.id} references missing parent ${descriptor.parentId}`);
213
+ }
214
+ if (hooks[descriptor.id] === undefined) {
215
+ throw new Error(`Lifecycle system ${descriptor.id} missing hooks`);
216
+ }
217
+ }
218
+ for (const descriptor of descriptors) {
219
+ validateParentChain(descriptor.id, new Set(), descriptors);
220
+ }
221
+ }
222
+ function validateParentChain(systemId, seen, descriptors) {
223
+ if (seen.has(systemId)) {
224
+ throw new Error(`Lifecycle system ${systemId} parent chain contains a cycle`);
225
+ }
226
+ seen.add(systemId);
227
+ const descriptor = descriptors.find((candidate) => candidate.id === systemId);
228
+ if (descriptor?.parentId !== undefined) {
229
+ validateParentChain(descriptor.parentId, seen, descriptors);
230
+ }
231
+ seen.delete(systemId);
232
+ }
233
+ function validateLifecycleSnapshot(snapshot, descriptors) {
234
+ if (!Number.isInteger(snapshot.frame) || snapshot.frame < 0) {
235
+ throw new Error('Lifecycle snapshot frame must be a non-negative integer');
236
+ }
237
+ for (const systemId of [...snapshot.initialized, ...snapshot.enabled]) {
238
+ if (!descriptors.has(systemId)) {
239
+ throw new Error(`Lifecycle snapshot references unknown system ${systemId}`);
240
+ }
241
+ }
242
+ for (const key of stableRecordKeys(snapshot.counters)) {
243
+ const value = snapshot.counters[key];
244
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
245
+ throw new Error(`Lifecycle snapshot counter ${key} must be finite`);
246
+ }
247
+ }
248
+ const expected = lifecycleSnapshot(snapshot.frame, new Set(snapshot.initialized), new Set(snapshot.enabled), new Map(stableRecordKeys(snapshot.counters).map((key) => [key, snapshot.counters[key]])), snapshot.pendingSignals, snapshot.log).checksum;
249
+ if (snapshot.checksum !== expected) {
250
+ throw new Error('Lifecycle snapshot checksum mismatch');
251
+ }
252
+ }
253
+ function assertLifecycleSystem(descriptors, systemId) {
254
+ if (!descriptors.has(systemId)) {
255
+ throw new Error(`Unknown lifecycle system ${systemId}`);
256
+ }
257
+ }
258
+ function countMissingChildUpdates(log, startFrame, endFrame) {
259
+ let missing = 0;
260
+ const observedFrames = new Set(log.map((entry) => Number(entry.split(':')[0])).filter((frame) => Number.isInteger(frame)));
261
+ for (let frame = startFrame; frame < endFrame; frame += 1) {
262
+ if (observedFrames.has(frame) && !log.includes(`${frame}:child:update`)) {
263
+ missing += 1;
264
+ }
265
+ }
266
+ return missing;
267
+ }
268
+ function firstFrameOrder(log) {
269
+ return log
270
+ .filter((entry) => entry.startsWith('0:') && entry.endsWith(':update'))
271
+ .map((entry) => entry.split(':')[1]);
272
+ }
273
+ function compareLifecycleDescriptors(left, right) {
274
+ return left.order - right.order || compareStrings(left.id, right.id);
275
+ }
276
+ function compareLifecycleSignals(left, right) {
277
+ return compareStrings(left.id, right.id) || compareStrings((0, canonical_js_1.defaultChecksum)(left.payload), (0, canonical_js_1.defaultChecksum)(right.payload));
278
+ }
279
+ function compareStrings(left, right) {
280
+ return left < right ? -1 : left > right ? 1 : 0;
281
+ }
282
+ function stableRecordKeys(record) {
283
+ return Reflect.ownKeys(record)
284
+ .filter((key) => typeof key === 'string')
285
+ .sort(compareStrings);
286
+ }
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.framesToMilliseconds = framesToMilliseconds;
4
+ exports.millisecondsToFrames = millisecondsToFrames;
5
+ exports.createCooldown = createCooldown;
6
+ exports.tickCooldown = tickCooldown;
7
+ exports.scheduleFrameEvent = scheduleFrameEvent;
8
+ exports.popDueFrameEvents = popDueFrameEvents;
9
+ function framesToMilliseconds(frames, tickRate) {
10
+ assertNonNegativeInteger('frames', frames);
11
+ assertPositiveInteger('tickRate', tickRate);
12
+ return Math.trunc((frames * 1000) / tickRate);
13
+ }
14
+ function millisecondsToFrames(milliseconds, tickRate) {
15
+ if (!Number.isFinite(milliseconds) || milliseconds < 0) {
16
+ throw new Error('milliseconds must be a finite non-negative number');
17
+ }
18
+ assertPositiveInteger('tickRate', tickRate);
19
+ return Math.ceil((milliseconds * tickRate) / 1000);
20
+ }
21
+ function createCooldown(durationFrames) {
22
+ assertNonNegativeInteger('durationFrames', durationFrames);
23
+ return {
24
+ remainingFrames: durationFrames,
25
+ ready: durationFrames === 0,
26
+ };
27
+ }
28
+ function tickCooldown(cooldown, frames = 1) {
29
+ assertNonNegativeInteger('frames', frames);
30
+ const remainingFrames = Math.max(0, cooldown.remainingFrames - frames);
31
+ return {
32
+ remainingFrames,
33
+ ready: remainingFrames === 0,
34
+ };
35
+ }
36
+ function scheduleFrameEvent(currentFrame, delayFrames, event) {
37
+ assertNonNegativeInteger('currentFrame', currentFrame);
38
+ assertNonNegativeInteger('delayFrames', delayFrames);
39
+ return {
40
+ frame: currentFrame + delayFrames,
41
+ event,
42
+ };
43
+ }
44
+ function popDueFrameEvents(currentFrame, events) {
45
+ assertNonNegativeInteger('currentFrame', currentFrame);
46
+ const sorted = [...events].sort((left, right) => left.frame - right.frame);
47
+ return {
48
+ due: sorted.filter((entry) => entry.frame <= currentFrame),
49
+ pending: sorted.filter((entry) => entry.frame > currentFrame),
50
+ };
51
+ }
52
+ function assertNonNegativeInteger(label, value) {
53
+ if (!Number.isInteger(value) || value < 0) {
54
+ throw new Error(`${label} must be a non-negative integer`);
55
+ }
56
+ }
57
+ function assertPositiveInteger(label, value) {
58
+ if (!Number.isInteger(value) || value <= 0) {
59
+ throw new Error(`${label} must be a positive integer`);
60
+ }
61
+ }