@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,810 @@
1
+ import { canonicalStringify, cloneCanonical, defaultChecksum } from './canonical.js';
2
+ import { createOfflineSession } from './offline-session.js';
3
+ import { DeterministicRandom } from './random.js';
4
+ import { createDeterministicReplayBundle, minimizeDeterministicReplayDesyncBundle, verifyDeterministicReplayBundle, } from './replay-bundle.js';
5
+ export function createDeterministicLocalAuthorityRuntime(players, options = {}) {
6
+ const maxFrameLead = options.maxFrameLead ?? 12;
7
+ const rollbackWindowFrames = options.rollbackWindowFrames ?? 0;
8
+ const stateReducer = options.stateReducer ?? defaultLocalAuthorityStateReducer;
9
+ const retainAcceptedMessages = rollbackWindowFrames > 0 || options.stateReducer !== undefined;
10
+ const authorityQueue = new Map();
11
+ const deliveryQueue = new Map();
12
+ const acceptedMessages = [];
13
+ const clientStateBySlot = Array.from({ length: players }, () => 0);
14
+ const lateJoinSlots = new Set();
15
+ const spectatorSlots = new Set();
16
+ const acceptedInputIds = new Set();
17
+ let commandChecksum = defaultChecksum({ kind: 'local-authority-runtime', players });
18
+ let state = {
19
+ frame: 0,
20
+ players,
21
+ clients: Array.from({ length: players }, (_, slot) => ({
22
+ slot,
23
+ connected: true,
24
+ spectator: false,
25
+ inputDelayFrames: 0,
26
+ lastAckFrame: 0,
27
+ })),
28
+ stateBySlot: Array.from({ length: players }, () => 0),
29
+ rejectedMessages: 0,
30
+ rollbackDepths: [],
31
+ clockDriftFrames: [],
32
+ };
33
+ function updateClient(slot, updater) {
34
+ assertSlot(state, slot);
35
+ state = {
36
+ ...state,
37
+ clients: state.clients.map((client) => client.slot === slot ? updater(client) : client),
38
+ };
39
+ }
40
+ const runtime = {
41
+ get state() {
42
+ return cloneCanonical(state);
43
+ },
44
+ submitInput(message) {
45
+ if (message.inputId !== undefined && !isValidInputId(message.inputId)) {
46
+ state = { ...state, rejectedMessages: state.rejectedMessages + 1 };
47
+ return false;
48
+ }
49
+ if (!Number.isInteger(message.slot) ||
50
+ message.slot < 0 ||
51
+ message.slot >= players ||
52
+ (message.senderSlot !== undefined && (!Number.isInteger(message.senderSlot) || message.senderSlot !== message.slot)) ||
53
+ !Number.isInteger(message.frame) ||
54
+ !Number.isFinite(message.value) ||
55
+ !Number.isInteger(message.value) ||
56
+ (message.delay !== undefined && (!Number.isInteger(message.delay) || message.delay < 0))) {
57
+ state = { ...state, rejectedMessages: state.rejectedMessages + 1 };
58
+ return false;
59
+ }
60
+ const frameInputId = defaultAuthorityInputId(message);
61
+ const inputId = message.inputId ?? frameInputId;
62
+ if (acceptedInputIds.has(inputId) || acceptedInputIds.has(frameInputId)) {
63
+ state = { ...state, rejectedMessages: state.rejectedMessages + 1 };
64
+ return false;
65
+ }
66
+ let inputData;
67
+ if (message.data !== undefined) {
68
+ try {
69
+ inputData = cloneCanonical(message.data);
70
+ }
71
+ catch {
72
+ state = { ...state, rejectedMessages: state.rejectedMessages + 1 };
73
+ return false;
74
+ }
75
+ }
76
+ if (message.frame < state.frame - rollbackWindowFrames ||
77
+ message.frame > state.frame + maxFrameLead) {
78
+ state = { ...state, rejectedMessages: state.rejectedMessages + 1 };
79
+ return false;
80
+ }
81
+ const client = state.clients[message.slot];
82
+ if (client.connected !== true || client.spectator === true) {
83
+ state = { ...state, rejectedMessages: state.rejectedMessages + 1 };
84
+ return false;
85
+ }
86
+ const delay = message.delay ?? client.inputDelayFrames;
87
+ const deliveryFrame = message.frame + Math.max(0, delay);
88
+ acceptedInputIds.add(inputId);
89
+ acceptedInputIds.add(frameInputId);
90
+ commandChecksum = defaultChecksum({ commandChecksum, inputId, frameInputId, slot: message.slot, frame: message.frame, value: message.value, data: inputData });
91
+ const acceptedMessage = {
92
+ slot: message.slot,
93
+ frame: message.frame,
94
+ delay,
95
+ deliveryFrame,
96
+ value: message.value,
97
+ ...(inputData === undefined ? {} : { data: inputData }),
98
+ };
99
+ enqueueAuthorityFrameMessage(authorityQueue, acceptedMessage);
100
+ enqueueAuthorityDeliveryMessage(deliveryQueue, acceptedMessage);
101
+ if (retainAcceptedMessages) {
102
+ acceptedMessages.push(acceptedMessage);
103
+ }
104
+ return true;
105
+ },
106
+ disconnect(slot) {
107
+ updateClient(slot, (client) => ({ ...client, connected: false }));
108
+ },
109
+ reconnect(slot) {
110
+ updateClient(slot, (client) => ({ ...client, connected: true, lastAckFrame: state.frame }));
111
+ },
112
+ lateJoin(slot) {
113
+ assertSlot(state, slot);
114
+ lateJoinSlots.add(slot);
115
+ clientStateBySlot[slot] = state.stateBySlot[slot];
116
+ updateClient(slot, (client) => ({ ...client, connected: true, lastAckFrame: state.frame }));
117
+ },
118
+ addSpectator(slot) {
119
+ assertSlot(state, slot);
120
+ spectatorSlots.add(slot);
121
+ removeQueuedMessagesForSlot(authorityQueue, slot);
122
+ removeQueuedMessagesForSlot(deliveryQueue, slot);
123
+ removeAcceptedMessagesForSlotFromFrame(acceptedMessages, slot, state.frame);
124
+ clientStateBySlot[slot] = state.stateBySlot[slot];
125
+ updateClient(slot, (client) => ({ ...client, spectator: true }));
126
+ },
127
+ step() {
128
+ state = retainAcceptedMessages
129
+ ? applyAuthorityMessages(state, authorityQueue, acceptedMessages, state.frame, stateReducer)
130
+ : applyQueuedAuthorityMessages(state, authorityQueue, state.frame, stateReducer);
131
+ deliverClientMessages(deliveryQueue, clientStateBySlot, state.frame, stateReducer);
132
+ state = { ...state, frame: state.frame + 1 };
133
+ },
134
+ snapshot() {
135
+ return cloneCanonical({
136
+ state,
137
+ authorityQueue: serializeAuthorityQueue(authorityQueue),
138
+ deliveryQueue: serializeDeliveryQueue(deliveryQueue),
139
+ acceptedMessages: [...acceptedMessages].sort(compareAuthorityFrameMessages),
140
+ clientStateBySlot,
141
+ commandChecksum,
142
+ lateJoinSlots: [...lateJoinSlots].sort(compareNumbers),
143
+ spectatorSlots: [...spectatorSlots].sort(compareNumbers),
144
+ acceptedInputIds: [...acceptedInputIds].sort(compareStrings),
145
+ });
146
+ },
147
+ restore(snapshot) {
148
+ const restoredState = cloneCanonical(snapshot.state);
149
+ state = {
150
+ ...restoredState,
151
+ stateBySlot: Array.from({ length: restoredState.players }, (_, slot) => restoredState.stateBySlot[slot] ?? 0),
152
+ };
153
+ restoreAuthorityQueue(authorityQueue, snapshot.authorityQueue ?? []);
154
+ restoreDeliveryQueue(deliveryQueue, snapshot.deliveryQueue);
155
+ acceptedMessages.length = 0;
156
+ acceptedMessages.push(...(snapshot.acceptedMessages ?? []).map((message) => cloneCanonical(message)));
157
+ for (let slot = 0; slot < clientStateBySlot.length; slot += 1) {
158
+ clientStateBySlot[slot] = snapshot.clientStateBySlot[slot] ?? 0;
159
+ }
160
+ commandChecksum = snapshot.commandChecksum;
161
+ lateJoinSlots.clear();
162
+ snapshot.lateJoinSlots.forEach((slot) => lateJoinSlots.add(slot));
163
+ spectatorSlots.clear();
164
+ snapshot.spectatorSlots.forEach((slot) => spectatorSlots.add(slot));
165
+ acceptedInputIds.clear();
166
+ (snapshot.acceptedInputIds ?? []).forEach((inputId) => acceptedInputIds.add(inputId));
167
+ },
168
+ checksums() {
169
+ const projectedClientStateBySlot = retainAcceptedMessages
170
+ ? projectClientMessages(players, acceptedMessages, stateReducer)
171
+ : projectDeliveryMessages(deliveryQueue, clientStateBySlot, stateReducer);
172
+ const authorityProjectionChecksum = defaultChecksum(state.stateBySlot);
173
+ const clientProjectionChecksum = defaultChecksum(projectedClientStateBySlot);
174
+ return {
175
+ authorityChecksum: defaultChecksum(state),
176
+ clientChecksum: defaultChecksum(clientStateBySlot),
177
+ authorityProjectionChecksum,
178
+ clientProjectionChecksum,
179
+ convergence: authorityProjectionChecksum === clientProjectionChecksum,
180
+ commandChecksum,
181
+ };
182
+ },
183
+ };
184
+ return runtime;
185
+ }
186
+ function defaultAuthorityInputId(message) {
187
+ return `slot:${message.slot}:frame:${message.frame}`;
188
+ }
189
+ function isValidInputId(inputId) {
190
+ return /^[A-Za-z0-9:._-]{1,128}$/.test(inputId);
191
+ }
192
+ export function runDeterministicLocalAuthorityFixture(players, frames, seed) {
193
+ const random = new DeterministicRandom(seed);
194
+ const lateJoinSlots = new Set(Array.from({ length: players }, (_, slot) => slot).filter((slot) => slot > 0 && slot % 47 === 0));
195
+ const spectatorSlots = new Set(Array.from({ length: players }, (_, slot) => slot).filter((slot) => slot > 0 && slot % 53 === 0 && !lateJoinSlots.has(slot)));
196
+ let state = {
197
+ frame: 0,
198
+ players,
199
+ clients: Array.from({ length: players }, (_, slot) => ({
200
+ slot,
201
+ connected: !lateJoinSlots.has(slot),
202
+ spectator: false,
203
+ inputDelayFrames: 2 + (slot % 4),
204
+ lastAckFrame: 0,
205
+ })),
206
+ stateBySlot: Array.from({ length: players }, () => 0),
207
+ rejectedMessages: 0,
208
+ rollbackDepths: [],
209
+ clockDriftFrames: [],
210
+ };
211
+ let reconnects = 0;
212
+ let lateJoins = 0;
213
+ let spectators = 0;
214
+ let absentBeforeJoinInputs = 0;
215
+ let lateJoinReconstructions = 0;
216
+ let spectatorReconstructions = 0;
217
+ let acceptedMessages = 0;
218
+ let commandChecksum = defaultChecksum({ kind: 'local-authority-command-stream', seed, players });
219
+ const migrationFrame = Math.floor(frames / 2);
220
+ let persisted;
221
+ let migrationQueueRestored = false;
222
+ let migrationCommandTimelineRestored = false;
223
+ let slotReservationsRestored = false;
224
+ const deliveryQueue = new Map();
225
+ const rollbackDepthHistogram = new Map();
226
+ const clockDriftHistogram = new Map();
227
+ const clientStateBySlot = Array.from({ length: players }, () => 0);
228
+ const costs = [];
229
+ for (let frame = 0; frame < frames; frame += 1) {
230
+ if (frame === migrationFrame) {
231
+ persisted = persistAuthorityRuntime(state, deliveryQueue, clientStateBySlot, commandChecksum, lateJoinSlots, spectatorSlots);
232
+ state = cloneCanonical(persisted.state);
233
+ restoreDeliveryQueue(deliveryQueue, persisted.deliveryQueue);
234
+ for (let slot = 0; slot < clientStateBySlot.length; slot += 1) {
235
+ clientStateBySlot[slot] = persisted.clientStateBySlot[slot];
236
+ }
237
+ migrationQueueRestored = defaultChecksum(serializeDeliveryQueue(deliveryQueue)) === defaultChecksum(persisted.deliveryQueue);
238
+ migrationCommandTimelineRestored = commandChecksum === persisted.commandChecksum;
239
+ slotReservationsRestored = defaultChecksum([...lateJoinSlots].sort(compareNumbers)) === defaultChecksum(persisted.lateJoinSlots)
240
+ && defaultChecksum([...spectatorSlots].sort(compareNumbers)) === defaultChecksum(persisted.spectatorSlots);
241
+ }
242
+ const clients = state.clients.map((client) => {
243
+ if (frame === 7 && client.slot % 31 === 0) {
244
+ reconnects += 1;
245
+ return { ...client, connected: false };
246
+ }
247
+ if (frame === 11 && client.slot % 31 === 0) {
248
+ return { ...client, connected: true, lastAckFrame: frame };
249
+ }
250
+ if (frame === 13 && lateJoinSlots.has(client.slot) && !client.connected) {
251
+ lateJoins += 1;
252
+ lateJoinReconstructions += 1;
253
+ return { ...client, connected: true, lastAckFrame: frame };
254
+ }
255
+ if (frame === 17 && spectatorSlots.has(client.slot) && !client.spectator) {
256
+ spectators += 1;
257
+ removeQueuedMessagesForSlot(deliveryQueue, client.slot);
258
+ clientStateBySlot[client.slot] = state.stateBySlot[client.slot];
259
+ spectatorReconstructions += 1;
260
+ return { ...client, spectator: true };
261
+ }
262
+ return client;
263
+ });
264
+ let rejectedMessages = state.rejectedMessages;
265
+ const nextStateBySlot = [...state.stateBySlot];
266
+ for (const client of clients) {
267
+ const drift = random.nextInt(0, 3) - 1;
268
+ incrementHistogram(clockDriftHistogram, Math.abs(drift));
269
+ if (!client.connected || client.spectator) {
270
+ absentBeforeJoinInputs += !client.connected && lateJoinSlots.has(client.slot) ? 1 : 0;
271
+ continue;
272
+ }
273
+ if (client.slot === players - 1 && frame % 29 === 0) {
274
+ rejectedMessages += 1;
275
+ }
276
+ else {
277
+ acceptedMessages += 1;
278
+ const delay = client.inputDelayFrames + drift;
279
+ const value = frame + client.slot + delay;
280
+ commandChecksum = defaultChecksum({ commandChecksum, slot: client.slot, frame, value });
281
+ enqueueAuthorityDeliveryMessage(deliveryQueue, {
282
+ slot: client.slot,
283
+ frame,
284
+ delay,
285
+ deliveryFrame: frame + Math.max(0, delay),
286
+ value,
287
+ });
288
+ nextStateBySlot[client.slot] += value;
289
+ }
290
+ const delay = client.inputDelayFrames + drift;
291
+ incrementHistogram(rollbackDepthHistogram, Math.max(0, delay));
292
+ }
293
+ deliverClientMessages(deliveryQueue, clientStateBySlot, frame, defaultLocalAuthorityStateReducer);
294
+ state = {
295
+ frame: frame + 1,
296
+ players,
297
+ clients,
298
+ stateBySlot: nextStateBySlot,
299
+ rejectedMessages,
300
+ rollbackDepths: [],
301
+ clockDriftFrames: [],
302
+ };
303
+ costs.push(players / 10_000 + acceptedMessages / 1_000_000);
304
+ }
305
+ deliverAllClientMessages(deliveryQueue, clientStateBySlot);
306
+ const authorityProjectionChecksum = defaultChecksum(state.stateBySlot);
307
+ const clientProjectionChecksum = defaultChecksum(clientStateBySlot);
308
+ return {
309
+ authorityChecksum: defaultChecksum(state),
310
+ clientChecksum: defaultChecksum(clientStateBySlot),
311
+ authorityProjectionChecksum,
312
+ clientProjectionChecksum,
313
+ convergence: authorityProjectionChecksum === clientProjectionChecksum,
314
+ players,
315
+ frames,
316
+ reconnects,
317
+ lateJoins,
318
+ spectators,
319
+ absentBeforeJoinInputs,
320
+ lateJoinReconstructions,
321
+ spectatorReconstructions,
322
+ rejectedMessages: state.rejectedMessages,
323
+ migrationFrame,
324
+ migrationPersistedSections: persisted === undefined ? 0 : 5,
325
+ migrationQueueRestored,
326
+ migrationCommandTimelineRestored,
327
+ slotReservationsRestored,
328
+ inputDelayP95: percentile(state.clients.map((client) => client.inputDelayFrames), 95),
329
+ rollbackDepthP95: percentileFromHistogram(rollbackDepthHistogram, 95),
330
+ clockDriftP95: percentileFromHistogram(clockDriftHistogram, 95),
331
+ p95FrameCost: percentile(costs, 95),
332
+ p99FrameCost: percentile(costs, 99),
333
+ };
334
+ }
335
+ export function runDeterministicLocalAuthorityRuntimePolicyFixture() {
336
+ const runtime = createDeterministicLocalAuthorityRuntime(2, { maxFrameLead: 4, rollbackWindowFrames: 2 });
337
+ runtime.step();
338
+ runtime.step();
339
+ const inputData = {
340
+ axisX: 2,
341
+ fire: true,
342
+ buttons: ['jump', 'dash'],
343
+ aim: { x: 7, y: -3 },
344
+ };
345
+ const lateWithinRollbackWindowAccepted = runtime.submitInput({
346
+ slot: 0,
347
+ frame: 1,
348
+ value: 5,
349
+ data: inputData,
350
+ delay: 0,
351
+ inputId: 'late-structured-input',
352
+ });
353
+ const queuedSnapshot = runtime.snapshot();
354
+ const queuedData = queuedSnapshot.authorityQueue
355
+ .flatMap((entry) => entry.messages)
356
+ .find((message) => message.slot === 0 && message.frame === 1)?.data;
357
+ runtime.step();
358
+ const afterLateStep = runtime.checksums();
359
+ const expiredLateInputRejected = runtime.submitInput({
360
+ slot: 1,
361
+ frame: 0,
362
+ value: 7,
363
+ inputId: 'expired-late-input',
364
+ }) === false;
365
+ const strictRuntime = createDeterministicLocalAuthorityRuntime(1, { maxFrameLead: 4 });
366
+ strictRuntime.step();
367
+ const strictStaleInputRejected = strictRuntime.submitInput({
368
+ slot: 0,
369
+ frame: 0,
370
+ value: 3,
371
+ inputId: 'strict-stale-input',
372
+ }) === false;
373
+ const malformedRuntime = createDeterministicLocalAuthorityRuntime(1, { maxFrameLead: 4, rollbackWindowFrames: 1 });
374
+ const malformedStructuredInputRejected = malformedRuntime.submitInput({
375
+ slot: 0,
376
+ frame: 0,
377
+ value: 1,
378
+ data: Symbol('malformed-structured-input'),
379
+ inputId: 'malformed-structured-input',
380
+ }) === false;
381
+ const restored = createDeterministicLocalAuthorityRuntime(2, { maxFrameLead: 4, rollbackWindowFrames: 2 });
382
+ restored.restore(queuedSnapshot);
383
+ restored.step();
384
+ const restoredAfterLateStep = restored.checksums();
385
+ let clientProjectionValueAfterLateInput = -1;
386
+ /* istanbul ignore else -- this fixture's rollbackEquivalent assertion requires convergence; the else value is diagnostic only. */
387
+ if (afterLateStep.convergence) {
388
+ clientProjectionValueAfterLateInput = runtime.state.stateBySlot[0];
389
+ }
390
+ const nonCommutative = runNonCommutativeLateInputResimulationFixture();
391
+ return {
392
+ checksum: afterLateStep.authorityProjectionChecksum,
393
+ restoredChecksum: restoredAfterLateStep.authorityProjectionChecksum,
394
+ rollbackEquivalent: afterLateStep.authorityProjectionChecksum === restoredAfterLateStep.authorityProjectionChecksum
395
+ && afterLateStep.clientProjectionChecksum === restoredAfterLateStep.clientProjectionChecksum,
396
+ nonCommutativeRuntimeChecksum: nonCommutative.runtimeChecksum,
397
+ nonCommutativeFullReplayChecksum: nonCommutative.fullReplayChecksum,
398
+ nonCommutativeAppendOnlyChecksum: nonCommutative.appendOnlyChecksum,
399
+ nonCommutativeLateInputResimulated: nonCommutative.runtimeChecksum === nonCommutative.fullReplayChecksum,
400
+ nonCommutativeAppendOnlyDiverged: nonCommutative.appendOnlyChecksum !== nonCommutative.fullReplayChecksum,
401
+ nonCommutativeResimulatedFrames: nonCommutative.resimulatedFrames,
402
+ lateWithinRollbackWindowAccepted,
403
+ expiredLateInputRejected,
404
+ strictStaleInputRejected,
405
+ structuredInputDataPreserved: defaultChecksum(queuedData) === defaultChecksum(inputData),
406
+ malformedStructuredInputRejected,
407
+ structuredInputChecksum: defaultChecksum(inputData),
408
+ authorityValueAfterLateInput: runtime.state.stateBySlot[0],
409
+ clientProjectionValueAfterLateInput,
410
+ rollbackWindowFrames: 2,
411
+ };
412
+ }
413
+ export function runDeterministicOfflineToAuthorityUpgradeFixture(players, frames, seed) {
414
+ const random = new DeterministicRandom(seed);
415
+ const inputsByFrame = [];
416
+ for (let frame = 0; frame < frames; frame += 1) {
417
+ const frameInputs = [];
418
+ for (let slot = 0; slot < players; slot += 1) {
419
+ frameInputs.push((random.nextInt(0, 7) + frame + slot) % 11);
420
+ }
421
+ inputsByFrame.push(frameInputs);
422
+ }
423
+ const offline = createOfflineSession({
424
+ tickRate: 60,
425
+ playerCount: players,
426
+ initialState: { stateBySlot: Array.from({ length: players }, () => 0), frame: 0 },
427
+ defaultInput: { value: 0 },
428
+ inputSchemaId: 'offline-authority-input/v1',
429
+ stateSchemaId: 'offline-authority-state/v1',
430
+ deterministicVersion: 'offline-authority-upgrade@1',
431
+ snapshotBufferSize: Math.max(8, Math.min(frames + 1, 512)),
432
+ step: (state, inputs, frame) => ({
433
+ stateBySlot: state.stateBySlot.map((value, slot) => value + inputs[slot].value),
434
+ frame: frame + 1,
435
+ }),
436
+ });
437
+ const authority = createDeterministicLocalAuthorityRuntime(players, { maxFrameLead: frames + 4 });
438
+ let snapshotFrame = Math.floor(frames / 2);
439
+ let authoritySnapshot;
440
+ for (let frame = 0; frame < frames; frame += 1) {
441
+ const frameInputs = inputsByFrame[frame];
442
+ for (let slot = 0; slot < players; slot += 1) {
443
+ const value = frameInputs[slot];
444
+ offline.setInput(slot, { value });
445
+ authority.submitInput({
446
+ slot,
447
+ frame,
448
+ value,
449
+ delay: 0,
450
+ inputId: `offline-authority:${frame}:${slot}`,
451
+ });
452
+ }
453
+ offline.stepFrames(1);
454
+ authority.step();
455
+ if (frame + 1 === snapshotFrame) {
456
+ authoritySnapshot = authority.snapshot();
457
+ }
458
+ }
459
+ if (authoritySnapshot === undefined) {
460
+ snapshotFrame = frames;
461
+ authoritySnapshot = authority.snapshot();
462
+ }
463
+ const restored = createDeterministicLocalAuthorityRuntime(players, { maxFrameLead: frames + 4 });
464
+ restored.restore(authoritySnapshot);
465
+ for (let frame = snapshotFrame; frame < frames; frame += 1) {
466
+ const frameInputs = inputsByFrame[frame];
467
+ for (let slot = 0; slot < players; slot += 1) {
468
+ restored.submitInput({
469
+ slot,
470
+ frame,
471
+ value: frameInputs[slot],
472
+ delay: 0,
473
+ inputId: `offline-authority-restored:${frame}:${slot}`,
474
+ });
475
+ }
476
+ restored.step();
477
+ }
478
+ const offlineState = offline.getState();
479
+ const replay = offline.exportReplay();
480
+ const authorityChecksums = authority.checksums();
481
+ const restoredChecksums = restored.checksums();
482
+ const offlineChecksum = defaultChecksum(offlineState.stateBySlot);
483
+ const replayFinalChecksum = replay.checksums.at(-1)?.checksum ?? '';
484
+ const offlineStateChecksum = defaultChecksum(offlineState);
485
+ const authorityChecksum = authorityChecksums.authorityProjectionChecksum;
486
+ const authorityRestoredChecksum = restoredChecksums.authorityProjectionChecksum;
487
+ return {
488
+ checksum: defaultChecksum({
489
+ offlineChecksum,
490
+ authorityChecksum,
491
+ authorityRestoredChecksum,
492
+ replayChecksum: defaultChecksum(replay),
493
+ }),
494
+ players,
495
+ frames,
496
+ offlineFrame: offline.currentFrame,
497
+ authorityFrame: authority.state.frame,
498
+ offlineChecksum,
499
+ authorityChecksum,
500
+ authorityRestoredChecksum,
501
+ replayChecksum: defaultChecksum(replay),
502
+ stateMatch: offlineChecksum === authorityChecksum,
503
+ replayVerified: replayFinalChecksum === offlineStateChecksum,
504
+ restoreVerified: authorityRestoredChecksum === authorityChecksum,
505
+ authorityConvergence: authorityChecksums.convergence,
506
+ inputRows: replay.inputFrames.reduce((sum, inputFrame) => sum + inputFrame.inputs.length, 0),
507
+ snapshotFrame,
508
+ };
509
+ }
510
+ export function runDeterministicLocalAuthorityReplayMinimizerFixture(players, frames, seed) {
511
+ const random = new DeterministicRandom(seed);
512
+ const runtime = createDeterministicLocalAuthorityRuntime(players);
513
+ const inputHistory = [];
514
+ const checksums = [];
515
+ const snapshots = [];
516
+ const debugEvents = [];
517
+ for (let frame = 0; frame < frames; frame += 1) {
518
+ const inputs = [];
519
+ for (let slot = 0; slot < players; slot += 1) {
520
+ const value = (random.nextInt(0, 9) + frame + slot) % 13;
521
+ runtime.submitInput({
522
+ slot,
523
+ frame,
524
+ value,
525
+ delay: 0,
526
+ inputId: `authority-replay:${frame}:${slot}`,
527
+ });
528
+ inputs.push({ slot, dx: value, dy: 0 });
529
+ }
530
+ runtime.step();
531
+ inputHistory.push({ frame, inputs });
532
+ const snapshotPayload = {
533
+ frame,
534
+ positions: runtime.state.stateBySlot,
535
+ commandScore: 0,
536
+ };
537
+ const bytes = canonicalStringify(snapshotPayload);
538
+ const checksum = defaultChecksum(bytes);
539
+ snapshots.push({ frame, bytes, checksum });
540
+ checksums.push({ frame, checksum });
541
+ debugEvents.push({
542
+ frame,
543
+ type: 'local-authority.snapshot',
544
+ payload: { frame, checksum },
545
+ });
546
+ }
547
+ const validBundle = createDeterministicReplayBundle({
548
+ runtimeVersion: 'local-authority-replay@1',
549
+ engineHash: 'local-authority-engine',
550
+ schemaHash: 'local-authority-schema',
551
+ assetDbHash: 'local-authority-assets',
552
+ configHash: 'local-authority-config',
553
+ sampleId: 'local-authority-replay-minimizer',
554
+ inputHistory,
555
+ commandHistory: [],
556
+ checksums,
557
+ debugEvents,
558
+ snapshots,
559
+ metadata: { players, frames, seed },
560
+ });
561
+ const mismatchFrame = Math.max(1, Math.floor(frames / 2));
562
+ const corruptedBundle = createDeterministicReplayBundle({
563
+ ...validBundle,
564
+ checksums: validBundle.checksums.map((checksum) => checksum.frame === mismatchFrame
565
+ ? { ...checksum, checksum: `corrupt:${checksum.frame}` }
566
+ : checksum),
567
+ });
568
+ const validVerification = verifyDeterministicReplayBundle(validBundle);
569
+ const corruptedVerification = verifyDeterministicReplayBundle(corruptedBundle);
570
+ const minimization = minimizeDeterministicReplayDesyncBundle(validBundle, corruptedBundle);
571
+ return {
572
+ checksum: defaultChecksum({
573
+ valid: validVerification.bundleHash,
574
+ corrupted: corruptedVerification.bundleHash,
575
+ minimized: minimization?.verifier.bundleHash ?? '',
576
+ }),
577
+ players,
578
+ frames,
579
+ validReplayVerified: validVerification.result === 'pass',
580
+ corruptedReplayFailed: corruptedVerification.result === 'fail',
581
+ minimizedReplayFailed: minimization?.verifier.result === 'fail',
582
+ mismatchFrame: corruptedVerification.firstMismatch.frame,
583
+ minimizedMismatchFrame: minimization?.verifier.firstMismatch?.frame ?? -1,
584
+ originalReplayBytes: minimization?.originalReplayBytes ?? 0,
585
+ minimizedReplayBytes: minimization?.minimizedReplayBytes ?? 0,
586
+ removedInputFrames: minimization?.removedInputFrames ?? 0,
587
+ removedSnapshots: minimization?.removedSnapshots ?? 0,
588
+ removedChecksumFrames: minimization?.removedChecksumFrames ?? 0,
589
+ debugEvents: validBundle.debugEvents.length,
590
+ };
591
+ }
592
+ function runNonCommutativeLateInputResimulationFixture() {
593
+ const stateReducer = (currentValue, message) => (currentValue * 31 + message.value) % 2_147_483_647;
594
+ const runtime = createDeterministicLocalAuthorityRuntime(1, { maxFrameLead: 4, rollbackWindowFrames: 4, stateReducer });
595
+ runtime.submitInput({ slot: 0, frame: 0, value: 2, inputId: 'noncommutative:f0' });
596
+ runtime.step();
597
+ runtime.submitInput({ slot: 0, frame: 2, value: 5, inputId: 'noncommutative:f2' });
598
+ runtime.step();
599
+ runtime.step();
600
+ const lateInputFrame = 1;
601
+ const verifiedFrame = runtime.state.frame;
602
+ runtime.submitInput({ slot: 0, frame: lateInputFrame, value: 3, inputId: 'noncommutative:f1-late' });
603
+ runtime.step();
604
+ const runtimeChecksum = runtime.checksums().authorityProjectionChecksum;
605
+ const fullReplay = createDeterministicLocalAuthorityRuntime(1, { maxFrameLead: 4, rollbackWindowFrames: 4, stateReducer });
606
+ fullReplay.submitInput({ slot: 0, frame: 0, value: 2, inputId: 'noncommutative-full:f0' });
607
+ fullReplay.step();
608
+ fullReplay.submitInput({ slot: 0, frame: 1, value: 3, inputId: 'noncommutative-full:f1' });
609
+ fullReplay.step();
610
+ fullReplay.submitInput({ slot: 0, frame: 2, value: 5, inputId: 'noncommutative-full:f2' });
611
+ fullReplay.step();
612
+ fullReplay.step();
613
+ const fullReplayChecksum = fullReplay.checksums().authorityProjectionChecksum;
614
+ const appendOnlyValue = stateReducer(stateReducer(stateReducer(0, { slot: 0, frame: 0, delay: 0, deliveryFrame: 0, value: 2 }), {
615
+ slot: 0,
616
+ frame: 2,
617
+ delay: 0,
618
+ deliveryFrame: 2,
619
+ value: 5,
620
+ }), {
621
+ slot: 0,
622
+ frame: lateInputFrame,
623
+ delay: 0,
624
+ deliveryFrame: lateInputFrame,
625
+ value: 3,
626
+ });
627
+ return {
628
+ runtimeChecksum,
629
+ fullReplayChecksum,
630
+ appendOnlyChecksum: defaultChecksum([appendOnlyValue]),
631
+ resimulatedFrames: Math.max(0, verifiedFrame - lateInputFrame),
632
+ };
633
+ }
634
+ function persistAuthorityRuntime(state, deliveryQueue, clientStateBySlot, commandChecksum, lateJoinSlots, spectatorSlots) {
635
+ return cloneCanonical({
636
+ state,
637
+ deliveryQueue: serializeDeliveryQueue(deliveryQueue),
638
+ clientStateBySlot,
639
+ commandChecksum,
640
+ lateJoinSlots: [...lateJoinSlots].sort(compareNumbers),
641
+ spectatorSlots: [...spectatorSlots].sort(compareNumbers),
642
+ });
643
+ }
644
+ function assertSlot(state, slot) {
645
+ if (!Number.isInteger(slot) || slot < 0 || slot >= state.players) {
646
+ throw new Error(`Invalid local authority slot ${slot}`);
647
+ }
648
+ }
649
+ function serializeDeliveryQueue(queue) {
650
+ return [...queue.entries()]
651
+ .map(([frame, messages]) => ({ frame, messages: [...messages].sort(compareAuthorityMessages) }))
652
+ .sort((left, right) => left.frame - right.frame);
653
+ }
654
+ function restoreDeliveryQueue(queue, entries) {
655
+ queue.clear();
656
+ for (const entry of entries) {
657
+ queue.set(entry.frame, entry.messages.map((message) => cloneCanonical(message)));
658
+ }
659
+ }
660
+ function serializeAuthorityQueue(queue) {
661
+ return [...queue.entries()]
662
+ .map(([frame, messages]) => ({ frame, messages: [...messages].sort(compareAuthorityMessages) }))
663
+ .sort((left, right) => left.frame - right.frame);
664
+ }
665
+ function restoreAuthorityQueue(queue, entries) {
666
+ queue.clear();
667
+ for (const entry of entries) {
668
+ queue.set(entry.frame, entry.messages.map((message) => cloneCanonical(message)));
669
+ }
670
+ }
671
+ function enqueueAuthorityFrameMessage(queue, message) {
672
+ const messages = queue.get(message.frame) ?? [];
673
+ messages.push(message);
674
+ queue.set(message.frame, messages);
675
+ }
676
+ function enqueueAuthorityDeliveryMessage(queue, message) {
677
+ const messages = queue.get(message.deliveryFrame) ?? [];
678
+ messages.push(message);
679
+ queue.set(message.deliveryFrame, messages);
680
+ }
681
+ function applyAuthorityMessages(state, queue, acceptedMessages, frame, stateReducer) {
682
+ const dueFrames = [...queue.keys()].filter((targetFrame) => targetFrame <= frame).sort((left, right) => left - right);
683
+ for (const targetFrame of dueFrames) {
684
+ queue.delete(targetFrame);
685
+ }
686
+ const nextStateBySlot = replayAuthorityMessages(state.players, acceptedMessages, frame, stateReducer);
687
+ return { ...state, stateBySlot: nextStateBySlot };
688
+ }
689
+ function applyQueuedAuthorityMessages(state, queue, frame, stateReducer) {
690
+ const dueFrames = [...queue.keys()].filter((targetFrame) => targetFrame <= frame).sort((left, right) => left - right);
691
+ if (dueFrames.length === 0) {
692
+ return state;
693
+ }
694
+ const nextStateBySlot = [...state.stateBySlot];
695
+ for (const targetFrame of dueFrames) {
696
+ const messages = queue.get(targetFrame);
697
+ for (const message of [...messages].sort(compareAuthorityMessages)) {
698
+ nextStateBySlot[message.slot] = stateReducer(nextStateBySlot[message.slot], message);
699
+ }
700
+ queue.delete(targetFrame);
701
+ }
702
+ return { ...state, stateBySlot: nextStateBySlot };
703
+ }
704
+ function deliverClientMessages(queue, clientStateBySlot, frame, stateReducer) {
705
+ const dueFrames = [...queue.keys()].filter((deliveryFrame) => deliveryFrame <= frame).sort((left, right) => left - right);
706
+ for (const deliveryFrame of dueFrames) {
707
+ const messages = queue.get(deliveryFrame);
708
+ for (const message of [...messages].sort(compareAuthorityMessages)) {
709
+ clientStateBySlot[message.slot] = stateReducer(clientStateBySlot[message.slot], message);
710
+ }
711
+ queue.delete(deliveryFrame);
712
+ }
713
+ }
714
+ function deliverAllClientMessages(queue, clientStateBySlot) {
715
+ for (const deliveryFrame of [...queue.keys()].sort((left, right) => left - right)) {
716
+ const messages = queue.get(deliveryFrame);
717
+ for (const message of [...messages].sort(compareAuthorityMessages)) {
718
+ clientStateBySlot[message.slot] += message.value;
719
+ }
720
+ queue.delete(deliveryFrame);
721
+ }
722
+ }
723
+ function projectClientMessages(players, acceptedMessages, stateReducer) {
724
+ return replayAuthorityMessages(players, acceptedMessages, Number.MAX_SAFE_INTEGER, stateReducer);
725
+ }
726
+ function projectDeliveryMessages(queue, clientStateBySlot, stateReducer) {
727
+ const projected = [...clientStateBySlot];
728
+ for (const deliveryFrame of [...queue.keys()].sort((left, right) => left - right)) {
729
+ const messages = queue.get(deliveryFrame);
730
+ for (const message of [...messages].sort(compareAuthorityMessages)) {
731
+ projected[message.slot] = stateReducer(projected[message.slot], message);
732
+ }
733
+ }
734
+ return projected;
735
+ }
736
+ function replayAuthorityMessages(players, acceptedMessages, frame, stateReducer) {
737
+ const stateBySlot = Array.from({ length: players }, () => 0);
738
+ for (const message of [...acceptedMessages]
739
+ .filter((candidate) => candidate.frame <= frame)
740
+ .sort(compareAuthorityFrameMessages)) {
741
+ stateBySlot[message.slot] = stateReducer(stateBySlot[message.slot], message);
742
+ }
743
+ return stateBySlot;
744
+ }
745
+ function defaultLocalAuthorityStateReducer(currentValue, message) {
746
+ return currentValue + message.value;
747
+ }
748
+ function removeQueuedMessagesForSlot(queue, slot) {
749
+ for (const deliveryFrame of [...queue.keys()].sort((left, right) => left - right)) {
750
+ const remaining = queue.get(deliveryFrame).filter((message) => message.slot !== slot);
751
+ if (remaining.length === 0) {
752
+ queue.delete(deliveryFrame);
753
+ }
754
+ else {
755
+ queue.set(deliveryFrame, remaining);
756
+ }
757
+ }
758
+ }
759
+ function removeAcceptedMessagesForSlotFromFrame(messages, slot, frame) {
760
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
761
+ const message = messages[index];
762
+ if (message.slot === slot && message.frame >= frame) {
763
+ messages.splice(index, 1);
764
+ }
765
+ }
766
+ }
767
+ function incrementHistogram(histogram, value) {
768
+ histogram.set(value, (histogram.get(value) ?? 0) + 1);
769
+ }
770
+ function percentileFromHistogram(histogram, percentileRank) {
771
+ const total = [...histogram.values()].reduce((sum, count) => sum + count, 0);
772
+ if (total === 0) {
773
+ return 0;
774
+ }
775
+ const target = Math.floor((percentileRank / 100) * total);
776
+ let seen = 0;
777
+ for (const [value, count] of [...histogram.entries()].sort((left, right) => left[0] - right[0])) {
778
+ seen += count;
779
+ if (seen > target) {
780
+ return value;
781
+ }
782
+ }
783
+ /* istanbul ignore next -- non-empty histogram entries exhaust the counted total above. */
784
+ throw new Error('Percentile histogram traversal ended before reaching its target');
785
+ }
786
+ function percentile(values, percentileRank) {
787
+ if (values.length === 0) {
788
+ return 0;
789
+ }
790
+ const sorted = [...values].sort((left, right) => left - right);
791
+ const percentileValue = sorted[Math.min(sorted.length - 1, Math.floor((percentileRank / 100) * sorted.length))];
792
+ return Math.round(percentileValue * 1e4) / 1e4;
793
+ }
794
+ function compareAuthorityMessages(left, right) {
795
+ return left.deliveryFrame - right.deliveryFrame
796
+ || left.frame - right.frame
797
+ || left.slot - right.slot;
798
+ }
799
+ function compareAuthorityFrameMessages(left, right) {
800
+ return left.frame - right.frame
801
+ || left.deliveryFrame - right.deliveryFrame
802
+ || left.slot - right.slot;
803
+ }
804
+ function compareNumbers(left, right) {
805
+ return left - right;
806
+ }
807
+ function compareStrings(left, right) {
808
+ /* istanbul ignore next -- accepted input ids are unique Set keys before sorting. */
809
+ return left < right ? -1 : left > right ? 1 : 0;
810
+ }