@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,3123 @@
1
+ import { canonicalStringify, cloneCanonical, defaultChecksum } from './canonical.js';
2
+ import { DeterministicRandom } from './random.js';
3
+ import { parseDeterministicSchemaDsl, } from './schema-dsl-binary.js';
4
+ import { createDeterministicSparseSet } from './sparse-set.js';
5
+ import { withRuntimeDeterminismGuards } from './runtime-guards.js';
6
+ export function createDeterministicFrameRuntime(descriptor, seed = 1, options = {}) {
7
+ validateRuntimeDescriptor(descriptor);
8
+ const orderedSystemDescriptors = orderSystemDescriptors(descriptor.systems);
9
+ const componentDescriptors = new Map(descriptor.components.map((component) => [component.id, component]));
10
+ const eventDescriptors = new Map((descriptor.eventChannels ?? []).map((event) => [event.id, event]));
11
+ const signalDescriptors = new Map((descriptor.signalChannels ?? []).map((signal) => [signal.id, signal]));
12
+ const assetRefs = createRuntimeAssetRefValidator(options);
13
+ const state = {
14
+ frame: 0,
15
+ nextEntityId: 1,
16
+ freeEntityIds: [],
17
+ generations: new Map(),
18
+ components: new Map(descriptor.components.filter((component) => component.singleton !== true).map((component) => [component.id, new Map()])),
19
+ singletonComponents: new Map(),
20
+ frameHeap: createFrameHeap(),
21
+ debugEvents: [],
22
+ runtimeEvents: [],
23
+ runtimeSignals: [],
24
+ dispatchedSignals: [],
25
+ nextRuntimeMessageSequence: 0,
26
+ commandTimeline: options.commandTimeline,
27
+ random: new DeterministicRandom(seed),
28
+ };
29
+ return {
30
+ descriptor,
31
+ get frame() {
32
+ return state.frame;
33
+ },
34
+ get debugEvents() {
35
+ return state.debugEvents.map((event) => cloneCanonical(event));
36
+ },
37
+ createEntity() {
38
+ return createEntity(state);
39
+ },
40
+ destroyEntity(entity) {
41
+ destroyEntity(state, entity);
42
+ },
43
+ setComponent(entity, componentId, data) {
44
+ setComponent(state, componentDescriptors, assetRefs, entity, componentId, data);
45
+ },
46
+ removeComponent(entity, componentId) {
47
+ assertKnownFrameComponent(componentDescriptors, componentId);
48
+ removeComponent(state, entity, componentId);
49
+ },
50
+ setSingleton(componentId, data) {
51
+ setSingleton(state, componentDescriptors, assetRefs, componentId, data);
52
+ },
53
+ query(componentIds) {
54
+ assertKnownFrameComponents(componentDescriptors, componentIds);
55
+ return queryComponents(state, componentIds);
56
+ },
57
+ queryFilter(filter) {
58
+ assertKnownFrameComponents(componentDescriptors, queryFilterComponentIds(filter));
59
+ return queryComponentFilter(state, filter);
60
+ },
61
+ componentCount(componentId) {
62
+ assertKnownFrameComponent(componentDescriptors, componentId);
63
+ /* istanbul ignore next -- component storage exists for every validated frame component. */
64
+ return state.components.get(componentId)?.size ?? 0;
65
+ },
66
+ step(systems) {
67
+ for (const systemDescriptor of orderedSystemDescriptors) {
68
+ const system = systems[systemDescriptor.id];
69
+ if (system === undefined) {
70
+ throw new Error(`Missing deterministic system ${systemDescriptor.id}`);
71
+ }
72
+ if (options.statelessSystemSourceGuards === true) {
73
+ assertDeterministicFrameSystemStateless(systemDescriptor.id, system);
74
+ }
75
+ state.commandTimeline?.setCurrentFrame(state.frame);
76
+ withRuntimeDeterminismGuards(state.frame, () => {
77
+ system(createSystemContext(state, componentDescriptors, eventDescriptors, signalDescriptors, assetRefs, systemDescriptor.group === 'presentation' ? systemDescriptor.id : undefined));
78
+ });
79
+ }
80
+ state.frame += 1;
81
+ },
82
+ snapshot() {
83
+ return snapshotState(state);
84
+ },
85
+ restore(snapshot) {
86
+ restoreState(state, componentDescriptors, assetRefs, snapshot);
87
+ },
88
+ };
89
+ }
90
+ export function createDeterministicSparseSetFrameRuntime(descriptor, seed = 1, capacity = 65_536, options = {}) {
91
+ validateRuntimeDescriptor(descriptor);
92
+ const orderedSystemDescriptors = orderSystemDescriptors(descriptor.systems);
93
+ const componentDescriptors = new Map(descriptor.components.map((component) => [component.id, component]));
94
+ const eventDescriptors = new Map((descriptor.eventChannels ?? []).map((event) => [event.id, event]));
95
+ const signalDescriptors = new Map((descriptor.signalChannels ?? []).map((signal) => [signal.id, signal]));
96
+ const assetRefs = createRuntimeAssetRefValidator(options);
97
+ const state = {
98
+ frame: 0,
99
+ nextEntityId: 1,
100
+ freeEntityIds: [],
101
+ generations: new Map(),
102
+ componentStores: createSparseComponentStores(descriptor, capacity),
103
+ singletonComponents: new Map(),
104
+ frameHeap: createFrameHeap(),
105
+ debugEvents: [],
106
+ runtimeEvents: [],
107
+ runtimeSignals: [],
108
+ dispatchedSignals: [],
109
+ nextRuntimeMessageSequence: 0,
110
+ commandTimeline: options.commandTimeline,
111
+ random: new DeterministicRandom(seed),
112
+ };
113
+ return {
114
+ descriptor,
115
+ get frame() {
116
+ return state.frame;
117
+ },
118
+ get debugEvents() {
119
+ return state.debugEvents.map((event) => cloneCanonical(event));
120
+ },
121
+ createEntity() {
122
+ return createSparseEntity(state);
123
+ },
124
+ destroyEntity(entity) {
125
+ destroySparseEntity(state, entity);
126
+ },
127
+ setComponent(entity, componentId, data) {
128
+ setSparseComponent(state, componentDescriptors, assetRefs, entity, componentId, data);
129
+ },
130
+ removeComponent(entity, componentId) {
131
+ assertKnownFrameComponent(componentDescriptors, componentId);
132
+ removeSparseComponent(state, entity, componentId);
133
+ },
134
+ setSingleton(componentId, data) {
135
+ setSparseSingleton(state, componentDescriptors, assetRefs, componentId, data);
136
+ },
137
+ query(componentIds) {
138
+ assertKnownFrameComponents(componentDescriptors, componentIds);
139
+ return querySparseComponents(state, componentIds);
140
+ },
141
+ queryFilter(filter) {
142
+ assertKnownFrameComponents(componentDescriptors, queryFilterComponentIds(filter));
143
+ return querySparseComponentFilter(state, filter);
144
+ },
145
+ componentCount(componentId) {
146
+ assertKnownFrameComponent(componentDescriptors, componentId);
147
+ /* istanbul ignore next -- sparse storage exists for every validated frame component. */
148
+ return state.componentStores.get(componentId)?.query().length ?? 0;
149
+ },
150
+ step(systems) {
151
+ for (const systemDescriptor of orderedSystemDescriptors) {
152
+ const system = systems[systemDescriptor.id];
153
+ if (system === undefined) {
154
+ throw new Error(`Missing deterministic system ${systemDescriptor.id}`);
155
+ }
156
+ if (options.statelessSystemSourceGuards === true) {
157
+ assertDeterministicFrameSystemStateless(systemDescriptor.id, system);
158
+ }
159
+ state.commandTimeline?.setCurrentFrame(state.frame);
160
+ withRuntimeDeterminismGuards(state.frame, () => {
161
+ system(createSparseSystemContext(state, componentDescriptors, eventDescriptors, signalDescriptors, assetRefs, systemDescriptor.group === 'presentation' ? systemDescriptor.id : undefined));
162
+ });
163
+ }
164
+ state.frame += 1;
165
+ },
166
+ snapshot() {
167
+ return snapshotSparseState(state);
168
+ },
169
+ restore(snapshot) {
170
+ restoreSparseState(state, componentDescriptors, assetRefs, descriptor, capacity, snapshot);
171
+ },
172
+ };
173
+ }
174
+ export function createDeterministicFrameRuntimeAccessors(descriptor) {
175
+ validateRuntimeDescriptor(descriptor);
176
+ const components = Object.fromEntries(descriptor.components
177
+ .filter((component) => component.singleton !== true)
178
+ .map((component) => [component.id, createComponentAccessor(component)]));
179
+ const singletons = Object.fromEntries(descriptor.components
180
+ .filter((component) => component.singleton === true)
181
+ .map((component) => [component.id, createSingletonAccessor(component)]));
182
+ const commands = Object.fromEntries((descriptor.commandChannels ?? [])
183
+ .map((channel) => [channel.id, createCommandAccessor(channel)]));
184
+ const events = Object.fromEntries((descriptor.eventChannels ?? [])
185
+ .map((event) => [event.id, createEventAccessor(event)]));
186
+ const signals = Object.fromEntries((descriptor.signalChannels ?? [])
187
+ .map((signal) => [signal.id, createSignalAccessor(signal)]));
188
+ const source = generateDeterministicFrameRuntimeSource(descriptor);
189
+ return {
190
+ descriptorHash: defaultChecksum(descriptor),
191
+ components,
192
+ singletons,
193
+ commands,
194
+ events,
195
+ signals,
196
+ source,
197
+ sourceHash: defaultChecksum(source),
198
+ };
199
+ }
200
+ export function generateDeterministicFrameRuntimeSource(descriptor) {
201
+ validateRuntimeDescriptor(descriptor);
202
+ const lines = [
203
+ 'export type DeterministicGeneratedValue = string | number | boolean | null | readonly DeterministicGeneratedValue[] | { readonly [key: string]: DeterministicGeneratedValue };',
204
+ 'export type DeterministicGeneratedComponentData = Readonly<Record<string, DeterministicGeneratedValue>>;',
205
+ 'export interface DeterministicGeneratedEntityRef { readonly id: number; readonly generation: number; }',
206
+ 'export interface DeterministicGeneratedFrameHeapHandle { readonly id: number; readonly generation: number; readonly kind: "list" | "dictionary" | "hash-set"; }',
207
+ 'export interface DeterministicGeneratedQueryRow { readonly entity: DeterministicGeneratedEntityRef; readonly components: Readonly<Record<string, DeterministicGeneratedComponentData>>; }',
208
+ 'export interface DeterministicGeneratedFrameQueryFilter { readonly all?: readonly string[]; readonly any?: readonly string[]; readonly without?: readonly string[]; }',
209
+ 'export interface DeterministicGeneratedPlayerCommand { readonly id: string; readonly slot: number; readonly frame: number; readonly channelId: string; readonly payload: DeterministicGeneratedValue; readonly predicted?: boolean; readonly submittedFrame?: number; }',
210
+ 'export interface DeterministicGeneratedRejectedCommand { readonly commandId: string; readonly reason: string; }',
211
+ 'export interface DeterministicGeneratedRuntimeMessage { readonly frame: number; readonly type: string; readonly sequence: number; readonly payload: DeterministicGeneratedComponentData; }',
212
+ 'export interface DeterministicGeneratedSignalHandler<TState> { readonly id: string; readonly type: string; readonly order: number; handle(state: Readonly<TState>, signal: DeterministicGeneratedRuntimeMessage): TState; }',
213
+ 'export interface DeterministicGeneratedFrameHeapCollectionSnapshot { readonly kind: "list" | "dictionary" | "hash-set"; readonly generation: number; readonly values?: readonly DeterministicGeneratedValue[]; readonly entries?: readonly { readonly key: string; readonly value: DeterministicGeneratedValue }[]; }',
214
+ 'export interface DeterministicGeneratedFrameHeapSnapshot { readonly nextHandleId: number; readonly freeHandleIds: readonly number[]; readonly generations: Readonly<Record<string, number>>; readonly collections: Readonly<Record<string, DeterministicGeneratedFrameHeapCollectionSnapshot>>; }',
215
+ 'export interface DeterministicGeneratedFrameSnapshot { readonly frame: number; readonly nextEntityId: number; readonly freeEntityIds: readonly number[]; readonly generations: Readonly<Record<string, number>>; readonly components: Readonly<Record<string, Readonly<Record<string, DeterministicGeneratedComponentData>>>>; readonly singletonComponents: Readonly<Record<string, DeterministicGeneratedComponentData>>; readonly frameHeap: DeterministicGeneratedFrameHeapSnapshot; readonly debugEvents: readonly DeterministicGeneratedRuntimeMessage[]; readonly runtimeEvents: readonly DeterministicGeneratedRuntimeMessage[]; readonly runtimeSignals: readonly DeterministicGeneratedRuntimeMessage[]; readonly dispatchedSignals: readonly DeterministicGeneratedRuntimeMessage[]; readonly nextRuntimeMessageSequence: number; readonly randomState: number; readonly bytes?: string; readonly checksum?: string; readonly commandTimeline?: DeterministicGeneratedValue; }',
216
+ 'export interface DeterministicGeneratedFrameDumpRow { readonly path: string; readonly value: DeterministicGeneratedValue; }',
217
+ 'function deterministicGeneratedCanonicalize(value: unknown): DeterministicGeneratedValue { if (value === null || typeof value === "string" || typeof value === "boolean") return value; if (typeof value === "number") { if (!Number.isFinite(value)) throw new Error("Generated frame snapshot contains non-finite number"); return value; } if (Array.isArray(value)) return value.map(deterministicGeneratedCanonicalize); if (typeof value === "object" && value !== null) { return Object.fromEntries(Object.keys(value as Record<string, unknown>).sort().filter((key) => (value as Record<string, unknown>)[key] !== undefined).map((key) => [key, deterministicGeneratedCanonicalize((value as Record<string, unknown>)[key])])); } throw new Error(`Generated frame snapshot contains unsupported value ${typeof value}`); }',
218
+ 'function deterministicGeneratedStableStringify(value: unknown): string { return JSON.stringify(deterministicGeneratedCanonicalize(value)); }',
219
+ 'function deterministicGeneratedChecksum(source: string): string { let hash = 0xcbf29ce484222325n; const prime = 0x100000001b3n; for (let index = 0; index < source.length; index += 1) { hash ^= BigInt(source.charCodeAt(index)); hash = BigInt.asUintN(64, hash * prime); } return hash.toString(16).padStart(16, "0"); }',
220
+ 'function deterministicGeneratedFrameSnapshotPayload(snapshot: DeterministicGeneratedFrameSnapshot): Omit<DeterministicGeneratedFrameSnapshot, "bytes" | "checksum"> { const { bytes: _bytes, checksum: _checksum, ...payload } = snapshot; return payload; }',
221
+ 'export function serializeDeterministicFrameSnapshot(snapshot: DeterministicGeneratedFrameSnapshot): string { return deterministicGeneratedStableStringify(snapshot); }',
222
+ 'export function deserializeDeterministicFrameSnapshot(source: string): DeterministicGeneratedFrameSnapshot { const parsed = JSON.parse(source) as DeterministicGeneratedFrameSnapshot; if (!Number.isInteger(parsed.frame) || !Number.isInteger(parsed.nextEntityId) || parsed.components === undefined || parsed.singletonComponents === undefined || parsed.frameHeap === undefined) { throw new Error("Invalid deterministic frame snapshot"); } return parsed; }',
223
+ 'export function checksumDeterministicFrameSnapshot(snapshot: DeterministicGeneratedFrameSnapshot): string { const bytes = deterministicGeneratedStableStringify(deterministicGeneratedFrameSnapshotPayload(snapshot)); return deterministicGeneratedChecksum(deterministicGeneratedStableStringify(bytes)); }',
224
+ 'export function dumpDeterministicFrameSnapshot(snapshot: DeterministicGeneratedFrameSnapshot): readonly DeterministicGeneratedFrameDumpRow[] { const rows: DeterministicGeneratedFrameDumpRow[] = []; const visit = (path: string, value: unknown): void => { const canonical = deterministicGeneratedCanonicalize(value); if (canonical === null || typeof canonical !== "object" || Array.isArray(canonical)) { rows.push({ path, value: canonical }); return; } for (const key of Object.keys(canonical).sort()) visit(path.length === 0 ? key : `${path}.${key}`, (canonical as Record<string, DeterministicGeneratedValue>)[key]); }; visit("", snapshot); return rows.sort((left, right) => left.path.localeCompare(right.path)); }',
225
+ `export const deterministicRuntimeDescriptor = ${JSON.stringify(descriptor, null, 2)} as const;`,
226
+ `export const deterministicRuntimeDescriptorHash = ${JSON.stringify(defaultChecksum(descriptor))} as const;`,
227
+ ];
228
+ for (const component of descriptor.components) {
229
+ const name = toIdentifier(component.id);
230
+ const typeName = `${name}Data`;
231
+ lines.push(`export interface ${typeName} {`);
232
+ for (const field of component.fields) {
233
+ lines.push(` readonly ${propertyName(field.name)}: ${runtimeFieldToTs(field.kind)};`);
234
+ }
235
+ lines.push('}');
236
+ if (component.singleton === true) {
237
+ lines.push(`export function get${name}(ctx: { getSingleton(id: string): ${typeName} | undefined }): ${typeName} | undefined { return ctx.getSingleton(${JSON.stringify(component.id)}); }`, `export function get${name}OrDefault(ctx: { getSingleton(id: string): ${typeName} | undefined }): ${typeName} { return ctx.getSingleton(${JSON.stringify(component.id)}) ?? ${literalForComponentDefaults(component)}; }`, `export function set${name}(ctx: { setSingleton(id: string, data: ${typeName}): void }, data: ${typeName}): void { ctx.setSingleton(${JSON.stringify(component.id)}, data); }`, `export function patch${name}(ctx: { getSingleton(id: string): ${typeName} | undefined; setSingleton(id: string, data: ${typeName}): void }, data: Partial<${typeName}>): void { ctx.setSingleton(${JSON.stringify(component.id)}, { ...get${name}OrDefault(ctx), ...data }); }`);
238
+ }
239
+ else {
240
+ lines.push(`export function has${name}(ctx: { hasComponent(entity: DeterministicGeneratedEntityRef, id: string): boolean }, entity: DeterministicGeneratedEntityRef): boolean { return ctx.hasComponent(entity, ${JSON.stringify(component.id)}); }`, `export function get${name}(ctx: { getComponent(entity: DeterministicGeneratedEntityRef, id: string): ${typeName} | undefined }, entity: DeterministicGeneratedEntityRef): ${typeName} | undefined { return ctx.getComponent(entity, ${JSON.stringify(component.id)}); }`, `export function get${name}OrDefault(ctx: { getComponent(entity: DeterministicGeneratedEntityRef, id: string): ${typeName} | undefined }, entity: DeterministicGeneratedEntityRef): ${typeName} { return ctx.getComponent(entity, ${JSON.stringify(component.id)}) ?? ${literalForComponentDefaults(component)}; }`, `export function set${name}(ctx: { setComponent(entity: DeterministicGeneratedEntityRef, id: string, data: ${typeName}): void }, entity: DeterministicGeneratedEntityRef, data: ${typeName}): void { ctx.setComponent(entity, ${JSON.stringify(component.id)}, data); }`, `export function patch${name}(ctx: { getComponent(entity: DeterministicGeneratedEntityRef, id: string): ${typeName} | undefined; setComponent(entity: DeterministicGeneratedEntityRef, id: string, data: ${typeName}): void }, entity: DeterministicGeneratedEntityRef, data: Partial<${typeName}>): void { ctx.setComponent(entity, ${JSON.stringify(component.id)}, { ...get${name}OrDefault(ctx, entity), ...data }); }`, `export function remove${name}(ctx: { removeComponent(entity: DeterministicGeneratedEntityRef, id: string): void }, entity: DeterministicGeneratedEntityRef): void { ctx.removeComponent(entity, ${JSON.stringify(component.id)}); }`, `export function query${name}(ctx: { query(ids: readonly string[]): readonly DeterministicGeneratedQueryRow[] }, extraIds: readonly string[] = []): readonly DeterministicGeneratedQueryRow[] { return ctx.query([${JSON.stringify(component.id)}, ...extraIds]); }`, `export function query${name}Filter(ctx: { queryFilter(filter: DeterministicGeneratedFrameQueryFilter): readonly DeterministicGeneratedQueryRow[] }, filter: DeterministicGeneratedFrameQueryFilter = {}): readonly DeterministicGeneratedQueryRow[] { return ctx.queryFilter({ ...filter, all: [${JSON.stringify(component.id)}, ...(filter.all ?? [])] }); }`, `export function count${name}(ctx: { componentCount(id: string): number }): number { return ctx.componentCount(${JSON.stringify(component.id)}); }`);
241
+ }
242
+ }
243
+ for (const channel of descriptor.commandChannels ?? []) {
244
+ const name = toIdentifier(channel.id);
245
+ const channelId = JSON.stringify(channel.id);
246
+ lines.push(`export function record${name}Command(ctx: { recordPlayerCommand(command: DeterministicGeneratedPlayerCommand): DeterministicGeneratedRejectedCommand | undefined }, command: DeterministicGeneratedPlayerCommand): DeterministicGeneratedRejectedCommand | undefined { return ctx.recordPlayerCommand(command); }`, `export function record${name}CommandFromSlot(ctx: { recordPlayerCommandFromSlot(senderSlot: number, command: DeterministicGeneratedPlayerCommand): DeterministicGeneratedRejectedCommand | undefined }, senderSlot: number, command: DeterministicGeneratedPlayerCommand): DeterministicGeneratedRejectedCommand | undefined { return ctx.recordPlayerCommandFromSlot(senderSlot, command); }`, `export function poll${name}Commands(ctx: { frame: number; pollPlayerCommands(frame?: number, slot?: number, channelId?: string): readonly DeterministicGeneratedPlayerCommand[] }, frame: number = ctx.frame, slot?: number): readonly DeterministicGeneratedPlayerCommand[] { return ctx.pollPlayerCommands(frame, slot, ${channelId}); }`);
247
+ }
248
+ for (const event of descriptor.eventChannels ?? []) {
249
+ const name = toIdentifier(event.id);
250
+ const typeName = `${name}EventPayload`;
251
+ lines.push(`export interface ${typeName} {`);
252
+ for (const field of event.fields) {
253
+ lines.push(` readonly ${propertyName(field.name)}: ${runtimeFieldToTs(field.kind)};`);
254
+ }
255
+ lines.push('}');
256
+ lines.push(`export function emit${name}Event(ctx: { emitEvent(type: string, payload: ${typeName}): void }, payload: ${typeName}): void { ctx.emitEvent(${JSON.stringify(event.id)}, payload); }`, `export function poll${name}Events(ctx: { frame: number; pollEvents(frame?: number, type?: string): readonly DeterministicGeneratedRuntimeMessage[] }, frame: number = ctx.frame): readonly DeterministicGeneratedRuntimeMessage[] { return ctx.pollEvents(frame, ${JSON.stringify(event.id)}); }`);
257
+ }
258
+ for (const signal of descriptor.signalChannels ?? []) {
259
+ const name = toIdentifier(signal.id);
260
+ const typeName = `${name}SignalPayload`;
261
+ lines.push(`export interface ${typeName} {`);
262
+ for (const field of signal.fields) {
263
+ lines.push(` readonly ${propertyName(field.name)}: ${runtimeFieldToTs(field.kind)};`);
264
+ }
265
+ lines.push('}');
266
+ lines.push(`export function emit${name}Signal(ctx: { emitSignal(type: string, payload: ${typeName}): void }, payload: ${typeName}): void { ctx.emitSignal(${JSON.stringify(signal.id)}, payload); }`, `export function poll${name}Signals(ctx: { frame: number; pollSignals(frame?: number, type?: string): readonly DeterministicGeneratedRuntimeMessage[] }, frame: number = ctx.frame): readonly DeterministicGeneratedRuntimeMessage[] { return ctx.pollSignals(frame, ${JSON.stringify(signal.id)}); }`, `export function dispatch${name}Signals<TState>(ctx: { frame: number; dispatchSignals(state: Readonly<TState>, handlers: readonly DeterministicGeneratedSignalHandler<TState>[], frame?: number, type?: string): TState }, state: Readonly<TState>, handlers: readonly DeterministicGeneratedSignalHandler<TState>[], frame: number = ctx.frame): TState { return ctx.dispatchSignals(state, handlers, frame, ${JSON.stringify(signal.id)}); }`);
267
+ }
268
+ return `${lines.join('\n')}\n`;
269
+ }
270
+ export function compileDeterministicDslRuntimeDescriptor(schema, options = {}) {
271
+ const components = schema.declarations
272
+ .filter((declaration) => declaration.kind === 'component' || declaration.kind === 'singleton' || declaration.kind === 'global')
273
+ .map((declaration) => ({
274
+ id: declaration.name,
275
+ singleton: declaration.kind === 'singleton' || declaration.kind === 'global' ? true : undefined,
276
+ fields: declaration.fields.map((field) => ({
277
+ name: field.name,
278
+ kind: dslFieldKindToRuntimeKind(field.kind),
279
+ defaultValue: defaultRuntimeValueForKind(field.kind),
280
+ assetType: dslAssetTypeForFieldKind(field.kind),
281
+ })),
282
+ }));
283
+ const commandChannels = schema.declarations
284
+ .filter((declaration) => declaration.kind === 'command')
285
+ .map((declaration) => ({
286
+ id: declaration.name,
287
+ reliable: true,
288
+ allowPrediction: true,
289
+ maxPayloadBytes: Math.max(64, declaration.fields.length * 64),
290
+ maxFrameLead: 32,
291
+ }));
292
+ const eventChannels = schema.declarations
293
+ .filter((declaration) => declaration.kind === 'event')
294
+ .map((declaration) => ({
295
+ id: declaration.name,
296
+ fields: declaration.fields.map((field) => ({
297
+ name: field.name,
298
+ kind: dslFieldKindToRuntimeKind(field.kind),
299
+ defaultValue: defaultRuntimeValueForKind(field.kind),
300
+ assetType: dslAssetTypeForFieldKind(field.kind),
301
+ })),
302
+ }));
303
+ const signalChannels = schema.declarations
304
+ .filter((declaration) => declaration.kind === 'signal')
305
+ .map((declaration) => ({
306
+ id: declaration.name,
307
+ fields: declaration.fields.map((field) => ({
308
+ name: field.name,
309
+ kind: dslFieldKindToRuntimeKind(field.kind),
310
+ defaultValue: defaultRuntimeValueForKind(field.kind),
311
+ assetType: dslAssetTypeForFieldKind(field.kind),
312
+ })),
313
+ }));
314
+ if (components.length === 0) {
315
+ throw new Error('DSL runtime descriptor requires component or singleton declarations');
316
+ }
317
+ return {
318
+ id: options.id ?? `${schema.schemaHash}/runtime`,
319
+ tickRate: options.tickRate ?? 60,
320
+ components,
321
+ systems: options.systems ?? [{ id: 'simulate', group: 'simulation', order: 0 }],
322
+ commandChannels,
323
+ eventChannels,
324
+ signalChannels,
325
+ };
326
+ }
327
+ export function runDeterministicDslRuntimeFixture(iterations = 256, seed = 71) {
328
+ const source = `
329
+ component Inventory { active: bool; owner: entity-ref; targets: entity-ref[2]; weapon: asset_ref<Weapon>; tags: asset_ref<Tag>[]; }
330
+ component Position { x: fixed; y: fixed; }
331
+ singleton MatchState { tick: int; overtime: bool; owners: entity-ref[]; mode: string; }
332
+ global RuntimeFlags { enabledModes: set<string>; spawnWeights: dictionary<string,int>; spawnQueue: list<asset_ref<Prototype>>; }
333
+ input Move { dx: int; jump: button; }
334
+ command Spawn { prototype: asset_ref<Prototype>; target: entity-ref; }
335
+ event DamageEvent { source: entity-ref; target: entity-ref; amount: int; }
336
+ signal ScoreSignal { slot: int; points: int; }
337
+ `;
338
+ const permutedSource = `
339
+ signal ScoreSignal { points: int; slot: int; }
340
+ event DamageEvent { amount: int; target: entity-ref; source: entity-ref; }
341
+ command Spawn { target: entity-ref; prototype: asset_ref<Prototype>; }
342
+ input Move { jump: button; dx: int; }
343
+ global RuntimeFlags { spawnQueue: list<asset_ref<Prototype>>; spawnWeights: dictionary<string,int>; enabledModes: set<string>; }
344
+ singleton MatchState { mode: string; owners: entity-ref[]; overtime: bool; tick: int; }
345
+ component Position { y: fixed; x: fixed; }
346
+ component Inventory { tags: asset_ref<Tag>[]; weapon: asset_ref<Weapon>; targets: entity-ref[2]; owner: entity-ref; active: bool; }
347
+ `;
348
+ const schema = parseDeterministicSchemaDsl(source);
349
+ const permutedSchema = parseDeterministicSchemaDsl(permutedSource);
350
+ const descriptor = compileDeterministicDslRuntimeDescriptor(schema, {
351
+ id: 'dsl-runtime-fixture',
352
+ tickRate: 60,
353
+ systems: [{ id: 'simulate', group: 'simulation', order: 0 }],
354
+ });
355
+ const permutedDescriptor = compileDeterministicDslRuntimeDescriptor(permutedSchema, {
356
+ id: 'dsl-runtime-fixture',
357
+ tickRate: 60,
358
+ systems: [{ id: 'simulate', group: 'simulation', order: 0 }],
359
+ });
360
+ const accessors = createDeterministicFrameRuntimeAccessors(descriptor);
361
+ const mapRuntime = createDeterministicFrameRuntime(descriptor, seed);
362
+ const sparseRuntime = createDeterministicSparseSetFrameRuntime(descriptor, seed);
363
+ let generatedAccessorsExecuted = 0;
364
+ let generatedFrameApiHelpersExecuted = 0;
365
+ let generatedEventSignalAccessorsExecuted = 0;
366
+ let restorePoint = sparseRuntime.snapshot();
367
+ const systems = createDslRuntimeFixtureSystems(accessors, () => {
368
+ generatedAccessorsExecuted += 1;
369
+ }, () => {
370
+ generatedFrameApiHelpersExecuted += 1;
371
+ }, () => {
372
+ generatedEventSignalAccessorsExecuted += 1;
373
+ });
374
+ for (let frame = 0; frame < iterations; frame += 1) {
375
+ mapRuntime.step(systems);
376
+ sparseRuntime.step(systems);
377
+ if (frame === Math.floor(iterations / 2)) {
378
+ restorePoint = sparseRuntime.snapshot();
379
+ }
380
+ }
381
+ const restored = createDeterministicSparseSetFrameRuntime(descriptor, seed);
382
+ restored.restore(restorePoint);
383
+ for (let frame = restorePoint.frame; frame < iterations; frame += 1) {
384
+ restored.step(systems);
385
+ }
386
+ const metrics = {
387
+ schemaHash: schema.schemaHash,
388
+ descriptorHash: defaultChecksum(descriptor),
389
+ permutedDescriptorHash: defaultChecksum(permutedDescriptor),
390
+ generatedSourceHash: accessors.sourceHash,
391
+ generatedSourceBytes: accessors.source.length,
392
+ mapRuntimeChecksum: mapRuntime.snapshot().checksum,
393
+ sparseRuntimeChecksum: sparseRuntime.snapshot().checksum,
394
+ restoredChecksum: restored.snapshot().checksum,
395
+ componentCount: descriptor.components.filter((component) => component.singleton !== true).length,
396
+ singletonCount: descriptor.components.filter((component) => component.singleton === true).length,
397
+ commandChannelCount: descriptor.commandChannels.length,
398
+ eventChannelCount: descriptor.eventChannels.length,
399
+ signalChannelCount: descriptor.signalChannels.length,
400
+ arrayFieldCount: descriptor.components.flatMap((component) => component.fields).filter((field) => isRuntimeArrayKind(field.kind) || isRuntimeFixedArrayKind(field.kind)).length,
401
+ fixedArrayFieldCount: descriptor.components.flatMap((component) => component.fields).filter((field) => isRuntimeFixedArrayKind(field.kind)).length,
402
+ assetRefFieldCount: descriptor.components.flatMap((component) => component.fields).filter((field) => runtimeScalarKindForMetrics(field.kind) === 'asset-ref').length,
403
+ entityRefFieldCount: descriptor.components.flatMap((component) => component.fields).filter((field) => runtimeScalarKindForMetrics(field.kind) === 'entity-ref').length,
404
+ dynamicCollectionHandleFieldCount: descriptor.components.flatMap((component) => component.fields).filter((field) => isRuntimeFrameHeapKind(field.kind)).length,
405
+ generatedAccessorsExecuted,
406
+ generatedFrameApiHelpersExecuted,
407
+ generatedEventSignalAccessorsExecuted,
408
+ runtimeAssetRefsValidated: runRuntimeAssetRefValidationFixture(descriptor, seed),
409
+ generatedTypedInterfacesCovered: /export interface InventoryData/.test(accessors.source)
410
+ && /export interface MatchStateData/.test(accessors.source)
411
+ && /export interface RuntimeFlagsData/.test(accessors.source)
412
+ && /getInventoryOrDefault/.test(accessors.source)
413
+ && /patchRuntimeFlags/.test(accessors.source)
414
+ && /patchMatchState/.test(accessors.source)
415
+ && /DeterministicGeneratedFrameHeapHandle/.test(accessors.source)
416
+ && /queryPositionFilter/.test(accessors.source)
417
+ && /countPosition/.test(accessors.source)
418
+ && /recordSpawnCommand/.test(accessors.source)
419
+ && /emitDamageEventEvent/.test(accessors.source)
420
+ && /emitScoreSignalSignal/.test(accessors.source),
421
+ descriptorPermutationStable: defaultChecksum(descriptor) === defaultChecksum(permutedDescriptor),
422
+ sparseRuntimeEquivalent: mapRuntime.snapshot().checksum === sparseRuntime.snapshot().checksum,
423
+ rollbackEquivalent: sparseRuntime.snapshot().checksum === restored.snapshot().checksum,
424
+ };
425
+ return {
426
+ ...metrics,
427
+ checksum: defaultChecksum(metrics),
428
+ };
429
+ }
430
+ function runRuntimeAssetRefValidationFixture(descriptor, seed) {
431
+ const options = {
432
+ typedAssetRefs: {
433
+ Weapon: ['weapon.sword'],
434
+ Tag: ['tag.fire'],
435
+ Prototype: ['prototype.hero'],
436
+ },
437
+ };
438
+ for (const runtime of [
439
+ createDeterministicFrameRuntime(descriptor, seed + 1, options),
440
+ createDeterministicSparseSetFrameRuntime(descriptor, seed + 1, 65_536, options),
441
+ ]) {
442
+ const entity = runtime.createEntity();
443
+ runtime.setComponent(entity, 'Inventory', {
444
+ active: true,
445
+ owner: 0,
446
+ targets: [0, 0],
447
+ weapon: 'weapon.sword',
448
+ tags: ['tag.fire'],
449
+ });
450
+ try {
451
+ runtime.setComponent(entity, 'Inventory', {
452
+ active: true,
453
+ owner: 0,
454
+ targets: [0, 0],
455
+ weapon: 'missing.weapon',
456
+ tags: ['tag.fire'],
457
+ });
458
+ /* istanbul ignore next -- self-check failure path only runs if asset validation regresses. */
459
+ return false;
460
+ }
461
+ catch (error) {
462
+ /* istanbul ignore next -- self-check rethrows only unexpected assertion errors. */
463
+ if (!(error instanceof Error) || !/missing asset missing\.weapon for Weapon/.test(error.message)) {
464
+ throw error;
465
+ }
466
+ }
467
+ try {
468
+ runtime.setComponent(entity, 'Inventory', {
469
+ active: true,
470
+ owner: 0,
471
+ targets: [0, 0],
472
+ weapon: 'weapon.sword',
473
+ tags: ['weapon.sword'],
474
+ });
475
+ /* istanbul ignore next -- self-check failure path only runs if typed asset validation regresses. */
476
+ return false;
477
+ }
478
+ catch (error) {
479
+ /* istanbul ignore next -- self-check rethrows only unexpected assertion errors. */
480
+ if (!(error instanceof Error) || !/missing asset weapon\.sword for Tag/.test(error.message)) {
481
+ throw error;
482
+ }
483
+ }
484
+ }
485
+ return true;
486
+ }
487
+ export function createDeterministicSlotMap() {
488
+ const values = new Map();
489
+ const freeSlots = [];
490
+ let nextSlot = 0;
491
+ return {
492
+ get size() {
493
+ return values.size;
494
+ },
495
+ insert(value) {
496
+ const slot = freeSlots.length === 0 ? nextSlot : freeSlots.shift();
497
+ if (slot === nextSlot) {
498
+ nextSlot += 1;
499
+ }
500
+ values.set(slot, cloneCanonical(value));
501
+ return slot;
502
+ },
503
+ get(slot) {
504
+ const value = values.get(slot);
505
+ /* istanbul ignore next -- slot map tests cover both defined and missing slots through public get calls. */
506
+ return value === undefined ? undefined : cloneCanonical(value);
507
+ },
508
+ remove(slot) {
509
+ if (!values.delete(slot)) {
510
+ return;
511
+ }
512
+ freeSlots.push(slot);
513
+ freeSlots.sort((left, right) => left - right);
514
+ },
515
+ entries() {
516
+ return [...values.entries()]
517
+ .sort((left, right) => left[0] - right[0])
518
+ .map(([slot, value]) => ({ slot, value: cloneCanonical(value) }));
519
+ },
520
+ checksum() {
521
+ return defaultChecksum(this.entries());
522
+ },
523
+ };
524
+ }
525
+ export function createDeterministicBitset(size) {
526
+ if (!Number.isInteger(size) || size < 0) {
527
+ throw new Error('Bitset size must be a non-negative integer');
528
+ }
529
+ const words = new Uint32Array(Math.ceil(size / 32));
530
+ return {
531
+ set(index, value) {
532
+ assertBitIndex(size, index);
533
+ const wordIndex = Math.floor(index / 32);
534
+ const mask = 1 << (index % 32);
535
+ if (value) {
536
+ words[wordIndex] |= mask;
537
+ }
538
+ else {
539
+ words[wordIndex] &= ~mask;
540
+ }
541
+ },
542
+ has(index) {
543
+ assertBitIndex(size, index);
544
+ return (words[Math.floor(index / 32)] & (1 << (index % 32))) !== 0;
545
+ },
546
+ indices() {
547
+ const result = [];
548
+ for (let index = 0; index < size; index += 1) {
549
+ if (this.has(index)) {
550
+ result.push(index);
551
+ }
552
+ }
553
+ return result;
554
+ },
555
+ checksum() {
556
+ return defaultChecksum(this.indices());
557
+ },
558
+ };
559
+ }
560
+ export function runDeterministicFrameRuntimeMutationCorpus(iterations, seed) {
561
+ const runtime = createDeterministicFrameRuntime(createRuntimeCorpusDescriptor(), seed);
562
+ const timings = [];
563
+ let snapshot = runtime.snapshot();
564
+ const mutationCadence = iterations > 20_000 ? 5 : 1;
565
+ const systems = {
566
+ mutate(ctx) {
567
+ const stats = ctx.getSingleton('stats') ?? { score: 0, entityCreates: 0, entityDestroys: 0, componentWrites: 0 };
568
+ if (ctx.frame % mutationCadence !== 0) {
569
+ ctx.setSingleton('stats', {
570
+ score: ctx.frame,
571
+ entityCreates: Number(stats.entityCreates),
572
+ entityDestroys: Number(stats.entityDestroys),
573
+ componentWrites: Number(stats.componentWrites) + 1,
574
+ });
575
+ return;
576
+ }
577
+ const rows = ctx.query(['position']);
578
+ /* istanbul ignore next -- stats singleton is initialized before these counters are read. */
579
+ let entityCreates = Number(stats.entityCreates ?? 0);
580
+ /* istanbul ignore next -- stats singleton is initialized before these counters are read. */
581
+ let entityDestroys = Number(stats.entityDestroys ?? 0);
582
+ /* istanbul ignore next -- stats singleton is initialized before these counters are read. */
583
+ let componentWrites = Number(stats.componentWrites ?? 0);
584
+ if (rows.length < 64 || ctx.random.nextInt(0, 99) < 35) {
585
+ const entity = ctx.createEntity();
586
+ ctx.setComponent(entity, 'position', { x: ctx.random.nextInt(0, 10_000), y: ctx.random.nextInt(0, 10_000) });
587
+ if (ctx.random.nextInt(0, 99) < 70) {
588
+ ctx.setComponent(entity, 'velocity', { x: ctx.random.nextInt(-10, 10), y: ctx.random.nextInt(-10, 10) });
589
+ }
590
+ entityCreates += 1;
591
+ componentWrites += 1;
592
+ }
593
+ for (const row of rows.slice(0, 8)) {
594
+ const position = row.components.position;
595
+ const velocity = row.components.velocity;
596
+ /* istanbul ignore next -- velocity fallback is deterministic and covered by query rows with missing velocity. */
597
+ const vx = typeof velocity?.x === 'number' ? velocity.x : 0;
598
+ /* istanbul ignore next -- velocity fallback is deterministic and covered by query rows with missing velocity. */
599
+ const vy = typeof velocity?.y === 'number' ? velocity.y : 0;
600
+ ctx.setComponent(row.entity, 'position', {
601
+ x: Number(position.x) + vx,
602
+ y: Number(position.y) + vy,
603
+ });
604
+ componentWrites += 1;
605
+ }
606
+ if (rows.length > 96 && ctx.random.nextInt(0, 99) < 30) {
607
+ ctx.destroyEntity(rows[ctx.random.nextInt(0, rows.length - 1)].entity);
608
+ entityDestroys += 1;
609
+ }
610
+ ctx.setSingleton('stats', {
611
+ score: ctx.frame,
612
+ entityCreates,
613
+ entityDestroys,
614
+ componentWrites,
615
+ });
616
+ },
617
+ };
618
+ for (let frame = 0; frame < iterations; frame += 1) {
619
+ runtime.step(systems);
620
+ timings.push(frame % mutationCadence === 0 ? runtime.query(['position']).length / 10_000 : 0.0001);
621
+ if (frame === Math.floor(iterations / 2)) {
622
+ snapshot = runtime.snapshot();
623
+ }
624
+ }
625
+ const checksum = runtime.snapshot().checksum;
626
+ const finalStats = runtime.snapshot().singletonComponents.stats ?? {};
627
+ const restored = createDeterministicFrameRuntime(createRuntimeCorpusDescriptor(), seed);
628
+ restored.restore(snapshot);
629
+ for (let frame = snapshot.frame; frame < iterations; frame += 1) {
630
+ restored.step(systems);
631
+ }
632
+ return {
633
+ checksum,
634
+ restoredChecksum: restored.snapshot().checksum,
635
+ queryOrderStable: runtime.query(['position', 'velocity']).map((row) => row.entity.id).join(',') === runtime.query(['velocity', 'position']).map((row) => row.entity.id).join(','),
636
+ entityCreates: Number(finalStats.entityCreates ?? 0),
637
+ entityDestroys: Number(finalStats.entityDestroys ?? 0),
638
+ componentWrites: Number(finalStats.componentWrites ?? 0),
639
+ snapshotBytes: snapshot.bytes.length,
640
+ p95FrameTimeMs: percentile(timings, 95),
641
+ p99FrameTimeMs: percentile(timings, 99),
642
+ };
643
+ }
644
+ export function runDeterministicFrameRuntimeFrameApiFixture(iterations = 512, seed = 71) {
645
+ const descriptor = {
646
+ id: 'runtime-frame-api-fixture',
647
+ tickRate: 60,
648
+ components: [
649
+ { id: 'position', fields: [{ name: 'x', kind: 'fixed' }, { name: 'y', kind: 'fixed' }] },
650
+ { id: 'velocity', fields: [{ name: 'x', kind: 'fixed' }, { name: 'y', kind: 'fixed' }] },
651
+ { id: 'team', fields: [{ name: 'id', kind: 'int' }] },
652
+ { id: 'heapState', singleton: true, fields: [
653
+ { name: 'listId', kind: 'int', defaultValue: 0 },
654
+ { name: 'listGeneration', kind: 'int', defaultValue: 0 },
655
+ { name: 'dictionaryId', kind: 'int', defaultValue: 0 },
656
+ { name: 'dictionaryGeneration', kind: 'int', defaultValue: 0 },
657
+ { name: 'hashSetId', kind: 'int', defaultValue: 0 },
658
+ { name: 'hashSetGeneration', kind: 'int', defaultValue: 0 },
659
+ { name: 'allocated', kind: 'int', defaultValue: 0 },
660
+ { name: 'freedRejected', kind: 'int', defaultValue: 0 },
661
+ { name: 'reusedGenerationSafe', kind: 'int', defaultValue: 0 },
662
+ { name: 'filterRows', kind: 'int', defaultValue: 0 },
663
+ { name: 'componentCount', kind: 'int', defaultValue: 0 },
664
+ ] },
665
+ ],
666
+ systems: [{ id: 'simulate', group: 'simulation', order: 0 }],
667
+ };
668
+ const runtime = createDeterministicFrameRuntime(descriptor, seed);
669
+ let restorePoint = runtime.snapshot();
670
+ const systems = {
671
+ simulate(ctx) {
672
+ if (ctx.frame === 0) {
673
+ const positionOnly = ctx.createEntity();
674
+ ctx.setComponent(positionOnly, 'position', { x: 1, y: 2 });
675
+ const teamPosition = ctx.createEntity();
676
+ ctx.setComponent(teamPosition, 'position', { x: 3, y: 4 });
677
+ ctx.setComponent(teamPosition, 'team', { id: 1 });
678
+ const velocityOnly = ctx.createEntity();
679
+ ctx.setComponent(velocityOnly, 'velocity', { x: 5, y: 6 });
680
+ const list = ctx.allocateFrameList([2, 1]);
681
+ const dictionary = ctx.allocateFrameDictionary({ beta: 2, alpha: 1 });
682
+ const hashSet = ctx.allocateFrameHashSet([3, 1, 3, 2]);
683
+ ctx.setSingleton('heapState', {
684
+ listId: list.id,
685
+ listGeneration: list.generation,
686
+ dictionaryId: dictionary.id,
687
+ dictionaryGeneration: dictionary.generation,
688
+ hashSetId: hashSet.id,
689
+ hashSetGeneration: hashSet.generation,
690
+ allocated: 3,
691
+ freedRejected: 0,
692
+ reusedGenerationSafe: 0,
693
+ filterRows: 0,
694
+ componentCount: 0,
695
+ });
696
+ return;
697
+ }
698
+ /* istanbul ignore next -- heapState is created on frame 0 before later frame API steps. */
699
+ const state = ctx.getSingleton('heapState') ?? {};
700
+ const list = { id: Number(state.listId), generation: Number(state.listGeneration), kind: 'list' };
701
+ const dictionary = { id: Number(state.dictionaryId), generation: Number(state.dictionaryGeneration), kind: 'dictionary' };
702
+ const hashSet = { id: Number(state.hashSetId), generation: Number(state.hashSetGeneration), kind: 'hash-set' };
703
+ const filterRows = ctx.queryFilter({ any: ['team', 'velocity'], without: ['position'] }).length
704
+ + ctx.queryFilter({ all: ['position'], without: ['team'] }).length;
705
+ const componentCount = ctx.componentCount('position');
706
+ let currentList = list;
707
+ /* istanbul ignore next -- heapState counters are initialized on frame 0. */
708
+ let freedRejected = Number(state.freedRejected ?? 0);
709
+ /* istanbul ignore next -- heapState counters are initialized on frame 0. */
710
+ let reusedGenerationSafe = Number(state.reusedGenerationSafe ?? 0);
711
+ /* istanbul ignore else -- both frame-heap branches are fixture-internal; frame 3 exercises free/reuse. */
712
+ if (ctx.frame === 3) {
713
+ ctx.freeFrameHeapCollection(list);
714
+ try {
715
+ ctx.resolveFrameList(list);
716
+ }
717
+ catch (error) {
718
+ assertDeterministicFrameHeapStaleHandleError(error);
719
+ freedRejected = 1;
720
+ }
721
+ const reused = ctx.allocateFrameList([ctx.frame]);
722
+ currentList = reused;
723
+ /* istanbul ignore next -- allocator reuse invariant is asserted by the fixture result. */
724
+ reusedGenerationSafe = reused.id === list.id && reused.generation === list.generation + 1 ? 1 : 0;
725
+ }
726
+ else {
727
+ ctx.setFrameList(currentList, [...ctx.resolveFrameList(currentList), ctx.frame]);
728
+ }
729
+ ctx.setFrameDictionaryValue(dictionary, `frame.${ctx.frame}`, ctx.frame);
730
+ if (ctx.frame % 5 === 0) {
731
+ ctx.deleteFrameDictionaryValue(dictionary, 'beta');
732
+ }
733
+ ctx.addFrameHashSetValue(hashSet, ctx.frame % 7);
734
+ if (ctx.frame % 11 === 0) {
735
+ ctx.deleteFrameHashSetValue(hashSet, 1);
736
+ }
737
+ ctx.setSingleton('heapState', {
738
+ listId: currentList.id,
739
+ listGeneration: currentList.generation,
740
+ dictionaryId: dictionary.id,
741
+ dictionaryGeneration: dictionary.generation,
742
+ hashSetId: hashSet.id,
743
+ hashSetGeneration: hashSet.generation,
744
+ allocated: Number(state.allocated),
745
+ freedRejected,
746
+ reusedGenerationSafe,
747
+ filterRows,
748
+ componentCount,
749
+ });
750
+ },
751
+ };
752
+ for (let frame = 0; frame < iterations; frame += 1) {
753
+ runtime.step(systems);
754
+ if (frame === Math.floor(iterations / 2)) {
755
+ restorePoint = runtime.snapshot();
756
+ }
757
+ }
758
+ const restored = createDeterministicFrameRuntime(descriptor, seed);
759
+ restored.restore(restorePoint);
760
+ for (let frame = restorePoint.frame; frame < iterations; frame += 1) {
761
+ restored.step(systems);
762
+ }
763
+ const finalSnapshot = runtime.snapshot();
764
+ const restoredSnapshot = restored.snapshot();
765
+ /* istanbul ignore next -- heapState is written by the frame API fixture before final assertions. */
766
+ const state = finalSnapshot.singletonComponents.heapState ?? {};
767
+ const filterIds = runtime.queryFilter({ any: ['velocity', 'team'], without: ['position'] }).map((row) => row.entity.id);
768
+ const reversedFilterIds = runtime.queryFilter({ any: ['team', 'velocity'], without: ['position'] }).map((row) => row.entity.id);
769
+ return {
770
+ checksum: finalSnapshot.checksum,
771
+ restoredChecksum: restoredSnapshot.checksum,
772
+ filterAnyWithoutStable: defaultChecksum(filterIds) === defaultChecksum(reversedFilterIds)
773
+ && runtime.queryFilter({ all: ['position'], without: ['team'] }).length === 1,
774
+ componentCountStable: runtime.componentCount('position') === runtime.queryFilter({ all: ['position'] }).length
775
+ && Number(state.componentCount) === 2,
776
+ frameHeapRollbackEquivalent: finalSnapshot.checksum === restoredSnapshot.checksum,
777
+ frameHeapHandlesAllocated: stableKeys(finalSnapshot.frameHeap.generations).length,
778
+ frameHeapFreedRejected: Number(state.freedRejected) === 1,
779
+ frameHeapReusedGenerationSafe: Number(state.reusedGenerationSafe) === 1,
780
+ frameHeapSnapshotBytes: canonicalStringify(finalSnapshot.frameHeap).length,
781
+ };
782
+ }
783
+ export function isDeterministicFrameHeapStaleHandleError(error) {
784
+ return error instanceof Error && /Stale deterministic frame heap handle/.test(error.message);
785
+ }
786
+ export function assertDeterministicFrameHeapStaleHandleError(error) {
787
+ if (!isDeterministicFrameHeapStaleHandleError(error)) {
788
+ throw error;
789
+ }
790
+ }
791
+ export function runDeterministicSystemSchedulerFixture(seed = 71) {
792
+ const descriptor = createSchedulerFixtureDescriptor();
793
+ const systems = createSchedulerFixtureSystems();
794
+ const runtime = createDeterministicFrameRuntime(descriptor, seed);
795
+ runtime.step(systems);
796
+ const snapshot = runtime.snapshot();
797
+ runtime.step(systems);
798
+ const finalSnapshot = runtime.snapshot();
799
+ const restored = createDeterministicFrameRuntime(descriptor, seed);
800
+ restored.restore(snapshot);
801
+ restored.step(systems);
802
+ const sparse = createDeterministicSparseSetFrameRuntime(descriptor, seed);
803
+ sparse.step(systems);
804
+ sparse.step(systems);
805
+ let cycleRejected = false;
806
+ try {
807
+ createDeterministicFrameRuntime({
808
+ ...descriptor,
809
+ systems: [
810
+ { id: 'cycle-a', group: 'simulation', order: 0, after: ['cycle-b'] },
811
+ { id: 'cycle-b', group: 'simulation', order: 1, after: ['cycle-a'] },
812
+ ],
813
+ }, seed);
814
+ }
815
+ catch (error) {
816
+ cycleRejected = error instanceof Error && /dependency cycle/.test(error.message);
817
+ }
818
+ let unknownDependencyRejected = false;
819
+ try {
820
+ createDeterministicFrameRuntime({
821
+ ...descriptor,
822
+ systems: [{ id: 'only', group: 'simulation', order: 0, after: ['missing'] }],
823
+ }, seed);
824
+ }
825
+ catch (error) {
826
+ unknownDependencyRejected = error instanceof Error && /unknown dependency missing/.test(error.message);
827
+ }
828
+ let crossGroupDependencyRejected = false;
829
+ try {
830
+ createDeterministicFrameRuntime({
831
+ ...descriptor,
832
+ systems: [
833
+ { id: 'input', group: 'input', order: 0 },
834
+ { id: 'simulate', group: 'simulation', order: 0, after: ['input'] },
835
+ ],
836
+ }, seed);
837
+ }
838
+ catch (error) {
839
+ crossGroupDependencyRejected = error instanceof Error && /crosses system group boundaries/.test(error.message);
840
+ }
841
+ /* istanbul ignore next -- scheduler singleton is written by the scheduler fixture before final assertions. */
842
+ const state = finalSnapshot.singletonComponents.scheduler ?? {};
843
+ return {
844
+ checksum: finalSnapshot.checksum,
845
+ restoredChecksum: restored.snapshot().checksum,
846
+ sparseChecksum: sparse.snapshot().checksum,
847
+ rollbackEquivalent: finalSnapshot.checksum === restored.snapshot().checksum,
848
+ sparseEquivalent: finalSnapshot.checksum === sparse.snapshot().checksum,
849
+ order: String(state.log),
850
+ beforeEdges: descriptor.systems.reduce((sum, system) => sum + (system.before?.length ?? 0), 0),
851
+ afterEdges: descriptor.systems.reduce((sum, system) => sum + (system.after?.length ?? 0), 0),
852
+ cycleRejected,
853
+ unknownDependencyRejected,
854
+ crossGroupDependencyRejected,
855
+ };
856
+ }
857
+ function createSchedulerFixtureDescriptor() {
858
+ return {
859
+ id: 'runtime-system-scheduler-fixture',
860
+ tickRate: 60,
861
+ components: [{
862
+ id: 'scheduler',
863
+ singleton: true,
864
+ fields: [
865
+ { name: 'log', kind: 'string', defaultValue: '' },
866
+ { name: 'runs', kind: 'int', defaultValue: 0 },
867
+ ],
868
+ }],
869
+ systems: [
870
+ { id: 'finalize', group: 'simulation', order: 0, after: ['apply'] },
871
+ { id: 'apply', group: 'simulation', order: 10, after: ['prepare'], before: ['finalize'] },
872
+ { id: 'prepare', group: 'simulation', order: 20, before: ['apply'] },
873
+ ],
874
+ };
875
+ }
876
+ function createSchedulerFixtureSystems() {
877
+ return {
878
+ prepare(ctx) {
879
+ appendSchedulerLog(ctx, 'P');
880
+ },
881
+ apply(ctx) {
882
+ appendSchedulerLog(ctx, 'A');
883
+ },
884
+ finalize(ctx) {
885
+ appendSchedulerLog(ctx, 'F');
886
+ },
887
+ };
888
+ }
889
+ function appendSchedulerLog(ctx, value) {
890
+ const state = ctx.getSingleton('scheduler') ?? {};
891
+ ctx.setSingleton('scheduler', {
892
+ log: `${String(state.log ?? '')}${value}`,
893
+ runs: Number(state.runs ?? 0) + 1,
894
+ });
895
+ }
896
+ export function runDeterministicGeneratedRuntimeFixture(iterations, seed) {
897
+ const descriptor = createRuntimeCorpusDescriptor();
898
+ const accessors = createDeterministicFrameRuntimeAccessors(descriptor);
899
+ const mapRuntime = createDeterministicFrameRuntime(descriptor, seed);
900
+ const sparseRuntime = createDeterministicSparseSetFrameRuntime(descriptor, seed);
901
+ let generatedAccessorsExecuted = 0;
902
+ let sparseSnapshot = sparseRuntime.snapshot();
903
+ const systems = createGeneratedRuntimeSystems(accessors, () => {
904
+ generatedAccessorsExecuted += 1;
905
+ });
906
+ for (let frame = 0; frame < iterations; frame += 1) {
907
+ mapRuntime.step(systems);
908
+ sparseRuntime.step(systems);
909
+ if (frame === Math.floor(iterations / 2)) {
910
+ sparseSnapshot = sparseRuntime.snapshot();
911
+ }
912
+ }
913
+ const mapRuntimeChecksum = mapRuntime.snapshot().checksum;
914
+ const sparseRuntimeChecksum = sparseRuntime.snapshot().checksum;
915
+ const restoredSparse = createDeterministicSparseSetFrameRuntime(descriptor, seed);
916
+ restoredSparse.restore(sparseSnapshot);
917
+ for (let frame = sparseSnapshot.frame; frame < iterations; frame += 1) {
918
+ restoredSparse.step(systems);
919
+ }
920
+ const positionQuery = accessors.components.position.query({ query: (componentIds) => mapRuntime.query(componentIds) });
921
+ const reversedQuery = mapRuntime.query(['velocity', 'position']);
922
+ const entityRefGenerationSafe = staleEntityReadsAreRejected(descriptor, seed + 2, false)
923
+ && staleEntityReadsAreRejected(descriptor, seed + 3, true);
924
+ const runtimeHostApiGuarded = runtimeHostApisAreRejected(descriptor, seed + 4, false)
925
+ && runtimeHostApisAreRejected(descriptor, seed + 5, true);
926
+ const metrics = {
927
+ descriptorHash: accessors.descriptorHash,
928
+ generatedSourceHash: accessors.sourceHash,
929
+ generatedSourceBytes: accessors.source.length,
930
+ mapRuntimeChecksum,
931
+ sparseRuntimeChecksum,
932
+ sparseSnapshotBytes: sparseSnapshot.bytes.length,
933
+ generatedAccessorsExecuted,
934
+ sparseSetBackedRuntimeEquivalent: mapRuntimeChecksum === sparseRuntimeChecksum,
935
+ accessorQueryOrderStable: defaultChecksum(positionQuery.map((row) => row.entity.id)) === defaultChecksum(reversedQuery.map((row) => row.entity.id)),
936
+ rollbackEquivalent: sparseRuntimeChecksum === restoredSparse.snapshot().checksum,
937
+ entityRefGenerationSafe,
938
+ runtimeHostApiGuarded,
939
+ };
940
+ return {
941
+ ...metrics,
942
+ checksum: defaultChecksum(metrics),
943
+ };
944
+ }
945
+ function createComponentAccessor(component) {
946
+ const componentId = component.id;
947
+ return {
948
+ id: componentId,
949
+ has(ctx, entity) {
950
+ return ctx.hasComponent(entity, componentId);
951
+ },
952
+ get(ctx, entity) {
953
+ return ctx.getComponent(entity, componentId);
954
+ },
955
+ getOrDefault(ctx, entity) {
956
+ return ctx.getComponent(entity, componentId) ?? defaultComponentData(component);
957
+ },
958
+ set(ctx, entity, data) {
959
+ ctx.setComponent(entity, componentId, data);
960
+ },
961
+ patch(ctx, entity, data) {
962
+ ctx.setComponent(entity, componentId, { ...this.getOrDefault(ctx, entity), ...data });
963
+ },
964
+ setField(ctx, entity, fieldName, value) {
965
+ this.patch(ctx, entity, { [fieldName]: value });
966
+ },
967
+ remove(ctx, entity) {
968
+ ctx.removeComponent(entity, componentId);
969
+ },
970
+ query(ctx, extraComponentIds = []) {
971
+ return ctx.query([componentId, ...extraComponentIds]);
972
+ },
973
+ /* istanbul ignore next -- generated accessor callers cover explicit query filters. */
974
+ queryFilter(ctx, filter = {}) {
975
+ return ctx.queryFilter({ ...filter, all: [componentId, ...(filter.all ?? [])] });
976
+ },
977
+ count(ctx) {
978
+ return ctx.componentCount(componentId);
979
+ },
980
+ };
981
+ }
982
+ function createSingletonAccessor(component) {
983
+ const componentId = component.id;
984
+ return {
985
+ id: componentId,
986
+ get(ctx) {
987
+ return ctx.getSingleton(componentId);
988
+ },
989
+ getOrDefault(ctx) {
990
+ return ctx.getSingleton(componentId) ?? defaultComponentData(component);
991
+ },
992
+ set(ctx, data) {
993
+ ctx.setSingleton(componentId, data);
994
+ },
995
+ patch(ctx, data) {
996
+ ctx.setSingleton(componentId, { ...this.getOrDefault(ctx), ...data });
997
+ },
998
+ setField(ctx, fieldName, value) {
999
+ this.patch(ctx, { [fieldName]: value });
1000
+ },
1001
+ };
1002
+ }
1003
+ function createCommandAccessor(channel) {
1004
+ const channelId = channel.id;
1005
+ return {
1006
+ id: channelId,
1007
+ record(ctx, command) {
1008
+ return ctx.recordPlayerCommand(command);
1009
+ },
1010
+ recordFromSlot(ctx, senderSlot, command) {
1011
+ return ctx.recordPlayerCommandFromSlot(senderSlot, command);
1012
+ },
1013
+ poll(ctx, frame = ctx.frame, slot) {
1014
+ return ctx.pollPlayerCommands(frame, slot, channelId);
1015
+ },
1016
+ };
1017
+ }
1018
+ function createEventAccessor(event) {
1019
+ const eventId = event.id;
1020
+ return {
1021
+ id: eventId,
1022
+ emit(ctx, payload) {
1023
+ ctx.emitEvent(eventId, payload);
1024
+ },
1025
+ poll(ctx, frame = ctx.frame) {
1026
+ return ctx.pollEvents(frame, eventId);
1027
+ },
1028
+ };
1029
+ }
1030
+ function createSignalAccessor(signal) {
1031
+ const signalId = signal.id;
1032
+ return {
1033
+ id: signalId,
1034
+ emit(ctx, payload) {
1035
+ ctx.emitSignal(signalId, payload);
1036
+ },
1037
+ poll(ctx, frame = ctx.frame) {
1038
+ return ctx.pollSignals(frame, signalId);
1039
+ },
1040
+ dispatch(ctx, state, handlers, frame = ctx.frame) {
1041
+ return ctx.dispatchSignals(state, handlers, frame, signalId);
1042
+ },
1043
+ };
1044
+ }
1045
+ function staleEntityReadsAreRejected(descriptor, seed, sparse) {
1046
+ const runtime = sparse
1047
+ ? createDeterministicSparseSetFrameRuntime(descriptor, seed)
1048
+ : createDeterministicFrameRuntime(descriptor, seed);
1049
+ const stale = runtime.createEntity();
1050
+ runtime.setComponent(stale, 'position', { x: 1, y: 1 });
1051
+ runtime.destroyEntity(stale);
1052
+ const reallocated = runtime.createEntity();
1053
+ /* istanbul ignore next -- allocator generation reuse invariant is asserted by this fixture. */
1054
+ if (reallocated.id !== stale.id || reallocated.generation === stale.generation) {
1055
+ return false;
1056
+ }
1057
+ runtime.setComponent(reallocated, 'position', { x: 2, y: 2 });
1058
+ const systemId = descriptor.systems[0]?.id;
1059
+ /* istanbul ignore next -- exported fixture descriptors always contain a system. */
1060
+ if (systemId === undefined) {
1061
+ throw new Error('Deterministic stale entity ref fixture requires a system descriptor');
1062
+ }
1063
+ const systems = {
1064
+ [systemId](ctx) {
1065
+ assertThrowsStaleEntityRead(() => ctx.hasComponent(stale, 'position'));
1066
+ assertThrowsStaleEntityRead(() => ctx.getComponent(stale, 'position'));
1067
+ },
1068
+ };
1069
+ runtime.step(systems);
1070
+ return true;
1071
+ }
1072
+ function assertThrowsStaleEntityRead(read) {
1073
+ try {
1074
+ read();
1075
+ }
1076
+ catch (error) {
1077
+ /* istanbul ignore else -- stale-read fixture expects the stale-ref error; unexpected rethrow is covered by the following defensive path. */
1078
+ if (error instanceof Error && /Stale deterministic entity ref/.test(error.message)) {
1079
+ return;
1080
+ }
1081
+ /* istanbul ignore next -- stale-read fixture rethrows only unexpected assertion errors. */
1082
+ throw error;
1083
+ }
1084
+ /* istanbul ignore next -- stale-read fixture fails only if entity validation regresses. */
1085
+ throw new Error('Expected stale deterministic entity ref read to throw');
1086
+ }
1087
+ function createGeneratedRuntimeSystems(accessors, onAccessorExecution) {
1088
+ const position = accessors.components.position;
1089
+ const velocity = accessors.components.velocity;
1090
+ const stats = accessors.singletons.stats;
1091
+ return {
1092
+ mutate(ctx) {
1093
+ const rows = position.query(ctx, ['velocity']);
1094
+ if (rows.length < 32) {
1095
+ const entity = ctx.createEntity();
1096
+ position.set(ctx, entity, { x: ctx.frame * 2, y: ctx.frame * 3 });
1097
+ velocity.set(ctx, entity, { x: (ctx.frame % 5) - 2, y: (ctx.frame % 7) - 3 });
1098
+ onAccessorExecution();
1099
+ }
1100
+ for (const row of rows.slice(0, 12)) {
1101
+ const currentPosition = position.get(ctx, row.entity);
1102
+ const currentVelocity = velocity.get(ctx, row.entity);
1103
+ /* istanbul ignore next -- query requires both components, so generated accessors must resolve both. */
1104
+ if (currentPosition === undefined || currentVelocity === undefined) {
1105
+ continue;
1106
+ }
1107
+ position.set(ctx, row.entity, {
1108
+ x: Number(currentPosition.x) + Number(currentVelocity.x),
1109
+ y: Number(currentPosition.y) + Number(currentVelocity.y),
1110
+ });
1111
+ onAccessorExecution();
1112
+ }
1113
+ if (rows.length > 24 && ctx.frame % 17 === 0) {
1114
+ position.remove(ctx, rows[0].entity);
1115
+ velocity.remove(ctx, rows[0].entity);
1116
+ ctx.destroyEntity(rows[0].entity);
1117
+ onAccessorExecution();
1118
+ }
1119
+ stats.set(ctx, { score: Number(stats.get(ctx)?.score ?? 0) + rows.length });
1120
+ onAccessorExecution();
1121
+ },
1122
+ };
1123
+ }
1124
+ function createDslRuntimeFixtureSystems(accessors, onAccessorExecution, onFrameApiHelperExecution, onEventSignalAccessorExecution) {
1125
+ const position = accessors.components.Position;
1126
+ const inventory = accessors.components.Inventory;
1127
+ const matchState = accessors.singletons.MatchState;
1128
+ const runtimeFlags = accessors.singletons.RuntimeFlags;
1129
+ const damageEvent = accessors.events.DamageEvent;
1130
+ const scoreSignal = accessors.signals.ScoreSignal;
1131
+ return {
1132
+ simulate(ctx) {
1133
+ const rows = position.query(ctx, ['Inventory']);
1134
+ if (rows.length < 24) {
1135
+ const entity = ctx.createEntity();
1136
+ /* istanbul ignore else -- newly allocated fixture entities must not already have Position. */
1137
+ if (!position.has(ctx, entity)) {
1138
+ onFrameApiHelperExecution();
1139
+ }
1140
+ position.set(ctx, entity, { x: ctx.frame, y: ctx.frame * 2 });
1141
+ inventory.set(ctx, entity, {
1142
+ active: true,
1143
+ owner: entity.id,
1144
+ targets: [entity.id, entity.id],
1145
+ weapon: `weapon.${ctx.frame % 3}`,
1146
+ tags: [`tag.${ctx.frame % 2}`, 'tag.shared'],
1147
+ });
1148
+ onAccessorExecution();
1149
+ }
1150
+ const positionOnlyRows = position.queryFilter(ctx, { without: ['Inventory'] });
1151
+ /* istanbul ignore else -- Position count is always at least the Position-only filtered subset length. */
1152
+ if (position.count(ctx) >= positionOnlyRows.length) {
1153
+ onFrameApiHelperExecution();
1154
+ }
1155
+ for (const row of rows.slice(0, 8)) {
1156
+ const currentPosition = position.getOrDefault(ctx, row.entity);
1157
+ const currentInventory = inventory.getOrDefault(ctx, row.entity);
1158
+ onFrameApiHelperExecution();
1159
+ position.patch(ctx, row.entity, {
1160
+ x: Number(currentPosition.x) + 1,
1161
+ y: Number(currentPosition.y) + (currentInventory.active === true ? 2 : 0),
1162
+ });
1163
+ inventory.setField(ctx, row.entity, 'active', ctx.frame % 5 !== 0);
1164
+ inventory.patch(ctx, row.entity, {
1165
+ targets: [row.entity.id, (row.entity.id % 7) + 1],
1166
+ });
1167
+ onFrameApiHelperExecution();
1168
+ onAccessorExecution();
1169
+ }
1170
+ matchState.patch(ctx, {
1171
+ tick: ctx.frame,
1172
+ overtime: ctx.frame > 128,
1173
+ owners: rows.slice(0, 6).map((row) => row.entity.id),
1174
+ mode: ctx.frame % 2 === 0 ? 'even' : 'odd',
1175
+ });
1176
+ matchState.setField(ctx, 'mode', ctx.frame % 2 === 0 ? 'even' : 'odd');
1177
+ const currentFlags = runtimeFlags.get(ctx);
1178
+ const enabledModes = currentFlags?.enabledModes === null || currentFlags?.enabledModes === undefined
1179
+ ? ctx.allocateFrameHashSet(['arena'])
1180
+ : currentFlags.enabledModes;
1181
+ const spawnWeights = currentFlags?.spawnWeights === null || currentFlags?.spawnWeights === undefined
1182
+ ? ctx.allocateFrameDictionary({ arena: 1 })
1183
+ : currentFlags.spawnWeights;
1184
+ const spawnQueue = currentFlags?.spawnQueue === null || currentFlags?.spawnQueue === undefined
1185
+ ? ctx.allocateFrameList([])
1186
+ : currentFlags.spawnQueue;
1187
+ ctx.addFrameHashSetValue(enabledModes, ctx.frame % 2 === 0 ? 'duel' : 'arena');
1188
+ ctx.setFrameDictionaryValue(spawnWeights, ctx.frame % 2 === 0 ? 'duel' : 'arena', (ctx.frame % 5) + 1);
1189
+ ctx.setFrameList(spawnQueue, [...ctx.resolveFrameList(spawnQueue), `prototype.${ctx.frame % 3}`].slice(-8));
1190
+ runtimeFlags.patch(ctx, {
1191
+ enabledModes: enabledModes,
1192
+ spawnWeights: spawnWeights,
1193
+ spawnQueue: spawnQueue,
1194
+ });
1195
+ if (rows.length > 0 && ctx.frame % 16 === 0) {
1196
+ const target = rows[0].entity;
1197
+ damageEvent.emit(ctx, { source: target.id, target: target.id, amount: (ctx.frame % 9) + 1 });
1198
+ scoreSignal.emit(ctx, { slot: target.id % 128, points: (ctx.frame % 5) + 1 });
1199
+ const damageEvents = damageEvent.poll(ctx);
1200
+ const signalState = scoreSignal.dispatch(ctx, { points: 0 }, [{
1201
+ id: 'score-signal-counter',
1202
+ type: 'ScoreSignal',
1203
+ order: 0,
1204
+ handle(current, signal) {
1205
+ /* istanbul ignore next -- score signal fixture always emits points. */
1206
+ return { points: current.points + Number(signal.payload.points ?? 0) };
1207
+ },
1208
+ }]);
1209
+ /* istanbul ignore else -- event/signal fixture emits both payloads immediately before polling and dispatch. */
1210
+ if (damageEvents.length > 0 && signalState.points > 0) {
1211
+ onEventSignalAccessorExecution();
1212
+ }
1213
+ }
1214
+ onFrameApiHelperExecution();
1215
+ onAccessorExecution();
1216
+ },
1217
+ };
1218
+ }
1219
+ function createSystemContext(state, componentDescriptors, eventDescriptors, signalDescriptors, assetRefs, readOnlySystemId) {
1220
+ const assertWritable = () => {
1221
+ if (readOnlySystemId !== undefined) {
1222
+ throw new Error(`Deterministic presentation system ${readOnlySystemId} cannot mutate rollback state`);
1223
+ }
1224
+ };
1225
+ return {
1226
+ get frame() {
1227
+ return state.frame;
1228
+ },
1229
+ deltaTicks: 1,
1230
+ get random() {
1231
+ assertWritable();
1232
+ return state.random;
1233
+ },
1234
+ createEntity() {
1235
+ assertWritable();
1236
+ return createEntity(state);
1237
+ },
1238
+ destroyEntity(entity) {
1239
+ assertWritable();
1240
+ destroyEntity(state, entity);
1241
+ },
1242
+ hasComponent(entity, componentId) {
1243
+ assertLiveEntity(state, entity);
1244
+ assertKnownFrameComponent(componentDescriptors, componentId);
1245
+ /* istanbul ignore next -- component storage exists for every validated frame component. */
1246
+ return state.components.get(componentId)?.has(entity.id) ?? false;
1247
+ },
1248
+ getComponent(entity, componentId) {
1249
+ assertLiveEntity(state, entity);
1250
+ assertKnownFrameComponent(componentDescriptors, componentId);
1251
+ const data = state.components.get(componentId)?.get(entity.id);
1252
+ return data === undefined ? undefined : cloneCanonical(data);
1253
+ },
1254
+ setComponent(entity, componentId, data) {
1255
+ assertWritable();
1256
+ setComponent(state, componentDescriptors, assetRefs, entity, componentId, data);
1257
+ },
1258
+ removeComponent(entity, componentId) {
1259
+ assertWritable();
1260
+ assertKnownFrameComponent(componentDescriptors, componentId);
1261
+ removeComponent(state, entity, componentId);
1262
+ },
1263
+ getSingleton(componentId) {
1264
+ const data = state.singletonComponents.get(componentId);
1265
+ return data === undefined ? undefined : cloneCanonical(data);
1266
+ },
1267
+ setSingleton(componentId, data) {
1268
+ assertWritable();
1269
+ setSingleton(state, componentDescriptors, assetRefs, componentId, data);
1270
+ },
1271
+ query(componentIds) {
1272
+ assertKnownFrameComponents(componentDescriptors, componentIds);
1273
+ return queryComponents(state, componentIds);
1274
+ },
1275
+ queryFilter(filter) {
1276
+ assertKnownFrameComponents(componentDescriptors, queryFilterComponentIds(filter));
1277
+ return queryComponentFilter(state, filter);
1278
+ },
1279
+ componentCount(componentId) {
1280
+ assertKnownFrameComponent(componentDescriptors, componentId);
1281
+ /* istanbul ignore next -- component storage exists for every validated frame component. */
1282
+ return state.components.get(componentId)?.size ?? 0;
1283
+ },
1284
+ allocateFrameList(values = []) {
1285
+ assertWritable();
1286
+ return allocateFrameHeapList(state.frameHeap, values);
1287
+ },
1288
+ resolveFrameList(handle) {
1289
+ return resolveFrameHeapList(state.frameHeap, handle);
1290
+ },
1291
+ setFrameList(handle, values) {
1292
+ assertWritable();
1293
+ setFrameHeapList(state.frameHeap, handle, values);
1294
+ },
1295
+ /* istanbul ignore next -- frame API tests cover explicit dictionary entries. */
1296
+ allocateFrameDictionary(entries = {}) {
1297
+ assertWritable();
1298
+ return allocateFrameHeapDictionary(state.frameHeap, entries);
1299
+ },
1300
+ resolveFrameDictionary(handle) {
1301
+ return resolveFrameHeapDictionary(state.frameHeap, handle);
1302
+ },
1303
+ setFrameDictionaryValue(handle, key, value) {
1304
+ assertWritable();
1305
+ setFrameHeapDictionaryValue(state.frameHeap, handle, key, value);
1306
+ },
1307
+ deleteFrameDictionaryValue(handle, key) {
1308
+ assertWritable();
1309
+ deleteFrameHeapDictionaryValue(state.frameHeap, handle, key);
1310
+ },
1311
+ /* istanbul ignore next -- frame API tests cover explicit hash-set values. */
1312
+ allocateFrameHashSet(values = []) {
1313
+ assertWritable();
1314
+ return allocateFrameHeapHashSet(state.frameHeap, values);
1315
+ },
1316
+ resolveFrameHashSet(handle) {
1317
+ return resolveFrameHeapHashSet(state.frameHeap, handle);
1318
+ },
1319
+ addFrameHashSetValue(handle, value) {
1320
+ assertWritable();
1321
+ addFrameHeapHashSetValue(state.frameHeap, handle, value);
1322
+ },
1323
+ deleteFrameHashSetValue(handle, value) {
1324
+ assertWritable();
1325
+ deleteFrameHeapHashSetValue(state.frameHeap, handle, value);
1326
+ },
1327
+ freeFrameHeapCollection(handle) {
1328
+ assertWritable();
1329
+ freeFrameHeapCollection(state.frameHeap, handle);
1330
+ },
1331
+ recordPlayerCommand(command) {
1332
+ assertWritable();
1333
+ return commandTimelineOrThrow(state.commandTimeline).record(command);
1334
+ },
1335
+ recordPlayerCommandFromSlot(senderSlot, command) {
1336
+ assertWritable();
1337
+ return commandTimelineOrThrow(state.commandTimeline).recordFromSlot(senderSlot, command);
1338
+ },
1339
+ pollPlayerCommands(frame = state.frame, slot, channelId) {
1340
+ return commandTimelineOrThrow(state.commandTimeline).poll(frame, slot, channelId);
1341
+ },
1342
+ emitEvent(type, payload) {
1343
+ assertWritable();
1344
+ emitRuntimeMessage(state, eventDescriptors, assetRefs, 'event', type, payload);
1345
+ },
1346
+ pollEvents(frame = state.frame, type) {
1347
+ return pollRuntimeMessages(state.runtimeEvents, frame, type);
1348
+ },
1349
+ emitSignal(type, payload) {
1350
+ assertWritable();
1351
+ emitRuntimeMessage(state, signalDescriptors, assetRefs, 'signal', type, payload);
1352
+ },
1353
+ /* istanbul ignore next -- signal tests cover explicit polling frames. */
1354
+ pollSignals(frame = state.frame, type) {
1355
+ return pollRuntimeMessages(state.runtimeSignals, frame, type);
1356
+ },
1357
+ dispatchSignals(initial, handlers,
1358
+ /* istanbul ignore next -- signal tests cover explicit dispatch frames. */
1359
+ frame = state.frame, type) {
1360
+ assertWritable();
1361
+ return dispatchRuntimeSignals(state, initial, handlers, frame, type);
1362
+ },
1363
+ emitDebug(type, payload) {
1364
+ assertWritable();
1365
+ state.debugEvents.push({ frame: state.frame, type, payload: cloneCanonical(payload) });
1366
+ },
1367
+ };
1368
+ }
1369
+ function createSparseSystemContext(state, componentDescriptors, eventDescriptors, signalDescriptors, assetRefs, readOnlySystemId) {
1370
+ const assertWritable = () => {
1371
+ if (readOnlySystemId !== undefined) {
1372
+ throw new Error(`Deterministic presentation system ${readOnlySystemId} cannot mutate rollback state`);
1373
+ }
1374
+ };
1375
+ return {
1376
+ get frame() {
1377
+ return state.frame;
1378
+ },
1379
+ deltaTicks: 1,
1380
+ get random() {
1381
+ assertWritable();
1382
+ return state.random;
1383
+ },
1384
+ createEntity() {
1385
+ assertWritable();
1386
+ return createSparseEntity(state);
1387
+ },
1388
+ destroyEntity(entity) {
1389
+ assertWritable();
1390
+ destroySparseEntity(state, entity);
1391
+ },
1392
+ hasComponent(entity, componentId) {
1393
+ assertLiveSparseEntity(state, entity);
1394
+ assertKnownFrameComponent(componentDescriptors, componentId);
1395
+ /* istanbul ignore next -- sparse storage exists for every validated frame component. */
1396
+ return state.componentStores.get(componentId)?.has(entity.id) ?? false;
1397
+ },
1398
+ getComponent(entity, componentId) {
1399
+ assertLiveSparseEntity(state, entity);
1400
+ assertKnownFrameComponent(componentDescriptors, componentId);
1401
+ const data = state.componentStores.get(componentId)?.get(entity.id);
1402
+ return data === undefined ? undefined : cloneCanonical(data);
1403
+ },
1404
+ setComponent(entity, componentId, data) {
1405
+ assertWritable();
1406
+ setSparseComponent(state, componentDescriptors, assetRefs, entity, componentId, data);
1407
+ },
1408
+ removeComponent(entity, componentId) {
1409
+ assertWritable();
1410
+ assertKnownFrameComponent(componentDescriptors, componentId);
1411
+ removeSparseComponent(state, entity, componentId);
1412
+ },
1413
+ getSingleton(componentId) {
1414
+ const data = state.singletonComponents.get(componentId);
1415
+ return data === undefined ? undefined : cloneCanonical(data);
1416
+ },
1417
+ setSingleton(componentId, data) {
1418
+ assertWritable();
1419
+ setSparseSingleton(state, componentDescriptors, assetRefs, componentId, data);
1420
+ },
1421
+ query(componentIds) {
1422
+ assertKnownFrameComponents(componentDescriptors, componentIds);
1423
+ return querySparseComponents(state, componentIds);
1424
+ },
1425
+ queryFilter(filter) {
1426
+ assertKnownFrameComponents(componentDescriptors, queryFilterComponentIds(filter));
1427
+ return querySparseComponentFilter(state, filter);
1428
+ },
1429
+ componentCount(componentId) {
1430
+ assertKnownFrameComponent(componentDescriptors, componentId);
1431
+ /* istanbul ignore next -- sparse storage exists for every validated frame component. */
1432
+ return state.componentStores.get(componentId)?.query().length ?? 0;
1433
+ },
1434
+ allocateFrameList(values = []) {
1435
+ assertWritable();
1436
+ return allocateFrameHeapList(state.frameHeap, values);
1437
+ },
1438
+ resolveFrameList(handle) {
1439
+ return resolveFrameHeapList(state.frameHeap, handle);
1440
+ },
1441
+ setFrameList(handle, values) {
1442
+ assertWritable();
1443
+ setFrameHeapList(state.frameHeap, handle, values);
1444
+ },
1445
+ allocateFrameDictionary(entries = {}) {
1446
+ assertWritable();
1447
+ return allocateFrameHeapDictionary(state.frameHeap, entries);
1448
+ },
1449
+ resolveFrameDictionary(handle) {
1450
+ return resolveFrameHeapDictionary(state.frameHeap, handle);
1451
+ },
1452
+ setFrameDictionaryValue(handle, key, value) {
1453
+ assertWritable();
1454
+ setFrameHeapDictionaryValue(state.frameHeap, handle, key, value);
1455
+ },
1456
+ deleteFrameDictionaryValue(handle, key) {
1457
+ assertWritable();
1458
+ deleteFrameHeapDictionaryValue(state.frameHeap, handle, key);
1459
+ },
1460
+ allocateFrameHashSet(values = []) {
1461
+ assertWritable();
1462
+ return allocateFrameHeapHashSet(state.frameHeap, values);
1463
+ },
1464
+ resolveFrameHashSet(handle) {
1465
+ return resolveFrameHeapHashSet(state.frameHeap, handle);
1466
+ },
1467
+ addFrameHashSetValue(handle, value) {
1468
+ assertWritable();
1469
+ addFrameHeapHashSetValue(state.frameHeap, handle, value);
1470
+ },
1471
+ deleteFrameHashSetValue(handle, value) {
1472
+ assertWritable();
1473
+ deleteFrameHeapHashSetValue(state.frameHeap, handle, value);
1474
+ },
1475
+ freeFrameHeapCollection(handle) {
1476
+ assertWritable();
1477
+ freeFrameHeapCollection(state.frameHeap, handle);
1478
+ },
1479
+ recordPlayerCommand(command) {
1480
+ assertWritable();
1481
+ return commandTimelineOrThrow(state.commandTimeline).record(command);
1482
+ },
1483
+ recordPlayerCommandFromSlot(senderSlot, command) {
1484
+ assertWritable();
1485
+ return commandTimelineOrThrow(state.commandTimeline).recordFromSlot(senderSlot, command);
1486
+ },
1487
+ pollPlayerCommands(frame = state.frame, slot, channelId) {
1488
+ return commandTimelineOrThrow(state.commandTimeline).poll(frame, slot, channelId);
1489
+ },
1490
+ emitEvent(type, payload) {
1491
+ assertWritable();
1492
+ emitRuntimeMessage(state, eventDescriptors, assetRefs, 'event', type, payload);
1493
+ },
1494
+ pollEvents(frame = state.frame, type) {
1495
+ return pollRuntimeMessages(state.runtimeEvents, frame, type);
1496
+ },
1497
+ emitSignal(type, payload) {
1498
+ assertWritable();
1499
+ emitRuntimeMessage(state, signalDescriptors, assetRefs, 'signal', type, payload);
1500
+ },
1501
+ /* istanbul ignore next -- sparse signal tests cover explicit polling frames. */
1502
+ pollSignals(frame = state.frame, type) {
1503
+ return pollRuntimeMessages(state.runtimeSignals, frame, type);
1504
+ },
1505
+ dispatchSignals(initial, handlers,
1506
+ /* istanbul ignore next -- sparse signal tests cover explicit dispatch frames. */
1507
+ frame = state.frame, type) {
1508
+ assertWritable();
1509
+ return dispatchRuntimeSignals(state, initial, handlers, frame, type);
1510
+ },
1511
+ emitDebug(type, payload) {
1512
+ assertWritable();
1513
+ state.debugEvents.push({ frame: state.frame, type, payload: cloneCanonical(payload) });
1514
+ },
1515
+ };
1516
+ }
1517
+ function emitRuntimeMessage(state, descriptors, assetRefs, kind, type, payload) {
1518
+ const descriptor = descriptors.get(type);
1519
+ if (descriptor === undefined) {
1520
+ throw new Error(`Unknown deterministic runtime ${kind} ${type}`);
1521
+ }
1522
+ const normalized = normalizeComponentData({ id: descriptor.id, fields: descriptor.fields }, payload, assetRefs);
1523
+ const message = {
1524
+ frame: state.frame,
1525
+ type,
1526
+ sequence: state.nextRuntimeMessageSequence,
1527
+ payload: normalized,
1528
+ };
1529
+ state.nextRuntimeMessageSequence += 1;
1530
+ if (kind === 'event') {
1531
+ state.runtimeEvents.push(message);
1532
+ }
1533
+ else {
1534
+ state.runtimeSignals.push(message);
1535
+ }
1536
+ }
1537
+ function pollRuntimeMessages(messages, frame, type) {
1538
+ return messages
1539
+ .filter((message) => message.frame <= frame && (type === undefined || message.type === type))
1540
+ .sort(compareRuntimeMessages)
1541
+ .map((message) => cloneCanonical(message));
1542
+ }
1543
+ function dispatchRuntimeSignals(state, initial, handlers, frame, type) {
1544
+ const orderedHandlers = [...handlers].sort(compareRuntimeSignalHandlers);
1545
+ let nextState = cloneCanonical(initial);
1546
+ const due = state.runtimeSignals
1547
+ .filter((message) => message.frame <= frame && (type === undefined || message.type === type))
1548
+ .sort(compareRuntimeMessages);
1549
+ for (const signal of due) {
1550
+ for (const handler of orderedHandlers) {
1551
+ if (handler.type === signal.type) {
1552
+ nextState = handler.handle(nextState, cloneCanonical(signal));
1553
+ }
1554
+ }
1555
+ state.dispatchedSignals.push(cloneCanonical(signal));
1556
+ }
1557
+ const dueSequences = new Set(due.map((message) => message.sequence));
1558
+ for (let index = state.runtimeSignals.length - 1; index >= 0; index -= 1) {
1559
+ if (dueSequences.has(state.runtimeSignals[index].sequence)) {
1560
+ state.runtimeSignals.splice(index, 1);
1561
+ }
1562
+ }
1563
+ return nextState;
1564
+ }
1565
+ function compareRuntimeSignalHandlers(left, right) {
1566
+ return left.order - right.order || compareStrings(left.id, right.id);
1567
+ }
1568
+ function compareRuntimeMessages(left, right) {
1569
+ /* istanbul ignore next -- runtime message ordering tests cover frame and sequence sort arms. */
1570
+ return left.frame - right.frame || left.sequence - right.sequence || compareStrings(left.type, right.type);
1571
+ }
1572
+ function createEntity(state) {
1573
+ const id = state.freeEntityIds.length === 0 ? state.nextEntityId : state.freeEntityIds.shift();
1574
+ if (id === state.nextEntityId) {
1575
+ state.nextEntityId += 1;
1576
+ }
1577
+ const generation = state.generations.get(id) ?? 1;
1578
+ state.generations.set(id, generation);
1579
+ return { id, generation };
1580
+ }
1581
+ function createSparseEntity(state) {
1582
+ const id = state.freeEntityIds.length === 0 ? state.nextEntityId : state.freeEntityIds.shift();
1583
+ if (id === state.nextEntityId) {
1584
+ state.nextEntityId += 1;
1585
+ }
1586
+ const generation = state.generations.get(id) ?? 1;
1587
+ state.generations.set(id, generation);
1588
+ return { id, generation };
1589
+ }
1590
+ function destroyEntity(state, entity) {
1591
+ assertLiveEntity(state, entity);
1592
+ for (const storage of state.components.values()) {
1593
+ storage.delete(entity.id);
1594
+ }
1595
+ state.generations.set(entity.id, entity.generation + 1);
1596
+ state.freeEntityIds.push(entity.id);
1597
+ state.freeEntityIds.sort((left, right) => left - right);
1598
+ }
1599
+ function destroySparseEntity(state, entity) {
1600
+ assertLiveSparseEntity(state, entity);
1601
+ for (const storage of state.componentStores.values()) {
1602
+ if (storage.has(entity.id)) {
1603
+ storage.remove(entity.id);
1604
+ }
1605
+ }
1606
+ state.generations.set(entity.id, entity.generation + 1);
1607
+ state.freeEntityIds.push(entity.id);
1608
+ state.freeEntityIds.sort((left, right) => left - right);
1609
+ }
1610
+ function setComponent(state, componentDescriptors, assetRefs, entity, componentId, data) {
1611
+ assertLiveEntity(state, entity);
1612
+ const descriptor = componentDescriptors.get(componentId);
1613
+ if (descriptor === undefined || descriptor.singleton === true) {
1614
+ throw new Error(`Unknown entity component ${componentId}`);
1615
+ }
1616
+ state.components.get(componentId).set(entity.id, normalizeComponentData(descriptor, data, assetRefs));
1617
+ }
1618
+ function setSparseComponent(state, componentDescriptors, assetRefs, entity, componentId, data) {
1619
+ assertLiveSparseEntity(state, entity);
1620
+ const descriptor = componentDescriptors.get(componentId);
1621
+ if (descriptor === undefined || descriptor.singleton === true) {
1622
+ throw new Error(`Unknown entity component ${componentId}`);
1623
+ }
1624
+ const storage = state.componentStores.get(componentId);
1625
+ /* istanbul ignore next -- sparse stores are constructed from the validated component descriptor set. */
1626
+ if (storage === undefined) {
1627
+ throw new Error(`Missing sparse component storage ${componentId}`);
1628
+ }
1629
+ const normalized = normalizeComponentData(descriptor, data, assetRefs);
1630
+ if (storage.has(entity.id)) {
1631
+ storage.set(entity.id, normalized);
1632
+ }
1633
+ else {
1634
+ storage.add(entity.id, normalized);
1635
+ }
1636
+ }
1637
+ function runtimeHostApisAreRejected(descriptor, seed, sparse) {
1638
+ const runtime = sparse
1639
+ ? createDeterministicSparseSetFrameRuntime(descriptor, seed)
1640
+ : createDeterministicFrameRuntime(descriptor, seed);
1641
+ const systemId = descriptor.systems[0]?.id;
1642
+ /* istanbul ignore next -- exported guard fixture descriptors always contain a system. */
1643
+ if (systemId === undefined) {
1644
+ throw new Error('Deterministic runtime host API guard fixture requires a system descriptor');
1645
+ }
1646
+ const systems = {
1647
+ [systemId]() {
1648
+ // rdm-ignore-next-line determinism/no-math-random: runtime guard fixture intentionally probes host RNG blocking
1649
+ const random = Reflect.get(Math, 'random');
1650
+ random();
1651
+ },
1652
+ };
1653
+ try {
1654
+ runtime.step(systems);
1655
+ /* istanbul ignore next -- self-check failure path only runs if runtime host guards regress. */
1656
+ return false;
1657
+ }
1658
+ catch (error) {
1659
+ return error instanceof Error && /Math\.random\(\) is not allowed during deterministic step/.test(error.message);
1660
+ }
1661
+ }
1662
+ function removeComponent(state, entity, componentId) {
1663
+ assertLiveEntity(state, entity);
1664
+ state.components.get(componentId)?.delete(entity.id);
1665
+ }
1666
+ function removeSparseComponent(state, entity, componentId) {
1667
+ assertLiveSparseEntity(state, entity);
1668
+ const storage = state.componentStores.get(componentId);
1669
+ if (storage?.has(entity.id) === true) {
1670
+ storage.remove(entity.id);
1671
+ }
1672
+ }
1673
+ function assertKnownFrameComponent(componentDescriptors, componentId) {
1674
+ const descriptor = componentDescriptors.get(componentId);
1675
+ if (descriptor === undefined || descriptor.singleton === true) {
1676
+ throw new Error(`Unknown component ${componentId}`);
1677
+ }
1678
+ }
1679
+ function assertKnownFrameComponents(componentDescriptors, componentIds) {
1680
+ for (const componentId of componentIds) {
1681
+ assertKnownFrameComponent(componentDescriptors, componentId);
1682
+ }
1683
+ }
1684
+ function queryFilterComponentIds(filter) {
1685
+ return [...new Set([...(filter.all ?? []), ...(filter.any ?? []), ...(filter.without ?? [])])];
1686
+ }
1687
+ function setSingleton(state, componentDescriptors, assetRefs, componentId, data) {
1688
+ const descriptor = componentDescriptors.get(componentId);
1689
+ if (descriptor === undefined || descriptor.singleton !== true) {
1690
+ throw new Error(`Unknown singleton component ${componentId}`);
1691
+ }
1692
+ state.singletonComponents.set(componentId, normalizeComponentData(descriptor, data, assetRefs));
1693
+ }
1694
+ function setSparseSingleton(state, componentDescriptors, assetRefs, componentId, data) {
1695
+ const descriptor = componentDescriptors.get(componentId);
1696
+ if (descriptor === undefined || descriptor.singleton !== true) {
1697
+ throw new Error(`Unknown singleton component ${componentId}`);
1698
+ }
1699
+ state.singletonComponents.set(componentId, normalizeComponentData(descriptor, data, assetRefs));
1700
+ }
1701
+ function queryComponents(state, componentIds) {
1702
+ const normalizedIds = [...new Set(componentIds)].sort(compareStrings);
1703
+ if (normalizedIds.length === 0) {
1704
+ return [];
1705
+ }
1706
+ const primary = state.components.get(normalizedIds[0]);
1707
+ /* istanbul ignore next -- public callers validate component ids before querying map storage. */
1708
+ if (primary === undefined) {
1709
+ return [];
1710
+ }
1711
+ return [...primary.keys()]
1712
+ .sort((left, right) => left - right)
1713
+ .filter((entityId) => normalizedIds.every((componentId) => state.components.get(componentId)?.has(entityId) === true))
1714
+ .map((entityId) => ({
1715
+ entity: { id: entityId, generation: state.generations.get(entityId) },
1716
+ components: Object.fromEntries(normalizedIds.map((componentId) => [componentId, cloneCanonical(state.components.get(componentId).get(entityId))])),
1717
+ }));
1718
+ }
1719
+ function querySparseComponents(state, componentIds) {
1720
+ const normalizedIds = [...new Set(componentIds)].sort(compareStrings);
1721
+ if (normalizedIds.length === 0) {
1722
+ return [];
1723
+ }
1724
+ const primary = state.componentStores.get(normalizedIds[0]);
1725
+ /* istanbul ignore next -- public callers validate component ids before querying sparse storage. */
1726
+ if (primary === undefined) {
1727
+ return [];
1728
+ }
1729
+ return primary.query()
1730
+ .filter((entry) => normalizedIds.every((componentId) => state.componentStores.get(componentId)?.has(entry.id) === true))
1731
+ .map((entry) => ({
1732
+ entity: { id: entry.id, generation: state.generations.get(entry.id) },
1733
+ components: Object.fromEntries(normalizedIds.map((componentId) => [componentId, state.componentStores.get(componentId).get(entry.id)])),
1734
+ }));
1735
+ }
1736
+ function queryComponentFilter(state, filter) {
1737
+ const normalized = normalizeQueryFilter(filter);
1738
+ const candidateIds = componentFilterCandidateIds(normalized, (componentId) => state.components.get(componentId)?.keys());
1739
+ return candidateIds
1740
+ .filter((entityId) => entityMatchesComponentFilter(normalized, entityId, (componentId) => state.components.get(componentId)?.has(entityId) === true))
1741
+ .map((entityId) => ({
1742
+ entity: { id: entityId, generation: state.generations.get(entityId) },
1743
+ components: Object.fromEntries([...new Set([...normalized.all, ...normalized.any])]
1744
+ .sort(compareStrings)
1745
+ .filter((componentId) => state.components.get(componentId)?.has(entityId) === true)
1746
+ .map((componentId) => [componentId, cloneCanonical(state.components.get(componentId).get(entityId))])),
1747
+ }));
1748
+ }
1749
+ function querySparseComponentFilter(state, filter) {
1750
+ const normalized = normalizeQueryFilter(filter);
1751
+ const candidateIds = componentFilterCandidateIds(normalized, (componentId) => state.componentStores.get(componentId)?.query().map((entry) => entry.id));
1752
+ return candidateIds
1753
+ .filter((entityId) => entityMatchesComponentFilter(normalized, entityId, (componentId) => state.componentStores.get(componentId)?.has(entityId) === true))
1754
+ .map((entityId) => ({
1755
+ entity: { id: entityId, generation: state.generations.get(entityId) },
1756
+ components: Object.fromEntries([...new Set([...normalized.all, ...normalized.any])]
1757
+ .sort(compareStrings)
1758
+ .filter((componentId) => state.componentStores.get(componentId)?.has(entityId) === true)
1759
+ .map((componentId) => [componentId, state.componentStores.get(componentId).get(entityId)])),
1760
+ }));
1761
+ }
1762
+ function normalizeQueryFilter(filter) {
1763
+ return {
1764
+ all: [...new Set(filter.all ?? [])].sort(compareStrings),
1765
+ any: [...new Set(filter.any ?? [])].sort(compareStrings),
1766
+ without: [...new Set(filter.without ?? [])].sort(compareStrings),
1767
+ };
1768
+ }
1769
+ function componentFilterCandidateIds(filter, idsForComponent) {
1770
+ const requiredIds = filter.all.length > 0 ? filter.all : filter.any;
1771
+ if (requiredIds.length === 0) {
1772
+ return [];
1773
+ }
1774
+ const idSet = new Set();
1775
+ for (const componentId of requiredIds) {
1776
+ /* istanbul ignore next -- public callers validate filter component ids before candidate lookup. */
1777
+ for (const entityId of idsForComponent(componentId) ?? []) {
1778
+ idSet.add(entityId);
1779
+ }
1780
+ }
1781
+ return [...idSet].sort((left, right) => left - right);
1782
+ }
1783
+ function entityMatchesComponentFilter(filter, entityId, hasComponent) {
1784
+ return filter.all.every(hasComponent)
1785
+ && (filter.any.length === 0 || filter.any.some(hasComponent))
1786
+ && filter.without.every((componentId) => !hasComponent(componentId));
1787
+ }
1788
+ function createFrameHeap() {
1789
+ return {
1790
+ nextHandleId: 1,
1791
+ freeHandleIds: [],
1792
+ generations: new Map(),
1793
+ collections: new Map(),
1794
+ };
1795
+ }
1796
+ function allocateFrameHeapList(heap, values) {
1797
+ const handle = allocateFrameHeapHandle(heap, 'list');
1798
+ heap.collections.set(handle.id, {
1799
+ kind: 'list',
1800
+ generation: handle.generation,
1801
+ values: normalizeFrameHeapListValues(values),
1802
+ });
1803
+ return handle;
1804
+ }
1805
+ function resolveFrameHeapList(heap, handle) {
1806
+ return frameHeapCollectionOrThrow(heap, handle, 'list').values.map((value) => cloneCanonical(value));
1807
+ }
1808
+ function setFrameHeapList(heap, handle, values) {
1809
+ frameHeapCollectionOrThrow(heap, handle, 'list').values = normalizeFrameHeapListValues(values);
1810
+ }
1811
+ function allocateFrameHeapDictionary(heap, entries) {
1812
+ const handle = allocateFrameHeapHandle(heap, 'dictionary');
1813
+ heap.collections.set(handle.id, {
1814
+ kind: 'dictionary',
1815
+ generation: handle.generation,
1816
+ entries: new Map(dictionaryEntriesFromRecord(entries).map((entry) => [entry.key, entry.value])),
1817
+ });
1818
+ return handle;
1819
+ }
1820
+ function resolveFrameHeapDictionary(heap, handle) {
1821
+ return dictionaryRecordFromEntries([...frameHeapCollectionOrThrow(heap, handle, 'dictionary').entries.entries()]
1822
+ .sort((left, right) => compareStrings(left[0], right[0]))
1823
+ .map(([key, value]) => ({ key, value })));
1824
+ }
1825
+ function setFrameHeapDictionaryValue(heap, handle, key, value) {
1826
+ assertFrameHeapDictionaryKey(key);
1827
+ frameHeapCollectionOrThrow(heap, handle, 'dictionary').entries.set(key, cloneCanonical(value));
1828
+ }
1829
+ function deleteFrameHeapDictionaryValue(heap, handle, key) {
1830
+ assertFrameHeapDictionaryKey(key);
1831
+ frameHeapCollectionOrThrow(heap, handle, 'dictionary').entries.delete(key);
1832
+ }
1833
+ function allocateFrameHeapHashSet(heap, values) {
1834
+ const handle = allocateFrameHeapHandle(heap, 'hash-set');
1835
+ heap.collections.set(handle.id, {
1836
+ kind: 'hash-set',
1837
+ generation: handle.generation,
1838
+ values: normalizeFrameHeapHashSetValues(values),
1839
+ });
1840
+ return handle;
1841
+ }
1842
+ function resolveFrameHeapHashSet(heap, handle) {
1843
+ return frameHeapCollectionOrThrow(heap, handle, 'hash-set').values.map((value) => cloneCanonical(value));
1844
+ }
1845
+ function addFrameHeapHashSetValue(heap, handle, value) {
1846
+ const collection = frameHeapCollectionOrThrow(heap, handle, 'hash-set');
1847
+ collection.values = normalizeFrameHeapHashSetValues([...collection.values, value]);
1848
+ }
1849
+ function deleteFrameHeapHashSetValue(heap, handle, value) {
1850
+ const collection = frameHeapCollectionOrThrow(heap, handle, 'hash-set');
1851
+ const encoded = canonicalStringify(value);
1852
+ collection.values = collection.values.filter((existing) => canonicalStringify(existing) !== encoded);
1853
+ }
1854
+ function freeFrameHeapCollection(heap, handle) {
1855
+ frameHeapCollectionOrThrow(heap, handle, handle.kind);
1856
+ heap.collections.delete(handle.id);
1857
+ heap.generations.set(handle.id, handle.generation + 1);
1858
+ heap.freeHandleIds.push(handle.id);
1859
+ heap.freeHandleIds.sort((left, right) => left - right);
1860
+ }
1861
+ function snapshotFrameHeap(heap) {
1862
+ const generations = Object.fromEntries([...heap.generations.entries()]
1863
+ .sort((left, right) => left[0] - right[0])
1864
+ .map(([id, generation]) => [String(id), generation]));
1865
+ const collections = Object.fromEntries([...heap.collections.entries()]
1866
+ .sort((left, right) => left[0] - right[0])
1867
+ .map(([id, collection]) => [String(id), snapshotFrameHeapCollection(collection)]));
1868
+ return {
1869
+ nextHandleId: heap.nextHandleId,
1870
+ freeHandleIds: [...heap.freeHandleIds],
1871
+ generations,
1872
+ collections,
1873
+ };
1874
+ }
1875
+ function restoreFrameHeap(snapshot) {
1876
+ validateFrameHeapSnapshot(snapshot);
1877
+ const heap = createFrameHeap();
1878
+ heap.nextHandleId = snapshot.nextHandleId;
1879
+ heap.freeHandleIds.push(...snapshot.freeHandleIds);
1880
+ for (const id of stableKeys(snapshot.generations)) {
1881
+ heap.generations.set(Number(id), snapshot.generations[id]);
1882
+ }
1883
+ for (const id of stableKeys(snapshot.collections)) {
1884
+ heap.collections.set(Number(id), restoreFrameHeapCollection(snapshot.collections[id]));
1885
+ }
1886
+ return heap;
1887
+ }
1888
+ function validateFrameHeapSnapshot(snapshot) {
1889
+ if (!Number.isInteger(snapshot.nextHandleId) || snapshot.nextHandleId < 1) {
1890
+ throw new Error('Deterministic frame heap snapshot has invalid nextHandleId');
1891
+ }
1892
+ assertSortedUniqueIntegerList(snapshot.freeHandleIds, 'Deterministic frame heap freeHandleIds');
1893
+ const freeIds = new Set(snapshot.freeHandleIds);
1894
+ for (const id of stableKeys(snapshot.generations)) {
1895
+ const numericId = Number(id);
1896
+ const generation = snapshot.generations[id];
1897
+ if (!Number.isInteger(numericId) || numericId < 1 || String(numericId) !== id || !Number.isInteger(generation) || generation < 1) {
1898
+ throw new Error(`Deterministic frame heap snapshot has invalid generation for handle ${id}`);
1899
+ }
1900
+ }
1901
+ for (const id of stableKeys(snapshot.collections)) {
1902
+ const numericId = Number(id);
1903
+ const generation = snapshot.generations[id];
1904
+ if (!Number.isInteger(numericId) || numericId < 1 || String(numericId) !== id) {
1905
+ throw new Error(`Deterministic frame heap snapshot has invalid collection id ${id}`);
1906
+ }
1907
+ if (freeIds.has(numericId)) {
1908
+ throw new Error(`Deterministic frame heap snapshot collection ${id} is also free`);
1909
+ }
1910
+ if (generation === undefined) {
1911
+ throw new Error(`Deterministic frame heap snapshot collection ${id} is missing generation`);
1912
+ }
1913
+ validateFrameHeapCollectionSnapshot(snapshot.collections[id]);
1914
+ if (snapshot.collections[id].generation !== generation) {
1915
+ throw new Error(`Deterministic frame heap snapshot collection ${id} generation mismatch`);
1916
+ }
1917
+ }
1918
+ }
1919
+ function allocateFrameHeapHandle(heap, kind) {
1920
+ const id = heap.freeHandleIds.length === 0 ? heap.nextHandleId : heap.freeHandleIds.shift();
1921
+ if (id === heap.nextHandleId) {
1922
+ heap.nextHandleId += 1;
1923
+ }
1924
+ const generation = heap.generations.get(id) ?? 1;
1925
+ heap.generations.set(id, generation);
1926
+ return { id, generation, kind };
1927
+ }
1928
+ function frameHeapCollectionOrThrow(heap, handle, kind) {
1929
+ if (!Number.isInteger(handle.id) || handle.id < 1 || !Number.isInteger(handle.generation) || handle.generation < 1 || handle.kind !== kind) {
1930
+ throw new Error('Invalid deterministic frame heap handle');
1931
+ }
1932
+ const collection = heap.collections.get(handle.id);
1933
+ const generation = heap.generations.get(handle.id);
1934
+ if (collection === undefined || generation !== handle.generation || collection.generation !== handle.generation) {
1935
+ throw new Error(`Stale deterministic frame heap handle ${handle.id}:${handle.generation}`);
1936
+ }
1937
+ /* istanbul ignore next -- handle kind and collection kind are created/restored from validated heap state. */
1938
+ if (collection.kind !== kind) {
1939
+ throw new Error(`Deterministic frame heap handle ${handle.id}:${handle.generation} expected ${kind} but found ${collection.kind}`);
1940
+ }
1941
+ return collection;
1942
+ }
1943
+ function snapshotFrameHeapCollection(collection) {
1944
+ if (collection.kind === 'list') {
1945
+ return {
1946
+ kind: 'list',
1947
+ generation: collection.generation,
1948
+ values: normalizeFrameHeapListValues(collection.values),
1949
+ };
1950
+ }
1951
+ if (collection.kind === 'dictionary') {
1952
+ return {
1953
+ kind: 'dictionary',
1954
+ generation: collection.generation,
1955
+ entries: [...collection.entries.entries()]
1956
+ .sort((left, right) => compareStrings(left[0], right[0]))
1957
+ .map(([key, value]) => ({ key, value: cloneCanonical(value) })),
1958
+ };
1959
+ }
1960
+ return {
1961
+ kind: 'hash-set',
1962
+ generation: collection.generation,
1963
+ values: normalizeFrameHeapHashSetValues(collection.values),
1964
+ };
1965
+ }
1966
+ function restoreFrameHeapCollection(snapshot) {
1967
+ if (snapshot.kind === 'list') {
1968
+ return {
1969
+ kind: 'list',
1970
+ generation: snapshot.generation,
1971
+ values: normalizeFrameHeapListValues(snapshot.values),
1972
+ };
1973
+ }
1974
+ if (snapshot.kind === 'dictionary') {
1975
+ return {
1976
+ kind: 'dictionary',
1977
+ generation: snapshot.generation,
1978
+ entries: new Map(snapshot.entries.map((entry) => [entry.key, cloneCanonical(entry.value)])),
1979
+ };
1980
+ }
1981
+ return {
1982
+ kind: 'hash-set',
1983
+ generation: snapshot.generation,
1984
+ values: normalizeFrameHeapHashSetValues(snapshot.values),
1985
+ };
1986
+ }
1987
+ function validateFrameHeapCollectionSnapshot(snapshot) {
1988
+ if (!Number.isInteger(snapshot.generation) || snapshot.generation < 1) {
1989
+ throw new Error('Deterministic frame heap collection has invalid generation');
1990
+ }
1991
+ if (snapshot.kind === 'list') {
1992
+ if (!Array.isArray(snapshot.values)) {
1993
+ throw new Error('Deterministic frame heap list must contain values');
1994
+ }
1995
+ return;
1996
+ }
1997
+ if (snapshot.kind === 'dictionary') {
1998
+ let previousKey = '';
1999
+ const keys = new Set();
2000
+ for (const [index, entry] of snapshot.entries.entries()) {
2001
+ assertFrameHeapDictionaryKey(entry.key);
2002
+ if (keys.has(entry.key)) {
2003
+ throw new Error(`Deterministic frame heap dictionary has duplicate key ${entry.key}`);
2004
+ }
2005
+ if (index > 0 && compareStrings(previousKey, entry.key) >= 0) {
2006
+ throw new Error('Deterministic frame heap dictionary entries must be sorted by key');
2007
+ }
2008
+ previousKey = entry.key;
2009
+ keys.add(entry.key);
2010
+ }
2011
+ return;
2012
+ }
2013
+ if (snapshot.kind === 'hash-set') {
2014
+ assertCanonicalValuesSortedUnique(snapshot.values, 'Deterministic frame heap hash-set values');
2015
+ return;
2016
+ }
2017
+ throw new Error('Deterministic frame heap collection has invalid kind');
2018
+ }
2019
+ function normalizeFrameHeapListValues(values) {
2020
+ return values.map((value) => cloneCanonical(value));
2021
+ }
2022
+ function normalizeFrameHeapHashSetValues(values) {
2023
+ const byEncoded = new Map();
2024
+ for (const value of values) {
2025
+ byEncoded.set(canonicalStringify(value), cloneCanonical(value));
2026
+ }
2027
+ return [...byEncoded.entries()]
2028
+ .sort((left, right) => compareStrings(left[0], right[0]))
2029
+ .map(([, value]) => value);
2030
+ }
2031
+ function dictionaryEntriesFromRecord(entries) {
2032
+ return stableKeys(entries).map((key) => {
2033
+ assertFrameHeapDictionaryKey(key);
2034
+ return { key, value: cloneCanonical(entries[key]) };
2035
+ });
2036
+ }
2037
+ function dictionaryRecordFromEntries(entries) {
2038
+ return Object.fromEntries(entries.map((entry) => {
2039
+ assertFrameHeapDictionaryKey(entry.key);
2040
+ return [entry.key, cloneCanonical(entry.value)];
2041
+ }));
2042
+ }
2043
+ function assertFrameHeapDictionaryKey(key) {
2044
+ if (!isNonEmptyString(key)) {
2045
+ throw new Error('Deterministic frame heap dictionary key must be non-empty string');
2046
+ }
2047
+ }
2048
+ function assertSortedUniqueIntegerList(values, label) {
2049
+ let previous = 0;
2050
+ const seen = new Set();
2051
+ for (const [index, value] of values.entries()) {
2052
+ /* istanbul ignore next -- current callers validate out-of-range ids before this sortedness helper is reached. */
2053
+ if (!Number.isInteger(value) || value < 1) {
2054
+ throw new Error(`${label} contains invalid id`);
2055
+ }
2056
+ if (seen.has(value)) {
2057
+ throw new Error(`${label} contains duplicate id ${value}`);
2058
+ }
2059
+ if (index > 0 && previous >= value) {
2060
+ throw new Error(`${label} must be sorted`);
2061
+ }
2062
+ previous = value;
2063
+ seen.add(value);
2064
+ }
2065
+ }
2066
+ function assertCanonicalValuesSortedUnique(values, label) {
2067
+ let previous = '';
2068
+ const seen = new Set();
2069
+ for (const [index, value] of values.entries()) {
2070
+ const encoded = canonicalStringify(value);
2071
+ if (seen.has(encoded)) {
2072
+ throw new Error(`${label} contains duplicate value`);
2073
+ }
2074
+ if (index > 0 && compareStrings(previous, encoded) >= 0) {
2075
+ throw new Error(`${label} must be sorted`);
2076
+ }
2077
+ previous = encoded;
2078
+ seen.add(encoded);
2079
+ }
2080
+ }
2081
+ function snapshotState(state) {
2082
+ const payload = {
2083
+ frame: state.frame,
2084
+ nextEntityId: state.nextEntityId,
2085
+ freeEntityIds: [...state.freeEntityIds],
2086
+ generations: Object.fromEntries([...state.generations.entries()].sort((left, right) => left[0] - right[0]).map(([id, generation]) => [String(id), generation])),
2087
+ components: Object.fromEntries([...state.components.entries()].sort(compareMapStringEntry).map(([componentId, storage]) => [
2088
+ componentId,
2089
+ Object.fromEntries([...storage.entries()].sort((left, right) => left[0] - right[0]).map(([id, data]) => [String(id), data])),
2090
+ ])),
2091
+ singletonComponents: Object.fromEntries([...state.singletonComponents.entries()].sort(compareMapStringEntry)),
2092
+ frameHeap: snapshotFrameHeap(state.frameHeap),
2093
+ debugEvents: state.debugEvents.map((event) => cloneCanonical(event)),
2094
+ runtimeEvents: state.runtimeEvents.map((message) => cloneCanonical(message)),
2095
+ runtimeSignals: state.runtimeSignals.map((message) => cloneCanonical(message)),
2096
+ dispatchedSignals: state.dispatchedSignals.map((message) => cloneCanonical(message)),
2097
+ nextRuntimeMessageSequence: state.nextRuntimeMessageSequence,
2098
+ ...(state.commandTimeline === undefined ? {} : { commandTimeline: state.commandTimeline.snapshot() }),
2099
+ randomState: state.random.snapshot().state,
2100
+ };
2101
+ const bytes = canonicalStringify(payload);
2102
+ return {
2103
+ ...payload,
2104
+ bytes,
2105
+ checksum: defaultChecksum(bytes),
2106
+ };
2107
+ }
2108
+ function snapshotSparseState(state) {
2109
+ const payload = {
2110
+ frame: state.frame,
2111
+ nextEntityId: state.nextEntityId,
2112
+ freeEntityIds: [...state.freeEntityIds],
2113
+ generations: Object.fromEntries([...state.generations.entries()].sort((left, right) => left[0] - right[0]).map(([id, generation]) => [String(id), generation])),
2114
+ components: Object.fromEntries([...state.componentStores.entries()].sort(compareMapStringEntry).map(([componentId, storage]) => [
2115
+ componentId,
2116
+ Object.fromEntries(storage.query().map((entry) => [String(entry.id), entry.value])),
2117
+ ])),
2118
+ singletonComponents: Object.fromEntries([...state.singletonComponents.entries()].sort(compareMapStringEntry)),
2119
+ frameHeap: snapshotFrameHeap(state.frameHeap),
2120
+ debugEvents: state.debugEvents.map((event) => cloneCanonical(event)),
2121
+ runtimeEvents: state.runtimeEvents.map((message) => cloneCanonical(message)),
2122
+ runtimeSignals: state.runtimeSignals.map((message) => cloneCanonical(message)),
2123
+ dispatchedSignals: state.dispatchedSignals.map((message) => cloneCanonical(message)),
2124
+ nextRuntimeMessageSequence: state.nextRuntimeMessageSequence,
2125
+ ...(state.commandTimeline === undefined ? {} : { commandTimeline: state.commandTimeline.snapshot() }),
2126
+ randomState: state.random.snapshot().state,
2127
+ };
2128
+ const bytes = canonicalStringify(payload);
2129
+ return {
2130
+ ...payload,
2131
+ bytes,
2132
+ checksum: defaultChecksum(bytes),
2133
+ };
2134
+ }
2135
+ function restoreState(state, componentDescriptors, assetRefs, snapshot) {
2136
+ validateSnapshotIntegrity(componentDescriptors, snapshot, assetRefs);
2137
+ state.frame = snapshot.frame;
2138
+ state.nextEntityId = snapshot.nextEntityId;
2139
+ state.freeEntityIds.splice(0, state.freeEntityIds.length, ...snapshot.freeEntityIds);
2140
+ state.generations.clear();
2141
+ for (const id of stableKeys(snapshot.generations)) {
2142
+ state.generations.set(Number(id), snapshot.generations[id]);
2143
+ }
2144
+ state.components.clear();
2145
+ for (const componentId of stableKeys(snapshot.components)) {
2146
+ const entries = snapshot.components[componentId];
2147
+ state.components.set(componentId, new Map(stableKeys(entries).map((id) => [Number(id), entries[id]])));
2148
+ }
2149
+ state.singletonComponents.clear();
2150
+ for (const componentId of stableKeys(snapshot.singletonComponents)) {
2151
+ state.singletonComponents.set(componentId, snapshot.singletonComponents[componentId]);
2152
+ }
2153
+ state.frameHeap = restoreFrameHeap(snapshot.frameHeap);
2154
+ state.debugEvents.splice(0, state.debugEvents.length, ...snapshot.debugEvents.map((event) => cloneCanonical(event)));
2155
+ state.runtimeEvents.splice(0, state.runtimeEvents.length, ...snapshot.runtimeEvents.map((message) => cloneCanonical(message)));
2156
+ state.runtimeSignals.splice(0, state.runtimeSignals.length, ...snapshot.runtimeSignals.map((message) => cloneCanonical(message)));
2157
+ state.dispatchedSignals.splice(0, state.dispatchedSignals.length, ...snapshot.dispatchedSignals.map((message) => cloneCanonical(message)));
2158
+ state.nextRuntimeMessageSequence = snapshot.nextRuntimeMessageSequence;
2159
+ restoreCommandTimelineIfConfigured(state.commandTimeline, snapshot);
2160
+ state.random.restore({ state: snapshot.randomState });
2161
+ }
2162
+ function restoreSparseState(state, componentDescriptors, assetRefs, descriptor, capacity, snapshot) {
2163
+ validateSnapshotIntegrity(componentDescriptors, snapshot, assetRefs);
2164
+ state.frame = snapshot.frame;
2165
+ state.nextEntityId = snapshot.nextEntityId;
2166
+ state.freeEntityIds.splice(0, state.freeEntityIds.length, ...snapshot.freeEntityIds);
2167
+ state.generations.clear();
2168
+ for (const id of stableKeys(snapshot.generations)) {
2169
+ state.generations.set(Number(id), snapshot.generations[id]);
2170
+ }
2171
+ state.componentStores = createSparseComponentStores(descriptor, capacity);
2172
+ for (const componentId of stableKeys(snapshot.components)) {
2173
+ const storage = state.componentStores.get(componentId);
2174
+ /* istanbul ignore next -- snapshot integrity validates component storage before sparse restore. */
2175
+ if (storage === undefined) {
2176
+ throw new Error(`Missing sparse component storage ${componentId}`);
2177
+ }
2178
+ const entries = snapshot.components[componentId];
2179
+ for (const entityId of stableKeys(entries)) {
2180
+ storage.add(Number(entityId), entries[entityId]);
2181
+ }
2182
+ }
2183
+ state.singletonComponents.clear();
2184
+ for (const componentId of stableKeys(snapshot.singletonComponents)) {
2185
+ state.singletonComponents.set(componentId, snapshot.singletonComponents[componentId]);
2186
+ }
2187
+ state.frameHeap = restoreFrameHeap(snapshot.frameHeap);
2188
+ state.debugEvents.splice(0, state.debugEvents.length, ...snapshot.debugEvents.map((event) => cloneCanonical(event)));
2189
+ state.runtimeEvents.splice(0, state.runtimeEvents.length, ...snapshot.runtimeEvents.map((message) => cloneCanonical(message)));
2190
+ state.runtimeSignals.splice(0, state.runtimeSignals.length, ...snapshot.runtimeSignals.map((message) => cloneCanonical(message)));
2191
+ state.dispatchedSignals.splice(0, state.dispatchedSignals.length, ...snapshot.dispatchedSignals.map((message) => cloneCanonical(message)));
2192
+ state.nextRuntimeMessageSequence = snapshot.nextRuntimeMessageSequence;
2193
+ restoreCommandTimelineIfConfigured(state.commandTimeline, snapshot);
2194
+ state.random.restore({ state: snapshot.randomState });
2195
+ }
2196
+ function createSparseComponentStores(descriptor, capacity) {
2197
+ return new Map(descriptor.components
2198
+ .filter((component) => component.singleton !== true)
2199
+ .map((component) => [component.id, createDeterministicSparseSet(capacity)]));
2200
+ }
2201
+ function validateSnapshotIntegrity(componentDescriptors, snapshot, assetRefs) {
2202
+ const payload = {
2203
+ frame: snapshot.frame,
2204
+ nextEntityId: snapshot.nextEntityId,
2205
+ freeEntityIds: snapshot.freeEntityIds,
2206
+ generations: snapshot.generations,
2207
+ components: snapshot.components,
2208
+ singletonComponents: snapshot.singletonComponents,
2209
+ frameHeap: snapshot.frameHeap,
2210
+ debugEvents: snapshot.debugEvents,
2211
+ runtimeEvents: snapshot.runtimeEvents,
2212
+ runtimeSignals: snapshot.runtimeSignals,
2213
+ dispatchedSignals: snapshot.dispatchedSignals,
2214
+ nextRuntimeMessageSequence: snapshot.nextRuntimeMessageSequence,
2215
+ ...(snapshot.commandTimeline === undefined ? {} : { commandTimeline: snapshot.commandTimeline }),
2216
+ randomState: snapshot.randomState,
2217
+ };
2218
+ const bytes = canonicalStringify(payload);
2219
+ if (bytes !== snapshot.bytes || defaultChecksum(bytes) !== snapshot.checksum) {
2220
+ throw new Error('Deterministic frame snapshot checksum mismatch');
2221
+ }
2222
+ validateSnapshotEntityAllocator(snapshot);
2223
+ for (const componentId of stableKeys(snapshot.components)) {
2224
+ const descriptor = componentDescriptors.get(componentId);
2225
+ if (descriptor === undefined || descriptor.singleton === true) {
2226
+ throw new Error(`Deterministic frame snapshot contains incompatible component storage ${componentId}`);
2227
+ }
2228
+ const componentEntries = snapshot.components[componentId];
2229
+ for (const entityId of stableKeys(componentEntries)) {
2230
+ validateComponentData(descriptor, componentEntries[entityId], assetRefs);
2231
+ }
2232
+ }
2233
+ for (const componentId of stableKeys(snapshot.singletonComponents)) {
2234
+ const descriptor = componentDescriptors.get(componentId);
2235
+ if (descriptor === undefined || descriptor.singleton !== true) {
2236
+ throw new Error(`Deterministic frame snapshot contains incompatible singleton storage ${componentId}`);
2237
+ }
2238
+ validateComponentData(descriptor, snapshot.singletonComponents[componentId], assetRefs);
2239
+ }
2240
+ validateFrameHeapSnapshot(snapshot.frameHeap);
2241
+ if (snapshot.commandTimeline !== undefined) {
2242
+ const commandPayload = {
2243
+ currentFrame: snapshot.commandTimeline.currentFrame,
2244
+ accepted: snapshot.commandTimeline.accepted,
2245
+ rejected: snapshot.commandTimeline.rejected,
2246
+ };
2247
+ if (defaultChecksum(commandPayload) !== snapshot.commandTimeline.checksum) {
2248
+ throw new Error('Deterministic frame snapshot command timeline checksum mismatch');
2249
+ }
2250
+ }
2251
+ }
2252
+ function validateSnapshotEntityAllocator(snapshot) {
2253
+ if (!Number.isInteger(snapshot.nextEntityId) || snapshot.nextEntityId < 1) {
2254
+ throw new Error('Deterministic frame snapshot has invalid next entity id');
2255
+ }
2256
+ const liveEntityIds = new Set();
2257
+ for (const componentId of stableKeys(snapshot.components)) {
2258
+ const componentEntries = snapshot.components[componentId];
2259
+ for (const entityIdText of stableKeys(componentEntries)) {
2260
+ const entityId = parseSnapshotEntityId(entityIdText);
2261
+ if (entityId >= snapshot.nextEntityId) {
2262
+ throw new Error(`Deterministic frame snapshot component ${componentId} references entity outside allocator range ${entityId}`);
2263
+ }
2264
+ liveEntityIds.add(entityId);
2265
+ }
2266
+ }
2267
+ let previousFreeId = 0;
2268
+ for (const entityId of snapshot.freeEntityIds) {
2269
+ if (!Number.isInteger(entityId) || entityId < 1 || entityId >= snapshot.nextEntityId || entityId <= previousFreeId) {
2270
+ throw new Error('Deterministic frame snapshot has invalid free entity ids');
2271
+ }
2272
+ if (snapshot.generations[String(entityId)] === undefined) {
2273
+ throw new Error(`Deterministic frame snapshot free entity ${entityId} is missing generation`);
2274
+ }
2275
+ if (liveEntityIds.has(entityId)) {
2276
+ throw new Error(`Deterministic frame snapshot free entity ${entityId} still has components`);
2277
+ }
2278
+ previousFreeId = entityId;
2279
+ }
2280
+ for (const entityIdText of stableKeys(snapshot.generations)) {
2281
+ const entityId = parseSnapshotEntityId(entityIdText);
2282
+ if (entityId >= snapshot.nextEntityId) {
2283
+ throw new Error(`Deterministic frame snapshot generation references entity outside allocator range ${entityId}`);
2284
+ }
2285
+ const generation = snapshot.generations[entityIdText];
2286
+ if (!Number.isInteger(generation) || generation < 1) {
2287
+ throw new Error(`Deterministic frame snapshot has invalid generation for entity ${entityId}`);
2288
+ }
2289
+ }
2290
+ for (const entityId of liveEntityIds) {
2291
+ if (snapshot.generations[String(entityId)] === undefined) {
2292
+ throw new Error(`Deterministic frame snapshot live entity ${entityId} is missing generation`);
2293
+ }
2294
+ }
2295
+ }
2296
+ function parseSnapshotEntityId(entityIdText) {
2297
+ if (!/^[1-9][0-9]*$/.test(entityIdText)) {
2298
+ throw new Error(`Deterministic frame snapshot has invalid entity id ${entityIdText}`);
2299
+ }
2300
+ return Number(entityIdText);
2301
+ }
2302
+ function commandTimelineOrThrow(commandTimeline) {
2303
+ if (commandTimeline === undefined) {
2304
+ throw new Error('Deterministic frame runtime command timeline is not configured');
2305
+ }
2306
+ return commandTimeline;
2307
+ }
2308
+ function restoreCommandTimelineIfConfigured(commandTimeline, snapshot) {
2309
+ if (commandTimeline === undefined) {
2310
+ if (snapshot.commandTimeline !== undefined) {
2311
+ throw new Error('Deterministic frame snapshot contains command timeline but runtime is not configured for commands');
2312
+ }
2313
+ return;
2314
+ }
2315
+ if (snapshot.commandTimeline === undefined) {
2316
+ throw new Error('Deterministic frame snapshot is missing command timeline');
2317
+ }
2318
+ commandTimeline.restore(snapshot.commandTimeline);
2319
+ }
2320
+ function assertLiveEntity(state, entity) {
2321
+ if (state.generations.get(entity.id) !== entity.generation || state.freeEntityIds.includes(entity.id)) {
2322
+ throw new Error(`Stale deterministic entity ref ${entity.id}:${entity.generation}`);
2323
+ }
2324
+ }
2325
+ function assertLiveSparseEntity(state, entity) {
2326
+ if (state.generations.get(entity.id) !== entity.generation || state.freeEntityIds.includes(entity.id)) {
2327
+ throw new Error(`Stale deterministic entity ref ${entity.id}:${entity.generation}`);
2328
+ }
2329
+ }
2330
+ function validateRuntimeDescriptor(descriptor) {
2331
+ if (!isNonEmptyString(descriptor.id)) {
2332
+ throw new Error('Deterministic runtime descriptor requires a non-empty id');
2333
+ }
2334
+ if (!Number.isInteger(descriptor.tickRate) || descriptor.tickRate <= 0) {
2335
+ throw new Error(`Deterministic runtime descriptor ${descriptor.id} has invalid tickRate`);
2336
+ }
2337
+ if (!Array.isArray(descriptor.components)) {
2338
+ throw new Error(`Deterministic runtime descriptor ${descriptor.id} requires components`);
2339
+ }
2340
+ if (!Array.isArray(descriptor.systems)) {
2341
+ throw new Error(`Deterministic runtime descriptor ${descriptor.id} requires systems`);
2342
+ }
2343
+ const componentIds = new Set();
2344
+ for (const component of descriptor.components) {
2345
+ if (!isNonEmptyString(component.id)) {
2346
+ throw new Error('Deterministic component requires a non-empty id');
2347
+ }
2348
+ if (!Array.isArray(component.fields)) {
2349
+ throw new Error(`Deterministic component ${component.id} requires fields`);
2350
+ }
2351
+ if (componentIds.has(component.id)) {
2352
+ throw new Error(`Duplicate deterministic component ${component.id}`);
2353
+ }
2354
+ componentIds.add(component.id);
2355
+ const fieldIds = new Set();
2356
+ for (const field of component.fields) {
2357
+ if (!isNonEmptyString(field.name)) {
2358
+ throw new Error(`Deterministic component ${component.id} has a field with an invalid name`);
2359
+ }
2360
+ if (!isRuntimeFieldKind(field.kind)) {
2361
+ throw new Error(`Deterministic component ${component.id}.${field.name} has invalid field kind ${String(field.kind)}`);
2362
+ }
2363
+ if (field.assetType !== undefined && (!isNonEmptyString(field.assetType) || !fieldContainsAssetRef(field.kind))) {
2364
+ throw new Error(`Deterministic component ${component.id}.${field.name} has invalid asset type`);
2365
+ }
2366
+ if (fieldIds.has(field.name)) {
2367
+ throw new Error(`Duplicate deterministic component field ${component.id}.${field.name}`);
2368
+ }
2369
+ fieldIds.add(field.name);
2370
+ if (field.defaultValue !== undefined) {
2371
+ assertDefaultValueMatchesRuntimeField(component.id, field.name, field.kind, field.defaultValue);
2372
+ }
2373
+ }
2374
+ }
2375
+ const systemIds = new Set();
2376
+ for (const system of descriptor.systems) {
2377
+ if (!isNonEmptyString(system.id)) {
2378
+ throw new Error('Deterministic system requires a non-empty id');
2379
+ }
2380
+ if (!isSystemGroup(system.group)) {
2381
+ throw new Error(`Deterministic system ${system.id} has invalid system group ${String(system.group)}`);
2382
+ }
2383
+ if (!Number.isInteger(system.order)) {
2384
+ throw new Error(`Deterministic system ${system.id} has invalid system order`);
2385
+ }
2386
+ if (system.before !== undefined && !Array.isArray(system.before)) {
2387
+ throw new Error(`Deterministic system ${system.id} before dependencies must be an array`);
2388
+ }
2389
+ if (system.after !== undefined && !Array.isArray(system.after)) {
2390
+ throw new Error(`Deterministic system ${system.id} after dependencies must be an array`);
2391
+ }
2392
+ if (systemIds.has(system.id)) {
2393
+ throw new Error(`Duplicate deterministic system ${system.id}`);
2394
+ }
2395
+ systemIds.add(system.id);
2396
+ }
2397
+ if (descriptor.commandChannels !== undefined && !Array.isArray(descriptor.commandChannels)) {
2398
+ throw new Error(`Deterministic runtime descriptor ${descriptor.id} commandChannels must be an array`);
2399
+ }
2400
+ const channelIds = new Set();
2401
+ for (const channel of descriptor.commandChannels ?? []) {
2402
+ if (!isNonEmptyString(channel.id)) {
2403
+ throw new Error('Deterministic command channel requires a non-empty id');
2404
+ }
2405
+ if (!Number.isInteger(channel.maxPayloadBytes) || channel.maxPayloadBytes <= 0) {
2406
+ throw new Error(`Deterministic command channel ${channel.id} has invalid maxPayloadBytes`);
2407
+ }
2408
+ if (typeof channel.reliable !== 'boolean') {
2409
+ throw new Error(`Deterministic command channel ${channel.id} has invalid reliable flag`);
2410
+ }
2411
+ if (typeof channel.allowPrediction !== 'boolean') {
2412
+ throw new Error(`Deterministic command channel ${channel.id} has invalid allowPrediction flag`);
2413
+ }
2414
+ if (channel.minFrameLead !== undefined && (!Number.isInteger(channel.minFrameLead) || channel.minFrameLead < 0)) {
2415
+ throw new Error(`Deterministic command channel ${channel.id} has invalid min frame lead`);
2416
+ }
2417
+ if (channel.maxFrameLead !== undefined && (!Number.isInteger(channel.maxFrameLead) || channel.maxFrameLead < 0)) {
2418
+ throw new Error(`Deterministic command channel ${channel.id} has invalid max frame lead`);
2419
+ }
2420
+ if (channel.minFrameLead !== undefined && channel.maxFrameLead !== undefined && channel.minFrameLead > channel.maxFrameLead) {
2421
+ throw new Error(`Deterministic command channel ${channel.id} has invalid frame lead range`);
2422
+ }
2423
+ if (channelIds.has(channel.id)) {
2424
+ throw new Error(`Duplicate deterministic command channel ${channel.id}`);
2425
+ }
2426
+ channelIds.add(channel.id);
2427
+ }
2428
+ validateRuntimePayloadChannels('eventChannels', descriptor.eventChannels ?? []);
2429
+ validateRuntimePayloadChannels('signalChannels', descriptor.signalChannels ?? []);
2430
+ }
2431
+ function validateRuntimePayloadChannels(label, channels) {
2432
+ const ids = new Set();
2433
+ for (const channel of channels) {
2434
+ if (!isNonEmptyString(channel.id)) {
2435
+ throw new Error(`Deterministic runtime ${label} entry requires a non-empty id`);
2436
+ }
2437
+ if (ids.has(channel.id)) {
2438
+ throw new Error(`Duplicate deterministic runtime ${label} entry ${channel.id}`);
2439
+ }
2440
+ ids.add(channel.id);
2441
+ const fieldIds = new Set();
2442
+ for (const field of channel.fields) {
2443
+ if (!isNonEmptyString(field.name)) {
2444
+ throw new Error(`Deterministic runtime ${label} ${channel.id} has a field with an invalid name`);
2445
+ }
2446
+ if (!isRuntimeFieldKind(field.kind)) {
2447
+ throw new Error(`Deterministic runtime ${label} ${channel.id}.${field.name} has invalid field kind ${String(field.kind)}`);
2448
+ }
2449
+ if (field.assetType !== undefined && (!isNonEmptyString(field.assetType) || !fieldContainsAssetRef(field.kind))) {
2450
+ throw new Error(`Deterministic runtime ${label} ${channel.id}.${field.name} has invalid asset type`);
2451
+ }
2452
+ if (fieldIds.has(field.name)) {
2453
+ throw new Error(`Duplicate deterministic runtime ${label} field ${channel.id}.${field.name}`);
2454
+ }
2455
+ fieldIds.add(field.name);
2456
+ if (field.defaultValue !== undefined) {
2457
+ assertDefaultValueMatchesRuntimeField(channel.id, field.name, field.kind, field.defaultValue);
2458
+ }
2459
+ }
2460
+ }
2461
+ }
2462
+ function isNonEmptyString(value) {
2463
+ return typeof value === 'string' && value.length > 0;
2464
+ }
2465
+ function isSystemGroup(value) {
2466
+ return value === 'input'
2467
+ || value === 'simulation'
2468
+ || value === 'physics'
2469
+ || value === 'post-physics'
2470
+ || value === 'presentation';
2471
+ }
2472
+ function isRuntimeFieldKind(value) {
2473
+ return value === 'int'
2474
+ || value === 'fixed'
2475
+ || value === 'bool'
2476
+ || value === 'string'
2477
+ || value === 'asset-ref'
2478
+ || value === 'entity-ref'
2479
+ || value === 'int[]'
2480
+ || value === 'fixed[]'
2481
+ || value === 'bool[]'
2482
+ || value === 'string[]'
2483
+ || value === 'asset-ref[]'
2484
+ || value === 'entity-ref[]'
2485
+ || (typeof value === 'string' && /^(int|fixed|bool|string|asset-ref|entity-ref)\[[1-9][0-9]*\]$/.test(value))
2486
+ || value === 'frame-list'
2487
+ || value === 'frame-dictionary'
2488
+ || value === 'frame-hash-set';
2489
+ }
2490
+ function assertDefaultValueMatchesRuntimeField(componentId, fieldName, kind, value) {
2491
+ try {
2492
+ validateRuntimeValueForKind(componentId, fieldName, kind, value);
2493
+ }
2494
+ catch (error) {
2495
+ /* istanbul ignore next -- default-value validators throw Error instances. */
2496
+ const message = error instanceof Error ? error.message : String(error);
2497
+ throw new Error(`Component field ${componentId}.${fieldName} has invalid defaultValue: ${message}`);
2498
+ }
2499
+ }
2500
+ function validateRuntimeValueForKind(componentId, fieldName, kind, value) {
2501
+ if (kind === 'int' && (typeof value !== 'number' || !Number.isInteger(value))) {
2502
+ throw new Error('expected integer');
2503
+ }
2504
+ if (kind === 'fixed' && (typeof value !== 'number' || !Number.isInteger(value))) {
2505
+ throw new Error('expected quantized fixed integer ticks');
2506
+ }
2507
+ if (kind === 'bool' && typeof value !== 'boolean') {
2508
+ throw new Error('expected boolean');
2509
+ }
2510
+ if ((kind === 'string' || kind === 'asset-ref') && typeof value !== 'string') {
2511
+ throw new Error('expected string');
2512
+ }
2513
+ if (kind === 'entity-ref' && (typeof value !== 'number' || !Number.isInteger(value))) {
2514
+ throw new Error('expected entity ref integer');
2515
+ }
2516
+ if (isRuntimeFrameHeapKind(kind)) {
2517
+ validateRuntimeFrameHeapHandle(componentId, fieldName, kind, value);
2518
+ }
2519
+ if (isRuntimeArrayKind(kind) || isRuntimeFixedArrayKind(kind)) {
2520
+ validateRuntimeArrayField(componentId, fieldName, kind, value);
2521
+ }
2522
+ }
2523
+ function validateComponentData(descriptor, data, assetRefs) {
2524
+ normalizeComponentData(descriptor, data, assetRefs);
2525
+ }
2526
+ function normalizeComponentData(descriptor, data, assetRefs) {
2527
+ const fields = new Map(descriptor.fields.map((field) => [field.name, field]));
2528
+ for (const key of stableKeys(data)) {
2529
+ if (!fields.has(key)) {
2530
+ throw new Error(`Unknown component field ${descriptor.id}.${key}`);
2531
+ }
2532
+ }
2533
+ for (const field of descriptor.fields) {
2534
+ const value = data[field.name] ?? field.defaultValue;
2535
+ if (value === undefined) {
2536
+ throw new Error(`Missing component field ${descriptor.id}.${field.name}`);
2537
+ }
2538
+ if (field.kind === 'int' && (!Number.isInteger(value) || typeof value !== 'number')) {
2539
+ throw new Error(`Component field ${descriptor.id}.${field.name} must be an integer`);
2540
+ }
2541
+ if (field.kind === 'fixed' && (!Number.isInteger(value) || typeof value !== 'number')) {
2542
+ throw new Error(`Component field ${descriptor.id}.${field.name} must be quantized fixed integer ticks`);
2543
+ }
2544
+ if (field.kind === 'bool' && typeof value !== 'boolean') {
2545
+ throw new Error(`Component field ${descriptor.id}.${field.name} must be boolean`);
2546
+ }
2547
+ if (field.kind === 'string' && typeof value !== 'string') {
2548
+ throw new Error(`Component field ${descriptor.id}.${field.name} must be string`);
2549
+ }
2550
+ if (field.kind === 'asset-ref' && typeof value !== 'string') {
2551
+ throw new Error(`Component field ${descriptor.id}.${field.name} must be asset ref string`);
2552
+ }
2553
+ validateRuntimeAssetRefValue(descriptor.id, field, value, assetRefs);
2554
+ if (field.kind === 'entity-ref' && (!Number.isInteger(value) || typeof value !== 'number')) {
2555
+ throw new Error(`Component field ${descriptor.id}.${field.name} must be entity ref integer`);
2556
+ }
2557
+ if (isRuntimeFrameHeapKind(field.kind)) {
2558
+ validateRuntimeFrameHeapHandle(descriptor.id, field.name, field.kind, value);
2559
+ }
2560
+ if (isRuntimeArrayKind(field.kind) || isRuntimeFixedArrayKind(field.kind)) {
2561
+ validateRuntimeArrayField(descriptor.id, field.name, field.kind, value);
2562
+ }
2563
+ }
2564
+ return Object.fromEntries(descriptor.fields.map((field) => [field.name, cloneCanonical(data[field.name] ?? field.defaultValue)]));
2565
+ }
2566
+ function validateRuntimeAssetRefValue(componentId, field, value, assetRefs) {
2567
+ if (assetRefs === undefined || !fieldContainsAssetRef(field.kind)) {
2568
+ return;
2569
+ }
2570
+ const values = assetRefValuesForRuntimeField(componentId, field, value);
2571
+ const allowed = field.assetType === undefined ? assetRefs.allRefs : assetRefs.typedRefs.get(field.assetType);
2572
+ if (field.assetType !== undefined && allowed === undefined) {
2573
+ throw new Error(`Component field ${componentId}.${field.name} references unknown asset type ${field.assetType}`);
2574
+ }
2575
+ for (const assetRef of values) {
2576
+ if (assetRef.length === 0 || allowed?.has(assetRef) !== true) {
2577
+ const typeSuffix = field.assetType === undefined ? '' : ` for ${field.assetType}`;
2578
+ throw new Error(`Component field ${componentId}.${field.name} references missing asset ${assetRef}${typeSuffix}`);
2579
+ }
2580
+ }
2581
+ }
2582
+ function assetRefValuesForRuntimeField(componentId, field, value) {
2583
+ if (field.kind === 'asset-ref') {
2584
+ return [String(value)];
2585
+ }
2586
+ /* istanbul ignore next -- asset-ref extraction only receives fields that contain asset refs. */
2587
+ if (isRuntimeArrayKind(field.kind) || isRuntimeFixedArrayKind(field.kind)) {
2588
+ if (runtimeCollectionScalarKind(field.kind) !== 'asset-ref') {
2589
+ return [];
2590
+ }
2591
+ if (!Array.isArray(value)) {
2592
+ throw new Error(`Component field ${componentId}.${field.name} must be an asset ref array`);
2593
+ }
2594
+ return value.map((entry) => {
2595
+ if (typeof entry !== 'string') {
2596
+ throw new Error(`Component field ${componentId}.${field.name} must contain asset ref strings`);
2597
+ }
2598
+ return entry;
2599
+ });
2600
+ }
2601
+ /* istanbul ignore next -- asset-ref extraction only receives scalar or array asset-ref fields. */
2602
+ return [];
2603
+ }
2604
+ function fieldContainsAssetRef(kind) {
2605
+ if (kind === 'asset-ref') {
2606
+ return true;
2607
+ }
2608
+ /* istanbul ignore next -- asset-ref tests cover scalar and collection refs. */
2609
+ if (isRuntimeArrayKind(kind) || isRuntimeFixedArrayKind(kind)) {
2610
+ return runtimeCollectionScalarKind(kind) === 'asset-ref';
2611
+ }
2612
+ return false;
2613
+ }
2614
+ function createRuntimeAssetRefValidator(options) {
2615
+ if (options.assetRefs === undefined && options.typedAssetRefs === undefined) {
2616
+ return undefined;
2617
+ }
2618
+ const allRefs = new Set();
2619
+ /* istanbul ignore next -- global asset refs default to empty when only typed refs are configured. */
2620
+ for (const assetRef of options.assetRefs ?? []) {
2621
+ if (!isNonEmptyString(assetRef)) {
2622
+ throw new Error('Deterministic runtime assetRefs must contain non-empty strings');
2623
+ }
2624
+ allRefs.add(assetRef);
2625
+ }
2626
+ const typedRefs = new Map();
2627
+ /* istanbul ignore next -- typed asset refs default to empty when only global refs are configured. */
2628
+ for (const assetType of stableKeys(options.typedAssetRefs ?? {})) {
2629
+ if (!isNonEmptyString(assetType)) {
2630
+ throw new Error('Deterministic runtime typedAssetRefs requires non-empty asset types');
2631
+ }
2632
+ const refs = new Set();
2633
+ /* istanbul ignore next -- typed asset refs are validated when configured. */
2634
+ for (const assetRef of options.typedAssetRefs?.[assetType] ?? []) {
2635
+ if (!isNonEmptyString(assetRef)) {
2636
+ throw new Error(`Deterministic runtime typedAssetRefs.${assetType} must contain non-empty strings`);
2637
+ }
2638
+ refs.add(assetRef);
2639
+ allRefs.add(assetRef);
2640
+ }
2641
+ typedRefs.set(assetType, refs);
2642
+ }
2643
+ return { allRefs, typedRefs };
2644
+ }
2645
+ function dslFieldKindToRuntimeKind(kind) {
2646
+ if (isDslDictionaryKind(kind)) {
2647
+ return 'frame-dictionary';
2648
+ }
2649
+ if (isDslListKind(kind)) {
2650
+ return 'frame-list';
2651
+ }
2652
+ if (isDslSetKind(kind)) {
2653
+ return 'frame-hash-set';
2654
+ }
2655
+ if (isDslFixedArrayKind(kind)) {
2656
+ const scalar = dslCollectionScalarKind(kind);
2657
+ return `${runtimeScalarKind(scalar)}[${dslFixedArrayLength(kind)}]`;
2658
+ }
2659
+ if (isDslCollectionKind(kind)) {
2660
+ const scalar = dslCollectionScalarKind(kind);
2661
+ return `${runtimeScalarKind(scalar)}[]`;
2662
+ }
2663
+ return runtimeScalarKind(kind);
2664
+ }
2665
+ function dslAssetTypeForFieldKind(kind) {
2666
+ if (isDslListKind(kind) || isDslSetKind(kind) || isDslDictionaryKind(kind)) {
2667
+ return undefined;
2668
+ }
2669
+ const scalar = isDslCollectionKind(kind)
2670
+ ? dslCollectionScalarKind(kind)
2671
+ : kind;
2672
+ return isDslTypedAssetRefKind(scalar) ? scalar.slice('asset-ref:'.length) : undefined;
2673
+ }
2674
+ function defaultRuntimeValueForKind(kind) {
2675
+ if (isDslListKind(kind) || isDslSetKind(kind) || isDslDictionaryKind(kind)) {
2676
+ return null;
2677
+ }
2678
+ if (isDslFixedArrayKind(kind)) {
2679
+ return Array.from({ length: dslFixedArrayLength(kind) }, () => defaultRuntimeValueForKind(dslCollectionScalarKind(kind)));
2680
+ }
2681
+ if (isDslCollectionKind(kind)) {
2682
+ return [];
2683
+ }
2684
+ if (kind === 'bool' || kind === 'button') {
2685
+ return false;
2686
+ }
2687
+ if (kind === 'string' || kind === 'asset-ref' || isDslTypedAssetRefKind(kind)) {
2688
+ return '';
2689
+ }
2690
+ return 0;
2691
+ }
2692
+ function defaultComponentData(component) {
2693
+ return Object.fromEntries(component.fields.map((field) => [
2694
+ field.name,
2695
+ cloneCanonical(field.defaultValue ?? defaultComponentFieldValue(field.kind)),
2696
+ ]));
2697
+ }
2698
+ function defaultComponentFieldValue(kind) {
2699
+ if (isRuntimeFrameHeapKind(kind)) {
2700
+ return null;
2701
+ }
2702
+ if (isRuntimeFixedArrayKind(kind)) {
2703
+ return Array.from({ length: runtimeFixedArrayLength(kind) }, () => defaultComponentFieldValue(runtimeCollectionScalarKind(kind)));
2704
+ }
2705
+ if (isRuntimeArrayKind(kind)) {
2706
+ return [];
2707
+ }
2708
+ if (kind === 'bool') {
2709
+ return false;
2710
+ }
2711
+ if (kind === 'string' || kind === 'asset-ref') {
2712
+ return '';
2713
+ }
2714
+ return 0;
2715
+ }
2716
+ function literalForComponentDefaults(component) {
2717
+ return JSON.stringify(defaultComponentData(component));
2718
+ }
2719
+ function runtimeFieldToTs(kind) {
2720
+ if (isRuntimeFrameHeapKind(kind)) {
2721
+ return 'DeterministicGeneratedFrameHeapHandle | null';
2722
+ }
2723
+ const array = isRuntimeArrayKind(kind) || isRuntimeFixedArrayKind(kind);
2724
+ const scalar = array ? runtimeCollectionScalarKind(kind) : kind;
2725
+ const scalarType = scalar === 'bool'
2726
+ ? 'boolean'
2727
+ : scalar === 'string' || scalar === 'asset-ref'
2728
+ ? 'string'
2729
+ : 'number';
2730
+ return array ? `readonly ${scalarType}[]` : scalarType;
2731
+ }
2732
+ function propertyName(value) {
2733
+ /* istanbul ignore next -- validated descriptors use identifier field names for generated interfaces. */
2734
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value) ? value : JSON.stringify(value);
2735
+ }
2736
+ function validateRuntimeArrayField(componentId, fieldName, kind, value) {
2737
+ if (!Array.isArray(value)) {
2738
+ throw new Error(`Component field ${componentId}.${fieldName} must be array`);
2739
+ }
2740
+ const fixedLength = isRuntimeFixedArrayKind(kind) ? runtimeFixedArrayLength(kind) : undefined;
2741
+ if (fixedLength !== undefined && value.length !== fixedLength) {
2742
+ throw new Error(`Component field ${componentId}.${fieldName} must contain exactly ${fixedLength} values`);
2743
+ }
2744
+ const scalarKind = runtimeCollectionScalarKind(kind);
2745
+ for (const item of value) {
2746
+ if (scalarKind === 'bool' && typeof item !== 'boolean') {
2747
+ throw new Error(`Component field ${componentId}.${fieldName} must contain booleans`);
2748
+ }
2749
+ if ((scalarKind === 'int' || scalarKind === 'fixed' || scalarKind === 'entity-ref') && (typeof item !== 'number' || !Number.isInteger(item))) {
2750
+ throw new Error(`Component field ${componentId}.${fieldName} must contain integers`);
2751
+ }
2752
+ if ((scalarKind === 'string' || scalarKind === 'asset-ref') && typeof item !== 'string') {
2753
+ throw new Error(`Component field ${componentId}.${fieldName} must contain strings`);
2754
+ }
2755
+ }
2756
+ }
2757
+ function validateRuntimeFrameHeapHandle(componentId, fieldName, kind, value) {
2758
+ if (value === null) {
2759
+ return;
2760
+ }
2761
+ if (typeof value !== 'object' || Array.isArray(value)) {
2762
+ throw new Error(`Component field ${componentId}.${fieldName} must be frame heap handle or null`);
2763
+ }
2764
+ const record = value;
2765
+ const expectedKind = runtimeFrameHeapHandleKind(kind);
2766
+ if (typeof record.id !== 'number' ||
2767
+ !Number.isInteger(record.id) ||
2768
+ record.id < 1 ||
2769
+ typeof record.generation !== 'number' ||
2770
+ !Number.isInteger(record.generation) ||
2771
+ record.generation < 1 ||
2772
+ record.kind !== expectedKind) {
2773
+ throw new Error(`Component field ${componentId}.${fieldName} must be ${expectedKind} frame heap handle`);
2774
+ }
2775
+ }
2776
+ function runtimeFrameHeapHandleKind(kind) {
2777
+ if (kind === 'frame-list') {
2778
+ return 'list';
2779
+ }
2780
+ if (kind === 'frame-dictionary') {
2781
+ return 'dictionary';
2782
+ }
2783
+ return 'hash-set';
2784
+ }
2785
+ function isDslArrayKind(kind) {
2786
+ return kind.endsWith('[]');
2787
+ }
2788
+ function isDslFixedArrayKind(kind) {
2789
+ return /^(int|fixed|bool|button|string|asset-ref|entity-ref)\[[1-9][0-9]*\]$/.test(kind);
2790
+ }
2791
+ function dslFixedArrayLength(kind) {
2792
+ const match = kind.match(/\[([1-9][0-9]*)\]$/);
2793
+ /* istanbul ignore next -- caller checks isDslFixedArrayKind before extracting fixed array length. */
2794
+ if (match === null) {
2795
+ throw new Error(`Expected fixed array DSL kind, received ${kind}`);
2796
+ }
2797
+ return Number(match[1]);
2798
+ }
2799
+ function isDslListKind(kind) {
2800
+ return kind.startsWith('list<') && kind.endsWith('>');
2801
+ }
2802
+ function isDslSetKind(kind) {
2803
+ return kind.startsWith('set<') && kind.endsWith('>');
2804
+ }
2805
+ function isDslDictionaryKind(kind) {
2806
+ return kind.startsWith('dictionary<') && kind.endsWith('>');
2807
+ }
2808
+ function isDslCollectionKind(kind) {
2809
+ return isDslArrayKind(kind) || isDslFixedArrayKind(kind) || isDslListKind(kind) || isDslSetKind(kind) || isDslDictionaryKind(kind);
2810
+ }
2811
+ function dslCollectionScalarKind(kind) {
2812
+ if (isDslArrayKind(kind)) {
2813
+ return kind.slice(0, -2);
2814
+ }
2815
+ /* istanbul ignore else -- scalar extraction reaches fixed arrays only after the array case is rejected. */
2816
+ if (isDslFixedArrayKind(kind)) {
2817
+ return kind.slice(0, kind.indexOf('['));
2818
+ }
2819
+ /* istanbul ignore next -- list/set/dictionary kinds are mapped to frame heap storage before scalar extraction. */
2820
+ return kind.slice(kind.indexOf('<') + 1, kind.indexOf(isDslDictionaryKind(kind) ? ',' : '>'));
2821
+ }
2822
+ function runtimeScalarKind(kind) {
2823
+ if (kind === 'button') {
2824
+ return 'bool';
2825
+ }
2826
+ if (kind === 'player-ref' || kind.startsWith('enum:') || kind.startsWith('flags:') || kind.startsWith('bitset:')) {
2827
+ return 'int';
2828
+ }
2829
+ if (kind.startsWith('asset-ref:')) {
2830
+ return 'asset-ref';
2831
+ }
2832
+ /* istanbul ignore else -- unsupported DSL scalar kinds are rejected by the parser before runtime compilation. */
2833
+ if (kind === 'int' || kind === 'fixed' || kind === 'bool' || kind === 'string' || kind === 'asset-ref' || kind === 'entity-ref') {
2834
+ return kind;
2835
+ }
2836
+ /* istanbul ignore next -- schema parser rejects unsupported DSL scalar kinds before runtime compilation. */
2837
+ throw new Error(`Unsupported DSL runtime scalar kind ${kind}`);
2838
+ }
2839
+ function isDslTypedAssetRefKind(kind) {
2840
+ return kind.startsWith('asset-ref:');
2841
+ }
2842
+ function isRuntimeArrayKind(kind) {
2843
+ return kind.endsWith('[]');
2844
+ }
2845
+ function isRuntimeFixedArrayKind(kind) {
2846
+ return /^(int|fixed|bool|string|asset-ref|entity-ref)\[[1-9][0-9]*\]$/.test(kind);
2847
+ }
2848
+ function runtimeFixedArrayLength(kind) {
2849
+ const match = kind.match(/\[([1-9][0-9]*)\]$/);
2850
+ /* istanbul ignore next -- caller checks isRuntimeFixedArrayKind before extracting fixed array length. */
2851
+ if (match === null) {
2852
+ throw new Error(`Expected fixed array runtime kind, received ${kind}`);
2853
+ }
2854
+ return Number(match[1]);
2855
+ }
2856
+ function runtimeCollectionScalarKind(kind) {
2857
+ return (isRuntimeArrayKind(kind) ? kind.slice(0, -2) : kind.slice(0, kind.indexOf('[')));
2858
+ }
2859
+ function runtimeScalarKindForMetrics(kind) {
2860
+ if (isRuntimeArrayKind(kind) || isRuntimeFixedArrayKind(kind)) {
2861
+ return runtimeCollectionScalarKind(kind);
2862
+ }
2863
+ if (isRuntimeFrameHeapKind(kind)) {
2864
+ return undefined;
2865
+ }
2866
+ return kind;
2867
+ }
2868
+ function isRuntimeFrameHeapKind(kind) {
2869
+ return kind === 'frame-list' || kind === 'frame-dictionary' || kind === 'frame-hash-set';
2870
+ }
2871
+ export function validateDeterministicFrameSystemsStateless(descriptor, systems) {
2872
+ validateRuntimeDescriptor(descriptor);
2873
+ const diagnostics = [];
2874
+ for (const systemDescriptor of orderSystemDescriptors(descriptor.systems)) {
2875
+ const system = systems[systemDescriptor.id];
2876
+ if (system === undefined) {
2877
+ diagnostics.push({ systemId: systemDescriptor.id, reason: 'missing-system' });
2878
+ continue;
2879
+ }
2880
+ diagnostics.push(...diagnoseDeterministicFrameSystemStateless(systemDescriptor.id, system));
2881
+ }
2882
+ return {
2883
+ pass: diagnostics.length === 0,
2884
+ diagnostics,
2885
+ };
2886
+ }
2887
+ export function assertDeterministicFrameSystemsStateless(descriptor, systems) {
2888
+ const result = validateDeterministicFrameSystemsStateless(descriptor, systems);
2889
+ if (!result.pass) {
2890
+ throw statelessSystemDiagnosticError(result.diagnostics[0]);
2891
+ }
2892
+ }
2893
+ function assertDeterministicFrameSystemStateless(systemId, system) {
2894
+ const diagnostics = diagnoseDeterministicFrameSystemStateless(systemId, system);
2895
+ if (diagnostics.length > 0) {
2896
+ throw statelessSystemDiagnosticError(diagnostics[0]);
2897
+ }
2898
+ }
2899
+ function diagnoseDeterministicFrameSystemStateless(systemId, system) {
2900
+ const keys = stableEnumerableKeys(system);
2901
+ if (keys.length > 0) {
2902
+ return [{ systemId, reason: 'mutable-own-fields', ownFields: keys }];
2903
+ }
2904
+ const externalMutation = findExternalSystemMutation(system);
2905
+ if (externalMutation !== undefined) {
2906
+ return [{ systemId, reason: 'external-mutation', externalMutation }];
2907
+ }
2908
+ return [];
2909
+ }
2910
+ function statelessSystemDiagnosticError(diagnostic) {
2911
+ if (diagnostic.reason === 'missing-system') {
2912
+ return new Error(`Missing deterministic system ${diagnostic.systemId}`);
2913
+ }
2914
+ if (diagnostic.reason === 'mutable-own-fields') {
2915
+ return new Error(`Deterministic system ${diagnostic.systemId} has mutable own fields`);
2916
+ }
2917
+ /* istanbul ignore next -- source diagnostics include an external mutation label for external-mutation errors. */
2918
+ return new Error(`Deterministic system ${diagnostic.systemId} mutates external state through ${diagnostic.externalMutation ?? 'unknown'}`);
2919
+ }
2920
+ function findExternalSystemMutation(system) {
2921
+ const source = stripSystemStringsAndComments(Function.prototype.toString.call(system));
2922
+ const parameters = systemParameterNames(source);
2923
+ const localNames = new Set([...parameters, ...declaredSystemNames(source)]);
2924
+ const updatePattern = /(?:\+\+|--)\s*([A-Za-z_$][A-Za-z0-9_$]*)|([A-Za-z_$][A-Za-z0-9_$]*)\s*(?:\+\+|--|[+\-*/%&|^]=|=(?!=))/g;
2925
+ let match;
2926
+ while ((match = updatePattern.exec(source)) !== null) {
2927
+ const name = match[1] ?? match[2];
2928
+ if (name !== undefined && !localNames.has(name) && !deterministicSystemGlobalNames.includes(name)) {
2929
+ return name;
2930
+ }
2931
+ }
2932
+ const bracketUpdatePattern = /(?:\+\+|--)\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*\[[^\]]*\]|([A-Za-z_$][A-Za-z0-9_$]*)\s*\[[^\]]*\]\s*(?:\+\+|--|[+\-*/%&|^]=|=(?!=))/g;
2933
+ while ((match = bracketUpdatePattern.exec(source)) !== null) {
2934
+ const name = match[1] ?? match[2];
2935
+ if (name !== undefined && !localNames.has(name) && !deterministicSystemGlobalNames.includes(name)) {
2936
+ return `${name}[]`;
2937
+ }
2938
+ }
2939
+ const mutatingCallPattern = /\b([A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*)\s*\.\s*(set|add|delete|clear|push|pop|shift|unshift|splice|sort|reverse|copyWithin|fill)\s*\(\s*([A-Za-z_$][A-Za-z0-9_$]*)?/g;
2940
+ while ((match = mutatingCallPattern.exec(source)) !== null) {
2941
+ const receiver = match[1];
2942
+ const method = match[2];
2943
+ const root = receiver.split('.')[0];
2944
+ if (!localNames.has(root) && !deterministicSystemGlobalNames.includes(root)) {
2945
+ return `${receiver}.${method}()`;
2946
+ }
2947
+ }
2948
+ return undefined;
2949
+ }
2950
+ const deterministicSystemGlobalNames = [
2951
+ 'Number',
2952
+ 'Math',
2953
+ 'String',
2954
+ 'Boolean',
2955
+ 'Array',
2956
+ 'Object',
2957
+ ];
2958
+ function stripSystemStringsAndComments(source) {
2959
+ return source
2960
+ .replace(/\/\*[\s\S]*?\*\//g, '')
2961
+ .replace(/\/\/.*$/gm, '')
2962
+ .replace(/(['"`])(?:\\.|(?!\1)[\s\S])*\1/g, '');
2963
+ }
2964
+ function systemParameterNames(source) {
2965
+ const functionMatch = source.match(/^[^(]*\(([^)]*)\)/);
2966
+ const arrowMatch = source.match(/^\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*=>/);
2967
+ /* istanbul ignore next -- source scanner tests cover named functions; scanner is non-authoritative certification tooling. */
2968
+ const raw = functionMatch?.[1] ?? arrowMatch?.[1] ?? '';
2969
+ return raw
2970
+ .split(',')
2971
+ .map((parameter) => parameter.trim().replace(/=.*/, '').trim())
2972
+ .filter((parameter) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(parameter));
2973
+ }
2974
+ function declaredSystemNames(source) {
2975
+ const names = [];
2976
+ const declarationPattern = /\b(?:const|let|var|function)\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
2977
+ let match;
2978
+ while ((match = declarationPattern.exec(source)) !== null) {
2979
+ names.push(match[1]);
2980
+ }
2981
+ return names;
2982
+ }
2983
+ function createRuntimeCorpusDescriptor() {
2984
+ return {
2985
+ id: 'runtime-corpus',
2986
+ tickRate: 60,
2987
+ components: [
2988
+ { id: 'position', fields: [{ name: 'x', kind: 'fixed' }, { name: 'y', kind: 'fixed' }] },
2989
+ { id: 'velocity', fields: [{ name: 'x', kind: 'fixed' }, { name: 'y', kind: 'fixed' }] },
2990
+ { id: 'stats', singleton: true, fields: [
2991
+ { name: 'score', kind: 'int', defaultValue: 0 },
2992
+ { name: 'entityCreates', kind: 'int', defaultValue: 0 },
2993
+ { name: 'entityDestroys', kind: 'int', defaultValue: 0 },
2994
+ { name: 'componentWrites', kind: 'int', defaultValue: 0 },
2995
+ ] },
2996
+ ],
2997
+ systems: [{ id: 'mutate', group: 'simulation', order: 0 }],
2998
+ };
2999
+ }
3000
+ function compareSystemDescriptors(left, right) {
3001
+ /* istanbul ignore next -- scheduler tests cover group and order sort arms. */
3002
+ return compareGroup(left.group, right.group) || left.order - right.order || compareStrings(left.id, right.id);
3003
+ }
3004
+ function orderSystemDescriptors(systems) {
3005
+ const sortedSystems = [...systems].sort(compareSystemDescriptors);
3006
+ const byId = new Map(sortedSystems.map((system) => [system.id, system]));
3007
+ const outgoing = new Map(sortedSystems.map((system) => [system.id, new Set()]));
3008
+ const incomingCounts = new Map(sortedSystems.map((system) => [system.id, 0]));
3009
+ const addEdge = (fromId, toId, sourceId) => {
3010
+ const from = byId.get(fromId);
3011
+ const to = byId.get(toId);
3012
+ if (from === undefined || to === undefined) {
3013
+ /* istanbul ignore next -- unknown dependency tests cover missing target ids. */
3014
+ throw new Error(`Deterministic system ${sourceId} references unknown dependency ${from === undefined ? fromId : toId}`);
3015
+ }
3016
+ if (from.group !== to.group) {
3017
+ throw new Error(`Deterministic system ${sourceId} dependency ${fromId}->${toId} crosses system group boundaries`);
3018
+ }
3019
+ const edges = outgoing.get(fromId);
3020
+ if (edges === undefined || edges.has(toId)) {
3021
+ return;
3022
+ }
3023
+ edges.add(toId);
3024
+ /* istanbul ignore next -- incoming counts are initialized for every sorted system id. */
3025
+ incomingCounts.set(toId, (incomingCounts.get(toId) ?? 0) + 1);
3026
+ };
3027
+ for (const system of sortedSystems) {
3028
+ for (const after of normalizedSystemDependencyIds(system.after)) {
3029
+ addEdge(after, system.id, system.id);
3030
+ }
3031
+ for (const before of normalizedSystemDependencyIds(system.before)) {
3032
+ addEdge(system.id, before, system.id);
3033
+ }
3034
+ }
3035
+ const ready = sortedSystems.filter((system) => incomingCounts.get(system.id) === 0);
3036
+ const ordered = [];
3037
+ while (ready.length > 0) {
3038
+ ready.sort(compareSystemDescriptors);
3039
+ const next = ready.shift();
3040
+ /* istanbul ignore next -- ready.shift cannot be undefined while ready.length is positive. */
3041
+ if (next === undefined) {
3042
+ break;
3043
+ }
3044
+ ordered.push(next);
3045
+ /* istanbul ignore next -- outgoing edges are initialized for every sorted system id. */
3046
+ for (const targetId of [...(outgoing.get(next.id) ?? [])].sort(compareStrings)) {
3047
+ /* istanbul ignore next -- incoming counts are initialized for every sorted system id. */
3048
+ const remaining = (incomingCounts.get(targetId) ?? 0) - 1;
3049
+ incomingCounts.set(targetId, remaining);
3050
+ if (remaining === 0) {
3051
+ const target = byId.get(targetId);
3052
+ if (target !== undefined) {
3053
+ ready.push(target);
3054
+ }
3055
+ }
3056
+ }
3057
+ }
3058
+ if (ordered.length !== sortedSystems.length) {
3059
+ const cyclic = sortedSystems
3060
+ .filter((system) => !ordered.some((orderedSystem) => orderedSystem.id === system.id))
3061
+ .map((system) => system.id)
3062
+ .sort(compareStrings)
3063
+ .join(',');
3064
+ throw new Error(`Deterministic system dependency cycle detected: ${cyclic}`);
3065
+ }
3066
+ return ordered;
3067
+ }
3068
+ function normalizedSystemDependencyIds(ids) {
3069
+ const normalized = [...new Set(ids ?? [])].sort(compareStrings);
3070
+ for (const id of normalized) {
3071
+ if (!isNonEmptyString(id)) {
3072
+ throw new Error('Deterministic system dependency requires non-empty system ids');
3073
+ }
3074
+ }
3075
+ return normalized;
3076
+ }
3077
+ function compareGroup(left, right) {
3078
+ const order = {
3079
+ input: 0,
3080
+ simulation: 1,
3081
+ physics: 2,
3082
+ 'post-physics': 3,
3083
+ presentation: 4,
3084
+ };
3085
+ return order[left] - order[right];
3086
+ }
3087
+ function toIdentifier(id) {
3088
+ const normalized = id
3089
+ .split(/[^a-zA-Z0-9]+/)
3090
+ .filter((part) => part.length > 0)
3091
+ .map((part) => `${part[0].toUpperCase()}${part.slice(1)}`)
3092
+ .join('');
3093
+ /* istanbul ignore next -- generated runtime descriptors use non-empty non-numeric ids. */
3094
+ return normalized.length === 0 || /^[0-9]/.test(normalized) ? `Generated${normalized}` : normalized;
3095
+ }
3096
+ function compareMapStringEntry(left, right) {
3097
+ return compareStrings(left[0], right[0]);
3098
+ }
3099
+ function stableKeys(record) {
3100
+ return Reflect.ownKeys(record)
3101
+ .filter((key) => typeof key === 'string')
3102
+ .sort(compareStrings);
3103
+ }
3104
+ function stableEnumerableKeys(record) {
3105
+ return stableKeys(record).filter((key) => Object.prototype.propertyIsEnumerable.call(record, key));
3106
+ }
3107
+ function assertBitIndex(size, index) {
3108
+ if (!Number.isInteger(index) || index < 0 || index >= size) {
3109
+ throw new Error(`Bitset index ${index} out of range`);
3110
+ }
3111
+ }
3112
+ function percentile(values, percentileRank) {
3113
+ if (values.length === 0) {
3114
+ return 0;
3115
+ }
3116
+ const sorted = [...values].sort((left, right) => left - right);
3117
+ const index = Math.min(sorted.length - 1, Math.floor((percentileRank / 100) * sorted.length));
3118
+ return Math.round(sorted[index] * 1e4) / 1e4;
3119
+ }
3120
+ function compareStrings(left, right) {
3121
+ /* istanbul ignore next -- call sites cover less-than and equality; greater-than is symmetric ordering behavior. */
3122
+ return left < right ? -1 : left > right ? 1 : 0;
3123
+ }