@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,174 @@
1
+ import { canonicalStringify, defaultChecksum } from './canonical.js';
2
+ export function createProtocolRollbackReport(options, result) {
3
+ const config = {
4
+ game: options.game ?? 'lab',
5
+ players: options.players,
6
+ frames: options.frames,
7
+ seed: options.seed,
8
+ minDelayFrames: options.minDelayFrames,
9
+ maxDelayFrames: options.maxDelayFrames,
10
+ bundleSizeFrames: options.bundleSizeFrames ?? 1,
11
+ inputDelayFrames: options.inputDelayFrames ?? 0,
12
+ duplicatePermille: options.duplicatePermille ?? 0,
13
+ lossPermille: options.lossPermille ?? 0,
14
+ resendIntervalFrames: options.resendIntervalFrames ?? Math.max(1, options.maxDelayFrames + 1),
15
+ maxResendAttempts: options.maxResendAttempts ?? 1,
16
+ botSlots: options.botSlots ?? [],
17
+ expectDesync: options.expectDesync === true,
18
+ eventTimelineLimit: options.eventTimelineLimit ?? 64,
19
+ predictionPolicy: options.predictionPolicy ?? 'repeat-last',
20
+ ...optionalNumber('disconnectStartFrame', options.disconnectStartFrame),
21
+ ...optionalNumber('disconnectEndFrame', options.disconnectEndFrame),
22
+ ...optionalNumber('lateJoinSlot', options.lateJoinSlot),
23
+ ...optionalNumber('lateJoinFrame', options.lateJoinFrame),
24
+ ...optionalNumber('latencySkewSlot', options.latencySkewSlot),
25
+ ...optionalNumber('latencySkewFrames', options.latencySkewFrames),
26
+ ...optionalNumber('desyncFrame', options.desyncFrame),
27
+ ...optionalNumber('desyncSlot', options.desyncSlot),
28
+ };
29
+ const reportWithoutId = {
30
+ schemaVersion: 1,
31
+ reportId: '',
32
+ kind: 'protocol-rollback',
33
+ generatedAtFrame: result.frames,
34
+ config,
35
+ metrics: result,
36
+ result: result.result,
37
+ };
38
+ return {
39
+ ...reportWithoutId,
40
+ reportId: defaultChecksum(reportWithoutId),
41
+ };
42
+ }
43
+ export function stableReportJson(report) {
44
+ return `${canonicalStringify(report)}\n`;
45
+ }
46
+ export function diffDeterministicReports(left, right, maxDifferences = 200) {
47
+ const entries = [];
48
+ let differences = 0;
49
+ let truncatedDifferences = 0;
50
+ const addDifference = (path, leftValue, rightValue) => {
51
+ differences += 1;
52
+ if (entries.length < maxDifferences) {
53
+ entries.push({
54
+ path,
55
+ left: summarizeReportValue(leftValue),
56
+ right: summarizeReportValue(rightValue),
57
+ });
58
+ }
59
+ else {
60
+ truncatedDifferences += 1;
61
+ }
62
+ };
63
+ const visit = (path, leftValue, rightValue) => {
64
+ if (Object.is(leftValue, rightValue)) {
65
+ return;
66
+ }
67
+ if (Array.isArray(leftValue) || Array.isArray(rightValue)) {
68
+ if (!Array.isArray(leftValue) || !Array.isArray(rightValue)) {
69
+ addDifference(path, leftValue, rightValue);
70
+ return;
71
+ }
72
+ if (leftValue.length !== rightValue.length) {
73
+ addDifference(`${path}.length`, leftValue.length, rightValue.length);
74
+ }
75
+ for (let index = 0; index < Math.min(leftValue.length, rightValue.length); index += 1) {
76
+ visit(`${path}[${index}]`, leftValue[index], rightValue[index]);
77
+ }
78
+ return;
79
+ }
80
+ if (isReportObject(leftValue) || isReportObject(rightValue)) {
81
+ if (!isReportObject(leftValue) || !isReportObject(rightValue)) {
82
+ addDifference(path, leftValue, rightValue);
83
+ return;
84
+ }
85
+ const keys = [...new Set([...Object.keys(leftValue), ...Object.keys(rightValue)])].sort();
86
+ for (const key of keys) {
87
+ visit(path === '$' ? `$.${key}` : `${path}.${key}`, leftValue[key], rightValue[key]);
88
+ }
89
+ return;
90
+ }
91
+ addDifference(path, leftValue, rightValue);
92
+ };
93
+ visit('$', left, right);
94
+ return {
95
+ schemaVersion: 1,
96
+ kind: 'report-diff',
97
+ leftReportId: reportIdentity(left),
98
+ rightReportId: reportIdentity(right),
99
+ differences,
100
+ truncatedDifferences,
101
+ entries,
102
+ result: differences === 0 ? 'pass' : 'diff',
103
+ };
104
+ }
105
+ export function stableReportDiffJson(diff) {
106
+ return `${canonicalStringify(diff)}\n`;
107
+ }
108
+ export function renderDeterministicReportHtml(report) {
109
+ const json = canonicalStringify(report);
110
+ return [
111
+ '<!doctype html>',
112
+ '<html lang="en">',
113
+ '<head>',
114
+ '<meta charset="utf-8">',
115
+ '<meta name="viewport" content="width=device-width,initial-scale=1">',
116
+ `<title>${escapeHtml(report.kind)} ${escapeHtml(report.result)}</title>`,
117
+ '<style>',
118
+ 'body{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;margin:24px;color:#171717;background:#fafafa}',
119
+ 'main{max-width:1120px;margin:0 auto}',
120
+ 'header{display:flex;gap:12px;align-items:baseline;justify-content:space-between;border-bottom:1px solid #d4d4d4;padding-bottom:12px}',
121
+ 'h1{font-size:20px;margin:0}',
122
+ 'section{margin-top:18px}',
123
+ 'pre{white-space:pre-wrap;overflow:auto;background:#fff;border:1px solid #d4d4d4;border-radius:6px;padding:12px;font-size:12px;line-height:1.45}',
124
+ '.pill{font-size:12px;border:1px solid #a3a3a3;border-radius:999px;padding:2px 8px;background:#fff}',
125
+ '.pass{border-color:#16a34a;color:#166534}.fail,.desync{border-color:#dc2626;color:#991b1b}.warn{border-color:#ca8a04;color:#854d0e}',
126
+ '</style>',
127
+ '</head>',
128
+ '<body>',
129
+ `<main data-report-id="${escapeHtml(report.reportId)}">`,
130
+ '<header>',
131
+ `<h1>${escapeHtml(report.kind)}</h1>`,
132
+ `<span class="pill ${escapeHtml(report.result)}">${escapeHtml(report.result)}</span>`,
133
+ '</header>',
134
+ '<section>',
135
+ `<p><strong>Report:</strong> ${escapeHtml(report.reportId)}</p>`,
136
+ `<p><strong>Generated at frame:</strong> ${report.generatedAtFrame}</p>`,
137
+ '</section>',
138
+ '<section>',
139
+ '<h2>Canonical JSON</h2>',
140
+ `<pre>${escapeHtml(JSON.stringify(JSON.parse(json), null, 2))}</pre>`,
141
+ '</section>',
142
+ '</main>',
143
+ '</body>',
144
+ '</html>',
145
+ '',
146
+ ].join('\n');
147
+ }
148
+ function optionalNumber(key, value) {
149
+ return value === undefined ? {} : { [key]: value };
150
+ }
151
+ function isReportObject(value) {
152
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
153
+ }
154
+ function reportIdentity(value) {
155
+ if (isReportObject(value) && typeof value.reportId === 'string' && value.reportId.length > 0) {
156
+ return value.reportId;
157
+ }
158
+ return defaultChecksum(value);
159
+ }
160
+ function summarizeReportValue(value) {
161
+ if (value === undefined) {
162
+ return '<missing>';
163
+ }
164
+ const text = typeof value === 'string' ? value : canonicalStringify(value);
165
+ return text.length > 240 ? `${text.slice(0, 237)}...` : text;
166
+ }
167
+ function escapeHtml(value) {
168
+ return value
169
+ .replaceAll('&', '&amp;')
170
+ .replaceAll('<', '&lt;')
171
+ .replaceAll('>', '&gt;')
172
+ .replaceAll('"', '&quot;')
173
+ .replaceAll("'", '&#39;');
174
+ }
@@ -0,0 +1,46 @@
1
+ export interface DeterministicRollbackHistoryEntry<TSnapshot> {
2
+ readonly frame: number;
3
+ readonly snapshot: TSnapshot;
4
+ readonly checksum: string;
5
+ readonly bytes: number;
6
+ }
7
+ export interface DeterministicRollbackHistorySnapshot<TSnapshot> {
8
+ readonly capacity: number;
9
+ readonly entries: readonly DeterministicRollbackHistoryEntry<TSnapshot>[];
10
+ readonly checksum: string;
11
+ }
12
+ export interface DeterministicRollbackHistory<TSnapshot> {
13
+ readonly capacity: number;
14
+ readonly size: number;
15
+ readonly oldestFrame: number | undefined;
16
+ readonly latestFrame: number | undefined;
17
+ record(frame: number, snapshot: TSnapshot): DeterministicRollbackHistoryEntry<TSnapshot>;
18
+ has(frame: number): boolean;
19
+ get(frame: number): DeterministicRollbackHistoryEntry<TSnapshot> | undefined;
20
+ require(frame: number): DeterministicRollbackHistoryEntry<TSnapshot>;
21
+ frames(): readonly number[];
22
+ snapshot(): DeterministicRollbackHistorySnapshot<TSnapshot>;
23
+ restore(snapshot: DeterministicRollbackHistorySnapshot<TSnapshot>): void;
24
+ checksum(): string;
25
+ }
26
+ export interface DeterministicRollbackHistoryFixtureResult {
27
+ readonly checksum: string;
28
+ readonly baselineChecksum: string;
29
+ readonly predictedChecksumBeforeCorrection: string;
30
+ readonly resimulatedChecksum: string;
31
+ readonly restoredHistoryChecksum: string;
32
+ readonly capacity: number;
33
+ readonly recordedFrames: number;
34
+ readonly evictedFrames: number;
35
+ readonly restoredFrame: number;
36
+ readonly predictedToFrame: number;
37
+ readonly resimulatedFrames: number;
38
+ readonly convergence: boolean;
39
+ readonly duplicateRejected: boolean;
40
+ readonly evictedRestoreRejected: boolean;
41
+ readonly malformedRestoreRejected: boolean;
42
+ readonly rollbackDepthBounded: boolean;
43
+ readonly orderStable: boolean;
44
+ }
45
+ export declare function createDeterministicRollbackHistory<TSnapshot>(capacity: number): DeterministicRollbackHistory<TSnapshot>;
46
+ export declare function runDeterministicRollbackHistoryFixture(frames?: number, capacity?: number, seed?: number): DeterministicRollbackHistoryFixtureResult;
@@ -0,0 +1,247 @@
1
+ import { cloneCanonical, defaultChecksum } from './canonical.js';
2
+ import { createDeterministicFrameRuntime, } from './engine-runtime.js';
3
+ export function createDeterministicRollbackHistory(capacity) {
4
+ if (!Number.isInteger(capacity) || capacity <= 0) {
5
+ throw new Error('Deterministic rollback history capacity must be a positive integer');
6
+ }
7
+ const entries = new Map();
8
+ function sortedFrames() {
9
+ return [...entries.keys()].sort((left, right) => left - right);
10
+ }
11
+ function prune() {
12
+ while (entries.size > capacity) {
13
+ const oldest = sortedFrames()[0];
14
+ entries.delete(oldest);
15
+ }
16
+ }
17
+ return {
18
+ capacity,
19
+ get size() {
20
+ return entries.size;
21
+ },
22
+ get oldestFrame() {
23
+ return sortedFrames()[0];
24
+ },
25
+ get latestFrame() {
26
+ return sortedFrames().at(-1);
27
+ },
28
+ record(frame, snapshot) {
29
+ assertRollbackFrame(frame);
30
+ if (entries.has(frame)) {
31
+ throw new Error(`Deterministic rollback history already has frame ${frame}`);
32
+ }
33
+ const cloned = cloneCanonical(snapshot);
34
+ const bytes = JSON.stringify(cloned).length;
35
+ const entry = {
36
+ frame,
37
+ snapshot: cloned,
38
+ checksum: defaultChecksum({ frame, snapshot: cloned }),
39
+ bytes,
40
+ };
41
+ entries.set(frame, entry);
42
+ prune();
43
+ return cloneCanonical(entry);
44
+ },
45
+ has(frame) {
46
+ assertRollbackFrame(frame);
47
+ return entries.has(frame);
48
+ },
49
+ get(frame) {
50
+ assertRollbackFrame(frame);
51
+ const entry = entries.get(frame);
52
+ return entry === undefined ? undefined : cloneCanonical(entry);
53
+ },
54
+ require(frame) {
55
+ const entry = this.get(frame);
56
+ if (entry === undefined) {
57
+ throw new Error(`Deterministic rollback history frame ${frame} is outside the retained window`);
58
+ }
59
+ return entry;
60
+ },
61
+ frames() {
62
+ return sortedFrames();
63
+ },
64
+ snapshot() {
65
+ const withoutChecksum = {
66
+ capacity,
67
+ entries: sortedFrames().map((frame) => {
68
+ const entry = entries.get(frame);
69
+ /* istanbul ignore next -- sortedFrames is derived from the same entries map being read here. */
70
+ if (entry === undefined) {
71
+ throw new Error(`Deterministic rollback history missing retained frame ${frame}`);
72
+ }
73
+ return cloneCanonical(entry);
74
+ }),
75
+ };
76
+ return {
77
+ ...withoutChecksum,
78
+ checksum: defaultChecksum(withoutChecksum),
79
+ };
80
+ },
81
+ restore(snapshot) {
82
+ validateRollbackHistorySnapshot(snapshot, capacity);
83
+ entries.clear();
84
+ for (const entry of snapshot.entries) {
85
+ entries.set(entry.frame, cloneCanonical(entry));
86
+ }
87
+ },
88
+ checksum() {
89
+ return this.snapshot().checksum;
90
+ },
91
+ };
92
+ }
93
+ export function runDeterministicRollbackHistoryFixture(frames = 96, capacity = 24, seed = 71) {
94
+ const restoreFrame = Math.max(8, Math.floor(frames / 3));
95
+ const predictedToFrame = Math.min(frames, restoreFrame + Math.max(6, Math.floor(capacity / 2)));
96
+ const descriptor = rollbackHistoryDescriptor();
97
+ const systems = rollbackHistorySystems();
98
+ const baseline = createDeterministicFrameRuntime(descriptor, seed);
99
+ const predicted = createDeterministicFrameRuntime(descriptor, seed);
100
+ const history = createDeterministicRollbackHistory(capacity);
101
+ for (let frame = 0; frame <= predictedToFrame; frame += 1) {
102
+ history.record(frame, predicted.snapshot());
103
+ stepRollbackRuntime(predicted, systems, predictedInput(frame, seed, restoreFrame));
104
+ stepRollbackRuntime(baseline, systems, actualInput(frame, seed));
105
+ }
106
+ const predictedChecksumBeforeCorrection = predicted.snapshot().checksum;
107
+ const restoreEntry = history.require(restoreFrame);
108
+ predicted.restore(restoreEntry.snapshot);
109
+ for (let frame = restoreFrame; frame <= predictedToFrame; frame += 1) {
110
+ stepRollbackRuntime(predicted, systems, actualInput(frame, seed));
111
+ }
112
+ const resimulatedChecksum = predicted.snapshot().checksum;
113
+ const baselineChecksum = baseline.snapshot().checksum;
114
+ let duplicateRejected = false;
115
+ try {
116
+ history.record(restoreFrame, predicted.snapshot());
117
+ }
118
+ catch {
119
+ duplicateRejected = true;
120
+ }
121
+ const evictingHistory = createDeterministicRollbackHistory(capacity);
122
+ for (let frame = 0; frame < capacity + 4; frame += 1) {
123
+ evictingHistory.record(frame, baseline.snapshot());
124
+ }
125
+ let evictedRestoreRejected = false;
126
+ try {
127
+ evictingHistory.require(0);
128
+ }
129
+ catch {
130
+ evictedRestoreRejected = true;
131
+ }
132
+ const historySnapshot = history.snapshot();
133
+ const restoredHistory = createDeterministicRollbackHistory(capacity);
134
+ restoredHistory.restore(historySnapshot);
135
+ let malformedRestoreRejected = false;
136
+ try {
137
+ restoredHistory.restore({ ...historySnapshot, checksum: 'tampered' });
138
+ }
139
+ catch {
140
+ malformedRestoreRejected = true;
141
+ }
142
+ const withoutChecksum = {
143
+ baselineChecksum,
144
+ predictedChecksumBeforeCorrection,
145
+ resimulatedChecksum,
146
+ restoredHistoryChecksum: restoredHistory.checksum(),
147
+ frames,
148
+ capacity,
149
+ retainedFrames: history.frames(),
150
+ restoreFrame,
151
+ predictedToFrame,
152
+ };
153
+ return {
154
+ checksum: defaultChecksum(withoutChecksum),
155
+ baselineChecksum,
156
+ predictedChecksumBeforeCorrection,
157
+ resimulatedChecksum,
158
+ restoredHistoryChecksum: restoredHistory.checksum(),
159
+ capacity,
160
+ recordedFrames: history.size,
161
+ evictedFrames: Math.max(0, predictedToFrame + 1 - history.size),
162
+ restoredFrame: restoreFrame,
163
+ predictedToFrame,
164
+ resimulatedFrames: predictedToFrame - restoreFrame + 1,
165
+ convergence: baselineChecksum === resimulatedChecksum && baselineChecksum !== predictedChecksumBeforeCorrection,
166
+ duplicateRejected,
167
+ evictedRestoreRejected,
168
+ malformedRestoreRejected,
169
+ rollbackDepthBounded: history.size <= capacity && evictingHistory.oldestFrame === 4,
170
+ orderStable: defaultChecksum(history.frames()) === defaultChecksum(restoredHistory.frames()),
171
+ };
172
+ }
173
+ function assertRollbackFrame(frame) {
174
+ if (!Number.isInteger(frame) || frame < 0) {
175
+ throw new Error(`Deterministic rollback history frame ${frame} must be a non-negative integer`);
176
+ }
177
+ }
178
+ function validateRollbackHistorySnapshot(snapshot, capacity) {
179
+ if (snapshot.capacity !== capacity) {
180
+ throw new Error(`Deterministic rollback history capacity mismatch ${snapshot.capacity} !== ${capacity}`);
181
+ }
182
+ if (snapshot.entries.length > capacity) {
183
+ throw new Error(`Deterministic rollback history snapshot has ${snapshot.entries.length} entries for capacity ${capacity}`);
184
+ }
185
+ const frames = new Set();
186
+ let previousFrame = -1;
187
+ for (const entry of snapshot.entries) {
188
+ assertRollbackFrame(entry.frame);
189
+ if (frames.has(entry.frame)) {
190
+ throw new Error(`Deterministic rollback history snapshot duplicate frame ${entry.frame}`);
191
+ }
192
+ if (entry.frame <= previousFrame) {
193
+ throw new Error('Deterministic rollback history snapshot entries must be sorted by frame');
194
+ }
195
+ frames.add(entry.frame);
196
+ previousFrame = entry.frame;
197
+ if (!Number.isInteger(entry.bytes) || entry.bytes <= 0) {
198
+ throw new Error(`Deterministic rollback history snapshot frame ${entry.frame} has invalid byte size`);
199
+ }
200
+ const expectedBytes = JSON.stringify(cloneCanonical(entry.snapshot)).length;
201
+ if (entry.bytes !== expectedBytes) {
202
+ throw new Error(`Deterministic rollback history snapshot frame ${entry.frame} byte size mismatch`);
203
+ }
204
+ const expected = defaultChecksum({ frame: entry.frame, snapshot: entry.snapshot });
205
+ if (entry.checksum !== expected) {
206
+ throw new Error(`Deterministic rollback history snapshot checksum mismatch at frame ${entry.frame}`);
207
+ }
208
+ }
209
+ const expectedSnapshotChecksum = defaultChecksum({
210
+ capacity: snapshot.capacity,
211
+ entries: snapshot.entries,
212
+ });
213
+ if (snapshot.checksum !== expectedSnapshotChecksum) {
214
+ throw new Error('Deterministic rollback history snapshot checksum mismatch');
215
+ }
216
+ }
217
+ function rollbackHistoryDescriptor() {
218
+ return {
219
+ id: 'rollback-history-fixture',
220
+ tickRate: 60,
221
+ components: [
222
+ { id: 'input', singleton: true, fields: [{ name: 'value', kind: 'int', defaultValue: 0 }] },
223
+ { id: 'score', singleton: true, fields: [{ name: 'total', kind: 'int', defaultValue: 0 }] },
224
+ ],
225
+ systems: [{ id: 'score', group: 'simulation', order: 0 }],
226
+ };
227
+ }
228
+ function rollbackHistorySystems() {
229
+ return {
230
+ score(ctx) {
231
+ const input = ctx.getSingleton('input');
232
+ const score = ctx.getSingleton('score');
233
+ const total = score === undefined ? 0 : Number(score.total);
234
+ ctx.setSingleton('score', { total: total + Number(input.value) });
235
+ },
236
+ };
237
+ }
238
+ function stepRollbackRuntime(runtime, systems, input) {
239
+ runtime.setSingleton('input', { value: input });
240
+ runtime.step(systems);
241
+ }
242
+ function actualInput(frame, seed) {
243
+ return ((frame * 17 + seed) % 11) - 5;
244
+ }
245
+ function predictedInput(frame, seed, correctionFrame) {
246
+ return frame < correctionFrame ? actualInput(frame, seed) : actualInput(frame, seed) + 1;
247
+ }
@@ -0,0 +1 @@
1
+ export declare function withRuntimeDeterminismGuards<State>(frame: number, run: () => State): State;
@@ -0,0 +1,108 @@
1
+ import { SyncplayError } from './errors.js';
2
+ const noopRestore = () => undefined;
3
+ const deterministicTickReplacement = 'ctx.frame or deterministic tick counters';
4
+ const deterministicStateTransitionReplacement = 'synchronous deterministic state transitions';
5
+ export function withRuntimeDeterminismGuards(frame, run) {
6
+ const restoreGuards = [
7
+ replaceValue(Math, 'random', blockedFunction(frame, 'Math.random()', 'ctx.random.nextInt() or ctx.random.nextFixed()')),
8
+ replaceDateConstructor(frame),
9
+ replacePrototypeValue(globalThis.performance, 'now', blockedFunction(frame, 'performance.now()', 'ctx.frame or deterministic tick counters')),
10
+ replacePrototypeValue(globalThis.crypto, 'randomUUID', blockedFunction(frame, 'crypto.randomUUID()', 'ctx.random.nextInt() or deterministic ids')),
11
+ replacePrototypeValue(globalThis.crypto, 'getRandomValues', blockedFunction(frame, 'crypto.getRandomValues()', 'ctx.random deterministic streams')),
12
+ replaceGlobalTimer(frame, 'setTimeout', 'deterministic countdown fields advanced by fixed ticks'),
13
+ replaceGlobalTimer(frame, 'setInterval', 'deterministic countdown fields advanced by fixed ticks'),
14
+ replaceGlobalTimer(frame, 'setImmediate', 'deterministic countdown fields advanced by fixed ticks'),
15
+ replaceGlobalTimer(frame, 'queueMicrotask', 'explicit deterministic event queues in frame state'),
16
+ replaceGlobalTimer(frame, 'requestAnimationFrame', 'render-only callbacks outside simulation'),
17
+ replacePromiseConstructor(frame),
18
+ ];
19
+ try {
20
+ return run();
21
+ }
22
+ finally {
23
+ for (let index = restoreGuards.length - 1; index >= 0; index -= 1) {
24
+ restoreGuards[index]();
25
+ }
26
+ }
27
+ }
28
+ function blockedFunction(frame, api, replacement) {
29
+ return () => {
30
+ throw new SyncplayError(`${api} is not allowed during deterministic step. Use ${replacement}.`, {
31
+ code: 'runtime-guard.blocked-api',
32
+ frame,
33
+ });
34
+ };
35
+ }
36
+ function replacePromiseConstructor(frame) {
37
+ const target = globalThis;
38
+ const original = target.Promise;
39
+ const blockedPromise = blockedFunction(frame, 'new Promise()', deterministicStateTransitionReplacement);
40
+ const guarded = function deterministicPromiseGuard() {
41
+ throw blockedPromise();
42
+ };
43
+ guarded.prototype = original.prototype;
44
+ for (const key of ['all', 'allSettled', 'any', 'race', 'reject', 'resolve']) {
45
+ guarded[key] = blockedFunction(frame, `Promise.${key}()`, deterministicStateTransitionReplacement);
46
+ }
47
+ return replaceValue(target, 'Promise', guarded);
48
+ }
49
+ function replaceGlobalTimer(frame, key, replacement) {
50
+ const target = globalThis;
51
+ if (target[key] === undefined) {
52
+ return noopRestore;
53
+ }
54
+ return replaceValue(target, key, blockedFunction(frame, `${key}()`, replacement));
55
+ }
56
+ function replaceDateConstructor(frame) {
57
+ const target = globalThis;
58
+ const original = target.Date;
59
+ const blockedDate = blockedFunction(frame, 'new Date()', deterministicTickReplacement);
60
+ const blockedDateNow = blockedFunction(frame, 'Date.now()', deterministicTickReplacement);
61
+ const guarded = function deterministicDateGuard() {
62
+ throw blockedDate();
63
+ };
64
+ guarded.now = blockedDateNow;
65
+ guarded.parse = original.parse.bind(original);
66
+ guarded.UTC = original.UTC.bind(original);
67
+ guarded.prototype = original.prototype;
68
+ return replaceValue(target, 'Date', guarded);
69
+ }
70
+ function replacePrototypeValue(target, key, replacement) {
71
+ if (target === undefined) {
72
+ return noopRestore;
73
+ }
74
+ let current = target;
75
+ while (current !== null) {
76
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
77
+ if (descriptor !== undefined) {
78
+ if (descriptor.configurable !== true) {
79
+ return noopRestore;
80
+ }
81
+ Object.defineProperty(current, key, {
82
+ configurable: true,
83
+ writable: true,
84
+ value: replacement,
85
+ });
86
+ return () => {
87
+ Object.defineProperty(current, key, descriptor);
88
+ };
89
+ }
90
+ current = Object.getPrototypeOf(current);
91
+ }
92
+ return noopRestore;
93
+ }
94
+ function replaceValue(target, key, replacement) {
95
+ const originalDescriptor = Object.getOwnPropertyDescriptor(target, key);
96
+ /* istanbul ignore next -- non-configurable core globals cannot be made safely reversible inside this process. */
97
+ if (originalDescriptor?.configurable !== true) {
98
+ return noopRestore;
99
+ }
100
+ Object.defineProperty(target, key, {
101
+ configurable: true,
102
+ writable: true,
103
+ value: replacement,
104
+ });
105
+ return () => {
106
+ Object.defineProperty(target, key, originalDescriptor);
107
+ };
108
+ }
@@ -0,0 +1,32 @@
1
+ import { DeterministicRandom } from './random.js';
2
+ import type { DeterministicStep, OfflineSession, RecordedChecksum, RecordedInputFrame, ReplayRecordingMode, TickRate } from './types.js';
3
+ export interface LabInput {
4
+ readonly dx: number;
5
+ readonly dy: number;
6
+ readonly action: 0 | 1;
7
+ }
8
+ export interface LabState {
9
+ readonly positions: readonly number[];
10
+ readonly actions: readonly number[];
11
+ readonly frame: number;
12
+ readonly payload?: string;
13
+ }
14
+ export interface LabSessionOptions {
15
+ readonly players: number;
16
+ readonly seed: number;
17
+ readonly tickRate?: TickRate;
18
+ readonly snapshotBufferSize?: number;
19
+ readonly payloadBytes?: number;
20
+ readonly replayRecordingMode?: ReplayRecordingMode;
21
+ readonly onInputFrameRecorded?: (inputFrame: RecordedInputFrame) => void;
22
+ readonly onChecksumRecorded?: (checksum: RecordedChecksum) => void;
23
+ }
24
+ export declare const labDefaultInput: LabInput;
25
+ export declare const labStep: DeterministicStep<LabState, LabInput>;
26
+ export declare function createLabSession(options: LabSessionOptions): OfflineSession<LabState, LabInput>;
27
+ export declare function canonicalizeLabInput(input: LabInput): readonly [number, number, 0 | 1];
28
+ export declare function hydrateLabInput(recordedInput: unknown): LabInput;
29
+ export declare function makeLabInput(random: DeterministicRandom): LabInput;
30
+ export declare function applyLabInputs(session: OfflineSession<LabState, LabInput>, players: number, random: DeterministicRandom): readonly LabInput[];
31
+ export declare function packLabInputFrame(inputs: readonly LabInput[]): Uint8Array;
32
+ export declare function unpackLabInputFrame(bytes: Uint8Array, players: number): readonly LabInput[];