@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,1299 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runProtocolRollbackLab = runProtocolRollbackLab;
4
+ exports.runProtocolLabInternalCoverageProbe = runProtocolLabInternalCoverageProbe;
5
+ const canonical_js_1 = require("./canonical.js");
6
+ const counter_duel_js_1 = require("./examples/counter-duel.js");
7
+ const offline_session_js_1 = require("./offline-session.js");
8
+ const random_js_1 = require("./random.js");
9
+ const sample_lab_js_1 = require("./sample-lab.js");
10
+ class MockNetwork {
11
+ minDelayFrames;
12
+ maxDelayFrames;
13
+ duplicatePermille;
14
+ lossPermille;
15
+ resendIntervalFrames;
16
+ maxResendAttempts;
17
+ disconnectStartFrame;
18
+ disconnectEndFrame;
19
+ latencySkewSlot;
20
+ latencySkewFrames;
21
+ random;
22
+ scheduled = [];
23
+ deliveries = 0;
24
+ duplicates = 0;
25
+ lost = 0;
26
+ disconnectDrops = 0;
27
+ canceledRetries = 0;
28
+ reorders = 0;
29
+ deliveryDelaySamples = [];
30
+ sequence = 0;
31
+ lastDeliveredSourceFrame = -1;
32
+ constructor(seed, minDelayFrames, maxDelayFrames, duplicatePermille, lossPermille, resendIntervalFrames, maxResendAttempts, disconnectStartFrame, disconnectEndFrame, latencySkewSlot, latencySkewFrames) {
33
+ this.minDelayFrames = minDelayFrames;
34
+ this.maxDelayFrames = maxDelayFrames;
35
+ this.duplicatePermille = duplicatePermille;
36
+ this.lossPermille = lossPermille;
37
+ this.resendIntervalFrames = resendIntervalFrames;
38
+ this.maxResendAttempts = maxResendAttempts;
39
+ this.disconnectStartFrame = disconnectStartFrame;
40
+ this.disconnectEndFrame = disconnectEndFrame;
41
+ this.latencySkewSlot = latencySkewSlot;
42
+ this.latencySkewFrames = latencySkewFrames;
43
+ this.random = new random_js_1.DeterministicRandom(seed);
44
+ }
45
+ send(currentFrame, message) {
46
+ for (let attempt = 0; attempt < this.maxResendAttempts; attempt += 1) {
47
+ this.schedule(currentFrame + attempt * this.resendIntervalFrames, message, false);
48
+ }
49
+ }
50
+ drain(deliveryFrame) {
51
+ const ready = this.scheduled
52
+ .filter((message) => message.deliveryFrame <= deliveryFrame)
53
+ .sort((a, b) => a.deliveryFrame - b.deliveryFrame || a.sequence - b.sequence);
54
+ const readySequences = new Set(ready.map((message) => message.sequence));
55
+ for (let index = this.scheduled.length - 1; index >= 0; index -= 1) {
56
+ if (readySequences.has(this.scheduled[index].sequence)) {
57
+ this.scheduled.splice(index, 1);
58
+ }
59
+ }
60
+ return ready.flatMap((scheduledMessage) => {
61
+ this.deliveries += 1;
62
+ this.deliveryDelaySamples.push(scheduledMessage.deliveryFrame - scheduledMessage.message.startFrame);
63
+ if (scheduledMessage.duplicate) {
64
+ this.duplicates += 1;
65
+ }
66
+ if (scheduledMessage.message.startFrame < this.lastDeliveredSourceFrame) {
67
+ this.reorders += 1;
68
+ }
69
+ this.lastDeliveredSourceFrame = Math.max(this.lastDeliveredSourceFrame, scheduledMessage.message.startFrame);
70
+ return scheduledMessage.message.inputs.map((input, index) => ({
71
+ frame: scheduledMessage.message.startFrame + index,
72
+ slot: scheduledMessage.message.slot,
73
+ input,
74
+ }));
75
+ });
76
+ }
77
+ hasPending() {
78
+ return this.scheduled.length > 0;
79
+ }
80
+ cancel(frame, slot) {
81
+ for (let index = this.scheduled.length - 1; index >= 0; index -= 1) {
82
+ const message = this.scheduled[index].message;
83
+ const frameInBundle = frame >= message.startFrame && frame < message.startFrame + message.inputs.length;
84
+ if (frameInBundle && message.slot === slot) {
85
+ this.scheduled.splice(index, 1);
86
+ this.canceledRetries += 1;
87
+ }
88
+ }
89
+ }
90
+ getStats() {
91
+ return {
92
+ deliveries: this.deliveries,
93
+ duplicates: this.duplicates,
94
+ lost: this.lost,
95
+ disconnectDrops: this.disconnectDrops,
96
+ canceledRetries: this.canceledRetries,
97
+ reorders: this.reorders,
98
+ deliveryDelaySamples: [...this.deliveryDelaySamples],
99
+ };
100
+ }
101
+ schedule(currentFrame, message, duplicate) {
102
+ if (this.isDisconnected(currentFrame)) {
103
+ this.disconnectDrops += 1;
104
+ return;
105
+ }
106
+ if (this.random.nextInt(0, 999) < this.lossPermille) {
107
+ this.lost += 1;
108
+ return;
109
+ }
110
+ const delay = this.random.nextInt(this.minDelayFrames, this.maxDelayFrames) + this.getLatencySkew(message.slot);
111
+ this.scheduled.push({
112
+ deliveryFrame: currentFrame + delay,
113
+ sequence: this.sequence,
114
+ message,
115
+ duplicate,
116
+ });
117
+ this.sequence += 1;
118
+ if (!duplicate && this.random.nextInt(0, 999) < this.duplicatePermille) {
119
+ this.schedule(currentFrame, message, true);
120
+ }
121
+ }
122
+ isDisconnected(frame) {
123
+ return (this.disconnectStartFrame !== undefined &&
124
+ this.disconnectEndFrame !== undefined &&
125
+ frame >= this.disconnectStartFrame &&
126
+ frame <= this.disconnectEndFrame);
127
+ }
128
+ getLatencySkew(slot) {
129
+ return this.latencySkewSlot === slot ? this.latencySkewFrames : 0;
130
+ }
131
+ }
132
+ function runProtocolRollbackLab(options) {
133
+ validateOptions(options);
134
+ const game = options.game ?? 'lab';
135
+ if (game === 'counter-duel') {
136
+ return runProtocolRollbackForGame(options, game, counterDuelAdapter);
137
+ }
138
+ if (game === 'moderate-state') {
139
+ return runProtocolRollbackForGame(options, game, moderateStateAdapter);
140
+ }
141
+ if (game === 'command-bursts') {
142
+ return runProtocolRollbackForGame(options, game, commandBurstsAdapter);
143
+ }
144
+ return runProtocolRollbackForGame(options, game, labAdapter);
145
+ }
146
+ function runProtocolRollbackForGame(options, game, adapter) {
147
+ validateOptions(options);
148
+ const authority = adapter.createSession(options.players, options.seed, options.frames + 1);
149
+ const client = adapter.createSession(options.players, options.seed, options.frames + 1);
150
+ const inputRandom = new random_js_1.DeterministicRandom(options.seed);
151
+ const network = new MockNetwork(options.seed ^ 0x9e3779b9, options.minDelayFrames, options.maxDelayFrames, options.duplicatePermille ?? 0, options.lossPermille ?? 0, options.resendIntervalFrames ?? Math.max(1, options.maxDelayFrames + 1), options.maxResendAttempts ?? 1, options.disconnectStartFrame, options.disconnectEndFrame, options.latencySkewSlot, options.latencySkewFrames ?? 0);
152
+ const commandNetwork = new MockNetwork((options.seed ^ 0x85ebca6b) >>> 0, options.minDelayFrames, options.maxDelayFrames, options.duplicatePermille ?? 0, options.lossPermille ?? 0, options.resendIntervalFrames ?? Math.max(1, options.maxDelayFrames + 1), options.maxResendAttempts ?? 1, options.disconnectStartFrame, options.disconnectEndFrame, options.latencySkewSlot, options.latencySkewFrames ?? 0);
153
+ const truthInputs = [];
154
+ const confirmedInputs = new Map();
155
+ const inputHistory = [];
156
+ const authorityCommandsByFrame = new Map();
157
+ const clientCommandsByFrame = new Map();
158
+ const acceptedClientCommandIds = new Set();
159
+ const pendingBundles = new Map();
160
+ const bundleSizeFrames = options.bundleSizeFrames ?? 1;
161
+ let inputDelayFrames = options.inputDelayFrames ?? 0;
162
+ const adaptiveInputDelay = options.adaptiveInputDelay ?? false;
163
+ const maxAdaptiveInputDelayFrames = options.maxAdaptiveInputDelayFrames ?? Math.max(inputDelayFrames, options.maxDelayFrames + (options.latencySkewFrames ?? 0));
164
+ let maxObservedInputDelayFrames = inputDelayFrames;
165
+ let inputDelayAdjustments = 0;
166
+ let stableFramesSinceDelayAdjustment = 0;
167
+ const predictionPolicy = options.predictionPolicy ?? 'repeat-last';
168
+ const botSlots = new Set(options.botSlots ?? []);
169
+ const eventTracker = createProtocolEventTracker(options.eventTimelineLimit ?? 64);
170
+ let confirmations = 0;
171
+ let localInputConfirmations = 0;
172
+ let localInputRollbacks = 0;
173
+ let bundledInputMessages = 0;
174
+ let absentInputFrames = 0;
175
+ let botInputFrames = 0;
176
+ let rollbacks = 0;
177
+ let maxRollbackDepth = 0;
178
+ let commandDeliveries = 0;
179
+ let commandRejected = 0;
180
+ let commandDuplicateDrops = 0;
181
+ let predictedInputFrames = 0;
182
+ let neutralPredictions = 0;
183
+ let repeatLastPredictions = 0;
184
+ let repeatLastNonDefaultPredictions = 0;
185
+ let spectatorInputFrames = 0;
186
+ let spectatorCommandRejected = 0;
187
+ const predictionFallbackSamples = [];
188
+ const commandRejectionCounts = new Map();
189
+ let commandRollbackFrame;
190
+ const rollbackDepthSamples = [];
191
+ let nextClientFrameToStep = 0;
192
+ let lastCompletelyConfirmedFrame = -1;
193
+ for (let networkFrame = 0; networkFrame < options.frames; networkFrame += 1) {
194
+ generateAuthorityFrame(networkFrame);
195
+ processNetworkFrame(networkFrame);
196
+ stepReadyClientFrames(networkFrame);
197
+ }
198
+ let networkFrame = options.frames;
199
+ while (network.hasPending() || commandNetwork.hasPending() || nextClientFrameToStep < options.frames) {
200
+ processNetworkFrame(networkFrame);
201
+ stepReadyClientFrames(networkFrame);
202
+ networkFrame += 1;
203
+ }
204
+ const authorityChecksum = (0, canonical_js_1.defaultChecksum)(authority.getState());
205
+ const clientChecksum = (0, canonical_js_1.defaultChecksum)(client.getState());
206
+ const commandAuthorityChecksum = (0, canonical_js_1.defaultChecksum)(normalizeCommandFrames(authorityCommandsByFrame));
207
+ const commandClientChecksum = (0, canonical_js_1.defaultChecksum)(normalizeCommandFrames(clientCommandsByFrame));
208
+ const lateJoinReconstruction = reconstructLateJoin();
209
+ const spectatorReconstruction = reconstructSpectator();
210
+ const didDesync = authorityChecksum !== clientChecksum;
211
+ if (didDesync && options.expectDesync !== true) {
212
+ throw new Error(`Protocol lab checksum mismatch: authority=${authorityChecksum} client=${clientChecksum}`);
213
+ }
214
+ if (!didDesync && options.expectDesync === true) {
215
+ throw new Error('Protocol lab expected a desync but checksums matched');
216
+ }
217
+ const commandRollbackObserved = commandRollbackFrame !== undefined;
218
+ const commandTimelineConverged = commandAuthorityChecksum === commandClientChecksum;
219
+ const stats = network.getStats();
220
+ const commandStats = commandNetwork.getStats();
221
+ return {
222
+ game,
223
+ frames: options.frames,
224
+ players: options.players,
225
+ confirmations,
226
+ localInputConfirmations,
227
+ localInputRollbacks,
228
+ bundledInputMessages,
229
+ bundleSizeFrames,
230
+ inputDelayFrames: options.inputDelayFrames ?? 0,
231
+ adaptiveInputDelay,
232
+ finalInputDelayFrames: inputDelayFrames,
233
+ maxObservedInputDelayFrames,
234
+ inputDelayAdjustments,
235
+ deliveries: stats.deliveries,
236
+ duplicates: stats.duplicates,
237
+ lost: stats.lost,
238
+ disconnectDrops: stats.disconnectDrops,
239
+ canceledRetries: stats.canceledRetries,
240
+ absentInputFrames,
241
+ botInputFrames,
242
+ reorders: stats.reorders,
243
+ rollbacks,
244
+ maxRollbackDepth,
245
+ deliveryDelaySamples: stats.deliveryDelaySamples,
246
+ rollbackDepthSamples,
247
+ eventTimeline: eventTracker.summary(),
248
+ commandDeliveries,
249
+ commandDuplicates: commandStats.duplicates,
250
+ commandLost: commandStats.lost,
251
+ commandDisconnectDrops: commandStats.disconnectDrops,
252
+ commandReorders: commandStats.reorders,
253
+ commandDeliveryDelaySamples: commandStats.deliveryDelaySamples,
254
+ commandRejected,
255
+ commandRejectionCounts: Object.fromEntries([...commandRejectionCounts.entries()].sort((left, right) => compareStrings(left[0], right[0]))),
256
+ commandDuplicateDrops,
257
+ commandRollbackConverged: commandTimelineConverged && commandDeliveries > 0,
258
+ commandRollbackObserved,
259
+ commandReplayChecksum: (0, canonical_js_1.defaultChecksum)({
260
+ authority: commandAuthorityChecksum,
261
+ client: commandClientChecksum,
262
+ delivered: commandDeliveries,
263
+ rollbackObserved: commandRollbackObserved,
264
+ rejected: commandRejected,
265
+ rejectionCounts: Object.fromEntries([...commandRejectionCounts.entries()].sort((left, right) => compareStrings(left[0], right[0]))),
266
+ }),
267
+ commandAuthorityChecksum,
268
+ commandClientChecksum,
269
+ spectatorReconstructions: spectatorReconstruction.reconstructions,
270
+ spectatorReconstructionChecksum: spectatorReconstruction.checksum,
271
+ spectatorInputFrames,
272
+ spectatorCommandRejected,
273
+ lateJoinReconstructions: lateJoinReconstruction.reconstructions,
274
+ lateJoinReconstructionChecksum: lateJoinReconstruction.checksum,
275
+ staleJoinRejected: lateJoinReconstruction.staleJoinRejected,
276
+ predictionPolicy,
277
+ predictedInputFrames,
278
+ neutralPredictions,
279
+ repeatLastPredictions,
280
+ repeatLastNonDefaultPredictions,
281
+ predictionFallbackChecksum: (0, canonical_js_1.defaultChecksum)(predictionFallbackSamples),
282
+ authorityChecksum,
283
+ clientChecksum,
284
+ result: didDesync ? 'desync' : 'pass',
285
+ };
286
+ function generateAuthorityFrame(frame) {
287
+ const frameInputs = Array.from({ length: options.players }, (_, slot) => {
288
+ if (botSlots.has(slot)) {
289
+ botInputFrames += 1;
290
+ return adapter.makeBotInput(options.seed, options.players, frame, slot);
291
+ }
292
+ if (isSpectatorSlotActive(options, frame, slot)) {
293
+ return adapter.defaultInput;
294
+ }
295
+ if (!isSlotPresent(options, frame, slot)) {
296
+ absentInputFrames += 1;
297
+ return adapter.defaultInput;
298
+ }
299
+ return adapter.makeInput(inputRandom, options.players, frame, slot);
300
+ });
301
+ truthInputs.push(frameInputs);
302
+ generateAuthorityCommands(frame);
303
+ stepSession(authority, frameInputs, authorityCommandsByFrame.get(frame) ?? []);
304
+ for (let slot = 0; slot < options.players; slot += 1) {
305
+ if (botSlots.has(slot)) {
306
+ continue;
307
+ }
308
+ if (isSpectatorSlotActive(options, frame, slot)) {
309
+ spectatorInputFrames += 1;
310
+ continue;
311
+ }
312
+ if (!isSlotPresent(options, frame, slot)) {
313
+ continue;
314
+ }
315
+ confirmations += 1;
316
+ if (slot === 0) {
317
+ localInputConfirmations += 1;
318
+ }
319
+ const pending = pendingBundles.get(slot) ?? { startFrame: frame, inputs: [] };
320
+ pending.inputs.push(maybeCorruptInput(options, adapter, frame, slot, frameInputs[slot]));
321
+ pendingBundles.set(slot, pending);
322
+ if (shouldFlushBundle(options, frame, bundleSizeFrames, pending.inputs.length)) {
323
+ network.send(frame, {
324
+ startFrame: pending.startFrame,
325
+ slot,
326
+ inputs: pending.inputs.slice(),
327
+ });
328
+ pendingBundles.delete(slot);
329
+ bundledInputMessages += 1;
330
+ }
331
+ }
332
+ }
333
+ function processNetworkFrame(frame) {
334
+ const inputRollbackFrame = applyDeliveries(network.drain(frame));
335
+ const commandRollbackFrame = applyCommandDeliveries(commandNetwork.drain(frame), frame);
336
+ const rollbackFrame = minDefined(inputRollbackFrame, commandRollbackFrame);
337
+ if (rollbackFrame !== undefined) {
338
+ rollbackAndReplay(rollbackFrame);
339
+ adaptInputDelayAfterRollback(frame, rollbackFrame);
340
+ }
341
+ else {
342
+ adaptInputDelayAfterStableFrame();
343
+ }
344
+ confirmCompleteFrames();
345
+ }
346
+ function stepReadyClientFrames(networkFrame) {
347
+ while (nextClientFrameToStep < options.frames && nextClientFrameToStep <= networkFrame - inputDelayFrames) {
348
+ const predictedInputs = buildFrameInputs(nextClientFrameToStep);
349
+ inputHistory[nextClientFrameToStep] = predictedInputs;
350
+ stepSession(client, predictedInputs, clientCommandsByFrame.get(nextClientFrameToStep) ?? []);
351
+ nextClientFrameToStep += 1;
352
+ }
353
+ }
354
+ function applyDeliveries(messages) {
355
+ let rollbackFrame;
356
+ for (const message of messages) {
357
+ const frameInputs = confirmedInputs.get(message.frame) ?? new Map();
358
+ confirmedInputs.set(message.frame, frameInputs);
359
+ const existing = frameInputs.get(message.slot);
360
+ if (existing !== undefined && adapter.inputsEqual(existing, message.input)) {
361
+ continue;
362
+ }
363
+ frameInputs.set(message.slot, message.input);
364
+ network.cancel(message.frame, message.slot);
365
+ const historicalInput = inputHistory[message.frame]?.[message.slot];
366
+ const previousInput =
367
+ /* istanbul ignore next -- rollback deliveries only target frames the client has already stepped. */
368
+ historicalInput ?? adapter.defaultInput;
369
+ if (message.frame < client.currentFrame &&
370
+ !adapter.inputsEqual(previousInput, message.input)) {
371
+ rollbackFrame = Math.min(rollbackFrame ?? message.frame, message.frame);
372
+ if (message.slot === 0) {
373
+ localInputRollbacks += 1;
374
+ }
375
+ }
376
+ }
377
+ return rollbackFrame;
378
+ }
379
+ function applyCommandDeliveries(messages, deliveryFrame) {
380
+ let rollbackFrame;
381
+ for (const message of messages) {
382
+ const rejection = validateProtocolCommandEnvelope(message.input, deliveryFrame, options.players, acceptedClientCommandIds);
383
+ const effectiveRejection = isSpectatorSlotActive(options, deliveryFrame, message.input.senderSlot) ? 'spectator-command' : rejection;
384
+ if (effectiveRejection !== undefined) {
385
+ commandRejected += 1;
386
+ commandRejectionCounts.set(effectiveRejection, (commandRejectionCounts.get(effectiveRejection) ?? 0) + 1);
387
+ spectatorCommandRejected += effectiveRejection === 'spectator-command' ? 1 : 0;
388
+ commandDuplicateDrops += effectiveRejection === 'duplicate-command-id' ? 1 : 0;
389
+ continue;
390
+ }
391
+ const command = message.input.command;
392
+ commandDeliveries += 1;
393
+ acceptedClientCommandIds.add(command.id);
394
+ insertCommand(clientCommandsByFrame, command);
395
+ if (command.frame < client.currentFrame) {
396
+ rollbackFrame = Math.min(rollbackFrame ?? command.frame, command.frame);
397
+ commandRollbackFrame = Math.min(commandRollbackFrame ?? command.frame, command.frame);
398
+ }
399
+ }
400
+ return rollbackFrame;
401
+ }
402
+ function rollbackAndReplay(frame) {
403
+ const targetFrame = client.currentFrame;
404
+ const rollbackDepth = targetFrame - frame;
405
+ /* istanbul ignore next -- private callers only pass rollback frames strictly before client.currentFrame. */
406
+ if (rollbackDepth <= 0) {
407
+ return;
408
+ }
409
+ rollbacks += 1;
410
+ maxRollbackDepth = Math.max(maxRollbackDepth, rollbackDepth);
411
+ rollbackDepthSamples.push(rollbackDepth);
412
+ eventTracker.cancelFromFrame(frame);
413
+ client.restoreToFrame(frame);
414
+ for (let replayFrame = frame; replayFrame < targetFrame; replayFrame += 1) {
415
+ const replayInputs = buildFrameInputs(replayFrame);
416
+ inputHistory[replayFrame] = replayInputs;
417
+ stepSession(client, replayInputs, clientCommandsByFrame.get(replayFrame) ?? []);
418
+ }
419
+ }
420
+ function adaptInputDelayAfterRollback(networkFrame, rollbackFrame) {
421
+ if (!adaptiveInputDelay) {
422
+ return;
423
+ }
424
+ stableFramesSinceDelayAdjustment = 0;
425
+ const observedDepth = Math.max(0, networkFrame - rollbackFrame);
426
+ const targetDelay = Math.min(maxAdaptiveInputDelayFrames, Math.max(inputDelayFrames + 1, observedDepth));
427
+ if (targetDelay > inputDelayFrames) {
428
+ inputDelayFrames = targetDelay;
429
+ maxObservedInputDelayFrames = Math.max(maxObservedInputDelayFrames, inputDelayFrames);
430
+ inputDelayAdjustments += 1;
431
+ }
432
+ }
433
+ function adaptInputDelayAfterStableFrame() {
434
+ if (!adaptiveInputDelay || inputDelayFrames === 0) {
435
+ return;
436
+ }
437
+ stableFramesSinceDelayAdjustment += 1;
438
+ const stableWindow = Math.max(8, maxAdaptiveInputDelayFrames * 2);
439
+ if (stableFramesSinceDelayAdjustment >= stableWindow) {
440
+ inputDelayFrames -= 1;
441
+ inputDelayAdjustments += 1;
442
+ stableFramesSinceDelayAdjustment = 0;
443
+ }
444
+ }
445
+ function buildFrameInputs(frame) {
446
+ return Array.from({ length: options.players }, (_, slot) => {
447
+ if (botSlots.has(slot)) {
448
+ return truthInputs[frame][slot];
449
+ }
450
+ const confirmedInput = confirmedInputs.get(frame)?.get(slot);
451
+ if (confirmedInput !== undefined) {
452
+ return confirmedInput;
453
+ }
454
+ if (isSpectatorSlotActive(options, frame, slot)) {
455
+ return adapter.defaultInput;
456
+ }
457
+ if (isSlotPresent(options, frame, slot)) {
458
+ eventTracker.emitPredictedInput(frame, slot);
459
+ }
460
+ return predictedFallbackInput(frame, slot);
461
+ });
462
+ }
463
+ function predictedFallbackInput(frame, slot) {
464
+ predictedInputFrames += 1;
465
+ if (predictionPolicy === 'neutral') {
466
+ neutralPredictions += 1;
467
+ recordPredictionFallback(frame, slot, adapter.defaultInput);
468
+ return adapter.defaultInput;
469
+ }
470
+ const previous = previousKnownInput(frame, slot);
471
+ const input = previous ?? adapter.defaultInput;
472
+ repeatLastPredictions += previous === undefined ? 0 : 1;
473
+ repeatLastNonDefaultPredictions += previous !== undefined && !adapter.inputsEqual(previous, adapter.defaultInput) ? 1 : 0;
474
+ neutralPredictions += previous === undefined ? 1 : 0;
475
+ recordPredictionFallback(frame, slot, input);
476
+ return input;
477
+ }
478
+ function previousKnownInput(frame, slot) {
479
+ for (let previousFrame = frame - 1; previousFrame >= 0; previousFrame -= 1) {
480
+ const confirmed = confirmedInputs.get(previousFrame)?.get(slot);
481
+ if (confirmed !== undefined) {
482
+ return confirmed;
483
+ }
484
+ const predicted = inputHistory[previousFrame]?.[slot];
485
+ /* istanbul ignore next -- every prior stepped protocol frame stores a full per-slot input history row. */
486
+ if (predicted !== undefined) {
487
+ return predicted;
488
+ }
489
+ }
490
+ return undefined;
491
+ }
492
+ function recordPredictionFallback(frame, slot, input) {
493
+ if (predictionFallbackSamples.length < 256) {
494
+ predictionFallbackSamples.push({ frame, slot, policy: predictionPolicy, input });
495
+ }
496
+ }
497
+ function generateAuthorityCommands(frame) {
498
+ if (game !== 'command-bursts') {
499
+ return;
500
+ }
501
+ for (let slot = 1; slot < options.players; slot += 1) {
502
+ if (botSlots.has(slot) || isSpectatorSlotActive(options, frame, slot) || !isSlotPresent(options, frame, slot)) {
503
+ continue;
504
+ }
505
+ const command = makeProtocolCommand(options.seed, options.players, frame, slot);
506
+ if (command === undefined || command.frame >= options.frames) {
507
+ continue;
508
+ }
509
+ insertCommand(authorityCommandsByFrame, command);
510
+ commandNetwork.send(frame, {
511
+ startFrame: frame,
512
+ slot,
513
+ inputs: [{ senderSlot: slot, command }],
514
+ });
515
+ if ((frame + slot + options.seed) % 113 === 0) {
516
+ commandNetwork.send(frame, {
517
+ startFrame: frame,
518
+ slot,
519
+ inputs: [{ senderSlot: slot, command }],
520
+ });
521
+ }
522
+ }
523
+ if (frame === Math.max(1, Math.floor(options.frames / 3))) {
524
+ for (const envelope of makeHostileProtocolCommandEnvelopes(options.players, frame)) {
525
+ commandNetwork.send(frame, {
526
+ startFrame: frame,
527
+ slot: Math.max(1, Math.min(options.players - 1, envelope.senderSlot)),
528
+ inputs: [envelope],
529
+ });
530
+ }
531
+ }
532
+ if (options.spectatorSlot !== undefined &&
533
+ options.spectatorJoinFrame !== undefined &&
534
+ frame === Math.min(options.frames - 1, options.spectatorJoinFrame + 1)) {
535
+ commandNetwork.send(frame, {
536
+ startFrame: frame,
537
+ slot: options.spectatorSlot,
538
+ inputs: [makeSpectatorProtocolCommandEnvelope(options.spectatorSlot, frame)],
539
+ });
540
+ }
541
+ }
542
+ function confirmCompleteFrames() {
543
+ while (lastCompletelyConfirmedFrame + 1 < options.frames && isFrameComplete(lastCompletelyConfirmedFrame + 1)) {
544
+ lastCompletelyConfirmedFrame += 1;
545
+ eventTracker.confirmThroughFrame(lastCompletelyConfirmedFrame);
546
+ }
547
+ }
548
+ function isFrameComplete(frame) {
549
+ for (let slot = 0; slot < options.players; slot += 1) {
550
+ if (botSlots.has(slot) || !isSlotPresent(options, frame, slot)) {
551
+ continue;
552
+ }
553
+ if (!confirmedInputs.get(frame)?.has(slot)) {
554
+ return false;
555
+ }
556
+ }
557
+ return true;
558
+ }
559
+ function reconstructLateJoin() {
560
+ if (options.lateJoinFrame === undefined || options.lateJoinSlot === undefined || adapter.stepState === undefined) {
561
+ return { reconstructions: 0, checksum: (0, canonical_js_1.defaultChecksum)('late-join:not-run'), staleJoinRejected: false };
562
+ }
563
+ const snapshot = authority.getSnapshot(options.lateJoinFrame);
564
+ /* istanbul ignore next -- the fixture snapshot buffer retains every validated late-join frame. */
565
+ if (snapshot === undefined) {
566
+ return { reconstructions: 0, checksum: (0, canonical_js_1.defaultChecksum)('late-join:missing-snapshot'), staleJoinRejected: false };
567
+ }
568
+ let state = snapshot.state;
569
+ for (let frame = options.lateJoinFrame; frame < options.frames; frame += 1) {
570
+ state = adapter.stepState(state, truthInputs[frame], frame, authorityCommandsByFrame.get(frame) ?? []);
571
+ }
572
+ const checksum = (0, canonical_js_1.defaultChecksum)(state);
573
+ const staleSnapshot = options.lateJoinFrame > 0 ? authority.getSnapshot(options.lateJoinFrame - 1) : undefined;
574
+ const staleJoinRejected = staleSnapshot !== undefined && staleSnapshot.frame !== options.lateJoinFrame;
575
+ return {
576
+ reconstructions: Number(checksum === authorityChecksum),
577
+ checksum,
578
+ staleJoinRejected,
579
+ };
580
+ }
581
+ function reconstructSpectator() {
582
+ if (options.spectatorJoinFrame === undefined || options.spectatorSlot === undefined || adapter.stepState === undefined) {
583
+ return { reconstructions: 0, checksum: (0, canonical_js_1.defaultChecksum)('spectator:not-run') };
584
+ }
585
+ const snapshot = authority.getSnapshot(options.spectatorJoinFrame);
586
+ /* istanbul ignore next -- the fixture snapshot buffer retains every validated spectator join frame. */
587
+ if (snapshot === undefined) {
588
+ return { reconstructions: 0, checksum: (0, canonical_js_1.defaultChecksum)('spectator:missing-snapshot') };
589
+ }
590
+ let state = snapshot.state;
591
+ for (let frame = options.spectatorJoinFrame; frame < options.frames; frame += 1) {
592
+ state = adapter.stepState(state, truthInputs[frame], frame, authorityCommandsByFrame.get(frame) ?? []);
593
+ }
594
+ const checksum = (0, canonical_js_1.defaultChecksum)(state);
595
+ return {
596
+ reconstructions: Number(checksum === authorityChecksum),
597
+ checksum,
598
+ };
599
+ }
600
+ }
601
+ function createProtocolEventTracker(sampleLimit) {
602
+ if (!Number.isInteger(sampleLimit) || sampleLimit < 0) {
603
+ throw new Error('eventTimelineLimit must be a non-negative integer');
604
+ }
605
+ const activePredictions = new Map();
606
+ const sampledRecords = [];
607
+ let sequence = 0;
608
+ let totalRecords = 0;
609
+ let predicted = 0;
610
+ let confirmed = 0;
611
+ let canceled = 0;
612
+ return {
613
+ emitPredictedInput(frame, slot) {
614
+ const dedupeKey = `protocol.input.predicted:${frame}:${slot}`;
615
+ if (activePredictions.has(dedupeKey)) {
616
+ return;
617
+ }
618
+ const record = {
619
+ eventId: 'protocol.input.predicted',
620
+ frame,
621
+ sequence,
622
+ payload: {
623
+ kind: 'input-prediction',
624
+ frame,
625
+ slot,
626
+ },
627
+ predictionState: 'predicted',
628
+ dedupeKey,
629
+ };
630
+ sequence += 1;
631
+ totalRecords += 1;
632
+ predicted += 1;
633
+ activePredictions.set(dedupeKey, record);
634
+ if (sampledRecords.length < sampleLimit) {
635
+ sampledRecords.push(record);
636
+ }
637
+ },
638
+ confirmThroughFrame(frame) {
639
+ for (const [dedupeKey, record] of activePredictions) {
640
+ if (record.frame > frame) {
641
+ continue;
642
+ }
643
+ record.predictionState = 'confirmed';
644
+ activePredictions.delete(dedupeKey);
645
+ predicted -= 1;
646
+ confirmed += 1;
647
+ }
648
+ },
649
+ cancelFromFrame(frame) {
650
+ for (const [dedupeKey, record] of activePredictions) {
651
+ if (record.frame < frame) {
652
+ continue;
653
+ }
654
+ record.predictionState = 'canceled';
655
+ activePredictions.delete(dedupeKey);
656
+ predicted -= 1;
657
+ canceled += 1;
658
+ }
659
+ },
660
+ summary() {
661
+ return {
662
+ totalRecords,
663
+ sampledRecords: sampledRecords.map(cloneProtocolEventRecord),
664
+ truncatedRecords: Math.max(0, totalRecords - sampledRecords.length),
665
+ predicted,
666
+ confirmed,
667
+ canceled,
668
+ verifiedOnly: 0,
669
+ };
670
+ },
671
+ };
672
+ }
673
+ function cloneProtocolEventRecord(record) {
674
+ return {
675
+ eventId: record.eventId,
676
+ frame: record.frame,
677
+ sequence: record.sequence,
678
+ payload: { ...record.payload },
679
+ predictionState: record.predictionState,
680
+ dedupeKey: record.dedupeKey,
681
+ notHashedKeys: [],
682
+ };
683
+ }
684
+ function isSlotPresent(options, frame, slot) {
685
+ if (isSpectatorSlot(options, slot)) {
686
+ return false;
687
+ }
688
+ return !(options.lateJoinSlot === slot && options.lateJoinFrame !== undefined && frame < options.lateJoinFrame);
689
+ }
690
+ function isSpectatorSlot(options, slot) {
691
+ return options.spectatorSlot === slot && options.spectatorJoinFrame !== undefined;
692
+ }
693
+ function isSpectatorSlotActive(options, frame, slot) {
694
+ return isSpectatorSlot(options, slot) && options.spectatorJoinFrame !== undefined && frame >= options.spectatorJoinFrame;
695
+ }
696
+ function shouldFlushBundle(options, frame, bundleSizeFrames, pendingInputs) {
697
+ if (pendingInputs >= bundleSizeFrames) {
698
+ return true;
699
+ }
700
+ return frame === options.frames - 1;
701
+ }
702
+ function maybeCorruptInput(options, adapter, frame, slot, input) {
703
+ if (options.desyncFrame === frame && options.desyncSlot === slot) {
704
+ return adapter.corruptInput(input);
705
+ }
706
+ return input;
707
+ }
708
+ function stepSession(session, inputs, commands) {
709
+ inputs.forEach((input, slot) => {
710
+ session.setInput(slot, input);
711
+ });
712
+ session.recordCommands(commands);
713
+ session.stepFrames(1);
714
+ }
715
+ const labAdapter = {
716
+ defaultInput: sample_lab_js_1.labDefaultInput,
717
+ createSession(players, seed, snapshotBufferSize) {
718
+ return (0, sample_lab_js_1.createLabSession)({
719
+ players,
720
+ seed,
721
+ replayRecordingMode: 'none',
722
+ snapshotBufferSize,
723
+ });
724
+ },
725
+ makeInput(random) {
726
+ return (0, sample_lab_js_1.makeLabInput)(random);
727
+ },
728
+ makeBotInput(seed, _players, frame, slot) {
729
+ const random = new random_js_1.DeterministicRandom(seed).fork(`bot:${slot}:${frame}`);
730
+ return (0, sample_lab_js_1.makeLabInput)(random);
731
+ },
732
+ corruptInput(input) {
733
+ return {
734
+ ...input,
735
+ dx: input.dx + 1,
736
+ };
737
+ },
738
+ inputsEqual(left, right) {
739
+ return left.dx === right.dx && left.dy === right.dy && left.action === right.action;
740
+ },
741
+ stepState(state, inputs, frame, commands) {
742
+ return (0, sample_lab_js_1.labStep)(state, inputs, frame, { frame, dtTicks: 1, commands: commands });
743
+ },
744
+ };
745
+ const counterDuelAdapter = {
746
+ defaultInput: counter_duel_js_1.counterDuelDefaultInput,
747
+ createSession(players, seed, snapshotBufferSize) {
748
+ return (0, counter_duel_js_1.createCounterDuelSession)(players, seed, {
749
+ replayRecordingMode: 'none',
750
+ snapshotBufferSize,
751
+ });
752
+ },
753
+ makeInput(random, players, _frame, slot) {
754
+ return (0, counter_duel_js_1.makeCounterDuelInput)(random, players, slot);
755
+ },
756
+ makeBotInput(seed, players, frame, slot) {
757
+ const random = new random_js_1.DeterministicRandom(seed).fork(`bot:${slot}:${frame}`);
758
+ return (0, counter_duel_js_1.makeCounterDuelInput)(random, players, slot);
759
+ },
760
+ corruptInput(input) {
761
+ return {
762
+ ...input,
763
+ attack: !input.attack,
764
+ };
765
+ },
766
+ inputsEqual(left, right) {
767
+ return left.attack === right.attack && left.targetSlot === right.targetSlot;
768
+ },
769
+ };
770
+ const moderateStateAdapter = {
771
+ defaultInput: sample_lab_js_1.labDefaultInput,
772
+ createSession(players, seed, snapshotBufferSize) {
773
+ return createModerateStateSession(players, seed, snapshotBufferSize, 'moderate-state@1');
774
+ },
775
+ makeInput(random) {
776
+ return (0, sample_lab_js_1.makeLabInput)(random);
777
+ },
778
+ makeBotInput(seed, _players, frame, slot) {
779
+ const random = new random_js_1.DeterministicRandom(seed).fork(`moderate-bot:${slot}:${frame}`);
780
+ return (0, sample_lab_js_1.makeLabInput)(random);
781
+ },
782
+ corruptInput(input) {
783
+ return {
784
+ ...input,
785
+ dx: input.dx + 1,
786
+ };
787
+ },
788
+ inputsEqual(left, right) {
789
+ return labInputsEqual(left, right);
790
+ },
791
+ stepState(state, inputs, frame, commands) {
792
+ return moderateStateStep(state, inputs, frame, { commands: commands });
793
+ },
794
+ };
795
+ const commandBurstsAdapter = {
796
+ ...moderateStateAdapter,
797
+ createSession(players, seed, snapshotBufferSize) {
798
+ return createModerateStateSession(players, seed, snapshotBufferSize, 'command-bursts@1');
799
+ },
800
+ makeInput(random, _players, frame) {
801
+ const input = (0, sample_lab_js_1.makeLabInput)(random);
802
+ return {
803
+ ...input,
804
+ action: frame % 45 < 5 ? 1 : input.action,
805
+ };
806
+ },
807
+ makeBotInput(seed, _players, frame, slot) {
808
+ const random = new random_js_1.DeterministicRandom(seed).fork(`command-burst-bot:${slot}:${frame}`);
809
+ const input = (0, sample_lab_js_1.makeLabInput)(random);
810
+ return {
811
+ ...input,
812
+ action: frame % 45 < 5 ? 1 : input.action,
813
+ };
814
+ },
815
+ };
816
+ function createModerateStateSession(players, seed, snapshotBufferSize, deterministicVersion) {
817
+ return (0, offline_session_js_1.createOfflineSession)({
818
+ tickRate: 30,
819
+ playerCount: players,
820
+ initialState: {
821
+ frame: 0,
822
+ positionX: Array.from({ length: players }, () => 0),
823
+ positionY: Array.from({ length: players }, () => 0),
824
+ velocityX: Array.from({ length: players }, () => 0),
825
+ velocityY: Array.from({ length: players }, () => 0),
826
+ health: Array.from({ length: players }, () => 10),
827
+ cooldown: Array.from({ length: players }, () => 0),
828
+ flags: Array.from({ length: players }, () => 0),
829
+ },
830
+ step: moderateStateStep,
831
+ defaultInput: sample_lab_js_1.labDefaultInput,
832
+ inputSchemaId: 'lab-input/v1',
833
+ stateSchemaId: 'moderate-state/v1',
834
+ deterministicVersion,
835
+ randomSeed: seed,
836
+ snapshotBufferSize,
837
+ replayRecordingMode: 'none',
838
+ canonicalizeInput: (input) => [input.dx, input.dy, input.action],
839
+ });
840
+ }
841
+ function moderateStateStep(state, inputs, _frame, ctx) {
842
+ const positionX = state.positionX.slice();
843
+ const positionY = state.positionY.slice();
844
+ const velocityX = state.velocityX.slice();
845
+ const velocityY = state.velocityY.slice();
846
+ const health = state.health.slice();
847
+ const cooldown = state.cooldown.map((value) => Math.max(0, value - 1));
848
+ const flags = state.flags.slice();
849
+ for (let slot = 0; slot < inputs.length; slot += 1) {
850
+ const input = inputs[slot];
851
+ velocityX[slot] = clampInt(velocityX[slot] + input.dx, -4, 4);
852
+ velocityY[slot] = clampInt(velocityY[slot] + input.dy, -4, 4);
853
+ positionX[slot] += velocityX[slot];
854
+ positionY[slot] += velocityY[slot];
855
+ if (input.action === 1 && cooldown[slot] === 0) {
856
+ const target = (slot + 1) % inputs.length;
857
+ health[target] -= 1;
858
+ cooldown[slot] = 15;
859
+ flags[slot] = (flags[slot] ^ 1) & 3;
860
+ if (health[target] <= 0) {
861
+ health[target] = 10;
862
+ flags[target] = (flags[target] + 1) & 3;
863
+ }
864
+ }
865
+ }
866
+ for (const command of ctx.commands) {
867
+ if (!isProtocolCommand(command)) {
868
+ continue;
869
+ }
870
+ const protocolCommand = command;
871
+ if (protocolCommand.channelId !== 'ability') {
872
+ continue;
873
+ }
874
+ const slot = protocolCommand.slot;
875
+ if (slot < 0 || slot >= inputs.length) {
876
+ continue;
877
+ }
878
+ const payload = protocolCommand.payload;
879
+ const power = typeof payload.power === 'number' ? payload.power : 1;
880
+ const target = (slot + 1 + (power % Math.max(1, inputs.length - 1))) % inputs.length;
881
+ health[target] -= 1 + (power % 3);
882
+ flags[slot] = (flags[slot] + power + 1) & 7;
883
+ cooldown[slot] = Math.max(cooldown[slot], 4 + (power % 5));
884
+ if (health[target] <= 0) {
885
+ health[target] = 10;
886
+ flags[target] = (flags[target] + 2) & 7;
887
+ }
888
+ }
889
+ return {
890
+ frame: state.frame + 1,
891
+ positionX,
892
+ positionY,
893
+ velocityX,
894
+ velocityY,
895
+ health,
896
+ cooldown,
897
+ flags,
898
+ };
899
+ }
900
+ function clampInt(value, min, max) {
901
+ return Math.min(max, Math.max(min, value));
902
+ }
903
+ function labInputsEqual(left, right) {
904
+ return left.dx === right.dx && left.dy === right.dy && left.action === right.action;
905
+ }
906
+ const protocolCommandChannels = [
907
+ { id: 'ability', reliable: true, allowPrediction: true, maxPayloadBytes: 128, maxFrameLead: 8 },
908
+ ];
909
+ const protocolCommandChannelIds = new Set(protocolCommandChannels.map((channel) => channel.id));
910
+ const protocolCommandAssetRefs = new Set(['ability.dash', 'ability.shield']);
911
+ function makeProtocolCommand(seed, players, frame, slot) {
912
+ if ((seed + frame * 7 + slot * 13) % 19 !== 0) {
913
+ return undefined;
914
+ }
915
+ return {
916
+ id: `protocol:${slot}:${frame}:ability`,
917
+ slot,
918
+ frame: frame + 2 + ((slot + seed) % 3),
919
+ channelId: 'ability',
920
+ payload: {
921
+ assetRef: slot % 2 === 0 ? 'ability.dash' : 'ability.shield',
922
+ power: (slot + frame + players + seed) % 10,
923
+ },
924
+ predicted: true,
925
+ submittedFrame: frame,
926
+ };
927
+ }
928
+ function makeHostileProtocolCommandEnvelopes(players, frame) {
929
+ return [
930
+ {
931
+ senderSlot: 0,
932
+ command: {
933
+ id: 'hostile:bad-sender',
934
+ slot: Math.max(1, players - 1),
935
+ frame: frame + 2,
936
+ channelId: 'ability',
937
+ payload: { assetRef: 'ability.dash' },
938
+ submittedFrame: frame,
939
+ },
940
+ },
941
+ {
942
+ senderSlot: 1,
943
+ command: {
944
+ id: 'hostile:bad-asset',
945
+ slot: 1,
946
+ frame: frame + 2,
947
+ channelId: 'ability',
948
+ payload: { assetRef: 'missing.asset' },
949
+ submittedFrame: frame,
950
+ },
951
+ },
952
+ {
953
+ senderSlot: 1,
954
+ command: {
955
+ id: 'hostile:oversized',
956
+ slot: 1,
957
+ frame: frame + 2,
958
+ channelId: 'ability',
959
+ payload: { assetRef: 'ability.dash', text: 'x'.repeat(180) },
960
+ submittedFrame: frame,
961
+ },
962
+ },
963
+ {
964
+ senderSlot: 1,
965
+ command: {
966
+ id: 'hostile:bad-channel',
967
+ slot: 1,
968
+ frame: frame + 2,
969
+ channelId: 'missing',
970
+ payload: { assetRef: 'ability.dash' },
971
+ submittedFrame: frame,
972
+ },
973
+ },
974
+ {
975
+ senderSlot: 0,
976
+ command: {
977
+ id: 'hostile:invalid-slot',
978
+ slot: 0,
979
+ frame: frame + 2,
980
+ channelId: 'ability',
981
+ payload: { assetRef: 'ability.dash' },
982
+ submittedFrame: frame,
983
+ },
984
+ },
985
+ {
986
+ senderSlot: 1,
987
+ command: {
988
+ id: 'hostile:stale',
989
+ slot: 1,
990
+ frame: frame + 2,
991
+ channelId: 'ability',
992
+ payload: { assetRef: 'ability.dash' },
993
+ submittedFrame: frame + 3,
994
+ },
995
+ },
996
+ {
997
+ senderSlot: 1,
998
+ command: {
999
+ id: 'hostile:bad-schema',
1000
+ slot: 1,
1001
+ frame: frame + 2,
1002
+ channelId: 'ability',
1003
+ payload: null,
1004
+ submittedFrame: frame,
1005
+ },
1006
+ },
1007
+ {
1008
+ senderSlot: Math.max(1, players - 1),
1009
+ command: {
1010
+ id: 'hostile:future',
1011
+ slot: Math.max(1, players - 1),
1012
+ frame: frame + 99,
1013
+ channelId: 'ability',
1014
+ payload: { assetRef: 'ability.shield' },
1015
+ submittedFrame: frame,
1016
+ },
1017
+ },
1018
+ ];
1019
+ }
1020
+ function makeSpectatorProtocolCommandEnvelope(slot, frame) {
1021
+ return {
1022
+ senderSlot: slot,
1023
+ command: {
1024
+ id: `spectator:${slot}:${frame}:ability`,
1025
+ slot,
1026
+ frame: frame + 1,
1027
+ channelId: 'ability',
1028
+ payload: { assetRef: 'ability.dash', power: 1 },
1029
+ submittedFrame: frame,
1030
+ },
1031
+ };
1032
+ }
1033
+ function validateProtocolCommandEnvelope(envelope, deliveryFrame, players, acceptedIds) {
1034
+ const command = envelope.command;
1035
+ if (acceptedIds.has(command.id)) {
1036
+ return 'duplicate-command-id';
1037
+ }
1038
+ if (!Number.isInteger(envelope.senderSlot) || envelope.senderSlot !== command.slot) {
1039
+ return 'unauthorized-slot';
1040
+ }
1041
+ if (!Number.isInteger(command.slot) || command.slot < 1 || command.slot >= players) {
1042
+ return 'unauthorized-slot';
1043
+ }
1044
+ if (!protocolCommandChannelIds.has(command.channelId)) {
1045
+ return 'unknown-channel';
1046
+ }
1047
+ const submittedFrame = command.submittedFrame ?? command.frame;
1048
+ if (command.frame < submittedFrame) {
1049
+ return 'stale-frame';
1050
+ }
1051
+ if (command.frame > submittedFrame + 8 || command.frame > deliveryFrame + 128) {
1052
+ return 'future-frame';
1053
+ }
1054
+ if (typeof command.payload !== 'object' || command.payload === null || Array.isArray(command.payload)) {
1055
+ return 'bad-schema';
1056
+ }
1057
+ if ((0, canonical_js_1.canonicalStringify)(command.payload).length > 128) {
1058
+ return 'oversized-payload';
1059
+ }
1060
+ const payload = command.payload;
1061
+ const assetRef = payload.assetRef;
1062
+ if (typeof assetRef !== 'string' || !protocolCommandAssetRefs.has(assetRef)) {
1063
+ return 'bad-asset-ref';
1064
+ }
1065
+ return undefined;
1066
+ }
1067
+ function insertCommand(commandsByFrame, command) {
1068
+ const frameCommands = commandsByFrame.get(command.frame) ?? [];
1069
+ if (!frameCommands.some((existing) => existing.id === command.id)) {
1070
+ frameCommands.push(command);
1071
+ frameCommands.sort(compareProtocolCommands);
1072
+ commandsByFrame.set(command.frame, frameCommands);
1073
+ }
1074
+ }
1075
+ function normalizeCommandFrames(commandsByFrame) {
1076
+ return [...commandsByFrame.entries()]
1077
+ .sort((left, right) => left[0] - right[0])
1078
+ .map(([frame, commands]) => ({ frame, commands: [...commands].sort(compareProtocolCommands) }));
1079
+ }
1080
+ function compareProtocolCommands(left, right) {
1081
+ return left.frame - right.frame
1082
+ || left.slot - right.slot
1083
+ || left.channelId.localeCompare(right.channelId)
1084
+ || left.id.localeCompare(right.id);
1085
+ }
1086
+ function minDefined(left, right) {
1087
+ if (left === undefined) {
1088
+ return right;
1089
+ }
1090
+ if (right === undefined) {
1091
+ return left;
1092
+ }
1093
+ return Math.min(left, right);
1094
+ }
1095
+ function isProtocolCommand(command) {
1096
+ if (typeof command !== 'object' || command === null || Array.isArray(command)) {
1097
+ return false;
1098
+ }
1099
+ const record = command;
1100
+ return typeof record.id === 'string'
1101
+ && typeof record.slot === 'number'
1102
+ && typeof record.frame === 'number'
1103
+ && typeof record.channelId === 'string'
1104
+ && typeof record.payload === 'object'
1105
+ && record.payload !== null
1106
+ && !Array.isArray(record.payload);
1107
+ }
1108
+ function validateOptions(options) {
1109
+ if (options.game !== undefined &&
1110
+ options.game !== 'lab' &&
1111
+ options.game !== 'counter-duel' &&
1112
+ options.game !== 'moderate-state' &&
1113
+ options.game !== 'command-bursts') {
1114
+ throw new Error('protocol rollback lab game must be lab, counter-duel, moderate-state, or command-bursts');
1115
+ }
1116
+ if (!Number.isInteger(options.players) || options.players < 2 || options.players > 128) {
1117
+ throw new Error('protocol rollback lab requires players between 2 and 128');
1118
+ }
1119
+ if (!Number.isInteger(options.frames) || options.frames < 1) {
1120
+ throw new Error('protocol rollback lab requires at least one frame');
1121
+ }
1122
+ if (!Number.isInteger(options.minDelayFrames) ||
1123
+ !Number.isInteger(options.maxDelayFrames) ||
1124
+ options.minDelayFrames < 0 ||
1125
+ options.maxDelayFrames < options.minDelayFrames) {
1126
+ throw new Error('protocol rollback lab delay bounds are invalid');
1127
+ }
1128
+ if (options.bundleSizeFrames !== undefined && (!Number.isInteger(options.bundleSizeFrames) || options.bundleSizeFrames < 1)) {
1129
+ throw new Error('bundleSizeFrames must be a positive integer');
1130
+ }
1131
+ if (options.inputDelayFrames !== undefined && (!Number.isInteger(options.inputDelayFrames) || options.inputDelayFrames < 0)) {
1132
+ throw new Error('inputDelayFrames must be a non-negative integer');
1133
+ }
1134
+ if (options.adaptiveInputDelay !== undefined && typeof options.adaptiveInputDelay !== 'boolean') {
1135
+ throw new Error('adaptiveInputDelay must be boolean');
1136
+ }
1137
+ if (options.maxAdaptiveInputDelayFrames !== undefined &&
1138
+ (!Number.isInteger(options.maxAdaptiveInputDelayFrames) || options.maxAdaptiveInputDelayFrames < 0)) {
1139
+ throw new Error('maxAdaptiveInputDelayFrames must be a non-negative integer');
1140
+ }
1141
+ if (options.predictionPolicy !== undefined &&
1142
+ options.predictionPolicy !== 'repeat-last' &&
1143
+ options.predictionPolicy !== 'neutral') {
1144
+ throw new Error('predictionPolicy must be repeat-last or neutral');
1145
+ }
1146
+ if (options.duplicatePermille !== undefined &&
1147
+ (!Number.isInteger(options.duplicatePermille) || options.duplicatePermille < 0 || options.duplicatePermille > 1000)) {
1148
+ throw new Error('duplicatePermille must be an integer between 0 and 1000');
1149
+ }
1150
+ if (options.lossPermille !== undefined && (!Number.isInteger(options.lossPermille) || options.lossPermille < 0 || options.lossPermille > 1000)) {
1151
+ throw new Error('lossPermille must be an integer between 0 and 1000');
1152
+ }
1153
+ if (options.resendIntervalFrames !== undefined &&
1154
+ (!Number.isInteger(options.resendIntervalFrames) || options.resendIntervalFrames < 1)) {
1155
+ throw new Error('resendIntervalFrames must be a positive integer');
1156
+ }
1157
+ if (options.maxResendAttempts !== undefined && (!Number.isInteger(options.maxResendAttempts) || options.maxResendAttempts < 1)) {
1158
+ throw new Error('maxResendAttempts must be a positive integer');
1159
+ }
1160
+ const hasDisconnectStart = options.disconnectStartFrame !== undefined;
1161
+ const hasDisconnectEnd = options.disconnectEndFrame !== undefined;
1162
+ if (hasDisconnectStart !== hasDisconnectEnd) {
1163
+ throw new Error('disconnect frame range is invalid');
1164
+ }
1165
+ if (hasDisconnectStart &&
1166
+ hasDisconnectEnd &&
1167
+ (!Number.isInteger(options.disconnectStartFrame) ||
1168
+ !Number.isInteger(options.disconnectEndFrame) ||
1169
+ options.disconnectStartFrame < 0 ||
1170
+ options.disconnectEndFrame < options.disconnectStartFrame)) {
1171
+ throw new Error('disconnect frame range is invalid');
1172
+ }
1173
+ if (options.desyncFrame !== undefined && (!Number.isInteger(options.desyncFrame) || options.desyncFrame < 0 || options.desyncFrame >= options.frames)) {
1174
+ throw new Error('desyncFrame must be within the simulated frame range');
1175
+ }
1176
+ if (options.desyncSlot !== undefined && (!Number.isInteger(options.desyncSlot) || options.desyncSlot < 1 || options.desyncSlot >= options.players)) {
1177
+ throw new Error('desyncSlot must be a remote player slot within range');
1178
+ }
1179
+ if ((options.desyncFrame === undefined) !== (options.desyncSlot === undefined)) {
1180
+ throw new Error('desyncFrame and desyncSlot must be provided together');
1181
+ }
1182
+ if ((options.lateJoinSlot === undefined) !== (options.lateJoinFrame === undefined)) {
1183
+ throw new Error('lateJoinSlot and lateJoinFrame must be provided together');
1184
+ }
1185
+ if (options.lateJoinSlot !== undefined &&
1186
+ (!Number.isInteger(options.lateJoinSlot) || options.lateJoinSlot < 1 || options.lateJoinSlot >= options.players)) {
1187
+ throw new Error('lateJoinSlot must be a remote player slot within range');
1188
+ }
1189
+ if (options.lateJoinFrame !== undefined &&
1190
+ (!Number.isInteger(options.lateJoinFrame) || options.lateJoinFrame < 0 || options.lateJoinFrame >= options.frames)) {
1191
+ throw new Error('lateJoinFrame must be within the simulated frame range');
1192
+ }
1193
+ if ((options.spectatorSlot === undefined) !== (options.spectatorJoinFrame === undefined)) {
1194
+ throw new Error('spectatorSlot and spectatorJoinFrame must be provided together');
1195
+ }
1196
+ if (options.spectatorSlot !== undefined &&
1197
+ (!Number.isInteger(options.spectatorSlot) || options.spectatorSlot < 1 || options.spectatorSlot >= options.players)) {
1198
+ throw new Error('spectatorSlot must be a remote player slot within range');
1199
+ }
1200
+ if (options.spectatorJoinFrame !== undefined &&
1201
+ (!Number.isInteger(options.spectatorJoinFrame) || options.spectatorJoinFrame < 0 || options.spectatorJoinFrame >= options.frames)) {
1202
+ throw new Error('spectatorJoinFrame must be within the simulated frame range');
1203
+ }
1204
+ if ((options.latencySkewSlot === undefined) !== (options.latencySkewFrames === undefined)) {
1205
+ throw new Error('latencySkewSlot and latencySkewFrames must be provided together');
1206
+ }
1207
+ if (options.latencySkewSlot !== undefined &&
1208
+ (!Number.isInteger(options.latencySkewSlot) || options.latencySkewSlot < 1 || options.latencySkewSlot >= options.players)) {
1209
+ throw new Error('latencySkewSlot must be a remote player slot within range');
1210
+ }
1211
+ if (options.latencySkewFrames !== undefined &&
1212
+ (!Number.isInteger(options.latencySkewFrames) || options.latencySkewFrames < 0)) {
1213
+ throw new Error('latencySkewFrames must be a non-negative integer');
1214
+ }
1215
+ if (options.botSlots !== undefined) {
1216
+ const seen = new Set();
1217
+ for (const slot of options.botSlots) {
1218
+ if (!Number.isInteger(slot) || slot < 1 || slot >= options.players) {
1219
+ throw new Error('botSlots must contain remote player slots within range');
1220
+ }
1221
+ if (seen.has(slot)) {
1222
+ throw new Error('botSlots cannot contain duplicates');
1223
+ }
1224
+ seen.add(slot);
1225
+ }
1226
+ }
1227
+ }
1228
+ function compareStrings(left, right) {
1229
+ return left < right ? -1 : left > right ? 1 : 0;
1230
+ }
1231
+ function runProtocolLabInternalCoverageProbe() {
1232
+ const tracker = createProtocolEventTracker(4);
1233
+ tracker.emitPredictedInput(2, 1);
1234
+ tracker.emitPredictedInput(2, 1);
1235
+ const eventSummary = tracker.summary();
1236
+ const state = {
1237
+ frame: 0,
1238
+ positionX: [0, 0],
1239
+ positionY: [0, 0],
1240
+ velocityX: [0, 0],
1241
+ velocityY: [0, 0],
1242
+ health: [10, 2],
1243
+ cooldown: [0, 0],
1244
+ flags: [0, 0],
1245
+ };
1246
+ const next = moderateStateStep(state, [sample_lab_js_1.labDefaultInput, sample_lab_js_1.labDefaultInput], 0, {
1247
+ commands: [
1248
+ null,
1249
+ { id: 'wrong-channel', slot: 0, frame: 0, channelId: 'other', payload: {} },
1250
+ { id: 'bad-slot', slot: 3, frame: 0, channelId: 'ability', payload: {} },
1251
+ { id: 'default-power', slot: 0, frame: 0, channelId: 'ability', payload: {} },
1252
+ ],
1253
+ });
1254
+ const commands = [
1255
+ { id: 'b', slot: 1, frame: 2, channelId: 'ability', payload: {}, submittedFrame: 0 },
1256
+ { id: 'a', slot: 1, frame: 2, channelId: 'ability', payload: {}, submittedFrame: 0 },
1257
+ { id: 'c', slot: 0, frame: 2, channelId: 'ability', payload: {}, submittedFrame: 0 },
1258
+ { id: 'd', slot: 0, frame: 1, channelId: 'ability', payload: {}, submittedFrame: 0 },
1259
+ ];
1260
+ const commandsByFrame = new Map();
1261
+ insertCommand(commandsByFrame, commands[0]);
1262
+ insertCommand(commandsByFrame, commands[1]);
1263
+ insertCommand(commandsByFrame, commands[2]);
1264
+ insertCommand(commandsByFrame, commands[3]);
1265
+ insertCommand(commandsByFrame, commands[1]);
1266
+ const validationReasons = [
1267
+ validateProtocolCommandEnvelope({
1268
+ senderSlot: 0,
1269
+ command: { id: 'invalid-slot', slot: 0, frame: 2, channelId: 'ability', payload: { assetRef: 'ability.dash' }, submittedFrame: 0 },
1270
+ }, 2, 2, new Set()),
1271
+ validateProtocolCommandEnvelope({
1272
+ senderSlot: 1,
1273
+ command: { id: 'stale', slot: 1, frame: 2, channelId: 'ability', payload: { assetRef: 'ability.dash' }, submittedFrame: 3 },
1274
+ }, 2, 2, new Set()),
1275
+ validateProtocolCommandEnvelope({
1276
+ senderSlot: 1,
1277
+ command: { id: 'bad-schema', slot: 1, frame: 2, channelId: 'ability', payload: null, submittedFrame: 0 },
1278
+ }, 2, 2, new Set()),
1279
+ validateProtocolCommandEnvelope({
1280
+ senderSlot: 1,
1281
+ command: { id: 'valid-with-default-submitted', slot: 1, frame: 2, channelId: 'ability', payload: { assetRef: 'ability.dash' } },
1282
+ }, 2, 2, new Set()),
1283
+ ];
1284
+ return {
1285
+ eventPredicted: eventSummary.predicted,
1286
+ eventSampled: eventSummary.sampledRecords.length,
1287
+ invalidCommandFrame: next.frame,
1288
+ defaultPowerHealth: next.health[1],
1289
+ validationReasons,
1290
+ normalizedCommandIds: normalizeCommandFrames(commandsByFrame).flatMap((entry) => entry.commands.map((command) => command.id)),
1291
+ sortedComparison: [
1292
+ compareProtocolCommands(commands[0], commands[1]),
1293
+ compareProtocolCommands(commands[0], commands[2]),
1294
+ compareProtocolCommands(commands[0], commands[3]),
1295
+ compareStrings('z', 'a'),
1296
+ compareStrings('a', 'a'),
1297
+ ],
1298
+ };
1299
+ }