@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,1860 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDefaultCertificationManifest = createDefaultCertificationManifest;
4
+ exports.runCertificationSuite = runCertificationSuite;
5
+ exports.stableCertificationJson = stableCertificationJson;
6
+ const canonical_js_1 = require("./canonical.js");
7
+ const collision_js_1 = require("./collision.js");
8
+ const collections_js_1 = require("./collections.js");
9
+ const events_js_1 = require("./events.js");
10
+ const math_js_1 = require("./math.js");
11
+ const physics_cert_js_1 = require("./physics-cert.js");
12
+ const prediction_culling_js_1 = require("./prediction-culling.js");
13
+ const protocol_lab_js_1 = require("./protocol-lab.js");
14
+ const random_js_1 = require("./random.js");
15
+ const replay_js_1 = require("./replay.js");
16
+ const schema_artifacts_js_1 = require("./schema-artifacts.js");
17
+ const schema_codegen_js_1 = require("./schema-codegen.js");
18
+ const schema_js_1 = require("./schema.js");
19
+ const sample_lab_js_1 = require("./sample-lab.js");
20
+ const timers_js_1 = require("./timers.js");
21
+ const static_checker_js_1 = require("./tools/static-checker.js");
22
+ const defaultArtifactDir = 'artifacts/certification';
23
+ const checksumCostIterations = 100;
24
+ function createDefaultCertificationManifest() {
25
+ return {
26
+ manifestId: 'syncplay-v1',
27
+ artifactDir: defaultArtifactDir,
28
+ benchmarkProfile: false,
29
+ fixtures: [
30
+ {
31
+ fixtureId: 'fixed-foundation',
32
+ kind: 'fixed-foundation',
33
+ required: true,
34
+ runtimeTarget: 'node',
35
+ players: 1,
36
+ frames: 1,
37
+ seed: 41,
38
+ budgets: {
39
+ requireChecksumConvergence: true,
40
+ },
41
+ },
42
+ {
43
+ fixtureId: 'static-analysis',
44
+ kind: 'static-analysis',
45
+ required: true,
46
+ runtimeTarget: 'node',
47
+ players: 1,
48
+ frames: 1,
49
+ seed: 1,
50
+ },
51
+ {
52
+ fixtureId: 'schema-tooling',
53
+ kind: 'schema-tooling',
54
+ required: true,
55
+ runtimeTarget: 'node',
56
+ players: 4,
57
+ frames: 1,
58
+ seed: 1,
59
+ budgets: {
60
+ requireChecksumConvergence: true,
61
+ },
62
+ },
63
+ {
64
+ fixtureId: 'event-callbacks',
65
+ kind: 'event-callbacks',
66
+ required: true,
67
+ runtimeTarget: 'node',
68
+ players: 1,
69
+ frames: 3,
70
+ seed: 1,
71
+ budgets: {
72
+ requireChecksumConvergence: true,
73
+ },
74
+ },
75
+ {
76
+ fixtureId: 'physics-builtin-fixed',
77
+ kind: 'physics-builtin',
78
+ required: true,
79
+ runtimeTarget: 'node',
80
+ players: 8,
81
+ frames: 240,
82
+ seed: 7,
83
+ budgets: {
84
+ requireChecksumConvergence: true,
85
+ },
86
+ },
87
+ {
88
+ fixtureId: 'prediction-culling',
89
+ kind: 'prediction-culling',
90
+ required: true,
91
+ runtimeTarget: 'node',
92
+ players: 16,
93
+ frames: 240,
94
+ seed: 17,
95
+ budgets: {
96
+ requireChecksumConvergence: true,
97
+ },
98
+ },
99
+ {
100
+ fixtureId: 'external-physics-gate',
101
+ kind: 'external-physics-gate',
102
+ required: true,
103
+ runtimeTarget: 'node',
104
+ players: 1,
105
+ frames: 1,
106
+ seed: 1,
107
+ budgets: { requireChecksumConvergence: true },
108
+ },
109
+ {
110
+ fixtureId: 'ecs-lite',
111
+ kind: 'ecs-lite',
112
+ required: true,
113
+ runtimeTarget: 'node',
114
+ players: 4,
115
+ frames: 4,
116
+ seed: 1,
117
+ budgets: { requireChecksumConvergence: true },
118
+ },
119
+ {
120
+ fixtureId: 'kcc-lite',
121
+ kind: 'kcc-lite',
122
+ required: true,
123
+ runtimeTarget: 'node',
124
+ players: 1,
125
+ frames: 180,
126
+ seed: 3,
127
+ budgets: { requireChecksumConvergence: true },
128
+ },
129
+ {
130
+ fixtureId: 'navigation-grid',
131
+ kind: 'navigation-grid',
132
+ required: true,
133
+ runtimeTarget: 'node',
134
+ players: 16,
135
+ frames: 120,
136
+ seed: 13,
137
+ budgets: { requireChecksumConvergence: true },
138
+ },
139
+ {
140
+ fixtureId: 'projectile-hitscan',
141
+ kind: 'projectile-hitscan',
142
+ required: true,
143
+ runtimeTarget: 'node',
144
+ players: 4,
145
+ frames: 240,
146
+ seed: 5,
147
+ budgets: { requireChecksumConvergence: true },
148
+ },
149
+ {
150
+ fixtureId: 'bot-replacement',
151
+ kind: 'bot-replacement',
152
+ required: true,
153
+ runtimeTarget: 'node',
154
+ players: 8,
155
+ frames: 300,
156
+ seed: 7,
157
+ budgets: { requireChecksumConvergence: true },
158
+ },
159
+ {
160
+ fixtureId: 'genre-128-top-down-bots',
161
+ kind: 'genre-128',
162
+ required: true,
163
+ runtimeTarget: 'node',
164
+ players: 128,
165
+ frames: 1000,
166
+ seed: 43,
167
+ budgets: {
168
+ maxRollbackDepth: 120,
169
+ requireChecksumConvergence: true,
170
+ },
171
+ },
172
+ {
173
+ fixtureId: 'reference-samples',
174
+ kind: 'reference-samples',
175
+ required: true,
176
+ runtimeTarget: 'node',
177
+ players: 16,
178
+ frames: 300,
179
+ seed: 59,
180
+ budgets: { requireChecksumConvergence: true },
181
+ },
182
+ createDefaultNetcodeFixture('p128-input-minimal', 'input-minimal', 19),
183
+ createDefaultNetcodeFixture('p128-moderate-state', 'moderate-state', 23),
184
+ createDefaultNetcodeFixture('p128-command-bursts', 'command-bursts', 29),
185
+ {
186
+ fixtureId: 'protocol-matrix',
187
+ kind: 'protocol-matrix',
188
+ required: true,
189
+ runtimeTarget: 'node',
190
+ players: 8,
191
+ frames: 300,
192
+ seed: 37,
193
+ profiles: defaultProtocolProfiles,
194
+ budgets: {
195
+ maxRollbackDepth: 80,
196
+ requireChecksumConvergence: true,
197
+ requireExpectedDesyncArtifacts: true,
198
+ },
199
+ },
200
+ {
201
+ fixtureId: 'node-replay-lab',
202
+ kind: 'replay-node',
203
+ required: true,
204
+ runtimeTarget: 'node',
205
+ players: 4,
206
+ frames: 240,
207
+ seed: 31,
208
+ budgets: {
209
+ maxReplayBytes: 1_000_000,
210
+ maxPackedInputBytesPerSecond: 10_000,
211
+ requireChecksumConvergence: true,
212
+ },
213
+ },
214
+ ],
215
+ };
216
+ }
217
+ function runCertificationSuite(manifestInput = {}) {
218
+ const manifest = normalizeManifest(manifestInput);
219
+ const fixtureReports = manifest.fixtures.map((fixture) => runCertificationFixture(manifest, fixture));
220
+ const failures = fixtureReports.flatMap((fixture) => collectRequiredFixtureFailures(fixture));
221
+ const reportWithoutId = {
222
+ schemaVersion: 1,
223
+ reportId: '',
224
+ manifestId: manifest.manifestId,
225
+ artifactDir: manifest.artifactDir,
226
+ benchmarkProfile: manifest.benchmarkProfile,
227
+ fixtures: fixtureReports,
228
+ failures,
229
+ result: failures.length === 0 ? 'pass' : 'fail',
230
+ };
231
+ const report = {
232
+ ...reportWithoutId,
233
+ reportId: (0, canonical_js_1.defaultChecksum)(reportWithoutId),
234
+ };
235
+ return { report, fixtureReports };
236
+ }
237
+ function collectRequiredFixtureFailures(fixture) {
238
+ if (!fixture.required || fixture.result !== 'fail') {
239
+ return [];
240
+ }
241
+ /* istanbul ignore next -- createFixtureReport supplies diagnostics for current failing fixtures; this suite fallback protects future fixture implementations. */
242
+ const failures = fixture.failures.length === 0
243
+ ? ['fixture failed without diagnostics']
244
+ : fixture.failures;
245
+ return failures.map((failure) => `${fixture.fixtureId}: ${failure}`);
246
+ }
247
+ function stableCertificationJson(value) {
248
+ return `${(0, canonical_js_1.canonicalStringify)(value)}\n`;
249
+ }
250
+ function certificationPassFail(pass) {
251
+ const results = ['fail', 'pass'];
252
+ return results[Number(pass)];
253
+ }
254
+ function runCertificationFixture(manifest, fixture) {
255
+ if (fixture.kind === 'static-analysis') {
256
+ return runStaticAnalysisFixture(manifest, fixture);
257
+ }
258
+ if (fixture.kind === 'schema-tooling') {
259
+ return runSchemaToolingFixture(manifest, fixture);
260
+ }
261
+ if (fixture.kind === 'event-callbacks') {
262
+ return runEventCallbacksFixture(manifest, fixture);
263
+ }
264
+ if (fixture.kind === 'physics-builtin') {
265
+ return runPhysicsBuiltinFixture(manifest, fixture);
266
+ }
267
+ if (fixture.kind === 'prediction-culling') {
268
+ return runPredictionCullingFixture(manifest, fixture);
269
+ }
270
+ if (fixture.kind === 'external-physics-gate') {
271
+ return runExternalPhysicsGateFixture(manifest, fixture);
272
+ }
273
+ if (fixture.kind === 'ecs-lite') {
274
+ return runEcsLiteFixture(manifest, fixture);
275
+ }
276
+ if (fixture.kind === 'kcc-lite') {
277
+ return runKccLiteFixture(manifest, fixture);
278
+ }
279
+ if (fixture.kind === 'navigation-grid') {
280
+ return runNavigationGridFixture(manifest, fixture);
281
+ }
282
+ if (fixture.kind === 'projectile-hitscan') {
283
+ return runProjectileHitscanFixture(manifest, fixture);
284
+ }
285
+ if (fixture.kind === 'bot-replacement') {
286
+ return runBotReplacementFixture(manifest, fixture);
287
+ }
288
+ if (fixture.kind === 'genre-128') {
289
+ return runGenre128Fixture(manifest, fixture);
290
+ }
291
+ if (fixture.kind === 'fixed-foundation') {
292
+ return runFixedFoundationFixture(manifest, fixture);
293
+ }
294
+ if (fixture.kind === 'netcode') {
295
+ return runNetcodeFixture(manifest, fixture);
296
+ }
297
+ if (fixture.kind === 'protocol-matrix') {
298
+ return runProtocolMatrixFixture(manifest, fixture);
299
+ }
300
+ if (fixture.kind === 'reference-samples') {
301
+ return runReferenceSamplesFixture(manifest, fixture);
302
+ }
303
+ if (fixture.kind === 'replay-node') {
304
+ return runReplayNodeFixture(manifest, fixture);
305
+ }
306
+ return createSkippedBrowserFixtureReport(manifest, fixture);
307
+ }
308
+ function runFixedFoundationFixture(manifest, fixture) {
309
+ const math = (0, math_js_1.createDeterministicMath)();
310
+ const a = math.toFixed(1.5);
311
+ const b = math.toFixed(2.5);
312
+ const circleA = (0, collision_js_1.fixedCircle)((0, collision_js_1.fixedVec2)(100, 200), 50);
313
+ const circleB = (0, collision_js_1.fixedCircle)((0, collision_js_1.fixedVec2)(140, 210), 40);
314
+ const rng = new random_js_1.DeterministicRandom(fixture.seed);
315
+ const rngValues = Array.from({ length: 5 }, () => rng.nextInt(0, 999));
316
+ const cooldown = (0, timers_js_1.createCooldown)(5);
317
+ const tickedCooldown = (0, timers_js_1.tickCooldown)(cooldown, 3);
318
+ const readyCooldown = (0, timers_js_1.tickCooldown)(tickedCooldown, 2);
319
+ const frameEvents = [
320
+ (0, timers_js_1.scheduleFrameEvent)(10, 5, { id: 'late' }),
321
+ (0, timers_js_1.scheduleFrameEvent)(10, 0, { id: 'now' }),
322
+ (0, timers_js_1.scheduleFrameEvent)(10, 2, { id: 'soon' }),
323
+ ];
324
+ const dueEvents = (0, timers_js_1.popDueFrameEvents)(12, frameEvents);
325
+ const record = { bravo: 2, alpha: 1, charlie: 3 };
326
+ const stableMap = (0, collections_js_1.createStableIndexedMap)([
327
+ { id: 'slot-2', value: 2 },
328
+ { id: 'slot-0', value: 0 },
329
+ { id: 'slot-1', value: 1 },
330
+ ]);
331
+ const goldenWithoutChecksum = {
332
+ fixedMath: {
333
+ a,
334
+ b,
335
+ add: math.add(a, b),
336
+ sub: math.sub(b, a),
337
+ mul: math.mul(a, b),
338
+ div: math.div(b, a),
339
+ clamp: math.clamp(4_000, 0, b),
340
+ },
341
+ vectors: {
342
+ a: circleA.center,
343
+ b: circleB.center,
344
+ circlesIntersect: (0, collision_js_1.circleIntersects)(circleA, circleB),
345
+ },
346
+ rng: rngValues,
347
+ timers: {
348
+ milliseconds: (0, timers_js_1.framesToMilliseconds)(3, 30),
349
+ frames: (0, timers_js_1.millisecondsToFrames)(101, 30),
350
+ cooldown: {
351
+ start: cooldown,
352
+ ticked: tickedCooldown,
353
+ ready: readyCooldown,
354
+ },
355
+ due: dueEvents.due,
356
+ pending: dueEvents.pending,
357
+ },
358
+ collections: {
359
+ sortedKeys: (0, collections_js_1.stableSortedKeys)(record),
360
+ stableEntries: (0, collections_js_1.stableEntries)(record),
361
+ stableMapEntries: [...stableMap],
362
+ stableSetValues: [...new Set(['bravo', 'alpha', 'charlie'])].sort((left, right) => left.localeCompare(right)),
363
+ shuffle: (0, collections_js_1.deterministicShuffle)(['a', 'b', 'c', 'd', 'e'], new random_js_1.DeterministicRandom(fixture.seed)),
364
+ },
365
+ };
366
+ const goldenChecksum = (0, canonical_js_1.defaultChecksum)(goldenWithoutChecksum);
367
+ const metrics = {
368
+ goldenChecksum,
369
+ checksumConverged: goldenChecksum === (0, canonical_js_1.defaultChecksum)(goldenWithoutChecksum),
370
+ ...goldenWithoutChecksum,
371
+ };
372
+ return createFixtureReport(manifest, fixture, metrics);
373
+ }
374
+ function runStaticAnalysisFixture(manifest, fixture) {
375
+ const diagnostics = (0, static_checker_js_1.checkSourceText)('certification/static-analysis-fixture.ts', [
376
+ 'export function step(state: { x: number }): { x: number } {',
377
+ ' Math.random();',
378
+ ' Date.now();',
379
+ ' new Date();',
380
+ ' performance.now();',
381
+ ' setTimeout(() => undefined, 1);',
382
+ ' fetch("/bad");',
383
+ ' crypto.randomUUID();',
384
+ ' [3, 2, 1].sort();',
385
+ ' return { x: state.x + 1 };',
386
+ '}',
387
+ ].join('\n'), 'strict');
388
+ const errors = diagnostics.filter((diagnostic) => diagnostic.severity === 'error').length;
389
+ const warnings = diagnostics.filter((diagnostic) => diagnostic.severity === 'warn').length;
390
+ const alternativesReported = diagnostics.filter((diagnostic) => diagnostic.suggestion.length > 0).length;
391
+ const metrics = {
392
+ parser: (0, static_checker_js_1.getDeterminismParserKind)(),
393
+ dependencyPosture: 'package-internal-no-runtime-parser-dependency',
394
+ diagnostics: diagnostics.map((diagnostic) => ({
395
+ ruleId: diagnostic.ruleId,
396
+ severity: diagnostic.severity,
397
+ suggestion: diagnostic.suggestion,
398
+ })),
399
+ errors,
400
+ warnings,
401
+ alternativesReported,
402
+ result: certificationPassFail(errors === 8 && alternativesReported === 8),
403
+ };
404
+ return createFixtureReport(manifest, fixture, metrics, metrics.result);
405
+ }
406
+ function runSchemaToolingFixture(manifest, fixture) {
407
+ const descriptor = (0, schema_js_1.defineSyncplayGame)(createCertificationDescriptor());
408
+ const artifacts = (0, schema_artifacts_js_1.createSchemaArtifacts)(descriptor);
409
+ const state = {
410
+ health: [3, 2, 1, 3],
411
+ score: [0, 1, 0, 2],
412
+ };
413
+ const input = {
414
+ attack: true,
415
+ targetSlot: 2,
416
+ };
417
+ const canonicalInput = artifacts.canonicalizeInput(input);
418
+ const serializedState = artifacts.serializeState(state);
419
+ const deserializedState = artifacts.deserializeState(serializedState);
420
+ const frameDump = artifacts.dumpFrame({
421
+ frame: fixture.frames,
422
+ state: deserializedState,
423
+ inputFrame: {
424
+ frame: fixture.frames,
425
+ inputs: [canonicalInput],
426
+ },
427
+ });
428
+ const generatedTypes = (0, schema_codegen_js_1.generateSchemaTypes)(descriptor, { rootName: 'CounterDuel' });
429
+ const generatedRuntimeValidatorsPresent = generatedTypes.includes('function validateCounterDuelInput')
430
+ && generatedTypes.includes('function validateCounterDuelState')
431
+ && generatedTypes.includes('function assertCounterDuelExactKeys');
432
+ const frameDumpChecksumMatch = frameDump.checksum === artifacts.checksumState(state);
433
+ const metrics = {
434
+ artifactId: descriptor.artifactId,
435
+ descriptorId: descriptor.artifactId,
436
+ commandSchemaId: descriptor.replayIdentity.commandSchemaId,
437
+ prototypeSchemaId: descriptor.replayIdentity.prototypeSchemaId,
438
+ defaultInputFactoryPresent: generatedTypes.includes('createCounterDuelDefaultInput'),
439
+ canonicalInput,
440
+ serializedStateBytes: serializedState.byteLength,
441
+ frameDumpChecksum: frameDump.checksum,
442
+ frameDumpChecksumMatch,
443
+ generatedTypesBytes: Buffer.byteLength(generatedTypes),
444
+ generatedHelpers: ['types', 'default-input-factory', 'canonical-serializer', 'runtime-validators', 'frame-dump'],
445
+ generatedRuntimeValidatorsPresent,
446
+ checksumConverged: frameDumpChecksumMatch,
447
+ };
448
+ return createFixtureReport(manifest, fixture, metrics);
449
+ }
450
+ function runEventCallbacksFixture(manifest, fixture) {
451
+ const timeline = (0, events_js_1.createDeterministicEventTimeline)();
452
+ timeline.emitPredicted('entity.created', 1, { slot: 0, kind: 'lifecycle' }, 'entity-created');
453
+ timeline.emitPredicted('physics.collision', 1, { slot: 0, kind: 'physics' }, 'collision');
454
+ timeline.emitPredicted('entity.destroyed', 2, { slot: 0, kind: 'lifecycle' }, 'entity-destroyed');
455
+ timeline.emitPredicted('physics.trigger', 2, { slot: 0, kind: 'physics' }, 'trigger');
456
+ timeline.confirmThroughFrame(1);
457
+ timeline.cancelFromFrame(2);
458
+ timeline.emitVerifiedOnly('verified.score', 3, { slot: 0, kind: 'verified' }, 'verified-score');
459
+ timeline.emitPredicted('view.predicted', 4, { slot: 0, kind: 'view' }, 'view-predicted');
460
+ const records = timeline.records();
461
+ const predictionStates = [...new Set(records.map((record) => record.predictionState))].sort();
462
+ const eventChecksum = (0, canonical_js_1.defaultChecksum)(records);
463
+ const metrics = {
464
+ predictionStates,
465
+ records: records.length,
466
+ confirmed: records.filter((record) => record.predictionState === 'confirmed').length,
467
+ canceled: records.filter((record) => record.predictionState === 'canceled').length,
468
+ verifiedOnly: records.filter((record) => record.predictionState === 'verifiedOnly').length,
469
+ lifecycleEvents: records.filter((record) => record.eventId.startsWith('entity.')).length,
470
+ physicsEvents: records.filter((record) => record.eventId.startsWith('physics.')).length,
471
+ eventChecksum,
472
+ checksumConverged: eventChecksum === (0, canonical_js_1.defaultChecksum)(records),
473
+ };
474
+ return createFixtureReport(manifest, fixture, metrics);
475
+ }
476
+ function runPhysicsBuiltinFixture(manifest, fixture) {
477
+ const result = (0, physics_cert_js_1.runPhysicsCert)({
478
+ engine: 'builtin-fixed',
479
+ shape: 'fixed-arena',
480
+ players: fixture.players,
481
+ frames: fixture.frames,
482
+ seed: fixture.seed,
483
+ snapshotEvery: 1,
484
+ });
485
+ const metrics = {
486
+ engine: result.engine,
487
+ uninterruptedChecksum: result.uninterruptedChecksum,
488
+ restoredChecksum: result.restoredChecksum,
489
+ checksumConverged: result.uninterruptedChecksum === result.restoredChecksum && result.result === 'pass',
490
+ collisionEventsStable: result.collisionEventsStable,
491
+ queryOrderStable: result.queryOrderStable,
492
+ snapshotHashStable: result.snapshotHashStable,
493
+ collisionEvents: result.collisionEvents,
494
+ snapshotBytesP95: result.snapshotBytesP95,
495
+ result: result.result,
496
+ };
497
+ return createFixtureReport(manifest, fixture, metrics, metrics.result);
498
+ }
499
+ function runPredictionCullingFixture(manifest, fixture) {
500
+ const passProfile = (0, prediction_culling_js_1.runPredictionCullingLab)({
501
+ mode: 'local-area',
502
+ players: fixture.players,
503
+ objectsPerPlayer: 6,
504
+ frames: fixture.frames,
505
+ seed: fixture.seed,
506
+ radius: 180,
507
+ });
508
+ const expectedFailureProfile = (0, prediction_culling_js_1.runPredictionCullingLab)({
509
+ mode: 'local-area',
510
+ players: fixture.players,
511
+ objectsPerPlayer: 6,
512
+ frames: fixture.frames,
513
+ seed: fixture.seed,
514
+ radius: 80,
515
+ includeHiddenThreat: true,
516
+ });
517
+ const metrics = {
518
+ passProfile,
519
+ expectedFailureProfile,
520
+ expectedFailureDetected: expectedFailureProfile.result === 'fail' && expectedFailureProfile.hiddenAuthoritativeInteractions > 0,
521
+ checksumConverged: passProfile.result === 'pass',
522
+ };
523
+ return createFixtureReport(manifest, fixture, metrics);
524
+ }
525
+ function runExternalPhysicsGateFixture(manifest, fixture) {
526
+ const certified = false;
527
+ const checksummedGameplayAllowed = false;
528
+ const requiredGates = [
529
+ 'uninterrupted-vs-restored-checksum-equality',
530
+ 'stable-query-order',
531
+ 'stable-collision-callbacks',
532
+ 'stable-snapshot-hashes',
533
+ 'documented-version-identity',
534
+ ];
535
+ const knownIssue = 'rapier cert currently reproduces snapshot/restore nondeterminism; keep external physics out of checksummed gameplay';
536
+ const checksummedGameplayRejected = certified === false &&
537
+ checksummedGameplayAllowed === false &&
538
+ requiredGates.length === 5 &&
539
+ knownIssue.includes('nondeterminism');
540
+ const metrics = {
541
+ engine: 'rapier2d-compat',
542
+ packageName: '@dimforge/rapier2d-compat',
543
+ certified,
544
+ checksummedGameplayAllowed,
545
+ checksummedGameplayRejected,
546
+ requiredGates,
547
+ knownIssue,
548
+ checksumConverged: checksummedGameplayRejected,
549
+ };
550
+ return createFixtureReport(manifest, fixture, metrics);
551
+ }
552
+ function runEcsLiteFixture(manifest, fixture) {
553
+ const world = createTinyEcsWorld();
554
+ const player = ecsCreateEntity(world);
555
+ const enemy = ecsCreateEntity(world);
556
+ ecsAddComponent(world, player, 'Position', { x: 10, y: 0 });
557
+ ecsAddComponent(world, player, 'Velocity', { x: 1, y: 0 });
558
+ ecsAddComponent(world, enemy, 'Position', { x: 20, y: 0 });
559
+ ecsSetSingleton(world, 'Match', { frame: fixture.frames });
560
+ const snapshot = ecsSnapshot(world);
561
+ const createdAfterSnapshot = ecsCreateEntity(world);
562
+ ecsAddComponent(world, createdAfterSnapshot, 'Position', { x: 30, y: 0 });
563
+ ecsDestroyEntity(world, enemy);
564
+ const restored = ecsRestore(snapshot);
565
+ const rollbackCreateDestroyEqual = ecsChecksum(restored) === ecsChecksum(ecsRestore(snapshot));
566
+ const query = ecsQuery(restored, ['Position']);
567
+ const stableEntityIds = player === 1 && enemy === 2 && createdAfterSnapshot === 3;
568
+ const queryOrderStable = (0, canonical_js_1.defaultChecksum)(query) === (0, canonical_js_1.defaultChecksum)(ecsQuery(restored, ['Position']));
569
+ const snapshotRestoreEqual = ecsChecksum(restored) === ecsChecksum(ecsRestore(snapshot));
570
+ const singletonStateStable = (0, canonical_js_1.defaultChecksum)(restored.singletons) === (0, canonical_js_1.defaultChecksum)({ Match: { frame: fixture.frames } });
571
+ const componentLifecycleStable = query.length === 2 && ecsQuery(restored, ['Position', 'Velocity']).length === 1;
572
+ const metrics = {
573
+ stableEntityIds,
574
+ queryOrderStable,
575
+ snapshotRestoreEqual,
576
+ rollbackCreateDestroyEqual,
577
+ singletonStateStable,
578
+ componentLifecycleStable,
579
+ checksumConverged: stableEntityIds &&
580
+ queryOrderStable &&
581
+ snapshotRestoreEqual &&
582
+ rollbackCreateDestroyEqual &&
583
+ singletonStateStable &&
584
+ componentLifecycleStable,
585
+ worldChecksum: ecsChecksum(restored),
586
+ };
587
+ return createFixtureReport(manifest, fixture, metrics);
588
+ }
589
+ function runKccLiteFixture(manifest, fixture) {
590
+ const uninterrupted = runKccSimulation(fixture.frames, fixture.seed, -1);
591
+ const restored = runKccSimulation(fixture.frames, fixture.seed, Math.floor(fixture.frames / 2));
592
+ const metrics = {
593
+ groundedFrames: restored.groundedFrames,
594
+ horizontalTravel: restored.horizontalTravel,
595
+ jumps: restored.jumps,
596
+ coyoteJumps: restored.coyoteJumps,
597
+ wallCollisions: restored.wallCollisions,
598
+ leftWallCollisions: restored.leftWallCollisions,
599
+ rightWallCollisions: restored.rightWallCollisions,
600
+ rollbackChecksumEqual: uninterrupted.finalChecksum === restored.finalChecksum,
601
+ queryOrderStable: restored.queryOrderStable,
602
+ finalChecksum: restored.finalChecksum,
603
+ checksumConverged: uninterrupted.finalChecksum === restored.finalChecksum && restored.queryOrderStable,
604
+ };
605
+ return createFixtureReport(manifest, fixture, metrics);
606
+ }
607
+ function runNavigationGridFixture(manifest, fixture) {
608
+ const uninterrupted = runGridNavigationSimulation(fixture.players, fixture.frames, fixture.seed, -1);
609
+ const restored = runGridNavigationSimulation(fixture.players, fixture.frames, fixture.seed, Math.floor(fixture.frames / 2));
610
+ const metrics = {
611
+ pathChecksum: restored.finalChecksum,
612
+ pathsFound: restored.pathsFound,
613
+ unreachableDetected: restored.unreachableDetected,
614
+ queryOrderStable: restored.queryOrderStable,
615
+ avoidanceStable: restored.avoidanceStable,
616
+ rollbackChecksumEqual: uninterrupted.finalChecksum === restored.finalChecksum,
617
+ checksumConverged: uninterrupted.finalChecksum === restored.finalChecksum &&
618
+ restored.queryOrderStable &&
619
+ restored.avoidanceStable &&
620
+ restored.unreachableDetected,
621
+ };
622
+ return createFixtureReport(manifest, fixture, metrics);
623
+ }
624
+ function runProjectileHitscanFixture(manifest, fixture) {
625
+ const uninterrupted = runProjectileSimulation(fixture.players, fixture.frames, fixture.seed, -1);
626
+ const restored = runProjectileSimulation(fixture.players, fixture.frames, fixture.seed, Math.floor(fixture.frames / 2));
627
+ const metrics = {
628
+ raycastOrderStable: restored.raycastOrderStable,
629
+ projectileHits: restored.projectileHits,
630
+ hitscanHits: restored.hitscanHits,
631
+ cooldownBlocks: restored.cooldownBlocks,
632
+ respawns: restored.respawns,
633
+ rollbackChecksumEqual: uninterrupted.finalChecksum === restored.finalChecksum,
634
+ eventCancellationCount: restored.eventCancellationCount,
635
+ finalChecksum: restored.finalChecksum,
636
+ checksumConverged: uninterrupted.finalChecksum === restored.finalChecksum && restored.raycastOrderStable,
637
+ };
638
+ return createFixtureReport(manifest, fixture, metrics);
639
+ }
640
+ function runReferenceSamplesFixture(manifest, fixture) {
641
+ const sampleIds = [
642
+ 'asteroids-basic',
643
+ 'platform-shooter-2d',
644
+ 'simple-fps',
645
+ 'twin-stick-shooter',
646
+ 'arena-brawler',
647
+ 'racer-25d',
648
+ 'turn-flow-mini-golf',
649
+ ];
650
+ const samples = sampleIds.map((sampleId, index) => runReferenceSampleSimulation(sampleId, Math.max(2, fixture.players), fixture.frames, fixture.seed + index));
651
+ const metrics = {
652
+ samples,
653
+ sampleIds,
654
+ checksumConverged: samples.every((sample) => sample.rollbackChecksumEqual && sample.events > 0),
655
+ };
656
+ return createFixtureReport(manifest, fixture, metrics);
657
+ }
658
+ function runBotReplacementFixture(manifest, fixture) {
659
+ const botSlots = [Math.min(1, fixture.players - 1), Math.min(3, fixture.players - 1)];
660
+ const authority = (0, protocol_lab_js_1.runProtocolRollbackLab)({
661
+ players: fixture.players,
662
+ frames: fixture.frames,
663
+ seed: fixture.seed,
664
+ minDelayFrames: 1,
665
+ maxDelayFrames: 6,
666
+ botSlots,
667
+ });
668
+ const replacement = (0, protocol_lab_js_1.runProtocolRollbackLab)({
669
+ players: fixture.players,
670
+ frames: fixture.frames,
671
+ seed: fixture.seed,
672
+ minDelayFrames: 1,
673
+ maxDelayFrames: 6,
674
+ botSlots,
675
+ });
676
+ const metrics = {
677
+ botInputFrames: replacement.botInputFrames,
678
+ disconnectedSlot: botSlots[0],
679
+ replacementChecksumEqual: authority.authorityChecksum === replacement.authorityChecksum,
680
+ authorityChecksum: authority.authorityChecksum,
681
+ replacementChecksum: replacement.authorityChecksum,
682
+ checksumConverged: authority.authorityChecksum === replacement.authorityChecksum && replacement.result === 'pass',
683
+ };
684
+ return createFixtureReport(manifest, fixture, metrics);
685
+ }
686
+ function runGenre128Fixture(manifest, fixture) {
687
+ const botSlots = Array.from({ length: 32 }, (_, index) => 96 + index);
688
+ const protocol = (0, protocol_lab_js_1.runProtocolRollbackLab)({
689
+ game: 'command-bursts',
690
+ players: fixture.players,
691
+ frames: fixture.frames,
692
+ seed: fixture.seed,
693
+ minDelayFrames: 1,
694
+ maxDelayFrames: 8,
695
+ duplicatePermille: 100,
696
+ lossPermille: 20,
697
+ resendIntervalFrames: 9,
698
+ maxResendAttempts: 12,
699
+ botSlots,
700
+ eventTimelineLimit: 8,
701
+ });
702
+ const metrics = {
703
+ players: fixture.players,
704
+ frames: fixture.frames,
705
+ movementFrames: fixture.players * fixture.frames,
706
+ collisionOrHitResolutions: protocol.eventTimeline.confirmed + protocol.eventTimeline.verifiedOnly,
707
+ events: protocol.eventTimeline.totalRecords,
708
+ botInputFrames: protocol.botInputFrames,
709
+ authorityChecksum: protocol.authorityChecksum,
710
+ clientChecksum: protocol.clientChecksum,
711
+ replayVerification: {
712
+ mismatches: Number(protocol.authorityChecksum !== protocol.clientChecksum),
713
+ finalChecksum: protocol.clientChecksum,
714
+ },
715
+ protocolRollback: {
716
+ rollbacks: protocol.rollbacks,
717
+ maxRollbackDepth: protocol.maxRollbackDepth,
718
+ },
719
+ checksumConverged: protocol.result === 'pass' && protocol.authorityChecksum === protocol.clientChecksum,
720
+ };
721
+ return createFixtureReport(manifest, fixture, metrics);
722
+ }
723
+ const defaultProtocolProfiles = [
724
+ 'delay-jitter',
725
+ 'loss-duplicate-reorder',
726
+ 'reconnect',
727
+ 'late-join',
728
+ 'asymmetric-latency',
729
+ 'input-delay',
730
+ 'bots',
731
+ 'command-bursts',
732
+ 'expected-desync',
733
+ ];
734
+ function runNetcodeFixture(manifest, fixture) {
735
+ const hz = fixture.hz ?? 10;
736
+ const rttMs = fixture.rttMs ?? 200;
737
+ const jitterMs = fixture.jitterMs ?? 100;
738
+ const oneWayDelayFrames = Math.max(0, Math.round((rttMs / 2 / 1000) * hz));
739
+ const jitterFrames = Math.max(0, Math.round((jitterMs / 2 / 1000) * hz));
740
+ const minDelayFrames = Math.max(0, oneWayDelayFrames - jitterFrames);
741
+ const maxDelayFrames = Math.max(minDelayFrames, oneWayDelayFrames + jitterFrames);
742
+ const startedAt = performance.now();
743
+ const protocol = (0, protocol_lab_js_1.runProtocolRollbackLab)({
744
+ game: netcodeShapeToGame(fixture.shape),
745
+ players: fixture.players,
746
+ frames: fixture.frames,
747
+ seed: fixture.seed,
748
+ minDelayFrames,
749
+ maxDelayFrames,
750
+ bundleSizeFrames: fixture.bundleSizeFrames ?? 1,
751
+ inputDelayFrames: fixture.inputDelayFrames ?? 0,
752
+ duplicatePermille: fixture.duplicatePermille ?? 0,
753
+ lossPermille: (fixture.lossPercent ?? 0) * 10,
754
+ resendIntervalFrames: Math.max(1, maxDelayFrames + 1),
755
+ maxResendAttempts: Math.max(1, maxDelayFrames + 6),
756
+ eventTimelineLimit: fixture.eventTimelineLimit ?? 0,
757
+ });
758
+ const elapsedMs = performance.now() - startedAt;
759
+ const sampleInputRandom = new random_js_1.DeterministicRandom(fixture.seed);
760
+ const sampleInputs = Array.from({ length: fixture.players }, () => (0, sample_lab_js_1.makeLabInput)(sampleInputRandom));
761
+ const packedInputBytesPerTick = (0, sample_lab_js_1.packLabInputFrame)(sampleInputs).byteLength;
762
+ const jsonInputBytesPerTick = estimateJsonBytes({ frame: 0, inputs: sampleInputs });
763
+ const replayBytes = estimateNetcodeReplayBytes(fixture.shape, fixture.players, fixture.seed, estimateJsonArrayBytes(jsonInputBytesPerTick * fixture.frames, fixture.frames), estimateJsonArrayBytes(estimateJsonBytes({ frame: 0, checksum: '0000000000000000' }) * fixture.frames, fixture.frames));
764
+ const metrics = {
765
+ shape: fixture.shape,
766
+ game: protocol.game,
767
+ authorityChecksum: protocol.authorityChecksum,
768
+ clientChecksum: protocol.clientChecksum,
769
+ checksumConverged: protocol.authorityChecksum === protocol.clientChecksum && protocol.result === 'pass',
770
+ rollbacks: protocol.rollbacks,
771
+ maxRollbackDepth: protocol.maxRollbackDepth,
772
+ replayBytes,
773
+ packedInputBytesPerTick: packedInputBytesPerTick.toFixed(2),
774
+ packedInputBytesPerSecond: (packedInputBytesPerTick * hz).toFixed(2),
775
+ inputBytesPerTick: jsonInputBytesPerTick.toFixed(2),
776
+ checksumCostMsP95: benchmarkChecksumCost(fixture.shape, fixture.players, fixture.seed),
777
+ elapsedMs: elapsedMs.toFixed(2),
778
+ heapUsedBytes: getHeapUsedBytes(),
779
+ rssBytes: getRssBytes(),
780
+ };
781
+ return createFixtureReport(manifest, fixture, metrics);
782
+ }
783
+ function runReplayNodeFixture(manifest, fixture) {
784
+ const startedAt = performance.now();
785
+ const session = (0, sample_lab_js_1.createLabSession)({
786
+ players: fixture.players,
787
+ seed: fixture.seed,
788
+ replayRecordingMode: 'memory',
789
+ });
790
+ const inputRandom = new random_js_1.DeterministicRandom(fixture.seed);
791
+ let packedInputBytes = 0;
792
+ for (let frame = 0; frame < fixture.frames; frame += 1) {
793
+ const frameInputs = (0, sample_lab_js_1.applyLabInputs)(session, fixture.players, inputRandom);
794
+ packedInputBytes += (0, sample_lab_js_1.packLabInputFrame)(frameInputs).byteLength;
795
+ session.stepFrames(1);
796
+ }
797
+ const replay = session.exportReplay();
798
+ const verification = (0, replay_js_1.verifyReplay)({
799
+ replay,
800
+ step: sample_lab_js_1.labStep,
801
+ defaultInput: sample_lab_js_1.labDefaultInput,
802
+ inputSchemaId: 'lab-input/v1',
803
+ stateSchemaId: 'lab-state/v1',
804
+ deterministicVersion: '0.0.0-source',
805
+ hydrateInput: sample_lab_js_1.hydrateLabInput,
806
+ throwOnMismatch: false,
807
+ });
808
+ const replayBytes = estimateJsonBytes(replay);
809
+ const elapsedMs = performance.now() - startedAt;
810
+ const metrics = {
811
+ finalChecksum: verification.finalChecksum,
812
+ replayBytes,
813
+ packedInputBytesPerTick: (packedInputBytes / fixture.frames).toFixed(2),
814
+ packedInputBytesPerSecond: ((packedInputBytes / fixture.frames) * 30).toFixed(2),
815
+ replayVerification: {
816
+ frames: verification.frames,
817
+ checksums: verification.checksums,
818
+ mismatches: verification.mismatches,
819
+ finalChecksum: verification.finalChecksum,
820
+ },
821
+ elapsedMs: elapsedMs.toFixed(2),
822
+ heapUsedBytes: getHeapUsedBytes(),
823
+ rssBytes: getRssBytes(),
824
+ };
825
+ return createFixtureReport(manifest, fixture, metrics);
826
+ }
827
+ function runProtocolMatrixFixture(manifest, fixture) {
828
+ const profiles = (fixture.profiles ?? defaultProtocolProfiles).map((profileId) => runProtocolMatrixProfile(fixture, profileId));
829
+ const frameModel = {
830
+ currentFrame: fixture.frames,
831
+ verifiedFrame: fixture.frames,
832
+ predictedFrame: fixture.frames,
833
+ previousPredictedFrame: Math.max(0, fixture.frames - 1),
834
+ rollbackCount: sumProfiles(profiles, 'rollbacks'),
835
+ correctionCount: profiles.reduce((sum, profile) => sum + profile.corrections, 0),
836
+ maxRollbackDepth: Math.max(0, ...profiles.map((profile) => profile.maxRollbackDepth)),
837
+ deterministicStats: {
838
+ confirmations: sumProfiles(profiles, 'confirmations'),
839
+ deliveries: sumProfiles(profiles, 'deliveries'),
840
+ duplicates: sumProfiles(profiles, 'duplicates'),
841
+ lost: sumProfiles(profiles, 'lost'),
842
+ reorders: sumProfiles(profiles, 'reorders'),
843
+ disconnectDrops: sumProfiles(profiles, 'disconnectDrops'),
844
+ absentInputFrames: sumProfiles(profiles, 'absentInputFrames'),
845
+ botInputFrames: sumProfiles(profiles, 'botInputFrames'),
846
+ commandProfiles: profiles.filter((profile) => profile.channel === 'command').length,
847
+ },
848
+ };
849
+ const metrics = {
850
+ schemaArtifactId: 'protocol-matrix/v1',
851
+ profiles,
852
+ frameModel,
853
+ checksumConverged: profiles.every((profile) => profile.profileId === 'expected-desync' ? profile.result === 'desync' : profile.checksumConverged),
854
+ expectedDesyncArtifacts: profiles
855
+ .filter((profile) => profile.profileId === 'expected-desync')
856
+ .every((profile) => profile.desyncArtifact !== undefined),
857
+ maxRollbackDepth: frameModel.maxRollbackDepth,
858
+ };
859
+ return createFixtureReport(manifest, fixture, metrics);
860
+ }
861
+ function runProtocolMatrixProfile(fixture, profileId) {
862
+ const options = createProtocolMatrixOptions(fixture, profileId);
863
+ const result = (0, protocol_lab_js_1.runProtocolRollbackLab)(options);
864
+ const expectedDesync = profileId === 'expected-desync';
865
+ const checksumConverged = result.authorityChecksum === result.clientChecksum && result.result === 'pass';
866
+ const profileResult = expectedDesync && result.result === 'desync'
867
+ ? 'desync'
868
+ : certificationPassFail(checksumConverged);
869
+ const desyncArtifact = expectedDesync && result.result === 'desync'
870
+ ? createDesyncArtifact(options, result)
871
+ : undefined;
872
+ const metrics = {
873
+ profileId,
874
+ channel: profileId === 'command-bursts' ? 'command' : 'input',
875
+ authorityChecksum: result.authorityChecksum,
876
+ clientChecksum: result.clientChecksum,
877
+ checksumConverged,
878
+ rollbacks: result.rollbacks,
879
+ corrections: result.eventTimeline.canceled,
880
+ maxRollbackDepth: result.maxRollbackDepth,
881
+ confirmations: result.confirmations,
882
+ deliveries: result.deliveries,
883
+ duplicates: result.duplicates,
884
+ lost: result.lost,
885
+ disconnectDrops: result.disconnectDrops,
886
+ absentInputFrames: result.absentInputFrames,
887
+ botInputFrames: result.botInputFrames,
888
+ inputDelayFrames: result.inputDelayFrames,
889
+ reorders: result.reorders,
890
+ result: profileResult,
891
+ };
892
+ return desyncArtifact === undefined ? metrics : { ...metrics, desyncArtifact };
893
+ }
894
+ function createProtocolMatrixOptions(fixture, profileId) {
895
+ const base = {
896
+ players: fixture.players,
897
+ frames: fixture.frames,
898
+ seed: fixture.seed,
899
+ minDelayFrames: 1,
900
+ maxDelayFrames: 6,
901
+ eventTimelineLimit: 8,
902
+ };
903
+ if (profileId === 'loss-duplicate-reorder') {
904
+ return {
905
+ ...base,
906
+ duplicatePermille: 250,
907
+ lossPermille: 80,
908
+ resendIntervalFrames: 7,
909
+ maxResendAttempts: 12,
910
+ };
911
+ }
912
+ if (profileId === 'reconnect') {
913
+ return {
914
+ ...base,
915
+ duplicatePermille: 100,
916
+ lossPermille: 80,
917
+ resendIntervalFrames: 7,
918
+ maxResendAttempts: 12,
919
+ disconnectStartFrame: Math.max(4, Math.floor(fixture.frames * 0.25)),
920
+ disconnectEndFrame: Math.max(6, Math.floor(fixture.frames * 0.35)),
921
+ };
922
+ }
923
+ if (profileId === 'late-join') {
924
+ return {
925
+ ...base,
926
+ lateJoinSlot: Math.max(0, fixture.players - 1),
927
+ lateJoinFrame: Math.floor(fixture.frames / 2),
928
+ };
929
+ }
930
+ if (profileId === 'asymmetric-latency') {
931
+ return {
932
+ ...base,
933
+ latencySkewSlot: Math.max(0, fixture.players - 2),
934
+ latencySkewFrames: 12,
935
+ };
936
+ }
937
+ if (profileId === 'input-delay') {
938
+ return {
939
+ ...base,
940
+ inputDelayFrames: 8,
941
+ };
942
+ }
943
+ if (profileId === 'bots') {
944
+ return {
945
+ ...base,
946
+ botSlots: [Math.min(1, fixture.players - 1), Math.min(3, fixture.players - 1)],
947
+ };
948
+ }
949
+ if (profileId === 'command-bursts') {
950
+ return {
951
+ ...base,
952
+ game: 'command-bursts',
953
+ duplicatePermille: 100,
954
+ };
955
+ }
956
+ if (profileId === 'expected-desync') {
957
+ return {
958
+ ...base,
959
+ desyncFrame: Math.floor(fixture.frames / 2),
960
+ desyncSlot: Math.min(2, fixture.players - 1),
961
+ expectDesync: true,
962
+ };
963
+ }
964
+ return {
965
+ ...base,
966
+ duplicatePermille: 100,
967
+ };
968
+ }
969
+ function createTinyEcsWorld() {
970
+ return {
971
+ nextEntityId: 1,
972
+ entities: [],
973
+ components: {},
974
+ singletons: {},
975
+ };
976
+ }
977
+ function ecsCreateEntity(world) {
978
+ const entityId = world.nextEntityId;
979
+ world.nextEntityId += 1;
980
+ world.entities.push(entityId);
981
+ world.entities.sort((left, right) => left - right);
982
+ return entityId;
983
+ }
984
+ function ecsDestroyEntity(world, entityId) {
985
+ world.entities = world.entities.filter((id) => id !== entityId);
986
+ for (const store of Object.values(world.components)) {
987
+ delete store[String(entityId)];
988
+ }
989
+ }
990
+ function ecsAddComponent(world, entityId, component, value) {
991
+ world.components[component] = world.components[component] ?? {};
992
+ world.components[component][String(entityId)] = value;
993
+ }
994
+ function ecsSetSingleton(world, name, value) {
995
+ world.singletons[name] = value;
996
+ }
997
+ function ecsQuery(world, components) {
998
+ return world.entities
999
+ .filter((entityId) => components.every((component) => world.components[component]?.[String(entityId)] !== undefined))
1000
+ .sort((left, right) => left - right);
1001
+ }
1002
+ function ecsSnapshot(world) {
1003
+ return (0, canonical_js_1.canonicalStringify)(world);
1004
+ }
1005
+ function ecsRestore(snapshot) {
1006
+ return JSON.parse(snapshot);
1007
+ }
1008
+ function ecsChecksum(world) {
1009
+ return (0, canonical_js_1.defaultChecksum)(world);
1010
+ }
1011
+ function runGridNavigationSimulation(players, frames, seed, restoreFrame) {
1012
+ const width = 16;
1013
+ const height = 16;
1014
+ const obstacles = createNavigationObstacles(width, height, seed);
1015
+ let state = Array.from({ length: players }, (_, slot) => ({
1016
+ x: slot % width,
1017
+ y: Math.floor(slot / width),
1018
+ targetX: (width - 1 - slot) % width,
1019
+ targetY: height - 1 - Math.floor(slot / width),
1020
+ }));
1021
+ let checkpoint;
1022
+ let pathsFound = 0;
1023
+ let avoidanceStable = true;
1024
+ const baselineQuery = findGridPath({ x: 0, y: 0 }, { x: width - 1, y: height - 1 }, width, height, obstacles);
1025
+ const repeatedQuery = findGridPath({ x: 0, y: 0 }, { x: width - 1, y: height - 1 }, width, height, obstacles);
1026
+ const blocked = new Set(obstacles);
1027
+ blocked.add(gridKey(1, 0));
1028
+ blocked.add(gridKey(0, 1));
1029
+ const unreachableDetected = findGridPath({ x: 0, y: 0 }, { x: 2, y: 2 }, width, height, blocked).length === 0;
1030
+ for (let frame = 0; frame < frames; frame += 1) {
1031
+ if (frame === restoreFrame) {
1032
+ checkpoint = (0, canonical_js_1.cloneCanonical)(state);
1033
+ }
1034
+ if (frame === restoreFrame + 3 && checkpoint !== undefined) {
1035
+ state = (0, canonical_js_1.cloneCanonical)(checkpoint);
1036
+ for (let replayFrame = restoreFrame; replayFrame < frame; replayFrame += 1) {
1037
+ state = stepGridNavigation(state, replayFrame, width, height, obstacles);
1038
+ }
1039
+ }
1040
+ state = stepGridNavigation(state, frame, width, height, obstacles);
1041
+ pathsFound += state.length;
1042
+ const occupied = new Set();
1043
+ for (const position of state) {
1044
+ const key = gridKey(position.x, position.y);
1045
+ avoidanceStable = avoidanceStable && !occupied.has(key);
1046
+ occupied.add(key);
1047
+ }
1048
+ }
1049
+ return {
1050
+ finalChecksum: (0, canonical_js_1.defaultChecksum)(state),
1051
+ pathsFound,
1052
+ unreachableDetected,
1053
+ queryOrderStable: (0, canonical_js_1.defaultChecksum)(baselineQuery) === (0, canonical_js_1.defaultChecksum)(repeatedQuery),
1054
+ avoidanceStable,
1055
+ };
1056
+ }
1057
+ function stepGridNavigation(state, frame, width, height, obstacles) {
1058
+ const reserved = new Set();
1059
+ const occupied = new Set(state.map((agent) => gridKey(agent.x, agent.y)));
1060
+ return state.map((agent, slot) => {
1061
+ const currentKey = gridKey(agent.x, agent.y);
1062
+ occupied.delete(currentKey);
1063
+ const target = {
1064
+ x: (agent.targetX + (frame % 7 === 0 ? slot : 0)) % width,
1065
+ y: (agent.targetY + (frame % 11 === 0 ? 1 : 0)) % height,
1066
+ };
1067
+ const path = findGridPath({ x: agent.x, y: agent.y }, target, width, height, obstacles);
1068
+ const next = path[1] ?? { x: agent.x, y: agent.y };
1069
+ const nextKey = gridKey(next.x, next.y);
1070
+ if (reserved.has(nextKey) || occupied.has(nextKey)) {
1071
+ reserved.add(currentKey);
1072
+ return { ...agent, targetX: target.x, targetY: target.y };
1073
+ }
1074
+ reserved.add(nextKey);
1075
+ return { x: next.x, y: next.y, targetX: target.x, targetY: target.y };
1076
+ });
1077
+ }
1078
+ function findGridPath(start, target, width, height, obstacles) {
1079
+ if (obstacles.has(gridKey(start.x, start.y)) || obstacles.has(gridKey(target.x, target.y))) {
1080
+ return [];
1081
+ }
1082
+ const queue = [start];
1083
+ const cameFrom = new Map([[gridKey(start.x, start.y), undefined]]);
1084
+ const points = new Map([[gridKey(start.x, start.y), start]]);
1085
+ const directions = [
1086
+ { x: 1, y: 0 },
1087
+ { x: 0, y: 1 },
1088
+ { x: -1, y: 0 },
1089
+ { x: 0, y: -1 },
1090
+ ];
1091
+ while (queue.length > 0) {
1092
+ const current = queue.shift();
1093
+ if (current.x === target.x && current.y === target.y) {
1094
+ return reconstructGridPath(target, cameFrom, points);
1095
+ }
1096
+ for (const direction of directions) {
1097
+ const next = { x: current.x + direction.x, y: current.y + direction.y };
1098
+ const key = gridKey(next.x, next.y);
1099
+ if (next.x < 0 ||
1100
+ next.y < 0 ||
1101
+ next.x >= width ||
1102
+ next.y >= height ||
1103
+ obstacles.has(key) ||
1104
+ cameFrom.has(key)) {
1105
+ continue;
1106
+ }
1107
+ cameFrom.set(key, gridKey(current.x, current.y));
1108
+ points.set(key, next);
1109
+ queue.push(next);
1110
+ }
1111
+ }
1112
+ return [];
1113
+ }
1114
+ function reconstructGridPath(target, cameFrom, points) {
1115
+ const path = [];
1116
+ let key = gridKey(target.x, target.y);
1117
+ while (key !== undefined) {
1118
+ const point = points.get(key);
1119
+ /* istanbul ignore next -- reconstructGridPath receives keys from the points map built by findGridPath. */
1120
+ if (point === undefined) {
1121
+ return [];
1122
+ }
1123
+ path.push(point);
1124
+ key = cameFrom.get(key);
1125
+ }
1126
+ return path.reverse();
1127
+ }
1128
+ function createNavigationObstacles(width, height, seed) {
1129
+ const obstacles = new Set();
1130
+ for (let y = 1; y < height - 1; y += 3) {
1131
+ for (let x = 2; x < width - 2; x += 5) {
1132
+ if ((x + y + seed) % 4 !== 0) {
1133
+ obstacles.add(gridKey(x, y));
1134
+ }
1135
+ }
1136
+ }
1137
+ return obstacles;
1138
+ }
1139
+ function gridKey(x, y) {
1140
+ return `${x},${y}`;
1141
+ }
1142
+ function runReferenceSampleSimulation(sampleId, players, frames, seed) {
1143
+ const uninterrupted = runReferenceSample(sampleId, players, frames, seed, -1);
1144
+ const restored = runReferenceSample(sampleId, players, frames, seed, Math.floor(frames / 2));
1145
+ return {
1146
+ sampleId,
1147
+ players,
1148
+ frames,
1149
+ mechanics: referenceSampleMechanics(sampleId),
1150
+ finalChecksum: uninterrupted.checksum,
1151
+ replayChecksum: restored.checksum,
1152
+ rollbackChecksumEqual: uninterrupted.checksum === restored.checksum,
1153
+ events: restored.events,
1154
+ };
1155
+ }
1156
+ function runReferenceSample(sampleId, players, frames, seed, restoreFrame) {
1157
+ let state = createReferenceSampleState(sampleId, players, seed);
1158
+ let checkpoint;
1159
+ for (let frame = 0; frame < frames; frame += 1) {
1160
+ if (frame === restoreFrame) {
1161
+ checkpoint = (0, canonical_js_1.cloneCanonical)(state);
1162
+ }
1163
+ if (frame === restoreFrame + 5 && checkpoint !== undefined) {
1164
+ state = (0, canonical_js_1.cloneCanonical)(checkpoint);
1165
+ for (let replayFrame = restoreFrame; replayFrame < frame; replayFrame += 1) {
1166
+ state = stepReferenceSample(state, replayFrame, seed);
1167
+ }
1168
+ }
1169
+ state = stepReferenceSample(state, frame, seed);
1170
+ }
1171
+ return { checksum: (0, canonical_js_1.defaultChecksum)(state), events: state.events };
1172
+ }
1173
+ function createReferenceSampleState(sampleId, players, seed) {
1174
+ return {
1175
+ frame: 0,
1176
+ sampleId,
1177
+ x: Array.from({ length: players }, (_, slot) => slot * 3 + seed % 5),
1178
+ y: Array.from({ length: players }, (_, slot) => (slot % 4) * 4),
1179
+ z: Array.from({ length: players }, () => 0),
1180
+ hp: Array.from({ length: players }, () => 10),
1181
+ cooldown: Array.from({ length: players }, () => 0),
1182
+ score: Array.from({ length: players }, () => 0),
1183
+ phase: 0,
1184
+ events: 0,
1185
+ };
1186
+ }
1187
+ function stepReferenceSample(state, frame, seed) {
1188
+ const x = state.x.slice();
1189
+ const y = state.y.slice();
1190
+ const z = state.z.slice();
1191
+ const hp = state.hp.slice();
1192
+ const cooldown = state.cooldown.map((value) => Math.max(0, value - 1));
1193
+ const score = state.score.slice();
1194
+ let events = state.events;
1195
+ for (let slot = 0; slot < x.length; slot += 1) {
1196
+ const direction = ((seed + frame + slot) % 3) - 1;
1197
+ if (state.sampleId === 'platform-shooter-2d') {
1198
+ x[slot] += direction;
1199
+ y[slot] = Math.max(0, y[slot] + (frame % 30 === 0 ? 5 : -1));
1200
+ if (frame % 17 === slot % 17) {
1201
+ events += hitReferenceTarget(slot, hp, score);
1202
+ }
1203
+ continue;
1204
+ }
1205
+ if (state.sampleId === 'simple-fps') {
1206
+ x[slot] += direction * 2;
1207
+ z[slot] += ((frame + slot) % 2) * 2 - 1;
1208
+ if (cooldown[slot] === 0) {
1209
+ cooldown[slot] = 12;
1210
+ events += hitReferenceTarget(slot, hp, score);
1211
+ }
1212
+ continue;
1213
+ }
1214
+ if (state.sampleId === 'racer-25d') {
1215
+ x[slot] = (x[slot] + 2 + slot % 3) % 400;
1216
+ z[slot] = (z[slot] + direction + 9) % 9;
1217
+ if (x[slot] < 3) {
1218
+ score[slot] += 1;
1219
+ events += 1;
1220
+ }
1221
+ continue;
1222
+ }
1223
+ if (state.sampleId === 'turn-flow-mini-golf') {
1224
+ if (frame % Math.max(8, x.length) === slot % Math.max(8, x.length)) {
1225
+ x[slot] += 8 - (slot % 3);
1226
+ y[slot] += direction;
1227
+ score[slot] += x[slot] > 120 ? 1 : 0;
1228
+ events += 1;
1229
+ }
1230
+ continue;
1231
+ }
1232
+ x[slot] += direction;
1233
+ y[slot] += ((frame + slot) % 2) * 2 - 1;
1234
+ if (frame % 13 === slot % 13) {
1235
+ events += hitReferenceTarget(slot, hp, score);
1236
+ }
1237
+ }
1238
+ return {
1239
+ frame: state.frame + 1,
1240
+ sampleId: state.sampleId,
1241
+ x,
1242
+ y,
1243
+ z,
1244
+ hp,
1245
+ cooldown,
1246
+ score,
1247
+ phase: (state.phase + 1) % 1024,
1248
+ events,
1249
+ };
1250
+ }
1251
+ function hitReferenceTarget(slot, hp, score) {
1252
+ const target = (slot + 1) % hp.length;
1253
+ hp[target] -= 1;
1254
+ score[slot] += 1;
1255
+ if (hp[target] <= 0) {
1256
+ hp[target] = 10;
1257
+ score[target] -= 1;
1258
+ return 2;
1259
+ }
1260
+ return 1;
1261
+ }
1262
+ function referenceSampleMechanics(sampleId) {
1263
+ if (sampleId === 'platform-shooter-2d') {
1264
+ return ['platformer-movement', 'jump-gravity', 'projectile-hit'];
1265
+ }
1266
+ if (sampleId === 'simple-fps') {
1267
+ return ['3d-position', 'cooldown-hitscan', 'health'];
1268
+ }
1269
+ if (sampleId === 'twin-stick-shooter') {
1270
+ return ['top-down-movement', 'projectile-hit', 'cooldown'];
1271
+ }
1272
+ if (sampleId === 'arena-brawler') {
1273
+ return ['melee-hit-resolution', 'health', 'score'];
1274
+ }
1275
+ if (sampleId === 'racer-25d') {
1276
+ return ['track-progress', 'lane-position', 'lap-score'];
1277
+ }
1278
+ if (sampleId === 'turn-flow-mini-golf') {
1279
+ return ['turn-order', 'ball-motion', 'hole-score'];
1280
+ }
1281
+ return ['wraparound-motion', 'collision-events', 'score'];
1282
+ }
1283
+ function runKccSimulation(frames, seed, restoreFrame) {
1284
+ let state = { frame: 0, x: 0, y: 0, vx: 0, vy: 0, grounded: true, coyote: 0 };
1285
+ let snapshot = '';
1286
+ let groundedFrames = 0;
1287
+ let horizontalTravel = 0;
1288
+ let jumps = 0;
1289
+ let coyoteJumps = 0;
1290
+ let wallCollisions = 0;
1291
+ let leftWallCollisions = 0;
1292
+ let rightWallCollisions = 0;
1293
+ let queryOrderStable = true;
1294
+ for (let frame = 0; frame < frames; frame += 1) {
1295
+ if (frame === restoreFrame) {
1296
+ snapshot = (0, canonical_js_1.canonicalStringify)(state);
1297
+ state = JSON.parse(snapshot);
1298
+ }
1299
+ if (frame === 18) {
1300
+ state = { ...state, grounded: false, coyote: 2 };
1301
+ }
1302
+ const move = frame >= 30 && frame < 50 ? 1 : frame >= 60 && frame < 90 ? -1 : ((frame + seed) % 3) - 1;
1303
+ const wantsJump = frame === 2 || frame === 18;
1304
+ const usedCoyoteJump = wantsJump && !state.grounded && state.coyote > 0;
1305
+ if (wantsJump && (state.grounded || state.coyote > 0)) {
1306
+ state = { ...state, vy: 9, grounded: false, coyote: 0 };
1307
+ jumps += 1;
1308
+ if (usedCoyoteJump) {
1309
+ coyoteJumps += 1;
1310
+ }
1311
+ }
1312
+ const previousX = state.x;
1313
+ let x = state.x + move * 2;
1314
+ if (x > 24) {
1315
+ x = 24;
1316
+ wallCollisions += 1;
1317
+ rightWallCollisions += 1;
1318
+ }
1319
+ if (x < -24) {
1320
+ x = -24;
1321
+ wallCollisions += 1;
1322
+ leftWallCollisions += 1;
1323
+ }
1324
+ horizontalTravel += Math.abs(x - previousX);
1325
+ const vy = state.vy - 1;
1326
+ let y = state.y + vy;
1327
+ let grounded = false;
1328
+ let nextVy = vy;
1329
+ if (y <= 0) {
1330
+ y = 0;
1331
+ nextVy = 0;
1332
+ grounded = true;
1333
+ groundedFrames += 1;
1334
+ }
1335
+ const coyote = grounded ? 4 : Math.max(0, state.coyote - 1);
1336
+ const queries = ['ground', 'wall-left', 'wall-right'].sort();
1337
+ queryOrderStable = queryOrderStable && (0, canonical_js_1.defaultChecksum)(queries) === (0, canonical_js_1.defaultChecksum)(['ground', 'wall-left', 'wall-right'].sort());
1338
+ state = { frame: frame + 1, x, y, vx: move * 2, vy: nextVy, grounded, coyote };
1339
+ }
1340
+ return {
1341
+ finalChecksum: (0, canonical_js_1.defaultChecksum)(state),
1342
+ groundedFrames,
1343
+ horizontalTravel,
1344
+ jumps,
1345
+ coyoteJumps,
1346
+ wallCollisions,
1347
+ leftWallCollisions,
1348
+ rightWallCollisions,
1349
+ queryOrderStable,
1350
+ };
1351
+ }
1352
+ function runProjectileSimulation(players, frames, seed, restoreFrame) {
1353
+ let state = {
1354
+ frame: 0,
1355
+ health: Array.from({ length: players }, () => 3),
1356
+ cooldowns: Array.from({ length: players }, () => 0),
1357
+ projectiles: [],
1358
+ nextProjectileId: 1,
1359
+ };
1360
+ let snapshot = '';
1361
+ let projectileHits = 0;
1362
+ let hitscanHits = 0;
1363
+ let cooldownBlocks = 0;
1364
+ let respawns = 0;
1365
+ let eventCancellationCount = 0;
1366
+ let raycastOrderStable = true;
1367
+ for (let frame = 0; frame < frames; frame += 1) {
1368
+ if (frame === restoreFrame) {
1369
+ snapshot = (0, canonical_js_1.canonicalStringify)(state);
1370
+ state = JSON.parse(snapshot);
1371
+ eventCancellationCount += 1;
1372
+ }
1373
+ const health = state.health.slice();
1374
+ const cooldowns = state.cooldowns.map((cooldown) => Math.max(0, cooldown - 1));
1375
+ const projectiles = state.projectiles
1376
+ .map((projectile) => ({ ...projectile, x: projectile.x + projectile.vx }))
1377
+ .filter((projectile) => Math.abs(projectile.x) < 80);
1378
+ let nextProjectileId = state.nextProjectileId;
1379
+ for (let slot = 0; slot < players; slot += 1) {
1380
+ const fireCadence = (frame + slot + seed) % 17;
1381
+ const wantsFire = fireCadence === 0 || fireCadence === 1;
1382
+ if (!wantsFire) {
1383
+ continue;
1384
+ }
1385
+ if (cooldowns[slot] > 0) {
1386
+ cooldownBlocks += 1;
1387
+ continue;
1388
+ }
1389
+ cooldowns[slot] = 6;
1390
+ projectiles.push({ id: nextProjectileId, owner: slot, x: slot * 10, vx: slot % 2 === 0 ? 3 : -3 });
1391
+ nextProjectileId += 1;
1392
+ const rayTargets = Array.from({ length: players }, (_, target) => target)
1393
+ .filter((target) => target !== slot)
1394
+ .sort((left, right) => left - right);
1395
+ raycastOrderStable = raycastOrderStable && (0, canonical_js_1.defaultChecksum)(rayTargets) === (0, canonical_js_1.defaultChecksum)([...rayTargets].sort((left, right) => left - right));
1396
+ const target = rayTargets[(frame + slot) % rayTargets.length];
1397
+ health[target] -= 1;
1398
+ hitscanHits += 1;
1399
+ }
1400
+ for (const projectile of projectiles) {
1401
+ const target = Math.abs(Math.trunc(projectile.x / 10)) % players;
1402
+ if (target !== projectile.owner && frame % 19 === 0) {
1403
+ health[target] -= 1;
1404
+ projectileHits += 1;
1405
+ }
1406
+ }
1407
+ for (let slot = 0; slot < players; slot += 1) {
1408
+ if (health[slot] <= 0) {
1409
+ health[slot] = 3;
1410
+ respawns += 1;
1411
+ }
1412
+ }
1413
+ state = { frame: frame + 1, health, cooldowns, projectiles, nextProjectileId };
1414
+ }
1415
+ return {
1416
+ finalChecksum: (0, canonical_js_1.defaultChecksum)(state),
1417
+ raycastOrderStable,
1418
+ projectileHits,
1419
+ hitscanHits,
1420
+ cooldownBlocks,
1421
+ respawns,
1422
+ eventCancellationCount,
1423
+ };
1424
+ }
1425
+ function createCertificationDescriptor() {
1426
+ return {
1427
+ deterministicVersion: 'counter-duel@1',
1428
+ tickRate: 30,
1429
+ maxPlayers: 8,
1430
+ input: {
1431
+ id: 'counter-duel-input/v1',
1432
+ fields: [
1433
+ {
1434
+ name: 'attack',
1435
+ kind: 'boolean',
1436
+ policy: 'command',
1437
+ defaultValue: false,
1438
+ },
1439
+ {
1440
+ name: 'targetSlot',
1441
+ kind: 'player-slot',
1442
+ policy: 'repeat',
1443
+ defaultValue: 0,
1444
+ },
1445
+ ],
1446
+ },
1447
+ commandChannels: [
1448
+ {
1449
+ id: 'combat',
1450
+ fields: [
1451
+ {
1452
+ name: 'attack',
1453
+ kind: 'boolean',
1454
+ },
1455
+ ],
1456
+ delivery: 'reliable',
1457
+ },
1458
+ ],
1459
+ state: {
1460
+ id: 'counter-duel-state/v1',
1461
+ fields: [
1462
+ {
1463
+ name: 'health',
1464
+ kind: 'integer',
1465
+ numericMode: 'integer',
1466
+ collectionOrdering: 'indexed',
1467
+ checksummed: true,
1468
+ },
1469
+ {
1470
+ name: 'score',
1471
+ kind: 'integer',
1472
+ numericMode: 'integer',
1473
+ collectionOrdering: 'indexed',
1474
+ checksummed: true,
1475
+ },
1476
+ ],
1477
+ },
1478
+ events: [
1479
+ {
1480
+ id: 'counter-duel.hit',
1481
+ rollback: 'verified-only',
1482
+ payloadFields: [
1483
+ {
1484
+ name: 'attacker',
1485
+ kind: 'player-slot',
1486
+ },
1487
+ {
1488
+ name: 'target',
1489
+ kind: 'player-slot',
1490
+ },
1491
+ ],
1492
+ },
1493
+ ],
1494
+ prototypes: [
1495
+ {
1496
+ id: 'player',
1497
+ components: ['Position', 'Health'],
1498
+ },
1499
+ ],
1500
+ systems: [
1501
+ {
1502
+ id: 'cooldowns',
1503
+ },
1504
+ {
1505
+ id: 'combat',
1506
+ },
1507
+ ],
1508
+ };
1509
+ }
1510
+ function createDesyncArtifact(options, result) {
1511
+ const firstDesyncFrame = options.desyncFrame;
1512
+ return {
1513
+ firstDesyncFrame,
1514
+ localChecksum: result.clientChecksum,
1515
+ authorityChecksum: result.authorityChecksum,
1516
+ inputDump: {
1517
+ frame: firstDesyncFrame,
1518
+ slot: options.desyncSlot,
1519
+ injected: true,
1520
+ },
1521
+ stateDump: {
1522
+ frame: result.frames,
1523
+ localChecksum: result.clientChecksum,
1524
+ authorityChecksum: result.authorityChecksum,
1525
+ },
1526
+ schemaArtifactId: 'protocol-matrix/v1',
1527
+ runtimeConfig: {
1528
+ ...options,
1529
+ disconnectDrops: result.disconnectDrops,
1530
+ },
1531
+ fieldDiff: [
1532
+ {
1533
+ path: '/checksum',
1534
+ local: result.clientChecksum,
1535
+ authority: result.authorityChecksum,
1536
+ },
1537
+ ],
1538
+ };
1539
+ }
1540
+ function createSkippedBrowserFixtureReport(manifest, fixture) {
1541
+ const metrics = {
1542
+ skippedReason: 'browser replay verification is optional and runs in a separate certification group',
1543
+ };
1544
+ const required = isFixtureRequired(fixture);
1545
+ return createFixtureReport(manifest, fixture, metrics, required ? 'fail' : 'skip', required ? ['required browser replay verification skipped'] : []);
1546
+ }
1547
+ function createFixtureReport(manifest, fixture, metrics, forcedResult, extraFailures = []) {
1548
+ const tier = resolveFixtureTier(fixture);
1549
+ const required = isFixtureRequired(fixture);
1550
+ const artifactPath = `${manifest.artifactDir}/${fixture.fixtureId}.json`;
1551
+ const reportPath = artifactPath;
1552
+ const budgets = evaluateBudgets(manifest, fixture, metrics);
1553
+ const budgetFailures = budgets
1554
+ .filter((budget) => budget.gating && budget.result === 'fail')
1555
+ .map((budget) => budget.name);
1556
+ const explicitFailures = [
1557
+ ...budgetFailures.map((name) => `budget ${name} failed`),
1558
+ ...extraFailures,
1559
+ ];
1560
+ /* istanbul ignore next -- current failing fixtures provide explicit failures; this fallback protects future forced-fail fixtures. */
1561
+ const failures = forcedResult === 'fail' && explicitFailures.length === 0
1562
+ ? ['fixture reported failure']
1563
+ : explicitFailures;
1564
+ const result = forcedResult === 'skip' ? 'skip' : failures.length === 0 ? (forcedResult ?? 'pass') : 'fail';
1565
+ const reportWithoutId = {
1566
+ schemaVersion: 1,
1567
+ reportId: '',
1568
+ manifestId: manifest.manifestId,
1569
+ fixtureId: fixture.fixtureId,
1570
+ kind: fixture.kind,
1571
+ tier,
1572
+ required,
1573
+ runtimeTarget: fixture.runtimeTarget ?? 'node',
1574
+ players: fixture.players,
1575
+ frames: fixture.frames,
1576
+ seed: fixture.seed,
1577
+ artifactPath,
1578
+ reportPath,
1579
+ metrics,
1580
+ budgets,
1581
+ failures,
1582
+ result,
1583
+ };
1584
+ return {
1585
+ ...reportWithoutId,
1586
+ reportId: (0, canonical_js_1.defaultChecksum)(reportWithoutId),
1587
+ };
1588
+ }
1589
+ function resolveFixtureTier(fixture) {
1590
+ if (fixture.tier !== undefined) {
1591
+ return fixture.tier;
1592
+ }
1593
+ if (fixture.runtimeTarget === 'chromium' || fixture.runtimeTarget === 'webkit') {
1594
+ return 'optional-browser';
1595
+ }
1596
+ return fixture.required === false ? 'optional' : 'required';
1597
+ }
1598
+ function isFixtureRequired(fixture) {
1599
+ if (fixture.required !== undefined) {
1600
+ return fixture.required;
1601
+ }
1602
+ return resolveFixtureTier(fixture) === 'required';
1603
+ }
1604
+ function evaluateBudgets(manifest, fixture, metrics) {
1605
+ const budgets = fixture.budgets ?? {};
1606
+ const results = [];
1607
+ const replayBytes = getReplayBytes(metrics);
1608
+ const packedInputBytesPerSecond = getNumberMetric(metrics, 'packedInputBytesPerSecond');
1609
+ const maxRollbackDepth = getMaxRollbackDepthMetric(metrics);
1610
+ const checksumConverged = getChecksumConverged(metrics);
1611
+ if (budgets.maxReplayBytes !== undefined) {
1612
+ results.push(replayBytes === undefined
1613
+ ? createMissingMetricBudget('maxReplayBytes', budgets.maxReplayBytes, true)
1614
+ : createNumberBudget('maxReplayBytes', replayBytes, budgets.maxReplayBytes, true, replayBytes <= budgets.maxReplayBytes));
1615
+ }
1616
+ if (budgets.maxPackedInputBytesPerSecond !== undefined) {
1617
+ results.push(packedInputBytesPerSecond === undefined
1618
+ ? createMissingMetricBudget('maxPackedInputBytesPerSecond', budgets.maxPackedInputBytesPerSecond, true)
1619
+ : createNumberBudget('maxPackedInputBytesPerSecond', packedInputBytesPerSecond, budgets.maxPackedInputBytesPerSecond, true, packedInputBytesPerSecond <= budgets.maxPackedInputBytesPerSecond));
1620
+ }
1621
+ if (budgets.maxRollbackDepth !== undefined) {
1622
+ results.push(maxRollbackDepth === undefined
1623
+ ? createMissingMetricBudget('maxRollbackDepth', budgets.maxRollbackDepth, true)
1624
+ : createNumberBudget('maxRollbackDepth', maxRollbackDepth, budgets.maxRollbackDepth, true, maxRollbackDepth <= budgets.maxRollbackDepth));
1625
+ }
1626
+ if (budgets.requireChecksumConvergence === true) {
1627
+ results.push(checksumConverged === undefined
1628
+ ? createMissingMetricBudget('requireChecksumConvergence', true, true)
1629
+ : {
1630
+ name: 'requireChecksumConvergence',
1631
+ limit: true,
1632
+ actual: checksumConverged,
1633
+ gating: true,
1634
+ result: certificationPassFail(checksumConverged),
1635
+ });
1636
+ }
1637
+ if (budgets.requireExpectedDesyncArtifacts === true) {
1638
+ const expectedDesyncArtifacts = getExpectedDesyncArtifacts(metrics);
1639
+ results.push({
1640
+ name: 'requireExpectedDesyncArtifacts',
1641
+ limit: true,
1642
+ actual: expectedDesyncArtifacts ?? false,
1643
+ gating: true,
1644
+ result: expectedDesyncArtifacts === true ? 'pass' : 'fail',
1645
+ });
1646
+ }
1647
+ if (budgets.maxElapsedMs !== undefined) {
1648
+ const elapsedMs = getNumberMetric(metrics, 'elapsedMs');
1649
+ results.push(elapsedMs === undefined
1650
+ ? createMissingMetricBudget('maxElapsedMs', budgets.maxElapsedMs, manifest.benchmarkProfile)
1651
+ : createNumberBudget('maxElapsedMs', elapsedMs, budgets.maxElapsedMs, manifest.benchmarkProfile, elapsedMs <= budgets.maxElapsedMs));
1652
+ }
1653
+ if (budgets.maxHeapUsedBytes !== undefined) {
1654
+ const heapUsedBytes = getNumberMetric(metrics, 'heapUsedBytes');
1655
+ results.push(heapUsedBytes === undefined
1656
+ ? createMissingMetricBudget('maxHeapUsedBytes', budgets.maxHeapUsedBytes, manifest.benchmarkProfile)
1657
+ : createNumberBudget('maxHeapUsedBytes', heapUsedBytes, budgets.maxHeapUsedBytes, manifest.benchmarkProfile, heapUsedBytes <= budgets.maxHeapUsedBytes));
1658
+ }
1659
+ if (budgets.maxRssBytes !== undefined) {
1660
+ const rssBytes = getNumberMetric(metrics, 'rssBytes');
1661
+ results.push(rssBytes === undefined
1662
+ ? createMissingMetricBudget('maxRssBytes', budgets.maxRssBytes, manifest.benchmarkProfile)
1663
+ : createNumberBudget('maxRssBytes', rssBytes, budgets.maxRssBytes, manifest.benchmarkProfile, rssBytes <= budgets.maxRssBytes));
1664
+ }
1665
+ return results;
1666
+ }
1667
+ function createNumberBudget(name, actual, limit, gating, pass) {
1668
+ return {
1669
+ name,
1670
+ limit,
1671
+ actual,
1672
+ gating,
1673
+ result: pass || !gating ? 'pass' : 'fail',
1674
+ };
1675
+ }
1676
+ function createMissingMetricBudget(name, limit, gating) {
1677
+ return {
1678
+ name,
1679
+ limit,
1680
+ gating,
1681
+ result: gating ? 'fail' : 'skip',
1682
+ };
1683
+ }
1684
+ function normalizeManifest(manifest) {
1685
+ const defaults = createDefaultCertificationManifest();
1686
+ return {
1687
+ manifestId: manifest.manifestId ?? defaults.manifestId,
1688
+ artifactDir: manifest.artifactDir ?? defaults.artifactDir,
1689
+ benchmarkProfile: manifest.benchmarkProfile ?? defaults.benchmarkProfile,
1690
+ fixtures: manifest.fixtures ?? defaults.fixtures,
1691
+ };
1692
+ }
1693
+ function createDefaultNetcodeFixture(fixtureId, shape, seed) {
1694
+ return {
1695
+ fixtureId,
1696
+ kind: 'netcode',
1697
+ required: true,
1698
+ runtimeTarget: 'node',
1699
+ shape,
1700
+ players: 128,
1701
+ frames: 1_000,
1702
+ seed,
1703
+ hz: 10,
1704
+ rttMs: 200,
1705
+ jitterMs: 100,
1706
+ lossPercent: 2,
1707
+ duplicatePermille: 100,
1708
+ eventTimelineLimit: 0,
1709
+ budgets: {
1710
+ maxReplayBytes: 16_000_000,
1711
+ maxPackedInputBytesPerSecond: 20_000,
1712
+ maxRollbackDepth: 120,
1713
+ requireChecksumConvergence: true,
1714
+ },
1715
+ };
1716
+ }
1717
+ function netcodeShapeToGame(shape) {
1718
+ if (shape === 'moderate-state') {
1719
+ return 'moderate-state';
1720
+ }
1721
+ if (shape === 'command-bursts') {
1722
+ return 'command-bursts';
1723
+ }
1724
+ return 'lab';
1725
+ }
1726
+ function benchmarkChecksumCost(shape, players, seed) {
1727
+ const state = makeNetcodeChecksumState(shape, players, seed);
1728
+ const timings = [];
1729
+ for (let index = 0; index < checksumCostIterations; index += 1) {
1730
+ const startedAt = performance.now();
1731
+ (0, canonical_js_1.defaultChecksum)(state);
1732
+ timings.push(performance.now() - startedAt);
1733
+ }
1734
+ timings.sort((left, right) => left - right);
1735
+ return timings[Math.min(timings.length - 1, Math.floor(timings.length * 0.95))].toFixed(4);
1736
+ }
1737
+ function makeNetcodeChecksumState(shape, players, seed) {
1738
+ if (shape === 'input-minimal') {
1739
+ return {
1740
+ positions: Array.from({ length: players * 2 }, () => 0),
1741
+ actions: Array.from({ length: players }, () => 0),
1742
+ frame: seed,
1743
+ };
1744
+ }
1745
+ return {
1746
+ frame: seed,
1747
+ positionX: Array.from({ length: players }, (_, slot) => slot * 10),
1748
+ positionY: Array.from({ length: players }, (_, slot) => slot * -10),
1749
+ velocityX: Array.from({ length: players }, (_, slot) => (slot % 3) - 1),
1750
+ velocityY: Array.from({ length: players }, (_, slot) => ((slot + 1) % 3) - 1),
1751
+ health: Array.from({ length: players }, () => 100),
1752
+ cooldown: Array.from({ length: players }, () => 0),
1753
+ flags: Array.from({ length: players }, (_, slot) => slot % 2),
1754
+ };
1755
+ }
1756
+ function getReplayBytes(metrics) {
1757
+ if ('replayBytes' in metrics) {
1758
+ return metrics.replayBytes;
1759
+ }
1760
+ return undefined;
1761
+ }
1762
+ function getChecksumConverged(metrics) {
1763
+ if ('checksumConverged' in metrics) {
1764
+ return metrics.checksumConverged;
1765
+ }
1766
+ if ('replayVerification' in metrics) {
1767
+ return metrics.replayVerification.mismatches === 0;
1768
+ }
1769
+ return undefined;
1770
+ }
1771
+ function getExpectedDesyncArtifacts(metrics) {
1772
+ if ('expectedDesyncArtifacts' in metrics) {
1773
+ return metrics.expectedDesyncArtifacts;
1774
+ }
1775
+ return undefined;
1776
+ }
1777
+ function sumProfiles(profiles, key) {
1778
+ return profiles.reduce((sum, profile) => sum + profile[key], 0);
1779
+ }
1780
+ function getNumberMetric(metrics, key) {
1781
+ const value = metrics[key];
1782
+ if (typeof value === 'number') {
1783
+ return value;
1784
+ }
1785
+ if (typeof value === 'string') {
1786
+ const parsed = Number(value);
1787
+ return Number.isFinite(parsed) ? parsed : undefined;
1788
+ }
1789
+ return undefined;
1790
+ }
1791
+ function getMaxRollbackDepthMetric(metrics) {
1792
+ const direct = getNumberMetric(metrics, 'maxRollbackDepth');
1793
+ if (direct !== undefined) {
1794
+ return direct;
1795
+ }
1796
+ const protocolRollback = metrics.protocolRollback;
1797
+ /* istanbul ignore next -- public metrics either expose a finite direct rollback depth or omit protocolRollback entirely. */
1798
+ if (protocolRollback !== null && typeof protocolRollback === 'object') {
1799
+ const value = protocolRollback.maxRollbackDepth;
1800
+ if (typeof value === 'number' && Number.isFinite(value)) {
1801
+ return value;
1802
+ }
1803
+ }
1804
+ return undefined;
1805
+ }
1806
+ function estimateJsonBytes(value) {
1807
+ if (value === null || typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string') {
1808
+ return Buffer.byteLength(JSON.stringify(value));
1809
+ }
1810
+ if (Array.isArray(value)) {
1811
+ if (value.length === 0) {
1812
+ return 2;
1813
+ }
1814
+ return 2 + value.length - 1 + value.reduce((sum, item) => sum + estimateJsonBytes(item), 0);
1815
+ }
1816
+ /* istanbul ignore else -- callers pass JSON-compatible replay/certification values before byte estimation. */
1817
+ if (typeof value === 'object') {
1818
+ const entries = Object.entries(value);
1819
+ /* istanbul ignore next -- byte estimation callers pass non-empty object records. */
1820
+ if (entries.length === 0) {
1821
+ return 2;
1822
+ }
1823
+ return (2 +
1824
+ entries.length -
1825
+ 1 +
1826
+ entries.reduce((sum, [key, entryValue]) => {
1827
+ return sum + Buffer.byteLength(JSON.stringify(key)) + 1 + estimateJsonBytes(entryValue);
1828
+ }, 0));
1829
+ }
1830
+ /* istanbul ignore next -- certification byte estimators receive JSON-compatible replay objects. */
1831
+ throw new Error(`Cannot estimate JSON bytes for ${typeof value}`);
1832
+ }
1833
+ function estimateJsonArrayBytes(itemBytes, itemCount) {
1834
+ /* istanbul ignore next -- replay byte estimation callers pass frame-derived positive counts. */
1835
+ if (itemCount === 0) {
1836
+ return 2;
1837
+ }
1838
+ return 2 + itemCount - 1 + itemBytes;
1839
+ }
1840
+ function estimateNetcodeReplayBytes(shape, players, seed, inputFramesBytes, checksumsBytes) {
1841
+ const emptyReplay = {
1842
+ formatVersion: 1,
1843
+ deterministicVersion: shape === 'input-minimal' ? '0.0.0-source' : `${shape}@1`,
1844
+ tickRate: 30,
1845
+ playerCount: players,
1846
+ inputSchemaId: 'lab-input/v1',
1847
+ stateSchemaId: shape === 'input-minimal' ? 'lab-state/v1' : 'moderate-state/v1',
1848
+ randomSeed: seed,
1849
+ initialState: (0, canonical_js_1.canonicalize)(makeNetcodeChecksumState(shape, players, seed)),
1850
+ inputFrames: [],
1851
+ checksums: [],
1852
+ };
1853
+ return estimateJsonBytes(emptyReplay) - 4 + inputFramesBytes + checksumsBytes;
1854
+ }
1855
+ function getHeapUsedBytes() {
1856
+ return process.memoryUsage().heapUsed;
1857
+ }
1858
+ function getRssBytes() {
1859
+ return process.memoryUsage().rss;
1860
+ }