@multitapio/multitap 0.0.1

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 (286) hide show
  1. package/dist/.stamp +0 -0
  2. package/dist/cache.d.ts +117 -0
  3. package/dist/cache.d.ts.map +1 -0
  4. package/dist/channel.d.ts +91 -0
  5. package/dist/channel.d.ts.map +1 -0
  6. package/dist/channel.worker.d.ts +3 -0
  7. package/dist/channel.worker.d.ts.map +1 -0
  8. package/dist/codegen/config.d.ts +107 -0
  9. package/dist/codegen/config.d.ts.map +1 -0
  10. package/dist/codegen/generator.d.ts +78 -0
  11. package/dist/codegen/generator.d.ts.map +1 -0
  12. package/dist/codegen/index.d.ts +13 -0
  13. package/dist/codegen/index.d.ts.map +1 -0
  14. package/dist/codegen/inject.d.ts +51 -0
  15. package/dist/codegen/inject.d.ts.map +1 -0
  16. package/dist/codegen/rust.d.ts +22 -0
  17. package/dist/codegen/rust.d.ts.map +1 -0
  18. package/dist/codegen/typescript.d.ts +22 -0
  19. package/dist/codegen/typescript.d.ts.map +1 -0
  20. package/dist/constants.d.ts +5 -0
  21. package/dist/constants.d.ts.map +1 -0
  22. package/dist/crypto.d.ts +141 -0
  23. package/dist/crypto.d.ts.map +1 -0
  24. package/dist/debug.d.ts +39 -0
  25. package/dist/debug.d.ts.map +1 -0
  26. package/dist/diagnostics/index.d.ts +12 -0
  27. package/dist/diagnostics/index.d.ts.map +1 -0
  28. package/dist/diagnostics.js +5502 -0
  29. package/dist/e2e/cli.d.ts +2 -0
  30. package/dist/e2e/cli.d.ts.map +1 -0
  31. package/dist/executor.d.ts +130 -0
  32. package/dist/executor.d.ts.map +1 -0
  33. package/dist/globals.d.ts +32 -0
  34. package/dist/helpers.d.ts +3 -0
  35. package/dist/helpers.d.ts.map +1 -0
  36. package/dist/input-codec.d.ts +136 -0
  37. package/dist/input-codec.d.ts.map +1 -0
  38. package/dist/input-graph.d.ts +106 -0
  39. package/dist/input-graph.d.ts.map +1 -0
  40. package/dist/lib.d.ts +10 -0
  41. package/dist/lib.d.ts.map +1 -0
  42. package/dist/lib.js +9409 -0
  43. package/dist/messages.d.ts +63 -0
  44. package/dist/messages.d.ts.map +1 -0
  45. package/dist/peer-mesh.d.ts +77 -0
  46. package/dist/peer-mesh.d.ts.map +1 -0
  47. package/dist/react/Crosshair.d.ts +17 -0
  48. package/dist/react/Crosshair.d.ts.map +1 -0
  49. package/dist/react/Player.d.ts +10 -0
  50. package/dist/react/Player.d.ts.map +1 -0
  51. package/dist/react/PlayerPoseContext.d.ts +31 -0
  52. package/dist/react/PlayerPoseContext.d.ts.map +1 -0
  53. package/dist/react/hooks/index.d.ts +7 -0
  54. package/dist/react/hooks/index.d.ts.map +1 -0
  55. package/dist/react/hooks/useEvent.d.ts +46 -0
  56. package/dist/react/hooks/useEvent.d.ts.map +1 -0
  57. package/dist/react/hooks/useFrame.d.ts +23 -0
  58. package/dist/react/hooks/useFrame.d.ts.map +1 -0
  59. package/dist/react/hooks/usePlayers.d.ts +24 -0
  60. package/dist/react/hooks/usePlayers.d.ts.map +1 -0
  61. package/dist/react/hooks/useQuery.d.ts +41 -0
  62. package/dist/react/hooks/useQuery.d.ts.map +1 -0
  63. package/dist/react/hooks/useSession.d.ts +10 -0
  64. package/dist/react/hooks/useSession.d.ts.map +1 -0
  65. package/dist/react/index.d.ts +18 -0
  66. package/dist/react/index.d.ts.map +1 -0
  67. package/dist/react/input/DesktopInputMapper.d.ts +38 -0
  68. package/dist/react/input/DesktopInputMapper.d.ts.map +1 -0
  69. package/dist/react/input/index.d.ts +4 -0
  70. package/dist/react/input/index.d.ts.map +1 -0
  71. package/dist/react/input/types.d.ts +46 -0
  72. package/dist/react/input/types.d.ts.map +1 -0
  73. package/dist/react/providers/SessionProvider.d.ts +17 -0
  74. package/dist/react/providers/SessionProvider.d.ts.map +1 -0
  75. package/dist/react/providers/index.d.ts +2 -0
  76. package/dist/react/providers/index.d.ts.map +1 -0
  77. package/dist/react/types/index.d.ts +2 -0
  78. package/dist/react/types/index.d.ts.map +1 -0
  79. package/dist/react/types/session.d.ts +88 -0
  80. package/dist/react/types/session.d.ts.map +1 -0
  81. package/dist/react/types.d.ts +134 -0
  82. package/dist/react/types.d.ts.map +1 -0
  83. package/dist/react/utils/math.d.ts +19 -0
  84. package/dist/react/utils/math.d.ts.map +1 -0
  85. package/dist/react/views/AsteroidsView.d.ts +24 -0
  86. package/dist/react/views/AsteroidsView.d.ts.map +1 -0
  87. package/dist/react/views/FirstPersonView.d.ts +10 -0
  88. package/dist/react/views/FirstPersonView.d.ts.map +1 -0
  89. package/dist/react/views/IsometricView.d.ts +21 -0
  90. package/dist/react/views/IsometricView.d.ts.map +1 -0
  91. package/dist/react/views/MapView.d.ts +16 -0
  92. package/dist/react/views/MapView.d.ts.map +1 -0
  93. package/dist/react/views/MobaView.d.ts +17 -0
  94. package/dist/react/views/MobaView.d.ts.map +1 -0
  95. package/dist/react/views/SideOnView.d.ts +23 -0
  96. package/dist/react/views/SideOnView.d.ts.map +1 -0
  97. package/dist/react/views/ThirdPersonFixedView.d.ts +21 -0
  98. package/dist/react/views/ThirdPersonFixedView.d.ts.map +1 -0
  99. package/dist/react/views/ThirdPersonView.d.ts +12 -0
  100. package/dist/react/views/ThirdPersonView.d.ts.map +1 -0
  101. package/dist/react/views/TopDownFixedView.d.ts +20 -0
  102. package/dist/react/views/TopDownFixedView.d.ts.map +1 -0
  103. package/dist/react/views/TopDownView.d.ts +12 -0
  104. package/dist/react/views/TopDownView.d.ts.map +1 -0
  105. package/dist/react/views/TwinStickView.d.ts +13 -0
  106. package/dist/react/views/TwinStickView.d.ts.map +1 -0
  107. package/dist/react/views/VehicleFixedView.d.ts +16 -0
  108. package/dist/react/views/VehicleFixedView.d.ts.map +1 -0
  109. package/dist/react/views/VehicleView.d.ts +13 -0
  110. package/dist/react/views/VehicleView.d.ts.map +1 -0
  111. package/dist/react/views/index.d.ts +21 -0
  112. package/dist/react/views/index.d.ts.map +1 -0
  113. package/dist/react/views/types.d.ts +136 -0
  114. package/dist/react/views/types.d.ts.map +1 -0
  115. package/dist/rollback.d.ts +193 -0
  116. package/dist/rollback.d.ts.map +1 -0
  117. package/dist/rollback.worker.d.ts +3 -0
  118. package/dist/rollback.worker.d.ts.map +1 -0
  119. package/dist/schema.d.ts +309 -0
  120. package/dist/schema.d.ts.map +1 -0
  121. package/dist/session-config.d.ts +119 -0
  122. package/dist/session-config.d.ts.map +1 -0
  123. package/dist/session.d.ts +120 -0
  124. package/dist/session.d.ts.map +1 -0
  125. package/dist/state.d.ts +400 -0
  126. package/dist/state.d.ts.map +1 -0
  127. package/dist/stats.d.ts +21 -0
  128. package/dist/stats.d.ts.map +1 -0
  129. package/dist/test-session.d.ts +170 -0
  130. package/dist/test-session.d.ts.map +1 -0
  131. package/dist/types/cache.d.ts +117 -0
  132. package/dist/types/cache.d.ts.map +1 -0
  133. package/dist/types/channel.d.ts +91 -0
  134. package/dist/types/channel.d.ts.map +1 -0
  135. package/dist/types/channel.worker.d.ts +3 -0
  136. package/dist/types/channel.worker.d.ts.map +1 -0
  137. package/dist/types/codegen/config.d.ts +107 -0
  138. package/dist/types/codegen/config.d.ts.map +1 -0
  139. package/dist/types/codegen/generator.d.ts +78 -0
  140. package/dist/types/codegen/generator.d.ts.map +1 -0
  141. package/dist/types/codegen/index.d.ts +13 -0
  142. package/dist/types/codegen/index.d.ts.map +1 -0
  143. package/dist/types/codegen/inject.d.ts +51 -0
  144. package/dist/types/codegen/inject.d.ts.map +1 -0
  145. package/dist/types/codegen/rust.d.ts +22 -0
  146. package/dist/types/codegen/rust.d.ts.map +1 -0
  147. package/dist/types/codegen/typescript.d.ts +22 -0
  148. package/dist/types/codegen/typescript.d.ts.map +1 -0
  149. package/dist/types/constants.d.ts +5 -0
  150. package/dist/types/constants.d.ts.map +1 -0
  151. package/dist/types/crypto.d.ts +141 -0
  152. package/dist/types/crypto.d.ts.map +1 -0
  153. package/dist/types/debug.d.ts +39 -0
  154. package/dist/types/debug.d.ts.map +1 -0
  155. package/dist/types/diagnostics/index.d.ts +12 -0
  156. package/dist/types/diagnostics/index.d.ts.map +1 -0
  157. package/dist/types/e2e/cli.d.ts +2 -0
  158. package/dist/types/e2e/cli.d.ts.map +1 -0
  159. package/dist/types/executor.d.ts +130 -0
  160. package/dist/types/executor.d.ts.map +1 -0
  161. package/dist/types/helpers.d.ts +3 -0
  162. package/dist/types/helpers.d.ts.map +1 -0
  163. package/dist/types/input-codec.d.ts +136 -0
  164. package/dist/types/input-codec.d.ts.map +1 -0
  165. package/dist/types/input-graph.d.ts +106 -0
  166. package/dist/types/input-graph.d.ts.map +1 -0
  167. package/dist/types/lib.d.ts +10 -0
  168. package/dist/types/lib.d.ts.map +1 -0
  169. package/dist/types/messages.d.ts +63 -0
  170. package/dist/types/messages.d.ts.map +1 -0
  171. package/dist/types/peer-mesh.d.ts +77 -0
  172. package/dist/types/peer-mesh.d.ts.map +1 -0
  173. package/dist/types/react/Crosshair.d.ts +17 -0
  174. package/dist/types/react/Crosshair.d.ts.map +1 -0
  175. package/dist/types/react/Player.d.ts +10 -0
  176. package/dist/types/react/Player.d.ts.map +1 -0
  177. package/dist/types/react/PlayerPoseContext.d.ts +31 -0
  178. package/dist/types/react/PlayerPoseContext.d.ts.map +1 -0
  179. package/dist/types/react/hooks/index.d.ts +7 -0
  180. package/dist/types/react/hooks/index.d.ts.map +1 -0
  181. package/dist/types/react/hooks/useEvent.d.ts +46 -0
  182. package/dist/types/react/hooks/useEvent.d.ts.map +1 -0
  183. package/dist/types/react/hooks/useFrame.d.ts +23 -0
  184. package/dist/types/react/hooks/useFrame.d.ts.map +1 -0
  185. package/dist/types/react/hooks/usePlayers.d.ts +24 -0
  186. package/dist/types/react/hooks/usePlayers.d.ts.map +1 -0
  187. package/dist/types/react/hooks/useQuery.d.ts +41 -0
  188. package/dist/types/react/hooks/useQuery.d.ts.map +1 -0
  189. package/dist/types/react/hooks/useSession.d.ts +10 -0
  190. package/dist/types/react/hooks/useSession.d.ts.map +1 -0
  191. package/dist/types/react/index.d.ts +18 -0
  192. package/dist/types/react/index.d.ts.map +1 -0
  193. package/dist/types/react/input/DesktopInputMapper.d.ts +38 -0
  194. package/dist/types/react/input/DesktopInputMapper.d.ts.map +1 -0
  195. package/dist/types/react/input/index.d.ts +4 -0
  196. package/dist/types/react/input/index.d.ts.map +1 -0
  197. package/dist/types/react/input/types.d.ts +46 -0
  198. package/dist/types/react/input/types.d.ts.map +1 -0
  199. package/dist/types/react/providers/SessionProvider.d.ts +17 -0
  200. package/dist/types/react/providers/SessionProvider.d.ts.map +1 -0
  201. package/dist/types/react/providers/index.d.ts +2 -0
  202. package/dist/types/react/providers/index.d.ts.map +1 -0
  203. package/dist/types/react/types/index.d.ts +2 -0
  204. package/dist/types/react/types/index.d.ts.map +1 -0
  205. package/dist/types/react/types/session.d.ts +88 -0
  206. package/dist/types/react/types/session.d.ts.map +1 -0
  207. package/dist/types/react/types.d.ts +134 -0
  208. package/dist/types/react/types.d.ts.map +1 -0
  209. package/dist/types/react/utils/math.d.ts +19 -0
  210. package/dist/types/react/utils/math.d.ts.map +1 -0
  211. package/dist/types/react/views/AsteroidsView.d.ts +24 -0
  212. package/dist/types/react/views/AsteroidsView.d.ts.map +1 -0
  213. package/dist/types/react/views/FirstPersonView.d.ts +10 -0
  214. package/dist/types/react/views/FirstPersonView.d.ts.map +1 -0
  215. package/dist/types/react/views/IsometricView.d.ts +21 -0
  216. package/dist/types/react/views/IsometricView.d.ts.map +1 -0
  217. package/dist/types/react/views/MapView.d.ts +16 -0
  218. package/dist/types/react/views/MapView.d.ts.map +1 -0
  219. package/dist/types/react/views/MobaView.d.ts +17 -0
  220. package/dist/types/react/views/MobaView.d.ts.map +1 -0
  221. package/dist/types/react/views/SideOnView.d.ts +23 -0
  222. package/dist/types/react/views/SideOnView.d.ts.map +1 -0
  223. package/dist/types/react/views/ThirdPersonFixedView.d.ts +21 -0
  224. package/dist/types/react/views/ThirdPersonFixedView.d.ts.map +1 -0
  225. package/dist/types/react/views/ThirdPersonView.d.ts +12 -0
  226. package/dist/types/react/views/ThirdPersonView.d.ts.map +1 -0
  227. package/dist/types/react/views/TopDownFixedView.d.ts +20 -0
  228. package/dist/types/react/views/TopDownFixedView.d.ts.map +1 -0
  229. package/dist/types/react/views/TopDownView.d.ts +12 -0
  230. package/dist/types/react/views/TopDownView.d.ts.map +1 -0
  231. package/dist/types/react/views/TwinStickView.d.ts +13 -0
  232. package/dist/types/react/views/TwinStickView.d.ts.map +1 -0
  233. package/dist/types/react/views/VehicleFixedView.d.ts +16 -0
  234. package/dist/types/react/views/VehicleFixedView.d.ts.map +1 -0
  235. package/dist/types/react/views/VehicleView.d.ts +13 -0
  236. package/dist/types/react/views/VehicleView.d.ts.map +1 -0
  237. package/dist/types/react/views/index.d.ts +21 -0
  238. package/dist/types/react/views/index.d.ts.map +1 -0
  239. package/dist/types/react/views/types.d.ts +136 -0
  240. package/dist/types/react/views/types.d.ts.map +1 -0
  241. package/dist/types/rollback.d.ts +193 -0
  242. package/dist/types/rollback.d.ts.map +1 -0
  243. package/dist/types/rollback.worker.d.ts +3 -0
  244. package/dist/types/rollback.worker.d.ts.map +1 -0
  245. package/dist/types/schema.d.ts +309 -0
  246. package/dist/types/schema.d.ts.map +1 -0
  247. package/dist/types/session-config.d.ts +119 -0
  248. package/dist/types/session-config.d.ts.map +1 -0
  249. package/dist/types/session.d.ts +120 -0
  250. package/dist/types/session.d.ts.map +1 -0
  251. package/dist/types/state.d.ts +400 -0
  252. package/dist/types/state.d.ts.map +1 -0
  253. package/dist/types/stats.d.ts +21 -0
  254. package/dist/types/stats.d.ts.map +1 -0
  255. package/dist/types/test-session.d.ts +170 -0
  256. package/dist/types/test-session.d.ts.map +1 -0
  257. package/dist/types/utils.d.ts +26 -0
  258. package/dist/types/utils.d.ts.map +1 -0
  259. package/dist/types/vite/codegen-runner.d.ts +27 -0
  260. package/dist/types/vite/codegen-runner.d.ts.map +1 -0
  261. package/dist/types/vite/index.d.ts +20 -0
  262. package/dist/types/vite/index.d.ts.map +1 -0
  263. package/dist/types/vite/module-builder.d.ts +28 -0
  264. package/dist/types/vite/module-builder.d.ts.map +1 -0
  265. package/dist/types/vite/plugin.d.ts +27 -0
  266. package/dist/types/vite/plugin.d.ts.map +1 -0
  267. package/dist/types/vite/types.d.ts +37 -0
  268. package/dist/types/vite/types.d.ts.map +1 -0
  269. package/dist/types/vite/wasm-compiler.d.ts +32 -0
  270. package/dist/types/vite/wasm-compiler.d.ts.map +1 -0
  271. package/dist/utils.d.ts +26 -0
  272. package/dist/utils.d.ts.map +1 -0
  273. package/dist/vite/codegen-runner.d.ts +27 -0
  274. package/dist/vite/codegen-runner.d.ts.map +1 -0
  275. package/dist/vite/index.d.ts +20 -0
  276. package/dist/vite/index.d.ts.map +1 -0
  277. package/dist/vite/index.js +4202 -0
  278. package/dist/vite/module-builder.d.ts +28 -0
  279. package/dist/vite/module-builder.d.ts.map +1 -0
  280. package/dist/vite/plugin.d.ts +27 -0
  281. package/dist/vite/plugin.d.ts.map +1 -0
  282. package/dist/vite/types.d.ts +37 -0
  283. package/dist/vite/types.d.ts.map +1 -0
  284. package/dist/vite/wasm-compiler.d.ts +32 -0
  285. package/dist/vite/wasm-compiler.d.ts.map +1 -0
  286. package/package.json +92 -0
@@ -0,0 +1,4202 @@
1
+ // src/vite/plugin.ts
2
+ import { resolve as resolve2, dirname as dirname3 } from "node:path";
3
+
4
+ // src/vite/codegen-runner.ts
5
+ import { writeFile, mkdir } from "node:fs/promises";
6
+ import { join as join2 } from "node:path";
7
+
8
+ // src/codegen/config.ts
9
+ import { readFileSync, readdirSync, statSync } from "node:fs";
10
+ import { join, dirname, isAbsolute, resolve } from "node:path";
11
+ function findConfig(path) {
12
+ const stat2 = statSync(path);
13
+ if (!stat2.isDirectory()) {
14
+ return path;
15
+ }
16
+ const entries = readdirSync(path);
17
+ const matches = entries.filter(
18
+ (entry) => entry.endsWith("mt.config.json") && statSync(join(path, entry)).isFile()
19
+ );
20
+ if (matches.length === 0) {
21
+ throw new Error(`No *mt.config.json found in ${path}`);
22
+ }
23
+ if (matches.length > 1) {
24
+ throw new Error(`Multiple config files found in ${path}: ${matches.join(", ")} (specify exact path)`);
25
+ }
26
+ return join(path, matches[0]);
27
+ }
28
+ function loadConfig(path) {
29
+ const data = readFileSync(path, "utf-8");
30
+ try {
31
+ return JSON.parse(data);
32
+ } catch (error) {
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ throw new Error(`Cannot parse config file ${path}: ${message}`);
35
+ }
36
+ }
37
+ function loadProject(path) {
38
+ const configPath = findConfig(path);
39
+ const absConfigPath = resolve(configPath);
40
+ const config = loadConfig(absConfigPath);
41
+ return {
42
+ config,
43
+ configPath: absConfigPath,
44
+ dir: dirname(absConfigPath)
45
+ };
46
+ }
47
+ function resolvePath(path, baseDir) {
48
+ if (isAbsolute(path)) {
49
+ return path;
50
+ }
51
+ return join(baseDir, path);
52
+ }
53
+ function loadConfigGraph(root) {
54
+ const plugins = /* @__PURE__ */ new Map();
55
+ const visited = /* @__PURE__ */ new Set();
56
+ const queue = [];
57
+ for (const p of root.config.plugins ?? []) {
58
+ const absPath = resolvePath(p, root.dir);
59
+ queue.push({ pluginPath: absPath, baseDir: root.dir });
60
+ }
61
+ while (queue.length > 0) {
62
+ const item = queue.shift();
63
+ if (visited.has(item.pluginPath)) {
64
+ continue;
65
+ }
66
+ visited.add(item.pluginPath);
67
+ const configPath = findConfig(item.pluginPath);
68
+ const config = loadConfig(configPath);
69
+ if (!config.wasm) {
70
+ throw new Error(`Plugin ${item.pluginPath}: missing required wasm.reservedBytes in config`);
71
+ }
72
+ if (config.wasm.reservedBytes <= 0) {
73
+ throw new Error(`Plugin ${item.pluginPath}: wasm.reservedBytes must be > 0`);
74
+ }
75
+ const maxReservedBytes = 256 * 1024 * 1024;
76
+ if (config.wasm.reservedBytes > maxReservedBytes) {
77
+ throw new Error(
78
+ `Plugin ${item.pluginPath}: wasm.reservedBytes ${config.wasm.reservedBytes} exceeds maximum ${maxReservedBytes}`
79
+ );
80
+ }
81
+ const pluginDir = dirname(configPath);
82
+ const deps = [];
83
+ for (const dep of config.plugins ?? []) {
84
+ const absDepPath = resolvePath(dep, pluginDir);
85
+ deps.push(absDepPath);
86
+ if (!visited.has(absDepPath)) {
87
+ queue.push({ pluginPath: absDepPath, baseDir: pluginDir });
88
+ }
89
+ }
90
+ plugins.set(item.pluginPath, {
91
+ path: item.pluginPath,
92
+ config,
93
+ dependencies: deps
94
+ });
95
+ }
96
+ const order = topologicalSort(plugins);
97
+ return {
98
+ root,
99
+ plugins,
100
+ order
101
+ };
102
+ }
103
+ function topologicalSort(plugins) {
104
+ const inDegree = /* @__PURE__ */ new Map();
105
+ for (const [path, node] of plugins) {
106
+ inDegree.set(path, node.dependencies.length);
107
+ }
108
+ let queue = [];
109
+ for (const [path, degree] of inDegree) {
110
+ if (degree === 0) {
111
+ queue.push(path);
112
+ }
113
+ }
114
+ queue.sort();
115
+ const order = [];
116
+ while (queue.length > 0) {
117
+ const path = queue.shift();
118
+ order.push(path);
119
+ for (const [otherPath, node] of plugins) {
120
+ for (const dep of node.dependencies) {
121
+ if (dep === path) {
122
+ const newDegree = inDegree.get(otherPath) - 1;
123
+ inDegree.set(otherPath, newDegree);
124
+ if (newDegree === 0) {
125
+ queue.push(otherPath);
126
+ queue.sort();
127
+ }
128
+ }
129
+ }
130
+ }
131
+ }
132
+ if (order.length !== plugins.size) {
133
+ throw new Error("Cycle detected in plugin dependencies");
134
+ }
135
+ return order;
136
+ }
137
+ function mergeConfigs(graph) {
138
+ const componentOwners = /* @__PURE__ */ new Map();
139
+ const eventOwners = /* @__PURE__ */ new Map();
140
+ const mergedComponents = [];
141
+ const mergedEvents = [];
142
+ for (const pluginPath of graph.order) {
143
+ const node = graph.plugins.get(pluginPath);
144
+ for (const comp of node.config.state.components ?? []) {
145
+ if (componentOwners.has(comp.name)) {
146
+ const owner = componentOwners.get(comp.name);
147
+ throw new Error(`Component "${comp.name}" defined in both ${owner} and ${pluginPath}`);
148
+ }
149
+ componentOwners.set(comp.name, pluginPath);
150
+ mergedComponents.push(comp);
151
+ }
152
+ }
153
+ for (const comp of graph.root.config.state.components ?? []) {
154
+ if (componentOwners.has(comp.name)) {
155
+ const owner = componentOwners.get(comp.name);
156
+ throw new Error(`Component "${comp.name}" defined in both ${owner} and root config`);
157
+ }
158
+ componentOwners.set(comp.name, graph.root.dir);
159
+ mergedComponents.push(comp);
160
+ }
161
+ for (const pluginPath of graph.order) {
162
+ const node = graph.plugins.get(pluginPath);
163
+ for (const event of node.config.state.events ?? []) {
164
+ if (eventOwners.has(event.name)) {
165
+ const owner = eventOwners.get(event.name);
166
+ throw new Error(`Event "${event.name}" defined in both ${owner} and ${pluginPath}`);
167
+ }
168
+ eventOwners.set(event.name, pluginPath);
169
+ mergedEvents.push(event);
170
+ }
171
+ }
172
+ for (const event of graph.root.config.state.events ?? []) {
173
+ if (eventOwners.has(event.name)) {
174
+ const owner = eventOwners.get(event.name);
175
+ throw new Error(`Event "${event.name}" defined in both ${owner} and root config`);
176
+ }
177
+ eventOwners.set(event.name, graph.root.dir);
178
+ mergedEvents.push(event);
179
+ }
180
+ const mergedState = {
181
+ maxEntities: graph.root.config.state.maxEntities,
182
+ components: mergedComponents
183
+ };
184
+ if (mergedEvents.length > 0) {
185
+ mergedState.events = mergedEvents;
186
+ }
187
+ const mergedConfig = {
188
+ appID: graph.root.config.appID,
189
+ entrypoint: graph.root.config.entrypoint,
190
+ output: graph.root.config.output,
191
+ tickRate: graph.root.config.tickRate,
192
+ maxTicks: graph.root.config.maxTicks,
193
+ maxParticipants: graph.root.config.maxParticipants,
194
+ input: graph.root.config.input,
195
+ state: mergedState
196
+ };
197
+ if (graph.root.config.tests) {
198
+ mergedConfig.tests = graph.root.config.tests;
199
+ }
200
+ if (graph.root.config.fixtures !== void 0) {
201
+ mergedConfig.fixtures = graph.root.config.fixtures;
202
+ }
203
+ return {
204
+ config: mergedConfig,
205
+ pluginOrder: graph.order,
206
+ componentOwners
207
+ };
208
+ }
209
+
210
+ // src/codegen/inject.ts
211
+ function buildPlayerComponent(inputSchema) {
212
+ const fields = [{ name: "index", type: "uint8" }];
213
+ for (const ctrl of inputSchema.controls) {
214
+ switch (ctrl.type) {
215
+ case "vec2":
216
+ fields.push(
217
+ { name: `${ctrl.name}_x`, type: "f32" },
218
+ { name: `${ctrl.name}_y`, type: "f32" }
219
+ );
220
+ break;
221
+ case "vec3":
222
+ fields.push(
223
+ { name: `${ctrl.name}_x`, type: "f32" },
224
+ { name: `${ctrl.name}_y`, type: "f32" },
225
+ { name: `${ctrl.name}_z`, type: "f32" }
226
+ );
227
+ break;
228
+ case "quat":
229
+ fields.push(
230
+ { name: `${ctrl.name}_x`, type: "f32" },
231
+ { name: `${ctrl.name}_y`, type: "f32" },
232
+ { name: `${ctrl.name}_z`, type: "f32" },
233
+ { name: `${ctrl.name}_w`, type: "f32" }
234
+ );
235
+ break;
236
+ case "flags8":
237
+ fields.push({ name: ctrl.name, type: "uint8" });
238
+ break;
239
+ case "flags16":
240
+ fields.push({ name: ctrl.name, type: "uint16" });
241
+ break;
242
+ case "flags32":
243
+ fields.push({ name: ctrl.name, type: "uint32" });
244
+ break;
245
+ default:
246
+ fields.push({ name: ctrl.name, type: ctrl.type });
247
+ break;
248
+ }
249
+ }
250
+ return {
251
+ name: "Player",
252
+ type: "compound",
253
+ fields
254
+ };
255
+ }
256
+ function injectPlayerComponent(schema, inputSchema) {
257
+ for (const comp of schema.components) {
258
+ if (comp.name === "Player") {
259
+ throw new Error(
260
+ "Schema already contains a 'Player' component; this is reserved for input-derived generation"
261
+ );
262
+ }
263
+ }
264
+ const playerComp = buildPlayerComponent(inputSchema);
265
+ schema.components.push(playerComp);
266
+ }
267
+ function buildCommandEvents(inputSchema) {
268
+ const events = [];
269
+ for (const cmd of inputSchema.commands) {
270
+ const fields = [{ name: "player_index", type: "uint8" }];
271
+ for (const arg of cmd.args) {
272
+ switch (arg.type) {
273
+ case "vec2":
274
+ fields.push(
275
+ { name: `${arg.name}_x`, type: "f32" },
276
+ { name: `${arg.name}_y`, type: "f32" }
277
+ );
278
+ break;
279
+ case "vec3":
280
+ fields.push(
281
+ { name: `${arg.name}_x`, type: "f32" },
282
+ { name: `${arg.name}_y`, type: "f32" },
283
+ { name: `${arg.name}_z`, type: "f32" }
284
+ );
285
+ break;
286
+ case "quat":
287
+ fields.push(
288
+ { name: `${arg.name}_x`, type: "f32" },
289
+ { name: `${arg.name}_y`, type: "f32" },
290
+ { name: `${arg.name}_z`, type: "f32" },
291
+ { name: `${arg.name}_w`, type: "f32" }
292
+ );
293
+ break;
294
+ case "flags8":
295
+ fields.push({ name: arg.name, type: "uint8" });
296
+ break;
297
+ case "flags16":
298
+ fields.push({ name: arg.name, type: "uint16" });
299
+ break;
300
+ case "flags32":
301
+ fields.push({ name: arg.name, type: "uint32" });
302
+ break;
303
+ default:
304
+ fields.push({ name: arg.name, type: arg.type });
305
+ break;
306
+ }
307
+ }
308
+ const eventName = cmd.name.charAt(0).toUpperCase() + cmd.name.slice(1) + "Command";
309
+ events.push({
310
+ name: eventName,
311
+ maxEvents: 16,
312
+ // Default max commands per tick
313
+ fields
314
+ });
315
+ }
316
+ return events;
317
+ }
318
+ function injectCommandEvents(schema, inputSchema) {
319
+ const commandEvents = buildCommandEvents(inputSchema);
320
+ const existingNames = /* @__PURE__ */ new Set();
321
+ for (const event of schema.events ?? []) {
322
+ existingNames.add(event.name);
323
+ }
324
+ for (const cmdEvent of commandEvents) {
325
+ if (existingNames.has(cmdEvent.name)) {
326
+ throw new Error(
327
+ `Schema already contains event '${cmdEvent.name}'; command-derived events use {Name}Command naming`
328
+ );
329
+ }
330
+ }
331
+ if (!schema.events) {
332
+ schema.events = [];
333
+ }
334
+ schema.events.push(...commandEvents);
335
+ }
336
+ function prepareStateSchema(stateSchema, inputSchema) {
337
+ const schema = {
338
+ maxEntities: stateSchema.maxEntities,
339
+ components: [...stateSchema.components ?? []]
340
+ };
341
+ if (stateSchema.events) {
342
+ schema.events = [...stateSchema.events];
343
+ }
344
+ injectPlayerComponent(schema, inputSchema);
345
+ injectCommandEvents(schema, inputSchema);
346
+ return schema;
347
+ }
348
+
349
+ // src/codegen/generator.ts
350
+ function toUpperSnake(s) {
351
+ let result = "";
352
+ for (let i = 0; i < s.length; i++) {
353
+ const c = s[i];
354
+ if (i > 0 && c >= "A" && c <= "Z") {
355
+ result += "_";
356
+ }
357
+ result += c.toUpperCase();
358
+ }
359
+ return result;
360
+ }
361
+ function toLowerSnake(s) {
362
+ let result = "";
363
+ for (let i = 0; i < s.length; i++) {
364
+ const c = s[i];
365
+ if (i > 0 && c >= "A" && c <= "Z") {
366
+ result += "_";
367
+ }
368
+ result += c.toLowerCase();
369
+ }
370
+ return result;
371
+ }
372
+ function toCamelCase(s) {
373
+ if (s === "") return "";
374
+ const pascal = toPascalCase(s);
375
+ if (pascal === "") return "";
376
+ return pascal[0].toLowerCase() + pascal.slice(1);
377
+ }
378
+ function toPascalCase(s) {
379
+ if (s === "") return "";
380
+ let result = "";
381
+ let capitalizeNext = true;
382
+ for (const c of s) {
383
+ if (c === "_") {
384
+ capitalizeNext = true;
385
+ continue;
386
+ }
387
+ if (capitalizeNext) {
388
+ result += c.toUpperCase();
389
+ capitalizeNext = false;
390
+ } else {
391
+ result += c;
392
+ }
393
+ }
394
+ return result;
395
+ }
396
+ function needsEndian(type) {
397
+ switch (type) {
398
+ case "uint16":
399
+ case "int16":
400
+ case "uint32":
401
+ case "int32":
402
+ case "f32":
403
+ case "entityRef":
404
+ return true;
405
+ default:
406
+ return false;
407
+ }
408
+ }
409
+ function getElementSize(type) {
410
+ switch (type) {
411
+ case "bool":
412
+ case "uint8":
413
+ case "int8":
414
+ return 1;
415
+ case "uint16":
416
+ case "int16":
417
+ return 2;
418
+ case "uint32":
419
+ case "int32":
420
+ case "f32":
421
+ case "entityRef":
422
+ return 4;
423
+ default:
424
+ return 0;
425
+ }
426
+ }
427
+
428
+ // src/codegen/typescript.ts
429
+ function tsType(field) {
430
+ if (field.arrayElementType) {
431
+ return tsTypeMapping(field.arrayElementType);
432
+ }
433
+ if (field.type === "vec2" || field.type === "vec3" || field.type === "quat") {
434
+ return "number";
435
+ }
436
+ if (field.type === "enum") {
437
+ return "number";
438
+ }
439
+ return tsTypeMapping(field.type);
440
+ }
441
+ function tsTypeMapping(typ) {
442
+ switch (typ) {
443
+ case "bool":
444
+ return "boolean";
445
+ case "uint8":
446
+ case "int8":
447
+ case "uint16":
448
+ case "int16":
449
+ case "uint32":
450
+ case "int32":
451
+ case "f32":
452
+ case "entityRef":
453
+ return "number";
454
+ default:
455
+ throw new Error(`Unknown type '${typ}' in TypeScript code generation`);
456
+ }
457
+ }
458
+ function tsInputType(typ) {
459
+ switch (typ) {
460
+ case "bool":
461
+ return "boolean";
462
+ case "uint8":
463
+ case "int8":
464
+ case "uint16":
465
+ case "int16":
466
+ case "uint32":
467
+ case "int32":
468
+ case "f32":
469
+ case "flags8":
470
+ case "flags16":
471
+ case "flags32":
472
+ case "vec2":
473
+ case "vec3":
474
+ case "quat":
475
+ return "number";
476
+ default:
477
+ throw new Error(`Unknown input type '${typ}' in TypeScript code generation`);
478
+ }
479
+ }
480
+ function tsLoadFn(field) {
481
+ let typ = field.type;
482
+ if (field.arrayElementType) {
483
+ typ = field.arrayElementType;
484
+ }
485
+ if (typ === "vec2" || typ === "vec3" || typ === "quat") {
486
+ typ = "f32";
487
+ }
488
+ if (typ === "enum") {
489
+ typ = "uint8";
490
+ }
491
+ return tsLoadFnForType(typ);
492
+ }
493
+ function tsLoadFnForType(typ) {
494
+ switch (typ) {
495
+ case "bool":
496
+ case "uint8":
497
+ return "getUint8";
498
+ case "int8":
499
+ return "getInt8";
500
+ case "uint16":
501
+ return "getUint16";
502
+ case "int16":
503
+ return "getInt16";
504
+ case "uint32":
505
+ case "entityRef":
506
+ return "getUint32";
507
+ case "int32":
508
+ return "getInt32";
509
+ case "f32":
510
+ return "getFloat32";
511
+ default:
512
+ throw new Error(`Unknown type '${typ}' in TypeScript load function generation`);
513
+ }
514
+ }
515
+ function tsStoreFn(field) {
516
+ let typ = field.type;
517
+ if (field.arrayElementType) {
518
+ typ = field.arrayElementType;
519
+ }
520
+ if (typ === "vec2" || typ === "vec3" || typ === "quat") {
521
+ typ = "f32";
522
+ }
523
+ if (typ === "enum") {
524
+ typ = "uint8";
525
+ }
526
+ return tsStoreFnForType(typ);
527
+ }
528
+ function tsStoreFnForType(typ) {
529
+ switch (typ) {
530
+ case "bool":
531
+ case "uint8":
532
+ return "setUint8";
533
+ case "int8":
534
+ return "setInt8";
535
+ case "uint16":
536
+ return "setUint16";
537
+ case "int16":
538
+ return "setInt16";
539
+ case "uint32":
540
+ case "entityRef":
541
+ return "setUint32";
542
+ case "int32":
543
+ return "setInt32";
544
+ case "f32":
545
+ return "setFloat32";
546
+ default:
547
+ throw new Error(`Unknown type '${typ}' in TypeScript store function generation`);
548
+ }
549
+ }
550
+ function fieldNeedsEndian(field) {
551
+ let typ = field.type;
552
+ if (field.arrayElementType) {
553
+ typ = field.arrayElementType;
554
+ }
555
+ if (typ === "vec2" || typ === "vec3" || typ === "quat") {
556
+ typ = "f32";
557
+ }
558
+ if (typ === "enum") {
559
+ typ = "uint8";
560
+ }
561
+ return needsEndian(typ);
562
+ }
563
+ function fieldElemSize(field) {
564
+ if (field.arrayElementType) {
565
+ return getElementSize(field.arrayElementType);
566
+ }
567
+ return getElementSize(field.type);
568
+ }
569
+ function generateStateHeader(schema) {
570
+ return `// TypeScript State Bindings
571
+ // Auto-generated - do not edit
572
+ // MaxEntities: ${schema.maxEntities}
573
+ // TotalSize: ${schema.totalSize} bytes
574
+
575
+ // =============================================================================
576
+ // State View Helper
577
+ // =============================================================================
578
+
579
+ /** Convert a Uint8Array state buffer to a DataView for use with accessor functions */
580
+ export function stateView(state: Uint8Array): DataView {
581
+ return new DataView(state.buffer, state.byteOffset, state.byteLength);
582
+ }
583
+
584
+ // =============================================================================
585
+ // Constants
586
+ // =============================================================================
587
+
588
+ export const MAX_ENTITIES = ${schema.maxEntities};
589
+ export const HEADER_SIZE = ${schema.headerSize};
590
+ export const ENTITY_TABLE_OFFSET = ${schema.entityTableOffset};
591
+ export const ENTITY_RECORD_SIZE = ${schema.entityRecordSize};
592
+ export const FREE_STACK_OFFSET = ${schema.freeStackOffset};
593
+ export const FREE_STACK_SIZE = ${schema.freeStackSize};
594
+ export const COMPONENT_STORAGE_OFFSET = ${schema.componentStorageOffset};
595
+ export const SINGLETON_STORAGE_OFFSET = ${schema.singletonStorageOffset};
596
+ export const BITMASK_SIZE = ${schema.bitmaskSize};
597
+ export const STATE_ID_OFFSET = 8;
598
+ export const STATE_ID_SIZE = 32;
599
+ export const TICK_OFFSET = 40;
600
+ export const TICK_RATE_OFFSET = 6;
601
+ `;
602
+ }
603
+ function generateComponentOffsets(schema) {
604
+ let out = `
605
+ // =============================================================================
606
+ // Component Offsets
607
+ // =============================================================================
608
+ `;
609
+ for (const comp of schema.components) {
610
+ if (!comp.isSingleton) {
611
+ out += `export const ${toUpperSnake(comp.name)}_BIT = ${comp.index};
612
+ `;
613
+ if (comp.size > 0) {
614
+ out += `const ${toUpperSnake(comp.name)}_OFFSET = ${comp.storageOffset};
615
+ `;
616
+ out += `const ${toUpperSnake(comp.name)}_SIZE = ${comp.size};
617
+ `;
618
+ }
619
+ } else {
620
+ if (comp.size > 0) {
621
+ out += `const ${toUpperSnake(comp.name)}_OFFSET = ${comp.storageOffset};
622
+ `;
623
+ }
624
+ }
625
+ }
626
+ out += `
627
+ // Component bit constants for query
628
+ `;
629
+ for (const comp of schema.entityComponents) {
630
+ out += `export const ${comp.name} = ${toUpperSnake(comp.name)}_BIT;
631
+ `;
632
+ }
633
+ return out;
634
+ }
635
+ function generateEntityHelpers() {
636
+ return `
637
+ // =============================================================================
638
+ // Entity Reference
639
+ // =============================================================================
640
+
641
+ export type EntityRef = number;
642
+ export const NULL_ENTITY: EntityRef = 0xFFFFFFFF;
643
+
644
+ /** Pack entity index and generation into an EntityRef */
645
+ export function packRef(index: number, generation: number): EntityRef {
646
+ return (((generation & 0xFFFF) << 16) | (index & 0xFFFF)) >>> 0;
647
+ }
648
+
649
+ /** Extract entity index from EntityRef */
650
+ export function unpackIndex(entity: EntityRef): number {
651
+ return entity & 0xFFFF;
652
+ }
653
+
654
+ /** Extract generation from EntityRef */
655
+ export function unpackGeneration(entity: EntityRef): number {
656
+ return (entity >>> 16) & 0xFFFF;
657
+ }
658
+
659
+ // =============================================================================
660
+ // Internal Helpers
661
+ // =============================================================================
662
+
663
+ function getEntityRecordOffset(index: number): number {
664
+ return ENTITY_TABLE_OFFSET + index * ENTITY_RECORD_SIZE;
665
+ }
666
+
667
+ function getEntityGeneration(state: DataView, index: number): number {
668
+ return state.getUint16(getEntityRecordOffset(index), true);
669
+ }
670
+
671
+ function setEntityGeneration(state: DataView, index: number, generation: number): void {
672
+ state.setUint16(getEntityRecordOffset(index), generation, true);
673
+ }
674
+
675
+ function getAliveCount(state: DataView): number {
676
+ return state.getUint16(4, true);
677
+ }
678
+
679
+ function setAliveCount(state: DataView, count: number): void {
680
+ state.setUint16(4, count, true);
681
+ }
682
+
683
+ /** Get current tick from header */
684
+ export function getTick(state: DataView): number {
685
+ return state.getUint32(TICK_OFFSET, true);
686
+ }
687
+
688
+ /** Set current tick in header */
689
+ export function setTick(state: DataView, tick: number): void {
690
+ state.setUint32(TICK_OFFSET, tick, true);
691
+ }
692
+
693
+ /** Get tick rate from header */
694
+ export function getTickRate(state: DataView): number {
695
+ return state.getUint8(TICK_RATE_OFFSET);
696
+ }
697
+
698
+ function getFreeStackCount(state: DataView): number {
699
+ return state.getUint16(FREE_STACK_OFFSET, true);
700
+ }
701
+
702
+ function setFreeStackCount(state: DataView, count: number): void {
703
+ state.setUint16(FREE_STACK_OFFSET, count, true);
704
+ }
705
+
706
+ function hasBit(state: DataView, index: number, bit: number): boolean {
707
+ const bitmaskOffset = getEntityRecordOffset(index) + 2;
708
+ const byteIndex = bit >>> 3;
709
+ const bitIndex = bit & 7;
710
+ return (state.getUint8(bitmaskOffset + byteIndex) & (1 << bitIndex)) !== 0;
711
+ }
712
+
713
+ function setBit(state: DataView, index: number, bit: number): void {
714
+ const bitmaskOffset = getEntityRecordOffset(index) + 2;
715
+ const byteIndex = bit >>> 3;
716
+ const bitIndex = bit & 7;
717
+ const current = state.getUint8(bitmaskOffset + byteIndex);
718
+ state.setUint8(bitmaskOffset + byteIndex, current | (1 << bitIndex));
719
+ }
720
+
721
+ function clearBit(state: DataView, index: number, bit: number): void {
722
+ const bitmaskOffset = getEntityRecordOffset(index) + 2;
723
+ const byteIndex = bit >>> 3;
724
+ const bitIndex = bit & 7;
725
+ const current = state.getUint8(bitmaskOffset + byteIndex);
726
+ state.setUint8(bitmaskOffset + byteIndex, current & ~(1 << bitIndex));
727
+ }
728
+ `;
729
+ }
730
+ function generateIterationHelpers(schema) {
731
+ let out = `
732
+ // =============================================================================
733
+ // Entity Iteration Helpers
734
+ // =============================================================================
735
+
736
+ /** Get generation for a raw slot index */
737
+ export function getGenerationAtSlot(state: DataView, index: number): number {
738
+ return getEntityGeneration(state, index);
739
+ }
740
+
741
+ /** Build an EntityRef for a slot using its current generation */
742
+ export function entityAtSlot(state: DataView, index: number): EntityRef {
743
+ return packRef(index, getEntityGeneration(state, index));
744
+ }
745
+
746
+ function hasAnyComponentAtSlot(state: DataView, index: number): boolean {
747
+ const bitmaskOffset = getEntityRecordOffset(index) + 2;
748
+ for (let i = 0; i < BITMASK_SIZE; i++) {
749
+ if (state.getUint8(bitmaskOffset + i) !== 0) {
750
+ return true;
751
+ }
752
+ }
753
+ return false;
754
+ }
755
+
756
+ // Per-component slot presence checks
757
+ `;
758
+ for (const comp of schema.entityComponents) {
759
+ out += `export function has${comp.name}AtSlot(state: DataView, index: number): boolean {
760
+ return hasBit(state, index, ${toUpperSnake(comp.name)}_BIT);
761
+ }
762
+ `;
763
+ }
764
+ return out;
765
+ }
766
+ function generateQueryHelpers() {
767
+ return `
768
+ // =============================================================================
769
+ // Entity Queries (Zero-Allocation)
770
+ // =============================================================================
771
+
772
+ /** Build a query mask from component bits */
773
+ export function queryMask(bits: number[]): bigint {
774
+ let mask = 0n;
775
+ for (const bit of bits) {
776
+ mask |= 1n << BigInt(bit);
777
+ }
778
+ return mask;
779
+ }
780
+
781
+ function loadComponentMask(state: DataView, bitmaskOffset: number): bigint {
782
+ let mask = 0n;
783
+ for (let i = 0; i < BITMASK_SIZE; i++) {
784
+ mask |= BigInt(state.getUint8(bitmaskOffset + i)) << BigInt(i * 8);
785
+ }
786
+ return mask;
787
+ }
788
+
789
+ function hasMask(state: DataView, index: number, mask: bigint): boolean {
790
+ const bitmaskOffset = getEntityRecordOffset(index) + 2;
791
+ const entityMask = loadComponentMask(state, bitmaskOffset);
792
+ return (entityMask & mask) === mask;
793
+ }
794
+
795
+ /** Iterate entities matching mask (generator) */
796
+ export function* iterQuery(state: DataView, requiredMask: bigint): Generator<EntityRef> {
797
+ for (let i = 0; i < MAX_ENTITIES; i++) {
798
+ if (!hasAnyComponentAtSlot(state, i)) {
799
+ continue;
800
+ }
801
+ if (!hasMask(state, i, requiredMask)) {
802
+ continue;
803
+ }
804
+ const entity = entityAtSlot(state, i);
805
+ if (isAlive(state, entity)) {
806
+ yield entity;
807
+ }
808
+ }
809
+ }
810
+ `;
811
+ }
812
+ function generateLifecycle(schema) {
813
+ let despawnZeroComponents = "";
814
+ for (const comp of schema.entityComponents) {
815
+ if (!comp.isTag && comp.size > 0) {
816
+ despawnZeroComponents += ` for (let i = 0; i < ${toUpperSnake(comp.name)}_SIZE; i++) {
817
+ state.setUint8(${toUpperSnake(comp.name)}_OFFSET + index * ${toUpperSnake(comp.name)}_SIZE + i, 0);
818
+ }
819
+ `;
820
+ }
821
+ }
822
+ return `
823
+ // =============================================================================
824
+ // Entity Lifecycle
825
+ // =============================================================================
826
+
827
+ /** Check if entity reference is valid (alive) */
828
+ export function isAlive(state: DataView, entity: EntityRef): boolean {
829
+ if (entity === NULL_ENTITY) {
830
+ return false;
831
+ }
832
+ const index = unpackIndex(entity);
833
+ if (index >= MAX_ENTITIES) {
834
+ return false;
835
+ }
836
+ const expectedGen = unpackGeneration(entity);
837
+ const currentGen = getEntityGeneration(state, index);
838
+ return currentGen === expectedGen;
839
+ }
840
+
841
+ /** Spawn a new entity, returns EntityRef */
842
+ export function spawn(state: DataView): EntityRef {
843
+ const count = getFreeStackCount(state);
844
+ if (count === 0) {
845
+ return NULL_ENTITY;
846
+ }
847
+
848
+ const entryOffset = FREE_STACK_OFFSET + 2 + (count - 1) * 2;
849
+ const index = state.getUint16(entryOffset, true);
850
+ setFreeStackCount(state, count - 1);
851
+
852
+ const generation = getEntityGeneration(state, index);
853
+ setAliveCount(state, getAliveCount(state) + 1);
854
+
855
+ return packRef(index, generation);
856
+ }
857
+
858
+ /** Despawn an entity */
859
+ export function despawn(state: DataView, entity: EntityRef): void {
860
+ if (!isAlive(state, entity)) {
861
+ return;
862
+ }
863
+
864
+ const index = unpackIndex(entity);
865
+ const oldGen = getEntityGeneration(state, index);
866
+ setEntityGeneration(state, index, (oldGen + 1) & 0xFFFF);
867
+
868
+ // Clear component bitmask
869
+ const bitmaskOffset = getEntityRecordOffset(index) + 2;
870
+ for (let i = 0; i < BITMASK_SIZE; i++) {
871
+ state.setUint8(bitmaskOffset + i, 0);
872
+ }
873
+
874
+ // Zero all component storage for this entity
875
+ ${despawnZeroComponents}
876
+ // Push index back onto free stack
877
+ const freeCount = getFreeStackCount(state);
878
+ const entryOffset = FREE_STACK_OFFSET + 2 + freeCount * 2;
879
+ state.setUint16(entryOffset, index, true);
880
+ setFreeStackCount(state, freeCount + 1);
881
+
882
+ setAliveCount(state, getAliveCount(state) - 1);
883
+ }
884
+ `;
885
+ }
886
+ function generateComponentAccessors(comp) {
887
+ const UPPER = toUpperSnake(comp.name);
888
+ let out = `
889
+ // ---------- ${comp.name} Component ----------
890
+ `;
891
+ if (comp.isTag) {
892
+ out += `
893
+ /** Check if entity has ${comp.name} tag */
894
+ export function has${comp.name}(state: DataView, entity: EntityRef): boolean {
895
+ return hasBit(state, unpackIndex(entity), ${UPPER}_BIT);
896
+ }
897
+
898
+ /** Add ${comp.name} tag to entity */
899
+ export function add${comp.name}(state: DataView, entity: EntityRef): void {
900
+ setBit(state, unpackIndex(entity), ${UPPER}_BIT);
901
+ }
902
+
903
+ /** Remove ${comp.name} tag from entity */
904
+ export function remove${comp.name}(state: DataView, entity: EntityRef): void {
905
+ clearBit(state, unpackIndex(entity), ${UPPER}_BIT);
906
+ }
907
+ `;
908
+ return out;
909
+ }
910
+ out += `
911
+ /** Check if entity has ${comp.name} component */
912
+ export function has${comp.name}(state: DataView, entity: EntityRef): boolean {
913
+ return hasBit(state, unpackIndex(entity), ${UPPER}_BIT);
914
+ }
915
+
916
+ /** Add ${comp.name} component to entity */
917
+ export function add${comp.name}(state: DataView, entity: EntityRef): void {
918
+ setBit(state, unpackIndex(entity), ${UPPER}_BIT);
919
+ }
920
+
921
+ /** Remove ${comp.name} component from entity */
922
+ export function remove${comp.name}(state: DataView, entity: EntityRef): void {
923
+ const index = unpackIndex(entity);
924
+ clearBit(state, index, ${UPPER}_BIT);
925
+ for (let i = 0; i < ${UPPER}_SIZE; i++) {
926
+ state.setUint8(${UPPER}_OFFSET + index * ${UPPER}_SIZE + i, 0);
927
+ }
928
+ }
929
+ `;
930
+ for (const field of comp.fields) {
931
+ out += generateFieldAccessors(comp, field, false);
932
+ }
933
+ return out;
934
+ }
935
+ function generateFieldAccessors(comp, field, isSingleton) {
936
+ const UPPER = toUpperSnake(comp.name);
937
+ const isScalar = field.name === "";
938
+ const suffix = isScalar ? "" : toPascalCase(field.name);
939
+ const offsetExpr = isSingleton ? field.offset > 0 ? `${UPPER}_OFFSET + ${field.offset}` : `${UPPER}_OFFSET` : field.offset > 0 ? `${UPPER}_OFFSET + unpackIndex(entity) * ${UPPER}_SIZE + ${field.offset}` : `${UPPER}_OFFSET + unpackIndex(entity) * ${UPPER}_SIZE`;
940
+ const entityParam = isSingleton ? "" : "entity: EntityRef, ";
941
+ if (field.arrayLength && field.arrayLength > 0) {
942
+ const elemSize = fieldElemSize(field);
943
+ const isBoolArray = field.arrayElementType === "bool";
944
+ const defaultVal = isBoolArray ? "false" : "0";
945
+ const endianArg2 = fieldNeedsEndian(field) ? ", true" : "";
946
+ return `
947
+ /** Get ${comp.name}${isScalar ? "" : "." + field.name}[index] */
948
+ export function get${comp.name}${suffix}(state: DataView, ${entityParam}index: number): ${tsType(field)} {
949
+ if (index < 0 || index >= ${field.arrayLength}) {
950
+ return ${defaultVal};
951
+ }
952
+ ${isBoolArray ? ` return state.${tsLoadFn(field)}(${offsetExpr} + index * ${elemSize}) !== 0;` : ` return state.${tsLoadFn(field)}(${offsetExpr} + index * ${elemSize}${endianArg2});`}
953
+ }
954
+
955
+ /** Set ${comp.name}${isScalar ? "" : "." + field.name}[index] */
956
+ export function set${comp.name}${suffix}(state: DataView, ${entityParam}index: number, value: ${tsType(field)}): void {
957
+ if (index < 0 || index >= ${field.arrayLength}) {
958
+ return;
959
+ }
960
+ ${isBoolArray ? ` state.${tsStoreFn(field)}(${offsetExpr} + index * ${elemSize}, value ? 1 : 0);` : ` state.${tsStoreFn(field)}(${offsetExpr} + index * ${elemSize}, value${endianArg2});`}
961
+ }
962
+ `;
963
+ }
964
+ if (field.type === "vec2") {
965
+ const baseOffset = isSingleton ? `${UPPER}_OFFSET${field.offset > 0 ? ` + ${field.offset}` : ""}` : `${UPPER}_OFFSET + unpackIndex(entity) * ${UPPER}_SIZE${field.offset > 0 ? ` + ${field.offset}` : ""}`;
966
+ return `
967
+ /** Get ${comp.name}${isScalar ? "" : "." + field.name} as [x, y] */
968
+ export function get${comp.name}${suffix}(state: DataView${isSingleton ? "" : ", entity: EntityRef"}): [number, number] {
969
+ const offset = ${baseOffset};
970
+ return [
971
+ state.getFloat32(offset, true),
972
+ state.getFloat32(offset + 4, true),
973
+ ];
974
+ }
975
+
976
+ /** Set ${comp.name}${isScalar ? "" : "." + field.name} from [x, y] */
977
+ export function set${comp.name}${suffix}(state: DataView${isSingleton ? "" : ", entity: EntityRef"}, value: [number, number]): void {
978
+ const offset = ${baseOffset};
979
+ state.setFloat32(offset, value[0], true);
980
+ state.setFloat32(offset + 4, value[1], true);
981
+ }
982
+
983
+ /** Get ${comp.name}${isScalar ? "" : "." + field.name}.x */
984
+ export function get${comp.name}${suffix}X(state: DataView${isSingleton ? "" : ", entity: EntityRef"}): number {
985
+ return state.getFloat32(${baseOffset}, true);
986
+ }
987
+
988
+ /** Set ${comp.name}${isScalar ? "" : "." + field.name}.x */
989
+ export function set${comp.name}${suffix}X(state: DataView${isSingleton ? "" : ", entity: EntityRef"}, value: number): void {
990
+ state.setFloat32(${baseOffset}, value, true);
991
+ }
992
+
993
+ /** Get ${comp.name}${isScalar ? "" : "." + field.name}.y */
994
+ export function get${comp.name}${suffix}Y(state: DataView${isSingleton ? "" : ", entity: EntityRef"}): number {
995
+ return state.getFloat32(${baseOffset} + 4, true);
996
+ }
997
+
998
+ /** Set ${comp.name}${isScalar ? "" : "." + field.name}.y */
999
+ export function set${comp.name}${suffix}Y(state: DataView${isSingleton ? "" : ", entity: EntityRef"}, value: number): void {
1000
+ state.setFloat32(${baseOffset} + 4, value, true);
1001
+ }
1002
+ `;
1003
+ }
1004
+ if (field.type === "vec3") {
1005
+ const baseOffset = isSingleton ? `${UPPER}_OFFSET${field.offset > 0 ? ` + ${field.offset}` : ""}` : `${UPPER}_OFFSET + unpackIndex(entity) * ${UPPER}_SIZE${field.offset > 0 ? ` + ${field.offset}` : ""}`;
1006
+ return `
1007
+ /** Get ${comp.name}${isScalar ? "" : "." + field.name} as [x, y, z] */
1008
+ export function get${comp.name}${suffix}(state: DataView${isSingleton ? "" : ", entity: EntityRef"}): [number, number, number] {
1009
+ const offset = ${baseOffset};
1010
+ return [
1011
+ state.getFloat32(offset, true),
1012
+ state.getFloat32(offset + 4, true),
1013
+ state.getFloat32(offset + 8, true),
1014
+ ];
1015
+ }
1016
+
1017
+ /** Set ${comp.name}${isScalar ? "" : "." + field.name} from [x, y, z] */
1018
+ export function set${comp.name}${suffix}(state: DataView${isSingleton ? "" : ", entity: EntityRef"}, value: [number, number, number]): void {
1019
+ const offset = ${baseOffset};
1020
+ state.setFloat32(offset, value[0], true);
1021
+ state.setFloat32(offset + 4, value[1], true);
1022
+ state.setFloat32(offset + 8, value[2], true);
1023
+ }
1024
+
1025
+ /** Get ${comp.name}${isScalar ? "" : "." + field.name}.x */
1026
+ export function get${comp.name}${suffix}X(state: DataView${isSingleton ? "" : ", entity: EntityRef"}): number {
1027
+ return state.getFloat32(${baseOffset}, true);
1028
+ }
1029
+
1030
+ /** Set ${comp.name}${isScalar ? "" : "." + field.name}.x */
1031
+ export function set${comp.name}${suffix}X(state: DataView${isSingleton ? "" : ", entity: EntityRef"}, value: number): void {
1032
+ state.setFloat32(${baseOffset}, value, true);
1033
+ }
1034
+
1035
+ /** Get ${comp.name}${isScalar ? "" : "." + field.name}.y */
1036
+ export function get${comp.name}${suffix}Y(state: DataView${isSingleton ? "" : ", entity: EntityRef"}): number {
1037
+ return state.getFloat32(${baseOffset} + 4, true);
1038
+ }
1039
+
1040
+ /** Set ${comp.name}${isScalar ? "" : "." + field.name}.y */
1041
+ export function set${comp.name}${suffix}Y(state: DataView${isSingleton ? "" : ", entity: EntityRef"}, value: number): void {
1042
+ state.setFloat32(${baseOffset} + 4, value, true);
1043
+ }
1044
+
1045
+ /** Get ${comp.name}${isScalar ? "" : "." + field.name}.z */
1046
+ export function get${comp.name}${suffix}Z(state: DataView${isSingleton ? "" : ", entity: EntityRef"}): number {
1047
+ return state.getFloat32(${baseOffset} + 8, true);
1048
+ }
1049
+
1050
+ /** Set ${comp.name}${isScalar ? "" : "." + field.name}.z */
1051
+ export function set${comp.name}${suffix}Z(state: DataView${isSingleton ? "" : ", entity: EntityRef"}, value: number): void {
1052
+ state.setFloat32(${baseOffset} + 8, value, true);
1053
+ }
1054
+ `;
1055
+ }
1056
+ if (field.type === "quat") {
1057
+ const baseOffset = isSingleton ? `${UPPER}_OFFSET${field.offset > 0 ? ` + ${field.offset}` : ""}` : `${UPPER}_OFFSET + unpackIndex(entity) * ${UPPER}_SIZE${field.offset > 0 ? ` + ${field.offset}` : ""}`;
1058
+ return `
1059
+ /** Get ${comp.name}${isScalar ? "" : "." + field.name} as [x, y, z, w] */
1060
+ export function get${comp.name}${suffix}(state: DataView${isSingleton ? "" : ", entity: EntityRef"}): [number, number, number, number] {
1061
+ const offset = ${baseOffset};
1062
+ return [
1063
+ state.getFloat32(offset, true),
1064
+ state.getFloat32(offset + 4, true),
1065
+ state.getFloat32(offset + 8, true),
1066
+ state.getFloat32(offset + 12, true),
1067
+ ];
1068
+ }
1069
+
1070
+ /** Set ${comp.name}${isScalar ? "" : "." + field.name} from [x, y, z, w] */
1071
+ export function set${comp.name}${suffix}(state: DataView${isSingleton ? "" : ", entity: EntityRef"}, value: [number, number, number, number]): void {
1072
+ const offset = ${baseOffset};
1073
+ state.setFloat32(offset, value[0], true);
1074
+ state.setFloat32(offset + 4, value[1], true);
1075
+ state.setFloat32(offset + 8, value[2], true);
1076
+ state.setFloat32(offset + 12, value[3], true);
1077
+ }
1078
+
1079
+ /** Get ${comp.name}${isScalar ? "" : "." + field.name}.x */
1080
+ export function get${comp.name}${suffix}X(state: DataView${isSingleton ? "" : ", entity: EntityRef"}): number {
1081
+ return state.getFloat32(${baseOffset}, true);
1082
+ }
1083
+
1084
+ /** Set ${comp.name}${isScalar ? "" : "." + field.name}.x */
1085
+ export function set${comp.name}${suffix}X(state: DataView${isSingleton ? "" : ", entity: EntityRef"}, value: number): void {
1086
+ state.setFloat32(${baseOffset}, value, true);
1087
+ }
1088
+
1089
+ /** Get ${comp.name}${isScalar ? "" : "." + field.name}.y */
1090
+ export function get${comp.name}${suffix}Y(state: DataView${isSingleton ? "" : ", entity: EntityRef"}): number {
1091
+ return state.getFloat32(${baseOffset} + 4, true);
1092
+ }
1093
+
1094
+ /** Set ${comp.name}${isScalar ? "" : "." + field.name}.y */
1095
+ export function set${comp.name}${suffix}Y(state: DataView${isSingleton ? "" : ", entity: EntityRef"}, value: number): void {
1096
+ state.setFloat32(${baseOffset} + 4, value, true);
1097
+ }
1098
+
1099
+ /** Get ${comp.name}${isScalar ? "" : "." + field.name}.z */
1100
+ export function get${comp.name}${suffix}Z(state: DataView${isSingleton ? "" : ", entity: EntityRef"}): number {
1101
+ return state.getFloat32(${baseOffset} + 8, true);
1102
+ }
1103
+
1104
+ /** Set ${comp.name}${isScalar ? "" : "." + field.name}.z */
1105
+ export function set${comp.name}${suffix}Z(state: DataView${isSingleton ? "" : ", entity: EntityRef"}, value: number): void {
1106
+ state.setFloat32(${baseOffset} + 8, value, true);
1107
+ }
1108
+
1109
+ /** Get ${comp.name}${isScalar ? "" : "." + field.name}.w */
1110
+ export function get${comp.name}${suffix}W(state: DataView${isSingleton ? "" : ", entity: EntityRef"}): number {
1111
+ return state.getFloat32(${baseOffset} + 12, true);
1112
+ }
1113
+
1114
+ /** Set ${comp.name}${isScalar ? "" : "." + field.name}.w */
1115
+ export function set${comp.name}${suffix}W(state: DataView${isSingleton ? "" : ", entity: EntityRef"}, value: number): void {
1116
+ state.setFloat32(${baseOffset} + 12, value, true);
1117
+ }
1118
+ `;
1119
+ }
1120
+ if (field.type === "enum" && field.variants) {
1121
+ let enumOut = `
1122
+ // ${comp.name}${isScalar ? "" : "." + field.name} enum values
1123
+ `;
1124
+ for (let i = 0; i < field.variants.length; i++) {
1125
+ const constName = isScalar ? `${UPPER}_${toUpperSnake(field.variants[i])}` : `${UPPER}_${toUpperSnake(field.name)}_${toUpperSnake(field.variants[i])}`;
1126
+ enumOut += `export const ${constName} = ${i};
1127
+ `;
1128
+ }
1129
+ enumOut += `
1130
+ /** Get ${comp.name}${isScalar ? "" : "." + field.name} value */
1131
+ export function get${comp.name}${suffix}(state: DataView${isSingleton ? "" : ", entity: EntityRef"}): number {
1132
+ return state.getUint8(${offsetExpr});
1133
+ }
1134
+
1135
+ /** Set ${comp.name}${isScalar ? "" : "." + field.name} value */
1136
+ export function set${comp.name}${suffix}(state: DataView${isSingleton ? "" : ", entity: EntityRef"}, value: number): void {
1137
+ state.setUint8(${offsetExpr}, value);
1138
+ }
1139
+ `;
1140
+ return enumOut;
1141
+ }
1142
+ const isBool = field.type === "bool";
1143
+ const endianArg = fieldNeedsEndian(field) ? ", true" : "";
1144
+ return `
1145
+ /** Get ${comp.name}${isScalar ? "" : "." + field.name} value */
1146
+ export function get${comp.name}${suffix}(state: DataView${isSingleton ? "" : ", entity: EntityRef"}): ${tsType(field)} {
1147
+ ${isBool ? ` return state.${tsLoadFn(field)}(${offsetExpr}) !== 0;` : ` return state.${tsLoadFn(field)}(${offsetExpr}${endianArg});`}
1148
+ }
1149
+
1150
+ /** Set ${comp.name}${isScalar ? "" : "." + field.name} value */
1151
+ export function set${comp.name}${suffix}(state: DataView${isSingleton ? "" : ", entity: EntityRef"}, value: ${tsType(field)}): void {
1152
+ ${isBool ? ` state.${tsStoreFn(field)}(${offsetExpr}, value ? 1 : 0);` : ` state.${tsStoreFn(field)}(${offsetExpr}, value${endianArg});`}
1153
+ }
1154
+ `;
1155
+ }
1156
+ function generateSingletonAccessors(comp) {
1157
+ let out = `
1158
+ // ---------- ${comp.name} Singleton ----------
1159
+ `;
1160
+ for (const field of comp.fields) {
1161
+ out += generateFieldAccessors(comp, field, true);
1162
+ }
1163
+ return out;
1164
+ }
1165
+ function generateEventAccessors(event) {
1166
+ const UPPER = toUpperSnake(event.name);
1167
+ let out = `
1168
+ // ---------- ${event.name} Event ----------
1169
+
1170
+ export const ${UPPER}_MAX_EVENTS = ${event.maxEvents};
1171
+ export const ${UPPER}_RECORD_SIZE = ${event.recordSize};
1172
+ const ${UPPER}_OFFSET = ${event.storageOffset};
1173
+
1174
+ /** Clear all ${event.name} events (call at tick start) */
1175
+ export function clear${event.name}Events(state: DataView): void {
1176
+ state.setUint16(${UPPER}_OFFSET, 0, true);
1177
+ }
1178
+
1179
+ /** Get count of ${event.name} events this tick */
1180
+ export function get${event.name}Count(state: DataView): number {
1181
+ return state.getUint16(${UPPER}_OFFSET, true);
1182
+ }
1183
+
1184
+ `;
1185
+ const pushParams = [];
1186
+ const pushWrites = [];
1187
+ for (const field of event.fields) {
1188
+ if (field.type === "vec2") {
1189
+ pushParams.push(`${toCamelCase(field.name)}X: number`);
1190
+ pushParams.push(`${toCamelCase(field.name)}Y: number`);
1191
+ pushWrites.push(` state.setFloat32(recordOffset + ${field.offset}, ${toCamelCase(field.name)}X, true);`);
1192
+ pushWrites.push(` state.setFloat32(recordOffset + ${field.offset} + 4, ${toCamelCase(field.name)}Y, true);`);
1193
+ } else if (field.type === "vec3") {
1194
+ pushParams.push(`${toCamelCase(field.name)}X: number`);
1195
+ pushParams.push(`${toCamelCase(field.name)}Y: number`);
1196
+ pushParams.push(`${toCamelCase(field.name)}Z: number`);
1197
+ pushWrites.push(` state.setFloat32(recordOffset + ${field.offset}, ${toCamelCase(field.name)}X, true);`);
1198
+ pushWrites.push(` state.setFloat32(recordOffset + ${field.offset} + 4, ${toCamelCase(field.name)}Y, true);`);
1199
+ pushWrites.push(` state.setFloat32(recordOffset + ${field.offset} + 8, ${toCamelCase(field.name)}Z, true);`);
1200
+ } else if (field.type === "quat") {
1201
+ pushParams.push(`${toCamelCase(field.name)}X: number`);
1202
+ pushParams.push(`${toCamelCase(field.name)}Y: number`);
1203
+ pushParams.push(`${toCamelCase(field.name)}Z: number`);
1204
+ pushParams.push(`${toCamelCase(field.name)}W: number`);
1205
+ pushWrites.push(` state.setFloat32(recordOffset + ${field.offset}, ${toCamelCase(field.name)}X, true);`);
1206
+ pushWrites.push(` state.setFloat32(recordOffset + ${field.offset} + 4, ${toCamelCase(field.name)}Y, true);`);
1207
+ pushWrites.push(` state.setFloat32(recordOffset + ${field.offset} + 8, ${toCamelCase(field.name)}Z, true);`);
1208
+ pushWrites.push(` state.setFloat32(recordOffset + ${field.offset} + 12, ${toCamelCase(field.name)}W, true);`);
1209
+ } else if (field.type === "bool") {
1210
+ pushParams.push(`${toCamelCase(field.name)}: ${tsType(field)}`);
1211
+ pushWrites.push(` state.${tsStoreFn(field)}(recordOffset + ${field.offset}, ${toCamelCase(field.name)} ? 1 : 0);`);
1212
+ } else {
1213
+ pushParams.push(`${toCamelCase(field.name)}: ${tsType(field)}`);
1214
+ const endianArg = fieldNeedsEndian(field) ? ", true" : "";
1215
+ pushWrites.push(` state.${tsStoreFn(field)}(recordOffset + ${field.offset}, ${toCamelCase(field.name)}${endianArg});`);
1216
+ }
1217
+ }
1218
+ out += `/** Push a ${event.name} event. Returns false if buffer is full. */
1219
+ export function push${event.name}(
1220
+ state: DataView,
1221
+ ${pushParams.join(",\n ")},
1222
+ ): boolean {
1223
+ const count = state.getUint16(${UPPER}_OFFSET, true);
1224
+ if (count >= ${UPPER}_MAX_EVENTS) {
1225
+ return false;
1226
+ }
1227
+ const recordOffset = ${UPPER}_OFFSET + 2 + count * ${UPPER}_RECORD_SIZE;
1228
+ ${pushWrites.join("\n")}
1229
+ state.setUint16(${UPPER}_OFFSET, count + 1, true);
1230
+ return true;
1231
+ }
1232
+
1233
+ // Indexed field accessors for ${event.name}
1234
+ `;
1235
+ for (const field of event.fields) {
1236
+ const suffix = toPascalCase(field.name);
1237
+ if (field.type === "vec2") {
1238
+ out += `
1239
+ /** Get ${event.name}.${field.name}.x at index */
1240
+ export function get${event.name}${suffix}X(state: DataView, index: number): number {
1241
+ return state.getFloat32(${UPPER}_OFFSET + 2 + index * ${UPPER}_RECORD_SIZE + ${field.offset}, true);
1242
+ }
1243
+
1244
+ /** Get ${event.name}.${field.name}.y at index */
1245
+ export function get${event.name}${suffix}Y(state: DataView, index: number): number {
1246
+ return state.getFloat32(${UPPER}_OFFSET + 2 + index * ${UPPER}_RECORD_SIZE + ${field.offset} + 4, true);
1247
+ }
1248
+ `;
1249
+ } else if (field.type === "vec3") {
1250
+ out += `
1251
+ /** Get ${event.name}.${field.name}.x at index */
1252
+ export function get${event.name}${suffix}X(state: DataView, index: number): number {
1253
+ return state.getFloat32(${UPPER}_OFFSET + 2 + index * ${UPPER}_RECORD_SIZE + ${field.offset}, true);
1254
+ }
1255
+
1256
+ /** Get ${event.name}.${field.name}.y at index */
1257
+ export function get${event.name}${suffix}Y(state: DataView, index: number): number {
1258
+ return state.getFloat32(${UPPER}_OFFSET + 2 + index * ${UPPER}_RECORD_SIZE + ${field.offset} + 4, true);
1259
+ }
1260
+
1261
+ /** Get ${event.name}.${field.name}.z at index */
1262
+ export function get${event.name}${suffix}Z(state: DataView, index: number): number {
1263
+ return state.getFloat32(${UPPER}_OFFSET + 2 + index * ${UPPER}_RECORD_SIZE + ${field.offset} + 8, true);
1264
+ }
1265
+ `;
1266
+ } else if (field.type === "quat") {
1267
+ out += `
1268
+ /** Get ${event.name}.${field.name}.x at index */
1269
+ export function get${event.name}${suffix}X(state: DataView, index: number): number {
1270
+ return state.getFloat32(${UPPER}_OFFSET + 2 + index * ${UPPER}_RECORD_SIZE + ${field.offset}, true);
1271
+ }
1272
+
1273
+ /** Get ${event.name}.${field.name}.y at index */
1274
+ export function get${event.name}${suffix}Y(state: DataView, index: number): number {
1275
+ return state.getFloat32(${UPPER}_OFFSET + 2 + index * ${UPPER}_RECORD_SIZE + ${field.offset} + 4, true);
1276
+ }
1277
+
1278
+ /** Get ${event.name}.${field.name}.z at index */
1279
+ export function get${event.name}${suffix}Z(state: DataView, index: number): number {
1280
+ return state.getFloat32(${UPPER}_OFFSET + 2 + index * ${UPPER}_RECORD_SIZE + ${field.offset} + 8, true);
1281
+ }
1282
+
1283
+ /** Get ${event.name}.${field.name}.w at index */
1284
+ export function get${event.name}${suffix}W(state: DataView, index: number): number {
1285
+ return state.getFloat32(${UPPER}_OFFSET + 2 + index * ${UPPER}_RECORD_SIZE + ${field.offset} + 12, true);
1286
+ }
1287
+ `;
1288
+ } else {
1289
+ const isBool = field.type === "bool";
1290
+ const endianArg = fieldNeedsEndian(field) ? ", true" : "";
1291
+ out += `
1292
+ /** Get ${event.name}.${field.name} at index */
1293
+ export function get${event.name}${suffix}(state: DataView, index: number): ${tsType(field)} {
1294
+ ${isBool ? ` return state.${tsLoadFn(field)}(${UPPER}_OFFSET + 2 + index * ${UPPER}_RECORD_SIZE + ${field.offset}) !== 0;` : ` return state.${tsLoadFn(field)}(${UPPER}_OFFSET + 2 + index * ${UPPER}_RECORD_SIZE + ${field.offset}${endianArg});`}
1295
+ }
1296
+ `;
1297
+ }
1298
+ }
1299
+ const iterTypes = [];
1300
+ const iterYields = [];
1301
+ for (const field of event.fields) {
1302
+ const suffix = toPascalCase(field.name);
1303
+ if (field.type === "vec2") {
1304
+ iterTypes.push("number", "number");
1305
+ iterYields.push(`get${event.name}${suffix}X(state, i)`);
1306
+ iterYields.push(`get${event.name}${suffix}Y(state, i)`);
1307
+ } else if (field.type === "vec3") {
1308
+ iterTypes.push("number", "number", "number");
1309
+ iterYields.push(`get${event.name}${suffix}X(state, i)`);
1310
+ iterYields.push(`get${event.name}${suffix}Y(state, i)`);
1311
+ iterYields.push(`get${event.name}${suffix}Z(state, i)`);
1312
+ } else if (field.type === "quat") {
1313
+ iterTypes.push("number", "number", "number", "number");
1314
+ iterYields.push(`get${event.name}${suffix}X(state, i)`);
1315
+ iterYields.push(`get${event.name}${suffix}Y(state, i)`);
1316
+ iterYields.push(`get${event.name}${suffix}Z(state, i)`);
1317
+ iterYields.push(`get${event.name}${suffix}W(state, i)`);
1318
+ } else {
1319
+ iterTypes.push(tsType(field));
1320
+ iterYields.push(`get${event.name}${suffix}(state, i)`);
1321
+ }
1322
+ }
1323
+ out += `
1324
+ /** Iterate ${event.name} events */
1325
+ export function* iter${event.name}(state: DataView): Generator<[${iterTypes.join(", ")}]> {
1326
+ const count = get${event.name}Count(state);
1327
+ for (let i = 0; i < count; i++) {
1328
+ yield [
1329
+ ${iterYields.join(",\n ")},
1330
+ ];
1331
+ }
1332
+ }
1333
+ `;
1334
+ return out;
1335
+ }
1336
+ function isArrayType(type) {
1337
+ return /^\w+\[\d+\]$/.test(type);
1338
+ }
1339
+ function validateInputSchemaForCodegen(schema) {
1340
+ for (const ctrl of schema.controls) {
1341
+ if (isArrayType(ctrl.type)) {
1342
+ throw new Error(
1343
+ `Input array types are not supported by codegen: control '${ctrl.name}' has type '${ctrl.type}'`
1344
+ );
1345
+ }
1346
+ }
1347
+ for (const cmd of schema.commands) {
1348
+ for (const arg of cmd.args) {
1349
+ if (isArrayType(arg.type) || arg.arrayLength) {
1350
+ throw new Error(
1351
+ `Input array types are not supported by codegen: command '${cmd.name}' argument '${arg.name}' has type '${arg.type}'`
1352
+ );
1353
+ }
1354
+ }
1355
+ }
1356
+ }
1357
+ function generateInputHeader() {
1358
+ return `// TypeScript Input Bindings
1359
+ // Auto-generated - do not edit
1360
+ // Input Schema for TLV-encoded payloads
1361
+
1362
+ /** Input payload size (matches protocol, always zero-padded to this size) */
1363
+ export const INPUT_PAYLOAD_SIZE = 255;
1364
+ `;
1365
+ }
1366
+ function generateControlConstants(schema) {
1367
+ let out = `
1368
+ // =============================================================================
1369
+ // Control Index Constants
1370
+ // =============================================================================
1371
+ `;
1372
+ for (const ctrl of schema.controls) {
1373
+ out += `export const CONTROL_${toUpperSnake(ctrl.name)} = ${ctrl.index};
1374
+ `;
1375
+ }
1376
+ return out;
1377
+ }
1378
+ function generateFlagConstants(schema) {
1379
+ let out = `
1380
+ // =============================================================================
1381
+ // Flag Bit Constants
1382
+ // =============================================================================
1383
+ `;
1384
+ for (const ctrl of schema.controls) {
1385
+ if (ctrl.options && ctrl.options.length > 0) {
1386
+ out += `// ${ctrl.name} flags
1387
+ `;
1388
+ for (let i = 0; i < ctrl.options.length; i++) {
1389
+ out += `export const ${toUpperSnake(ctrl.name)}_${toUpperSnake(ctrl.options[i])} = ${i};
1390
+ `;
1391
+ }
1392
+ }
1393
+ }
1394
+ return out;
1395
+ }
1396
+ function generateTLVDecoders() {
1397
+ return `
1398
+ // =============================================================================
1399
+ // TLV Decoders
1400
+ // =============================================================================
1401
+
1402
+ /** Helper to get a DataView from a Uint8Array payload */
1403
+ function payloadView(payload: Uint8Array): DataView {
1404
+ return new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
1405
+ }
1406
+
1407
+ /**
1408
+ * Find a control in a TLV-encoded payload by index.
1409
+ * Returns the data offset, or -1 if not found.
1410
+ * TLV format: [controlIndex:1][length:1][data:length]...
1411
+ */
1412
+ function findControl(payload: Uint8Array, index: number): number {
1413
+ const view = payloadView(payload);
1414
+ let offset = 0;
1415
+ while (offset + 2 <= INPUT_PAYLOAD_SIZE) {
1416
+ const ctrlIndex = view.getUint8(offset);
1417
+ const dataLen = view.getUint8(offset + 1);
1418
+ const nextOffset = offset + 2 + dataLen;
1419
+
1420
+ if (nextOffset > INPUT_PAYLOAD_SIZE) {
1421
+ return -1; // malformed
1422
+ }
1423
+
1424
+ if (dataLen > 0 && ctrlIndex === index) {
1425
+ return offset + 2;
1426
+ }
1427
+
1428
+ offset = nextOffset;
1429
+ }
1430
+ return -1;
1431
+ }
1432
+
1433
+ /** Iterator over TLV entries in a payload. */
1434
+ export function* iterControls(payload: Uint8Array): Generator<[number, number, number]> {
1435
+ const view = payloadView(payload);
1436
+ let offset = 0;
1437
+ while (offset + 2 <= INPUT_PAYLOAD_SIZE) {
1438
+ const ctrlIndex = view.getUint8(offset);
1439
+ const dataLen = view.getUint8(offset + 1);
1440
+ const nextOffset = offset + 2 + dataLen;
1441
+
1442
+ if (nextOffset > INPUT_PAYLOAD_SIZE) {
1443
+ return;
1444
+ }
1445
+
1446
+ if (dataLen > 0) {
1447
+ yield [ctrlIndex, offset + 2, dataLen];
1448
+ }
1449
+
1450
+ offset = nextOffset;
1451
+ }
1452
+ }
1453
+
1454
+ /** Iterator over TLV entries matching a specific control index. */
1455
+ export function* iterControlsWithIndex(payload: Uint8Array, index: number): Generator<[number, number]> {
1456
+ for (const [ctrlIndex, dataOffset, dataLen] of iterControls(payload)) {
1457
+ if (ctrlIndex === index) {
1458
+ yield [dataOffset, dataLen];
1459
+ }
1460
+ }
1461
+ }
1462
+
1463
+ // Helper to read values from payload using DataView for type safety
1464
+ export function readU8(payload: Uint8Array, offset: number): number {
1465
+ return payloadView(payload).getUint8(offset);
1466
+ }
1467
+
1468
+ export function readI8(payload: Uint8Array, offset: number): number {
1469
+ return payloadView(payload).getInt8(offset);
1470
+ }
1471
+
1472
+ export function readU16(payload: Uint8Array, offset: number): number {
1473
+ return payloadView(payload).getUint16(offset, true);
1474
+ }
1475
+
1476
+ export function readI16(payload: Uint8Array, offset: number): number {
1477
+ return payloadView(payload).getInt16(offset, true);
1478
+ }
1479
+
1480
+ export function readU32(payload: Uint8Array, offset: number): number {
1481
+ return payloadView(payload).getUint32(offset, true);
1482
+ }
1483
+
1484
+ export function readI32(payload: Uint8Array, offset: number): number {
1485
+ return payloadView(payload).getInt32(offset, true);
1486
+ }
1487
+
1488
+ export function readF32(payload: Uint8Array, offset: number): number {
1489
+ return payloadView(payload).getFloat32(offset, true);
1490
+ }
1491
+ `;
1492
+ }
1493
+ function generateControlAccessors(ctrl) {
1494
+ const pascal = toPascalCase(ctrl.name);
1495
+ let out = `
1496
+ // ---------- ${ctrl.name} (index ${ctrl.index}, type ${ctrl.type}) ----------
1497
+ `;
1498
+ switch (ctrl.type) {
1499
+ case "flags8":
1500
+ out += `
1501
+ /** Get ${ctrl.name} flags byte from payload, or 0 if not present. */
1502
+ export function get${pascal}FromPayload(payload: Uint8Array): number {
1503
+ const offset = findControl(payload, ${ctrl.index});
1504
+ if (offset < 0) {
1505
+ return 0;
1506
+ }
1507
+ return readU8(payload, offset);
1508
+ }
1509
+
1510
+ /** Check if a specific bit is set in ${ctrl.name}. */
1511
+ export function is${pascal}SetOnPayload(payload: Uint8Array, bit: number): boolean {
1512
+ return (get${pascal}FromPayload(payload) & (1 << bit)) !== 0;
1513
+ }
1514
+ `;
1515
+ break;
1516
+ case "flags16":
1517
+ out += `
1518
+ /** Get ${ctrl.name} flags word from payload, or 0 if not present. */
1519
+ export function get${pascal}FromPayload(payload: Uint8Array): number {
1520
+ const offset = findControl(payload, ${ctrl.index});
1521
+ if (offset < 0) {
1522
+ return 0;
1523
+ }
1524
+ return readU16(payload, offset);
1525
+ }
1526
+
1527
+ /** Check if a specific bit is set in ${ctrl.name}. */
1528
+ export function is${pascal}SetOnPayload(payload: Uint8Array, bit: number): boolean {
1529
+ return (get${pascal}FromPayload(payload) & (1 << bit)) !== 0;
1530
+ }
1531
+ `;
1532
+ break;
1533
+ case "flags32":
1534
+ out += `
1535
+ /** Get ${ctrl.name} flags dword from payload, or 0 if not present. */
1536
+ export function get${pascal}FromPayload(payload: Uint8Array): number {
1537
+ const offset = findControl(payload, ${ctrl.index});
1538
+ if (offset < 0) {
1539
+ return 0;
1540
+ }
1541
+ return readU32(payload, offset);
1542
+ }
1543
+
1544
+ /** Check if a specific bit is set in ${ctrl.name}. */
1545
+ export function is${pascal}SetOnPayload(payload: Uint8Array, bit: number): boolean {
1546
+ return (get${pascal}FromPayload(payload) & (1 << bit)) !== 0;
1547
+ }
1548
+ `;
1549
+ break;
1550
+ case "bool":
1551
+ out += `
1552
+ /** Get ${ctrl.name} boolean from payload, or false if not present. */
1553
+ export function get${pascal}FromPayload(payload: Uint8Array): boolean {
1554
+ const offset = findControl(payload, ${ctrl.index});
1555
+ if (offset < 0) {
1556
+ return false;
1557
+ }
1558
+ return readU8(payload, offset) !== 0;
1559
+ }
1560
+ `;
1561
+ break;
1562
+ case "uint8":
1563
+ out += `
1564
+ /** Get ${ctrl.name} value from payload, or 0 if not present. */
1565
+ export function get${pascal}FromPayload(payload: Uint8Array): number {
1566
+ const offset = findControl(payload, ${ctrl.index});
1567
+ if (offset < 0) {
1568
+ return 0;
1569
+ }
1570
+ return readU8(payload, offset);
1571
+ }
1572
+ `;
1573
+ break;
1574
+ case "int8":
1575
+ out += `
1576
+ /** Get ${ctrl.name} value from payload, or 0 if not present. */
1577
+ export function get${pascal}FromPayload(payload: Uint8Array): number {
1578
+ const offset = findControl(payload, ${ctrl.index});
1579
+ if (offset < 0) {
1580
+ return 0;
1581
+ }
1582
+ return readI8(payload, offset);
1583
+ }
1584
+ `;
1585
+ break;
1586
+ case "uint16":
1587
+ out += `
1588
+ /** Get ${ctrl.name} value from payload, or 0 if not present. */
1589
+ export function get${pascal}FromPayload(payload: Uint8Array): number {
1590
+ const offset = findControl(payload, ${ctrl.index});
1591
+ if (offset < 0) {
1592
+ return 0;
1593
+ }
1594
+ return readU16(payload, offset);
1595
+ }
1596
+ `;
1597
+ break;
1598
+ case "int16":
1599
+ out += `
1600
+ /** Get ${ctrl.name} value from payload, or 0 if not present. */
1601
+ export function get${pascal}FromPayload(payload: Uint8Array): number {
1602
+ const offset = findControl(payload, ${ctrl.index});
1603
+ if (offset < 0) {
1604
+ return 0;
1605
+ }
1606
+ return readI16(payload, offset);
1607
+ }
1608
+ `;
1609
+ break;
1610
+ case "uint32":
1611
+ out += `
1612
+ /** Get ${ctrl.name} value from payload, or 0 if not present. */
1613
+ export function get${pascal}FromPayload(payload: Uint8Array): number {
1614
+ const offset = findControl(payload, ${ctrl.index});
1615
+ if (offset < 0) {
1616
+ return 0;
1617
+ }
1618
+ return readU32(payload, offset);
1619
+ }
1620
+ `;
1621
+ break;
1622
+ case "int32":
1623
+ out += `
1624
+ /** Get ${ctrl.name} value from payload, or 0 if not present. */
1625
+ export function get${pascal}FromPayload(payload: Uint8Array): number {
1626
+ const offset = findControl(payload, ${ctrl.index});
1627
+ if (offset < 0) {
1628
+ return 0;
1629
+ }
1630
+ return readI32(payload, offset);
1631
+ }
1632
+ `;
1633
+ break;
1634
+ case "f32":
1635
+ out += `
1636
+ /** Get ${ctrl.name} value (f32) from payload, or 0 if not present. */
1637
+ export function get${pascal}FromPayload(payload: Uint8Array): number {
1638
+ const offset = findControl(payload, ${ctrl.index});
1639
+ if (offset < 0) {
1640
+ return 0;
1641
+ }
1642
+ return readF32(payload, offset);
1643
+ }
1644
+ `;
1645
+ break;
1646
+ case "vec2":
1647
+ out += `
1648
+ /** Get ${ctrl.name}.x (f32) from payload, or 0 if not present. */
1649
+ export function get${pascal}XFromPayload(payload: Uint8Array): number {
1650
+ const offset = findControl(payload, ${ctrl.index});
1651
+ if (offset < 0) {
1652
+ return 0;
1653
+ }
1654
+ return readF32(payload, offset);
1655
+ }
1656
+
1657
+ /** Get ${ctrl.name}.y (f32) from payload, or 0 if not present. */
1658
+ export function get${pascal}YFromPayload(payload: Uint8Array): number {
1659
+ const offset = findControl(payload, ${ctrl.index});
1660
+ if (offset < 0) {
1661
+ return 0;
1662
+ }
1663
+ return readF32(payload, offset + 4);
1664
+ }
1665
+
1666
+ /** Get ${ctrl.name} as [x, y] from payload, or [0, 0] if not present. */
1667
+ export function get${pascal}FromPayload(payload: Uint8Array): [number, number] {
1668
+ const offset = findControl(payload, ${ctrl.index});
1669
+ if (offset < 0) {
1670
+ return [0, 0];
1671
+ }
1672
+ return [readF32(payload, offset), readF32(payload, offset + 4)];
1673
+ }
1674
+ `;
1675
+ break;
1676
+ case "vec3":
1677
+ out += `
1678
+ /** Get ${ctrl.name}.x (f32) from payload, or 0 if not present. */
1679
+ export function get${pascal}XFromPayload(payload: Uint8Array): number {
1680
+ const offset = findControl(payload, ${ctrl.index});
1681
+ if (offset < 0) {
1682
+ return 0;
1683
+ }
1684
+ return readF32(payload, offset);
1685
+ }
1686
+
1687
+ /** Get ${ctrl.name}.y (f32) from payload, or 0 if not present. */
1688
+ export function get${pascal}YFromPayload(payload: Uint8Array): number {
1689
+ const offset = findControl(payload, ${ctrl.index});
1690
+ if (offset < 0) {
1691
+ return 0;
1692
+ }
1693
+ return readF32(payload, offset + 4);
1694
+ }
1695
+
1696
+ /** Get ${ctrl.name}.z (f32) from payload, or 0 if not present. */
1697
+ export function get${pascal}ZFromPayload(payload: Uint8Array): number {
1698
+ const offset = findControl(payload, ${ctrl.index});
1699
+ if (offset < 0) {
1700
+ return 0;
1701
+ }
1702
+ return readF32(payload, offset + 8);
1703
+ }
1704
+
1705
+ /** Get ${ctrl.name} as [x, y, z] from payload, or [0, 0, 0] if not present. */
1706
+ export function get${pascal}FromPayload(payload: Uint8Array): [number, number, number] {
1707
+ const offset = findControl(payload, ${ctrl.index});
1708
+ if (offset < 0) {
1709
+ return [0, 0, 0];
1710
+ }
1711
+ return [readF32(payload, offset), readF32(payload, offset + 4), readF32(payload, offset + 8)];
1712
+ }
1713
+ `;
1714
+ break;
1715
+ case "quat":
1716
+ out += `
1717
+ /** Get ${ctrl.name}.x (f32) from payload, or 0 if not present. */
1718
+ export function get${pascal}XFromPayload(payload: Uint8Array): number {
1719
+ const offset = findControl(payload, ${ctrl.index});
1720
+ if (offset < 0) {
1721
+ return 0;
1722
+ }
1723
+ return readF32(payload, offset);
1724
+ }
1725
+
1726
+ /** Get ${ctrl.name}.y (f32) from payload, or 0 if not present. */
1727
+ export function get${pascal}YFromPayload(payload: Uint8Array): number {
1728
+ const offset = findControl(payload, ${ctrl.index});
1729
+ if (offset < 0) {
1730
+ return 0;
1731
+ }
1732
+ return readF32(payload, offset + 4);
1733
+ }
1734
+
1735
+ /** Get ${ctrl.name}.z (f32) from payload, or 0 if not present. */
1736
+ export function get${pascal}ZFromPayload(payload: Uint8Array): number {
1737
+ const offset = findControl(payload, ${ctrl.index});
1738
+ if (offset < 0) {
1739
+ return 0;
1740
+ }
1741
+ return readF32(payload, offset + 8);
1742
+ }
1743
+
1744
+ /** Get ${ctrl.name}.w (f32) from payload, or 1 if not present (identity quaternion). */
1745
+ export function get${pascal}WFromPayload(payload: Uint8Array): number {
1746
+ const offset = findControl(payload, ${ctrl.index});
1747
+ if (offset < 0) {
1748
+ return 1;
1749
+ }
1750
+ return readF32(payload, offset + 12);
1751
+ }
1752
+
1753
+ /** Get ${ctrl.name} as [x, y, z, w] from payload, or [0, 0, 0, 1] if not present. */
1754
+ export function get${pascal}FromPayload(payload: Uint8Array): [number, number, number, number] {
1755
+ const offset = findControl(payload, ${ctrl.index});
1756
+ if (offset < 0) {
1757
+ return [0, 0, 0, 1];
1758
+ }
1759
+ return [readF32(payload, offset), readF32(payload, offset + 4), readF32(payload, offset + 8), readF32(payload, offset + 12)];
1760
+ }
1761
+ `;
1762
+ break;
1763
+ }
1764
+ return out;
1765
+ }
1766
+ function generateCommandAccessors(cmd) {
1767
+ const pascal = toPascalCase(cmd.name);
1768
+ let out = `
1769
+ // ---------- ${cmd.name} (index ${cmd.index}) ----------
1770
+
1771
+ /** Check if ${cmd.name} command is present in payload. */
1772
+ export function has${pascal}OnPayload(payload: Uint8Array): boolean {
1773
+ return findControl(payload, ${cmd.index}) >= 0;
1774
+ }
1775
+ `;
1776
+ const iterTypes = [];
1777
+ const iterVars = [];
1778
+ const iterYields = [];
1779
+ for (const arg of cmd.args) {
1780
+ const camel = toCamelCase(arg.name);
1781
+ if (arg.type === "vec2") {
1782
+ iterTypes.push("number", "number");
1783
+ iterVars.push(` const ${camel}X = readF32(payload, dataOffset + ${arg.offset});`);
1784
+ iterVars.push(` const ${camel}Y = readF32(payload, dataOffset + ${arg.offset} + 4);`);
1785
+ iterYields.push(`${camel}X`, `${camel}Y`);
1786
+ } else if (arg.type === "vec3") {
1787
+ iterTypes.push("number", "number", "number");
1788
+ iterVars.push(` const ${camel}X = readF32(payload, dataOffset + ${arg.offset});`);
1789
+ iterVars.push(` const ${camel}Y = readF32(payload, dataOffset + ${arg.offset} + 4);`);
1790
+ iterVars.push(` const ${camel}Z = readF32(payload, dataOffset + ${arg.offset} + 8);`);
1791
+ iterYields.push(`${camel}X`, `${camel}Y`, `${camel}Z`);
1792
+ } else if (arg.type === "quat") {
1793
+ iterTypes.push("number", "number", "number", "number");
1794
+ iterVars.push(` const ${camel}X = readF32(payload, dataOffset + ${arg.offset});`);
1795
+ iterVars.push(` const ${camel}Y = readF32(payload, dataOffset + ${arg.offset} + 4);`);
1796
+ iterVars.push(` const ${camel}Z = readF32(payload, dataOffset + ${arg.offset} + 8);`);
1797
+ iterVars.push(` const ${camel}W = readF32(payload, dataOffset + ${arg.offset} + 12);`);
1798
+ iterYields.push(`${camel}X`, `${camel}Y`, `${camel}Z`, `${camel}W`);
1799
+ } else {
1800
+ iterTypes.push(tsInputType(arg.type));
1801
+ const readFn = arg.type === "f32" ? "readF32" : arg.type === "uint8" ? "readU8" : arg.type === "int8" ? "readI8" : arg.type === "uint16" ? "readU16" : arg.type === "int16" ? "readI16" : arg.type === "uint32" ? "readU32" : arg.type === "int32" ? "readI32" : "readU8";
1802
+ iterVars.push(` const ${camel} = ${readFn}(payload, dataOffset + ${arg.offset});`);
1803
+ iterYields.push(camel);
1804
+ }
1805
+ }
1806
+ if (cmd.args.length > 0) {
1807
+ out += `
1808
+ /** Iterate all ${cmd.name} commands in a payload. */
1809
+ export function* iter${pascal}OnPayload(payload: Uint8Array): Generator<[${iterTypes.join(", ")}]> {
1810
+ for (const [dataOffset, dataLen] of iterControlsWithIndex(payload, ${cmd.index})) {
1811
+ if (dataLen < ${cmd.totalSize}) {
1812
+ continue;
1813
+ }
1814
+ ${iterVars.join("\n")}
1815
+ yield [${iterYields.join(", ")}];
1816
+ }
1817
+ }
1818
+ `;
1819
+ } else {
1820
+ out += `
1821
+ /** Iterate all ${cmd.name} commands in a payload. */
1822
+ export function* iter${pascal}OnPayload(payload: Uint8Array): Generator<void> {
1823
+ for (const [dataOffset, dataLen] of iterControlsWithIndex(payload, ${cmd.index})) {
1824
+ if (dataLen < ${cmd.totalSize}) {
1825
+ continue;
1826
+ }
1827
+ yield;
1828
+ }
1829
+ }
1830
+ `;
1831
+ }
1832
+ for (const arg of cmd.args) {
1833
+ const argPascal = toPascalCase(arg.name);
1834
+ if (arg.type === "vec2") {
1835
+ out += `
1836
+ /** Get ${cmd.name}.${arg.name}.x (f32), or 0 if command not present. */
1837
+ export function get${pascal}${argPascal}XFromPayload(payload: Uint8Array): number {
1838
+ const offset = findControl(payload, ${cmd.index});
1839
+ if (offset < 0) {
1840
+ return 0;
1841
+ }
1842
+ return readF32(payload, offset + ${arg.offset});
1843
+ }
1844
+
1845
+ /** Get ${cmd.name}.${arg.name}.y (f32), or 0 if command not present. */
1846
+ export function get${pascal}${argPascal}YFromPayload(payload: Uint8Array): number {
1847
+ const offset = findControl(payload, ${cmd.index});
1848
+ if (offset < 0) {
1849
+ return 0;
1850
+ }
1851
+ return readF32(payload, offset + ${arg.offset} + 4);
1852
+ }
1853
+ `;
1854
+ } else if (arg.type === "vec3") {
1855
+ out += `
1856
+ /** Get ${cmd.name}.${arg.name}.x (f32), or 0 if command not present. */
1857
+ export function get${pascal}${argPascal}XFromPayload(payload: Uint8Array): number {
1858
+ const offset = findControl(payload, ${cmd.index});
1859
+ if (offset < 0) {
1860
+ return 0;
1861
+ }
1862
+ return readF32(payload, offset + ${arg.offset});
1863
+ }
1864
+
1865
+ /** Get ${cmd.name}.${arg.name}.y (f32), or 0 if command not present. */
1866
+ export function get${pascal}${argPascal}YFromPayload(payload: Uint8Array): number {
1867
+ const offset = findControl(payload, ${cmd.index});
1868
+ if (offset < 0) {
1869
+ return 0;
1870
+ }
1871
+ return readF32(payload, offset + ${arg.offset} + 4);
1872
+ }
1873
+
1874
+ /** Get ${cmd.name}.${arg.name}.z (f32), or 0 if command not present. */
1875
+ export function get${pascal}${argPascal}ZFromPayload(payload: Uint8Array): number {
1876
+ const offset = findControl(payload, ${cmd.index});
1877
+ if (offset < 0) {
1878
+ return 0;
1879
+ }
1880
+ return readF32(payload, offset + ${arg.offset} + 8);
1881
+ }
1882
+ `;
1883
+ } else if (arg.type === "quat") {
1884
+ out += `
1885
+ /** Get ${cmd.name}.${arg.name}.x (f32), or 0 if command not present. */
1886
+ export function get${pascal}${argPascal}XFromPayload(payload: Uint8Array): number {
1887
+ const offset = findControl(payload, ${cmd.index});
1888
+ if (offset < 0) {
1889
+ return 0;
1890
+ }
1891
+ return readF32(payload, offset + ${arg.offset});
1892
+ }
1893
+
1894
+ /** Get ${cmd.name}.${arg.name}.y (f32), or 0 if command not present. */
1895
+ export function get${pascal}${argPascal}YFromPayload(payload: Uint8Array): number {
1896
+ const offset = findControl(payload, ${cmd.index});
1897
+ if (offset < 0) {
1898
+ return 0;
1899
+ }
1900
+ return readF32(payload, offset + ${arg.offset} + 4);
1901
+ }
1902
+
1903
+ /** Get ${cmd.name}.${arg.name}.z (f32), or 0 if command not present. */
1904
+ export function get${pascal}${argPascal}ZFromPayload(payload: Uint8Array): number {
1905
+ const offset = findControl(payload, ${cmd.index});
1906
+ if (offset < 0) {
1907
+ return 0;
1908
+ }
1909
+ return readF32(payload, offset + ${arg.offset} + 8);
1910
+ }
1911
+
1912
+ /** Get ${cmd.name}.${arg.name}.w (f32), or 1 if command not present (identity quaternion). */
1913
+ export function get${pascal}${argPascal}WFromPayload(payload: Uint8Array): number {
1914
+ const offset = findControl(payload, ${cmd.index});
1915
+ if (offset < 0) {
1916
+ return 1;
1917
+ }
1918
+ return readF32(payload, offset + ${arg.offset} + 12);
1919
+ }
1920
+ `;
1921
+ } else {
1922
+ const readFn = arg.type === "f32" ? "readF32" : arg.type === "uint8" ? "readU8" : arg.type === "int8" ? "readI8" : arg.type === "uint16" ? "readU16" : arg.type === "int16" ? "readI16" : arg.type === "uint32" ? "readU32" : arg.type === "int32" ? "readI32" : "readU8";
1923
+ out += `
1924
+ /** Get ${cmd.name}.${arg.name} value, or 0 if command not present. */
1925
+ export function get${pascal}${argPascal}FromPayload(payload: Uint8Array): ${tsInputType(arg.type)} {
1926
+ const offset = findControl(payload, ${cmd.index});
1927
+ if (offset < 0) {
1928
+ return 0;
1929
+ }
1930
+ return ${readFn}(payload, offset + ${arg.offset});
1931
+ }
1932
+ `;
1933
+ }
1934
+ }
1935
+ return out;
1936
+ }
1937
+ var TypeScriptGenerator = class {
1938
+ language = "typescript";
1939
+ opts;
1940
+ constructor(opts = {}) {
1941
+ this.opts = opts;
1942
+ }
1943
+ generateState(schema) {
1944
+ let out = generateStateHeader(schema);
1945
+ out += generateComponentOffsets(schema);
1946
+ out += generateEntityHelpers();
1947
+ out += generateIterationHelpers(schema);
1948
+ out += generateQueryHelpers();
1949
+ out += generateLifecycle(schema);
1950
+ out += `
1951
+ // =============================================================================
1952
+ // Component Operations
1953
+ // =============================================================================
1954
+ `;
1955
+ for (const comp of schema.entityComponents) {
1956
+ out += generateComponentAccessors(comp);
1957
+ }
1958
+ if (schema.singletonComponents.length > 0) {
1959
+ out += `
1960
+ // =============================================================================
1961
+ // Singleton Components
1962
+ // =============================================================================
1963
+ `;
1964
+ for (const comp of schema.singletonComponents) {
1965
+ out += generateSingletonAccessors(comp);
1966
+ }
1967
+ }
1968
+ if (schema.events && schema.events.length > 0) {
1969
+ out += `
1970
+ // =============================================================================
1971
+ // Events
1972
+ // =============================================================================
1973
+ `;
1974
+ for (const event of schema.events) {
1975
+ out += generateEventAccessors(event);
1976
+ }
1977
+ }
1978
+ return out;
1979
+ }
1980
+ generateInput(schema) {
1981
+ validateInputSchemaForCodegen(schema);
1982
+ let out = generateInputHeader();
1983
+ out += generateControlConstants(schema);
1984
+ out += generateFlagConstants(schema);
1985
+ out += generateTLVDecoders();
1986
+ out += `
1987
+ // =============================================================================
1988
+ // Payload-Level Control Accessors
1989
+ // =============================================================================
1990
+ `;
1991
+ for (const ctrl of schema.controls) {
1992
+ out += generateControlAccessors(ctrl);
1993
+ }
1994
+ if (schema.commands.length > 0) {
1995
+ out += `
1996
+ // =============================================================================
1997
+ // Command Index Constants
1998
+ // =============================================================================
1999
+ `;
2000
+ for (const cmd of schema.commands) {
2001
+ out += `export const COMMAND_${toUpperSnake(cmd.name)} = ${cmd.index};
2002
+ `;
2003
+ }
2004
+ out += `
2005
+ // =============================================================================
2006
+ // Payload-Level Command Accessors
2007
+ // =============================================================================
2008
+ `;
2009
+ for (const cmd of schema.commands) {
2010
+ out += generateCommandAccessors(cmd);
2011
+ }
2012
+ }
2013
+ return out;
2014
+ }
2015
+ generateGame(state, input) {
2016
+ return this.generateState(state) + "\n" + this.generateInput(input);
2017
+ }
2018
+ };
2019
+
2020
+ // src/codegen/rust.ts
2021
+ function rustType(field) {
2022
+ if (field.arrayElementType) {
2023
+ return rustTypeMapping(field.arrayElementType);
2024
+ }
2025
+ if (field.type === "vec2" || field.type === "vec3" || field.type === "quat") {
2026
+ return "f32";
2027
+ }
2028
+ if (field.type === "enum") {
2029
+ return "u8";
2030
+ }
2031
+ return rustTypeMapping(field.type);
2032
+ }
2033
+ function rustTypeMapping(typ) {
2034
+ switch (typ) {
2035
+ case "bool":
2036
+ return "bool";
2037
+ case "uint8":
2038
+ return "u8";
2039
+ case "int8":
2040
+ return "i8";
2041
+ case "uint16":
2042
+ return "u16";
2043
+ case "int16":
2044
+ return "i16";
2045
+ case "uint32":
2046
+ case "entityRef":
2047
+ return "u32";
2048
+ case "int32":
2049
+ return "i32";
2050
+ case "f32":
2051
+ return "f32";
2052
+ default:
2053
+ throw new Error(`Unknown type '${typ}' in Rust code generation`);
2054
+ }
2055
+ }
2056
+ function rustInputType(typ) {
2057
+ switch (typ) {
2058
+ case "bool":
2059
+ return "bool";
2060
+ case "uint8":
2061
+ case "flags8":
2062
+ return "u8";
2063
+ case "int8":
2064
+ return "i8";
2065
+ case "uint16":
2066
+ case "flags16":
2067
+ return "u16";
2068
+ case "int16":
2069
+ return "i16";
2070
+ case "uint32":
2071
+ case "flags32":
2072
+ return "u32";
2073
+ case "int32":
2074
+ return "i32";
2075
+ case "f32":
2076
+ case "vec2":
2077
+ case "vec3":
2078
+ case "quat":
2079
+ return "f32";
2080
+ default:
2081
+ throw new Error(`Unknown input type '${typ}' in Rust code generation`);
2082
+ }
2083
+ }
2084
+ function loadFn(field) {
2085
+ let typ = field.type;
2086
+ if (field.arrayElementType) {
2087
+ typ = field.arrayElementType;
2088
+ }
2089
+ if (typ === "vec2" || typ === "vec3" || typ === "quat") {
2090
+ typ = "f32";
2091
+ }
2092
+ if (typ === "enum") {
2093
+ typ = "uint8";
2094
+ }
2095
+ return loadFnForType(typ);
2096
+ }
2097
+ function loadFnForType(typ) {
2098
+ switch (typ) {
2099
+ case "bool":
2100
+ return "load_bool";
2101
+ case "uint8":
2102
+ return "load_u8";
2103
+ case "int8":
2104
+ return "load_i8";
2105
+ case "uint16":
2106
+ return "load_u16";
2107
+ case "int16":
2108
+ return "load_i16";
2109
+ case "uint32":
2110
+ case "entityRef":
2111
+ return "load_u32";
2112
+ case "int32":
2113
+ return "load_i32";
2114
+ case "f32":
2115
+ return "load_f32";
2116
+ default:
2117
+ throw new Error(`Unknown type '${typ}' in Rust load function generation`);
2118
+ }
2119
+ }
2120
+ function storeFn(field) {
2121
+ let typ = field.type;
2122
+ if (field.arrayElementType) {
2123
+ typ = field.arrayElementType;
2124
+ }
2125
+ if (typ === "vec2" || typ === "vec3" || typ === "quat") {
2126
+ typ = "f32";
2127
+ }
2128
+ if (typ === "enum") {
2129
+ typ = "uint8";
2130
+ }
2131
+ return storeFnForType(typ);
2132
+ }
2133
+ function storeFnForType(typ) {
2134
+ switch (typ) {
2135
+ case "bool":
2136
+ return "store_bool";
2137
+ case "uint8":
2138
+ return "store_u8";
2139
+ case "int8":
2140
+ return "store_i8";
2141
+ case "uint16":
2142
+ return "store_u16";
2143
+ case "int16":
2144
+ return "store_i16";
2145
+ case "uint32":
2146
+ case "entityRef":
2147
+ return "store_u32";
2148
+ case "int32":
2149
+ return "store_i32";
2150
+ case "f32":
2151
+ return "store_f32";
2152
+ default:
2153
+ throw new Error(`Unknown type '${typ}' in Rust store function generation`);
2154
+ }
2155
+ }
2156
+ function fieldElemSize2(field) {
2157
+ if (field.arrayElementType) {
2158
+ return getElementSize(field.arrayElementType);
2159
+ }
2160
+ return getElementSize(field.type);
2161
+ }
2162
+ function generateStateHeader2(schema) {
2163
+ return `// Language: Rust
2164
+ // MaxEntities: ${schema.maxEntities}
2165
+ // TotalSize: ${schema.totalSize} bytes
2166
+
2167
+ use core::ptr;
2168
+
2169
+ // =============================================================================
2170
+ // Constants
2171
+ // =============================================================================
2172
+
2173
+ pub const MAX_ENTITIES: u32 = ${schema.maxEntities};
2174
+ pub const HEADER_SIZE: u32 = ${schema.headerSize};
2175
+ pub const ENTITY_TABLE_OFFSET: u32 = ${schema.entityTableOffset};
2176
+ pub const ENTITY_RECORD_SIZE: u32 = ${schema.entityRecordSize};
2177
+ pub const FREE_STACK_OFFSET: u32 = ${schema.freeStackOffset};
2178
+ pub const FREE_STACK_SIZE: u32 = ${schema.freeStackSize};
2179
+ pub const COMPONENT_STORAGE_OFFSET: u32 = ${schema.componentStorageOffset};
2180
+ pub const SINGLETON_STORAGE_OFFSET: u32 = ${schema.singletonStorageOffset};
2181
+ pub const BITMASK_SIZE: u32 = ${schema.bitmaskSize};
2182
+
2183
+ // State ID location in header
2184
+ pub const STATE_ID_OFFSET: u32 = 8;
2185
+ pub const STATE_ID_SIZE: u32 = 32;
2186
+
2187
+ // Tick location in header
2188
+ pub const TICK_OFFSET: u32 = 40;
2189
+
2190
+ // Tick rate location in header (uses one of the reserved bytes)
2191
+ pub const TICK_RATE_OFFSET: u32 = 6;
2192
+ `;
2193
+ }
2194
+ function generateComponentOffsets2(schema) {
2195
+ let out = `
2196
+ // =============================================================================
2197
+ // Component Offsets
2198
+ // =============================================================================
2199
+ `;
2200
+ for (const comp of schema.components) {
2201
+ if (!comp.isSingleton) {
2202
+ out += `pub const ${toUpperSnake(comp.name)}_BIT: u8 = ${comp.index};
2203
+ `;
2204
+ }
2205
+ if (comp.size > 0) {
2206
+ out += `const ${toUpperSnake(comp.name)}_OFFSET: u32 = ${comp.storageOffset};
2207
+ `;
2208
+ out += `const ${toUpperSnake(comp.name)}_SIZE: u32 = ${comp.size};
2209
+ `;
2210
+ }
2211
+ }
2212
+ out += `
2213
+ // Component bit constants for query() - PascalCase for API ergonomics
2214
+ `;
2215
+ out += `#[allow(non_upper_case_globals)]
2216
+ `;
2217
+ for (const comp of schema.entityComponents) {
2218
+ out += `pub const ${comp.name}: u8 = ${toUpperSnake(comp.name)}_BIT;
2219
+ `;
2220
+ }
2221
+ return out;
2222
+ }
2223
+ function generateMemoryHelpers() {
2224
+ return `
2225
+ // =============================================================================
2226
+ // Entity Reference
2227
+ // =============================================================================
2228
+
2229
+ pub type EntityRef = u32;
2230
+ pub const NULL_ENTITY: EntityRef = 0xFFFFFFFF;
2231
+
2232
+ /// Pack entity index and generation into an EntityRef
2233
+ #[inline]
2234
+ pub fn pack_ref(index: u16, generation: u16) -> EntityRef {
2235
+ ((generation as u32) << 16) | (index as u32)
2236
+ }
2237
+
2238
+ /// Extract entity index from EntityRef
2239
+ #[inline]
2240
+ pub fn unpack_index(entity: EntityRef) -> u16 {
2241
+ (entity & 0xFFFF) as u16
2242
+ }
2243
+
2244
+ /// Extract generation from EntityRef
2245
+ #[inline]
2246
+ pub fn unpack_generation(entity: EntityRef) -> u16 {
2247
+ (entity >> 16) as u16
2248
+ }
2249
+
2250
+ // =============================================================================
2251
+ // Low-Level Memory Access
2252
+ // =============================================================================
2253
+
2254
+ #[inline]
2255
+ unsafe fn load_u8(ptr: *const u8) -> u8 {
2256
+ ptr::read_unaligned(ptr)
2257
+ }
2258
+
2259
+ #[inline]
2260
+ unsafe fn load_bool(ptr: *const u8) -> bool {
2261
+ ptr::read_unaligned(ptr) != 0
2262
+ }
2263
+
2264
+ #[inline]
2265
+ unsafe fn load_i8(ptr: *const u8) -> i8 {
2266
+ ptr::read_unaligned(ptr as *const i8)
2267
+ }
2268
+
2269
+ #[inline]
2270
+ unsafe fn load_u16(ptr: *const u8) -> u16 {
2271
+ ptr::read_unaligned(ptr as *const u16)
2272
+ }
2273
+
2274
+ #[inline]
2275
+ unsafe fn load_i16(ptr: *const u8) -> i16 {
2276
+ ptr::read_unaligned(ptr as *const i16)
2277
+ }
2278
+
2279
+ #[inline]
2280
+ unsafe fn load_u32(ptr: *const u8) -> u32 {
2281
+ ptr::read_unaligned(ptr as *const u32)
2282
+ }
2283
+
2284
+ #[inline]
2285
+ unsafe fn load_i32(ptr: *const u8) -> i32 {
2286
+ ptr::read_unaligned(ptr as *const i32)
2287
+ }
2288
+
2289
+ #[inline]
2290
+ unsafe fn store_u8(ptr: *mut u8, value: u8) {
2291
+ ptr::write_unaligned(ptr, value);
2292
+ }
2293
+
2294
+ #[inline]
2295
+ unsafe fn store_bool(ptr: *mut u8, value: bool) {
2296
+ ptr::write_unaligned(ptr, value as u8);
2297
+ }
2298
+
2299
+ #[inline]
2300
+ unsafe fn store_i8(ptr: *mut u8, value: i8) {
2301
+ ptr::write_unaligned(ptr as *mut i8, value);
2302
+ }
2303
+
2304
+ #[inline]
2305
+ unsafe fn store_u16(ptr: *mut u8, value: u16) {
2306
+ ptr::write_unaligned(ptr as *mut u16, value);
2307
+ }
2308
+
2309
+ #[inline]
2310
+ unsafe fn store_i16(ptr: *mut u8, value: i16) {
2311
+ ptr::write_unaligned(ptr as *mut i16, value);
2312
+ }
2313
+
2314
+ #[inline]
2315
+ unsafe fn store_u32(ptr: *mut u8, value: u32) {
2316
+ ptr::write_unaligned(ptr as *mut u32, value);
2317
+ }
2318
+
2319
+ #[inline]
2320
+ unsafe fn store_i32(ptr: *mut u8, value: i32) {
2321
+ ptr::write_unaligned(ptr as *mut i32, value);
2322
+ }
2323
+
2324
+ #[inline]
2325
+ unsafe fn load_f32(ptr: *const u8) -> f32 {
2326
+ ptr::read_unaligned(ptr as *const f32)
2327
+ }
2328
+
2329
+ #[inline]
2330
+ unsafe fn store_f32(ptr: *mut u8, value: f32) {
2331
+ ptr::write_unaligned(ptr as *mut f32, value);
2332
+ }
2333
+
2334
+ #[inline]
2335
+ unsafe fn mem_fill(ptr: *mut u8, value: u8, len: usize) {
2336
+ ptr::write_bytes(ptr, value, len);
2337
+ }
2338
+ `;
2339
+ }
2340
+ function generateInternalHelpers() {
2341
+ return `
2342
+ // =============================================================================
2343
+ // Internal Helpers
2344
+ // =============================================================================
2345
+
2346
+ /// Get entity record offset in entity table
2347
+ #[inline]
2348
+ fn get_entity_record_offset(index: u16) -> u32 {
2349
+ ENTITY_TABLE_OFFSET + (index as u32) * ENTITY_RECORD_SIZE
2350
+ }
2351
+
2352
+ /// Get entity generation from state
2353
+ #[inline]
2354
+ unsafe fn get_entity_generation(state: *const u8, index: u16) -> u16 {
2355
+ load_u16(state.add(get_entity_record_offset(index) as usize))
2356
+ }
2357
+
2358
+ /// Set entity generation in state
2359
+ #[inline]
2360
+ unsafe fn set_entity_generation(state: *mut u8, index: u16, generation: u16) {
2361
+ store_u16(state.add(get_entity_record_offset(index) as usize), generation);
2362
+ }
2363
+
2364
+ /// Get alive count from header
2365
+ #[inline]
2366
+ unsafe fn get_alive_count(state: *const u8) -> u16 {
2367
+ load_u16(state.add(4))
2368
+ }
2369
+
2370
+ /// Set alive count in header
2371
+ #[inline]
2372
+ unsafe fn set_alive_count(state: *mut u8, count: u16) {
2373
+ store_u16(state.add(4), count);
2374
+ }
2375
+
2376
+ /// Get current tick from header
2377
+ #[inline]
2378
+ pub unsafe fn get_tick(state: *const u8) -> u32 {
2379
+ load_u32(state.add(TICK_OFFSET as usize))
2380
+ }
2381
+
2382
+ /// Set current tick in header
2383
+ #[inline]
2384
+ pub unsafe fn set_tick(state: *mut u8, tick: u32) {
2385
+ store_u32(state.add(TICK_OFFSET as usize), tick);
2386
+ }
2387
+
2388
+ /// Get tick rate from header (ticks per second)
2389
+ #[inline]
2390
+ pub unsafe fn get_tick_rate(state: *const u8) -> u8 {
2391
+ load_u8(state.add(TICK_RATE_OFFSET as usize))
2392
+ }
2393
+
2394
+ /// Get free stack count
2395
+ #[inline]
2396
+ unsafe fn get_free_stack_count(state: *const u8) -> u16 {
2397
+ load_u16(state.add(FREE_STACK_OFFSET as usize))
2398
+ }
2399
+
2400
+ /// Set free stack count
2401
+ #[inline]
2402
+ unsafe fn set_free_stack_count(state: *mut u8, count: u16) {
2403
+ store_u16(state.add(FREE_STACK_OFFSET as usize), count);
2404
+ }
2405
+
2406
+ /// Check if component bit is set for entity
2407
+ #[inline]
2408
+ unsafe fn has_bit(state: *const u8, index: u16, bit: u8) -> bool {
2409
+ let bitmask_offset = get_entity_record_offset(index) + 2;
2410
+ let byte_index = (bit >> 3) as u32;
2411
+ let bit_index = bit & 7;
2412
+ (load_u8(state.add((bitmask_offset + byte_index) as usize)) & (1 << bit_index)) != 0
2413
+ }
2414
+
2415
+ /// Set component bit for entity
2416
+ #[inline]
2417
+ unsafe fn set_bit(state: *mut u8, index: u16, bit: u8) {
2418
+ let bitmask_offset = get_entity_record_offset(index) + 2;
2419
+ let byte_index = (bit >> 3) as u32;
2420
+ let bit_index = bit & 7;
2421
+ let ptr = state.add((bitmask_offset + byte_index) as usize);
2422
+ let current = load_u8(ptr);
2423
+ store_u8(ptr, current | (1 << bit_index));
2424
+ }
2425
+
2426
+ /// Clear component bit for entity
2427
+ #[inline]
2428
+ unsafe fn clear_bit(state: *mut u8, index: u16, bit: u8) {
2429
+ let bitmask_offset = get_entity_record_offset(index) + 2;
2430
+ let byte_index = (bit >> 3) as u32;
2431
+ let bit_index = bit & 7;
2432
+ let ptr = state.add((bitmask_offset + byte_index) as usize);
2433
+ let current = load_u8(ptr);
2434
+ store_u8(ptr, current & !(1 << bit_index));
2435
+ }
2436
+ `;
2437
+ }
2438
+ function generateIterationHelpers2(schema) {
2439
+ let out = `
2440
+ // =============================================================================
2441
+ // Entity Iteration Helpers
2442
+ // =============================================================================
2443
+
2444
+ /// Get generation for a raw slot index
2445
+ #[inline]
2446
+ pub unsafe fn get_generation_at_slot(state: *const u8, index: u16) -> u16 {
2447
+ get_entity_generation(state, index)
2448
+ }
2449
+
2450
+ /// Build an EntityRef for a slot using its current generation
2451
+ #[inline]
2452
+ pub unsafe fn entity_at_slot(state: *const u8, index: u16) -> EntityRef {
2453
+ pack_ref(index, get_entity_generation(state, index))
2454
+ }
2455
+
2456
+ /// Check if a slot has any component bits set
2457
+ #[inline]
2458
+ unsafe fn has_any_component_at_slot(state: *const u8, index: u16) -> bool {
2459
+ let bitmask_offset = get_entity_record_offset(index) + 2;
2460
+ for i in 0..BITMASK_SIZE {
2461
+ if load_u8(state.add((bitmask_offset + i) as usize)) != 0 {
2462
+ return true;
2463
+ }
2464
+ }
2465
+ false
2466
+ }
2467
+
2468
+ // Per-component slot presence checks
2469
+ `;
2470
+ for (const comp of schema.entityComponents) {
2471
+ out += `#[inline]
2472
+ pub unsafe fn has_${toLowerSnake(comp.name)}_at_slot(state: *const u8, index: u16) -> bool {
2473
+ has_bit(state, index, ${toUpperSnake(comp.name)}_BIT)
2474
+ }
2475
+ `;
2476
+ }
2477
+ return out;
2478
+ }
2479
+ function generateQueryHelpers2() {
2480
+ return `
2481
+ // =============================================================================
2482
+ // Entity Queries (Zero-Allocation)
2483
+ // =============================================================================
2484
+
2485
+ /// Build a query mask from component bits (const-evaluable)
2486
+ pub const fn query_mask(bits: &[u8]) -> u64 {
2487
+ let mut mask = 0u64;
2488
+ let mut i = 0;
2489
+ while i < bits.len() {
2490
+ mask |= 1u64 << bits[i];
2491
+ i += 1;
2492
+ }
2493
+ mask
2494
+ }
2495
+
2496
+ /// Load entity component mask as u64 (handles variable BITMASK_SIZE)
2497
+ #[inline]
2498
+ unsafe fn load_component_mask(state: *const u8, bitmask_offset: u32) -> u64 {
2499
+ let mut mask = 0u64;
2500
+ let mut i = 0u32;
2501
+ while i < BITMASK_SIZE {
2502
+ mask |= (load_u8(state.add((bitmask_offset + i) as usize)) as u64) << (i * 8);
2503
+ i += 1;
2504
+ }
2505
+ mask
2506
+ }
2507
+
2508
+ /// Check if entity has all bits in mask set
2509
+ #[inline]
2510
+ unsafe fn has_mask(state: *const u8, index: u16, mask: u64) -> bool {
2511
+ let bitmask_offset = get_entity_record_offset(index) + 2;
2512
+ let entity_mask = load_component_mask(state, bitmask_offset);
2513
+ (entity_mask & mask) == mask
2514
+ }
2515
+
2516
+ /// Zero-allocation entity query iterator
2517
+ pub struct QueryIter {
2518
+ state: *const u8,
2519
+ index: u16,
2520
+ required_mask: u64,
2521
+ }
2522
+
2523
+ impl Iterator for QueryIter {
2524
+ type Item = EntityRef;
2525
+
2526
+ #[inline]
2527
+ fn next(&mut self) -> Option<Self::Item> {
2528
+ while self.index < MAX_ENTITIES as u16 {
2529
+ let i = self.index;
2530
+ self.index += 1;
2531
+
2532
+ // Fast path: skip empty slots
2533
+ unsafe {
2534
+ if !has_any_component_at_slot(self.state, i) {
2535
+ continue;
2536
+ }
2537
+
2538
+ // Check if all required bits are set
2539
+ if !has_mask(self.state, i, self.required_mask) {
2540
+ continue;
2541
+ }
2542
+
2543
+ let entity = entity_at_slot(self.state, i);
2544
+ if is_alive(self.state, entity) {
2545
+ return Some(entity);
2546
+ }
2547
+ }
2548
+ }
2549
+ None
2550
+ }
2551
+ }
2552
+
2553
+ /// Create iterator for entities matching mask (zero allocation)
2554
+ pub unsafe fn iter_query(state: *const u8, required_mask: u64) -> QueryIter {
2555
+ QueryIter {
2556
+ state,
2557
+ index: 0,
2558
+ required_mask,
2559
+ }
2560
+ }
2561
+ `;
2562
+ }
2563
+ function generateLifecycle2(schema) {
2564
+ let despawnZeroComponents = "";
2565
+ for (const comp of schema.entityComponents) {
2566
+ if (!comp.isTag && comp.size > 0) {
2567
+ despawnZeroComponents += ` mem_fill(state.add((${toUpperSnake(comp.name)}_OFFSET + (index as u32) * ${toUpperSnake(comp.name)}_SIZE) as usize), 0, ${toUpperSnake(comp.name)}_SIZE as usize);
2568
+ `;
2569
+ }
2570
+ }
2571
+ return `
2572
+ // =============================================================================
2573
+ // Entity Lifecycle
2574
+ // =============================================================================
2575
+
2576
+ /// Check if entity reference is valid (alive)
2577
+ pub unsafe fn is_alive(state: *const u8, entity: EntityRef) -> bool {
2578
+ if entity == NULL_ENTITY {
2579
+ return false;
2580
+ }
2581
+ let index = unpack_index(entity);
2582
+ if (index as u32) >= MAX_ENTITIES {
2583
+ return false;
2584
+ }
2585
+ let expected_gen = unpack_generation(entity);
2586
+ let current_gen = get_entity_generation(state, index);
2587
+ current_gen == expected_gen
2588
+ }
2589
+
2590
+ /// Spawn a new entity, returns EntityRef
2591
+ pub unsafe fn spawn(state: *mut u8) -> EntityRef {
2592
+ let count = get_free_stack_count(state);
2593
+ if count == 0 {
2594
+ return NULL_ENTITY;
2595
+ }
2596
+
2597
+ // Pop from free stack (LIFO)
2598
+ let entry_offset = FREE_STACK_OFFSET + 2 + ((count - 1) as u32) * 2;
2599
+ let index = load_u16(state.add(entry_offset as usize));
2600
+ set_free_stack_count(state, count - 1);
2601
+
2602
+ // Get current generation
2603
+ let generation = get_entity_generation(state, index);
2604
+
2605
+ // Increment alive count
2606
+ set_alive_count(state, get_alive_count(state) + 1);
2607
+
2608
+ pack_ref(index, generation)
2609
+ }
2610
+
2611
+ /// Despawn an entity
2612
+ pub unsafe fn despawn(state: *mut u8, entity: EntityRef) {
2613
+ if !is_alive(state, entity) {
2614
+ return;
2615
+ }
2616
+
2617
+ let index = unpack_index(entity);
2618
+
2619
+ // Increment generation
2620
+ let old_gen = get_entity_generation(state, index);
2621
+ set_entity_generation(state, index, (old_gen + 1) & 0xFFFF);
2622
+
2623
+ // Clear component bitmask
2624
+ let bitmask_offset = get_entity_record_offset(index) + 2;
2625
+ mem_fill(state.add(bitmask_offset as usize), 0, BITMASK_SIZE as usize);
2626
+
2627
+ // Zero all component storage for this entity
2628
+ ${despawnZeroComponents}
2629
+ // Push index back onto free stack
2630
+ let free_count = get_free_stack_count(state);
2631
+ let entry_offset = FREE_STACK_OFFSET + 2 + (free_count as u32) * 2;
2632
+ store_u16(state.add(entry_offset as usize), index);
2633
+ set_free_stack_count(state, free_count + 1);
2634
+
2635
+ // Decrement alive count
2636
+ set_alive_count(state, get_alive_count(state) - 1);
2637
+ }
2638
+ `;
2639
+ }
2640
+ function generateComponentAccessors2(comp) {
2641
+ const UPPER = toUpperSnake(comp.name);
2642
+ const lower = toLowerSnake(comp.name);
2643
+ let out = `
2644
+ // ---------- ${comp.name} Component ----------
2645
+ `;
2646
+ if (comp.isTag) {
2647
+ out += `
2648
+ /// Check if entity has ${comp.name} tag
2649
+ #[inline]
2650
+ pub unsafe fn has_${lower}(state: *const u8, entity: EntityRef) -> bool {
2651
+ has_bit(state, unpack_index(entity), ${UPPER}_BIT)
2652
+ }
2653
+
2654
+ /// Add ${comp.name} tag to entity
2655
+ #[inline]
2656
+ pub unsafe fn add_${lower}(state: *mut u8, entity: EntityRef) {
2657
+ set_bit(state, unpack_index(entity), ${UPPER}_BIT);
2658
+ }
2659
+
2660
+ /// Remove ${comp.name} tag from entity
2661
+ #[inline]
2662
+ pub unsafe fn remove_${lower}(state: *mut u8, entity: EntityRef) {
2663
+ clear_bit(state, unpack_index(entity), ${UPPER}_BIT);
2664
+ }
2665
+ `;
2666
+ return out;
2667
+ }
2668
+ out += `
2669
+ /// Check if entity has ${comp.name} component
2670
+ #[inline]
2671
+ pub unsafe fn has_${lower}(state: *const u8, entity: EntityRef) -> bool {
2672
+ has_bit(state, unpack_index(entity), ${UPPER}_BIT)
2673
+ }
2674
+
2675
+ /// Add ${comp.name} component to entity
2676
+ #[inline]
2677
+ pub unsafe fn add_${lower}(state: *mut u8, entity: EntityRef) {
2678
+ set_bit(state, unpack_index(entity), ${UPPER}_BIT);
2679
+ }
2680
+
2681
+ /// Remove ${comp.name} component from entity
2682
+ #[inline]
2683
+ pub unsafe fn remove_${lower}(state: *mut u8, entity: EntityRef) {
2684
+ let index = unpack_index(entity);
2685
+ clear_bit(state, index, ${UPPER}_BIT);
2686
+ mem_fill(state.add((${UPPER}_OFFSET + (index as u32) * ${UPPER}_SIZE) as usize), 0, ${UPPER}_SIZE as usize);
2687
+ }
2688
+ `;
2689
+ for (const field of comp.fields) {
2690
+ out += generateFieldAccessors2(comp, field, false);
2691
+ }
2692
+ return out;
2693
+ }
2694
+ function generateFieldAccessors2(comp, field, isSingleton) {
2695
+ const UPPER = toUpperSnake(comp.name);
2696
+ const lower = toLowerSnake(comp.name);
2697
+ const isScalar = field.name === "";
2698
+ const fieldLower = isScalar ? "" : `_${toLowerSnake(field.name)}`;
2699
+ const baseOffset = isSingleton ? field.offset > 0 ? `${UPPER}_OFFSET + ${field.offset}` : `${UPPER}_OFFSET` : field.offset > 0 ? `${UPPER}_OFFSET + (unpack_index(entity) as u32) * ${UPPER}_SIZE + ${field.offset}` : `${UPPER}_OFFSET + (unpack_index(entity) as u32) * ${UPPER}_SIZE`;
2700
+ const stateParam = isSingleton ? "state: *const u8" : "state: *const u8, entity: EntityRef";
2701
+ const stateParamMut = isSingleton ? "state: *mut u8" : "state: *mut u8, entity: EntityRef";
2702
+ if (field.arrayLength && field.arrayLength > 0) {
2703
+ const elemSize = fieldElemSize2(field);
2704
+ const isBoolArray = field.arrayElementType === "bool";
2705
+ const defaultVal = isBoolArray ? "false" : "0";
2706
+ return `
2707
+ /// Get ${comp.name}${isScalar ? "" : "." + field.name}[index]
2708
+ #[inline]
2709
+ pub unsafe fn get_${lower}${fieldLower}(${stateParam}, index: i32) -> ${rustType(field)} {
2710
+ if (index as u32) >= ${field.arrayLength} {
2711
+ return ${defaultVal};
2712
+ }
2713
+ ${loadFn(field)}(state.add((${baseOffset} + (index as u32) * ${elemSize}) as usize))
2714
+ }
2715
+
2716
+ /// Set ${comp.name}${isScalar ? "" : "." + field.name}[index]
2717
+ #[inline]
2718
+ pub unsafe fn set_${lower}${fieldLower}(${stateParamMut}, index: i32, value: ${rustType(field)}) {
2719
+ if (index as u32) >= ${field.arrayLength} {
2720
+ return;
2721
+ }
2722
+ ${storeFn(field)}(state.add((${baseOffset} + (index as u32) * ${elemSize}) as usize), value);
2723
+ }
2724
+ `;
2725
+ }
2726
+ if (field.type === "vec2") {
2727
+ return `
2728
+ /// Get ${comp.name}${isScalar ? "" : "." + field.name}.x
2729
+ #[inline]
2730
+ pub unsafe fn get_${lower}${fieldLower}_x(${stateParam}) -> f32 {
2731
+ load_f32(state.add((${baseOffset}) as usize))
2732
+ }
2733
+
2734
+ /// Set ${comp.name}${isScalar ? "" : "." + field.name}.x
2735
+ #[inline]
2736
+ pub unsafe fn set_${lower}${fieldLower}_x(${stateParamMut}, value: f32) {
2737
+ store_f32(state.add((${baseOffset}) as usize), value);
2738
+ }
2739
+
2740
+ /// Get ${comp.name}${isScalar ? "" : "." + field.name}.y
2741
+ #[inline]
2742
+ pub unsafe fn get_${lower}${fieldLower}_y(${stateParam}) -> f32 {
2743
+ load_f32(state.add((${baseOffset} + 4) as usize))
2744
+ }
2745
+
2746
+ /// Set ${comp.name}${isScalar ? "" : "." + field.name}.y
2747
+ #[inline]
2748
+ pub unsafe fn set_${lower}${fieldLower}_y(${stateParamMut}, value: f32) {
2749
+ store_f32(state.add((${baseOffset} + 4) as usize), value);
2750
+ }
2751
+ `;
2752
+ }
2753
+ if (field.type === "vec3") {
2754
+ return `
2755
+ /// Get ${comp.name}${isScalar ? "" : "." + field.name}.x
2756
+ #[inline]
2757
+ pub unsafe fn get_${lower}${fieldLower}_x(${stateParam}) -> f32 {
2758
+ load_f32(state.add((${baseOffset}) as usize))
2759
+ }
2760
+
2761
+ /// Set ${comp.name}${isScalar ? "" : "." + field.name}.x
2762
+ #[inline]
2763
+ pub unsafe fn set_${lower}${fieldLower}_x(${stateParamMut}, value: f32) {
2764
+ store_f32(state.add((${baseOffset}) as usize), value);
2765
+ }
2766
+
2767
+ /// Get ${comp.name}${isScalar ? "" : "." + field.name}.y
2768
+ #[inline]
2769
+ pub unsafe fn get_${lower}${fieldLower}_y(${stateParam}) -> f32 {
2770
+ load_f32(state.add((${baseOffset} + 4) as usize))
2771
+ }
2772
+
2773
+ /// Set ${comp.name}${isScalar ? "" : "." + field.name}.y
2774
+ #[inline]
2775
+ pub unsafe fn set_${lower}${fieldLower}_y(${stateParamMut}, value: f32) {
2776
+ store_f32(state.add((${baseOffset} + 4) as usize), value);
2777
+ }
2778
+
2779
+ /// Get ${comp.name}${isScalar ? "" : "." + field.name}.z
2780
+ #[inline]
2781
+ pub unsafe fn get_${lower}${fieldLower}_z(${stateParam}) -> f32 {
2782
+ load_f32(state.add((${baseOffset} + 8) as usize))
2783
+ }
2784
+
2785
+ /// Set ${comp.name}${isScalar ? "" : "." + field.name}.z
2786
+ #[inline]
2787
+ pub unsafe fn set_${lower}${fieldLower}_z(${stateParamMut}, value: f32) {
2788
+ store_f32(state.add((${baseOffset} + 8) as usize), value);
2789
+ }
2790
+ `;
2791
+ }
2792
+ if (field.type === "quat") {
2793
+ return `
2794
+ /// Get ${comp.name}${isScalar ? "" : "." + field.name}.x
2795
+ #[inline]
2796
+ pub unsafe fn get_${lower}${fieldLower}_x(${stateParam}) -> f32 {
2797
+ load_f32(state.add((${baseOffset}) as usize))
2798
+ }
2799
+
2800
+ /// Set ${comp.name}${isScalar ? "" : "." + field.name}.x
2801
+ #[inline]
2802
+ pub unsafe fn set_${lower}${fieldLower}_x(${stateParamMut}, value: f32) {
2803
+ store_f32(state.add((${baseOffset}) as usize), value);
2804
+ }
2805
+
2806
+ /// Get ${comp.name}${isScalar ? "" : "." + field.name}.y
2807
+ #[inline]
2808
+ pub unsafe fn get_${lower}${fieldLower}_y(${stateParam}) -> f32 {
2809
+ load_f32(state.add((${baseOffset} + 4) as usize))
2810
+ }
2811
+
2812
+ /// Set ${comp.name}${isScalar ? "" : "." + field.name}.y
2813
+ #[inline]
2814
+ pub unsafe fn set_${lower}${fieldLower}_y(${stateParamMut}, value: f32) {
2815
+ store_f32(state.add((${baseOffset} + 4) as usize), value);
2816
+ }
2817
+
2818
+ /// Get ${comp.name}${isScalar ? "" : "." + field.name}.z
2819
+ #[inline]
2820
+ pub unsafe fn get_${lower}${fieldLower}_z(${stateParam}) -> f32 {
2821
+ load_f32(state.add((${baseOffset} + 8) as usize))
2822
+ }
2823
+
2824
+ /// Set ${comp.name}${isScalar ? "" : "." + field.name}.z
2825
+ #[inline]
2826
+ pub unsafe fn set_${lower}${fieldLower}_z(${stateParamMut}, value: f32) {
2827
+ store_f32(state.add((${baseOffset} + 8) as usize), value);
2828
+ }
2829
+
2830
+ /// Get ${comp.name}${isScalar ? "" : "." + field.name}.w
2831
+ #[inline]
2832
+ pub unsafe fn get_${lower}${fieldLower}_w(${stateParam}) -> f32 {
2833
+ load_f32(state.add((${baseOffset} + 12) as usize))
2834
+ }
2835
+
2836
+ /// Set ${comp.name}${isScalar ? "" : "." + field.name}.w
2837
+ #[inline]
2838
+ pub unsafe fn set_${lower}${fieldLower}_w(${stateParamMut}, value: f32) {
2839
+ store_f32(state.add((${baseOffset} + 12) as usize), value);
2840
+ }
2841
+ `;
2842
+ }
2843
+ if (field.type === "enum" && field.variants) {
2844
+ let enumOut = `
2845
+ // ${comp.name}${isScalar ? "" : "." + field.name} enum values
2846
+ `;
2847
+ for (let i = 0; i < field.variants.length; i++) {
2848
+ const constName = isScalar ? `${UPPER}_${toUpperSnake(field.variants[i])}` : `${UPPER}_${toUpperSnake(field.name)}_${toUpperSnake(field.variants[i])}`;
2849
+ enumOut += `pub const ${constName}: u8 = ${i};
2850
+ `;
2851
+ }
2852
+ enumOut += `
2853
+ /// Get ${comp.name}${isScalar ? "" : "." + field.name} value
2854
+ #[inline]
2855
+ pub unsafe fn get_${lower}${fieldLower}(${stateParam}) -> u8 {
2856
+ load_u8(state.add((${baseOffset}) as usize))
2857
+ }
2858
+
2859
+ /// Set ${comp.name}${isScalar ? "" : "." + field.name} value
2860
+ #[inline]
2861
+ pub unsafe fn set_${lower}${fieldLower}(${stateParamMut}, value: u8) {
2862
+ store_u8(state.add((${baseOffset}) as usize), value);
2863
+ }
2864
+ `;
2865
+ return enumOut;
2866
+ }
2867
+ return `
2868
+ /// Get ${comp.name}${isScalar ? "" : "." + field.name} value
2869
+ #[inline]
2870
+ pub unsafe fn get_${lower}${fieldLower}(${stateParam}) -> ${rustType(field)} {
2871
+ ${loadFn(field)}(state.add((${baseOffset}) as usize))
2872
+ }
2873
+
2874
+ /// Set ${comp.name}${isScalar ? "" : "." + field.name} value
2875
+ #[inline]
2876
+ pub unsafe fn set_${lower}${fieldLower}(${stateParamMut}, value: ${rustType(field)}) {
2877
+ ${storeFn(field)}(state.add((${baseOffset}) as usize), value);
2878
+ }
2879
+ `;
2880
+ }
2881
+ function generateSingletonAccessors2(comp) {
2882
+ let out = `
2883
+ // ---------- ${comp.name} Singleton ----------
2884
+ `;
2885
+ for (const field of comp.fields) {
2886
+ out += generateFieldAccessors2(comp, field, true);
2887
+ }
2888
+ return out;
2889
+ }
2890
+ function generateEventAccessors2(event) {
2891
+ const UPPER = toUpperSnake(event.name);
2892
+ const lower = toLowerSnake(event.name);
2893
+ let out = `
2894
+ // ---------- ${event.name} Event ----------
2895
+
2896
+ pub const ${UPPER}_MAX_EVENTS: u16 = ${event.maxEvents};
2897
+ pub const ${UPPER}_RECORD_SIZE: u32 = ${event.recordSize};
2898
+ const ${UPPER}_OFFSET: u32 = ${event.storageOffset};
2899
+
2900
+ /// Clear all ${event.name} events (call at tick start)
2901
+ #[inline]
2902
+ pub unsafe fn clear_${lower}_events(state: *mut u8) {
2903
+ store_u16(state.add(${UPPER}_OFFSET as usize), 0);
2904
+ }
2905
+
2906
+ /// Get count of ${event.name} events this tick
2907
+ #[inline]
2908
+ pub unsafe fn get_${lower}_count(state: *const u8) -> u16 {
2909
+ load_u16(state.add(${UPPER}_OFFSET as usize))
2910
+ }
2911
+
2912
+ `;
2913
+ const pushParams = [];
2914
+ const pushWrites = [];
2915
+ for (const field of event.fields) {
2916
+ const fieldLower = toLowerSnake(field.name);
2917
+ if (field.type === "vec2") {
2918
+ pushParams.push(`${fieldLower}_x: f32`);
2919
+ pushParams.push(`${fieldLower}_y: f32`);
2920
+ pushWrites.push(` store_f32(record_ptr.add(${field.offset}), ${fieldLower}_x);`);
2921
+ pushWrites.push(` store_f32(record_ptr.add(${field.offset} + 4), ${fieldLower}_y);`);
2922
+ } else if (field.type === "vec3") {
2923
+ pushParams.push(`${fieldLower}_x: f32`);
2924
+ pushParams.push(`${fieldLower}_y: f32`);
2925
+ pushParams.push(`${fieldLower}_z: f32`);
2926
+ pushWrites.push(` store_f32(record_ptr.add(${field.offset}), ${fieldLower}_x);`);
2927
+ pushWrites.push(` store_f32(record_ptr.add(${field.offset} + 4), ${fieldLower}_y);`);
2928
+ pushWrites.push(` store_f32(record_ptr.add(${field.offset} + 8), ${fieldLower}_z);`);
2929
+ } else if (field.type === "quat") {
2930
+ pushParams.push(`${fieldLower}_x: f32`);
2931
+ pushParams.push(`${fieldLower}_y: f32`);
2932
+ pushParams.push(`${fieldLower}_z: f32`);
2933
+ pushParams.push(`${fieldLower}_w: f32`);
2934
+ pushWrites.push(` store_f32(record_ptr.add(${field.offset}), ${fieldLower}_x);`);
2935
+ pushWrites.push(` store_f32(record_ptr.add(${field.offset} + 4), ${fieldLower}_y);`);
2936
+ pushWrites.push(` store_f32(record_ptr.add(${field.offset} + 8), ${fieldLower}_z);`);
2937
+ pushWrites.push(` store_f32(record_ptr.add(${field.offset} + 12), ${fieldLower}_w);`);
2938
+ } else {
2939
+ pushParams.push(`${fieldLower}: ${rustType(field)}`);
2940
+ pushWrites.push(` ${storeFn(field)}(record_ptr.add(${field.offset}), ${fieldLower});`);
2941
+ }
2942
+ }
2943
+ out += `/// Push a ${event.name} event. Returns false if buffer is full.
2944
+ #[inline]
2945
+ pub unsafe fn push_${lower}(
2946
+ state: *mut u8,
2947
+ ${pushParams.join(",\n ")},
2948
+ ) -> bool {
2949
+ let count = load_u16(state.add(${UPPER}_OFFSET as usize));
2950
+ if count >= ${UPPER}_MAX_EVENTS {
2951
+ return false;
2952
+ }
2953
+ let record_ptr = state.add((${UPPER}_OFFSET + 2 + (count as u32) * ${UPPER}_RECORD_SIZE) as usize);
2954
+ ${pushWrites.join("\n")}
2955
+ store_u16(state.add(${UPPER}_OFFSET as usize), count + 1);
2956
+ true
2957
+ }
2958
+
2959
+ // Indexed field accessors for ${event.name}
2960
+ `;
2961
+ for (const field of event.fields) {
2962
+ const fieldLower = toLowerSnake(field.name);
2963
+ if (field.type === "vec2") {
2964
+ out += `
2965
+ /// Get ${event.name}.${field.name}.x at index
2966
+ #[inline]
2967
+ pub unsafe fn get_${lower}_${fieldLower}_x(state: *const u8, index: u16) -> f32 {
2968
+ load_f32(state.add((${UPPER}_OFFSET + 2 + (index as u32) * ${UPPER}_RECORD_SIZE + ${field.offset}) as usize))
2969
+ }
2970
+
2971
+ /// Get ${event.name}.${field.name}.y at index
2972
+ #[inline]
2973
+ pub unsafe fn get_${lower}_${fieldLower}_y(state: *const u8, index: u16) -> f32 {
2974
+ load_f32(state.add((${UPPER}_OFFSET + 2 + (index as u32) * ${UPPER}_RECORD_SIZE + ${field.offset} + 4) as usize))
2975
+ }
2976
+ `;
2977
+ } else if (field.type === "vec3") {
2978
+ out += `
2979
+ /// Get ${event.name}.${field.name}.x at index
2980
+ #[inline]
2981
+ pub unsafe fn get_${lower}_${fieldLower}_x(state: *const u8, index: u16) -> f32 {
2982
+ load_f32(state.add((${UPPER}_OFFSET + 2 + (index as u32) * ${UPPER}_RECORD_SIZE + ${field.offset}) as usize))
2983
+ }
2984
+
2985
+ /// Get ${event.name}.${field.name}.y at index
2986
+ #[inline]
2987
+ pub unsafe fn get_${lower}_${fieldLower}_y(state: *const u8, index: u16) -> f32 {
2988
+ load_f32(state.add((${UPPER}_OFFSET + 2 + (index as u32) * ${UPPER}_RECORD_SIZE + ${field.offset} + 4) as usize))
2989
+ }
2990
+
2991
+ /// Get ${event.name}.${field.name}.z at index
2992
+ #[inline]
2993
+ pub unsafe fn get_${lower}_${fieldLower}_z(state: *const u8, index: u16) -> f32 {
2994
+ load_f32(state.add((${UPPER}_OFFSET + 2 + (index as u32) * ${UPPER}_RECORD_SIZE + ${field.offset} + 8) as usize))
2995
+ }
2996
+ `;
2997
+ } else if (field.type === "quat") {
2998
+ out += `
2999
+ /// Get ${event.name}.${field.name}.x at index
3000
+ #[inline]
3001
+ pub unsafe fn get_${lower}_${fieldLower}_x(state: *const u8, index: u16) -> f32 {
3002
+ load_f32(state.add((${UPPER}_OFFSET + 2 + (index as u32) * ${UPPER}_RECORD_SIZE + ${field.offset}) as usize))
3003
+ }
3004
+
3005
+ /// Get ${event.name}.${field.name}.y at index
3006
+ #[inline]
3007
+ pub unsafe fn get_${lower}_${fieldLower}_y(state: *const u8, index: u16) -> f32 {
3008
+ load_f32(state.add((${UPPER}_OFFSET + 2 + (index as u32) * ${UPPER}_RECORD_SIZE + ${field.offset} + 4) as usize))
3009
+ }
3010
+
3011
+ /// Get ${event.name}.${field.name}.z at index
3012
+ #[inline]
3013
+ pub unsafe fn get_${lower}_${fieldLower}_z(state: *const u8, index: u16) -> f32 {
3014
+ load_f32(state.add((${UPPER}_OFFSET + 2 + (index as u32) * ${UPPER}_RECORD_SIZE + ${field.offset} + 8) as usize))
3015
+ }
3016
+
3017
+ /// Get ${event.name}.${field.name}.w at index
3018
+ #[inline]
3019
+ pub unsafe fn get_${lower}_${fieldLower}_w(state: *const u8, index: u16) -> f32 {
3020
+ load_f32(state.add((${UPPER}_OFFSET + 2 + (index as u32) * ${UPPER}_RECORD_SIZE + ${field.offset} + 12) as usize))
3021
+ }
3022
+ `;
3023
+ } else {
3024
+ out += `
3025
+ /// Get ${event.name}.${field.name} at index
3026
+ #[inline]
3027
+ pub unsafe fn get_${lower}_${fieldLower}(state: *const u8, index: u16) -> ${rustType(field)} {
3028
+ ${loadFn(field)}(state.add((${UPPER}_OFFSET + 2 + (index as u32) * ${UPPER}_RECORD_SIZE + ${field.offset}) as usize))
3029
+ }
3030
+ `;
3031
+ }
3032
+ }
3033
+ const iterTypes = [];
3034
+ const iterYields = [];
3035
+ for (const field of event.fields) {
3036
+ const fieldLower = toLowerSnake(field.name);
3037
+ if (field.type === "vec2") {
3038
+ iterTypes.push("f32", "f32");
3039
+ iterYields.push(`get_${lower}_${fieldLower}_x(self.state, i)`);
3040
+ iterYields.push(`get_${lower}_${fieldLower}_y(self.state, i)`);
3041
+ } else if (field.type === "vec3") {
3042
+ iterTypes.push("f32", "f32", "f32");
3043
+ iterYields.push(`get_${lower}_${fieldLower}_x(self.state, i)`);
3044
+ iterYields.push(`get_${lower}_${fieldLower}_y(self.state, i)`);
3045
+ iterYields.push(`get_${lower}_${fieldLower}_z(self.state, i)`);
3046
+ } else if (field.type === "quat") {
3047
+ iterTypes.push("f32", "f32", "f32", "f32");
3048
+ iterYields.push(`get_${lower}_${fieldLower}_x(self.state, i)`);
3049
+ iterYields.push(`get_${lower}_${fieldLower}_y(self.state, i)`);
3050
+ iterYields.push(`get_${lower}_${fieldLower}_z(self.state, i)`);
3051
+ iterYields.push(`get_${lower}_${fieldLower}_w(self.state, i)`);
3052
+ } else {
3053
+ iterTypes.push(rustType(field));
3054
+ iterYields.push(`get_${lower}_${fieldLower}(self.state, i)`);
3055
+ }
3056
+ }
3057
+ out += `
3058
+ // Iterator for ${event.name} events
3059
+ pub struct ${event.name}Iter {
3060
+ state: *const u8,
3061
+ index: u16,
3062
+ count: u16,
3063
+ }
3064
+
3065
+ impl Iterator for ${event.name}Iter {
3066
+ // Note: Single-element tuples require trailing comma: (T,)
3067
+ type Item = (${iterTypes.join(", ")},);
3068
+
3069
+ fn next(&mut self) -> Option<Self::Item> {
3070
+ if self.index >= self.count {
3071
+ return None;
3072
+ }
3073
+ let i = self.index;
3074
+ self.index += 1;
3075
+ unsafe {
3076
+ Some((
3077
+ ${iterYields.join(",\n ")},
3078
+ ))
3079
+ }
3080
+ }
3081
+ }
3082
+
3083
+ /// Create an iterator over ${event.name} events
3084
+ pub unsafe fn iter_${lower}(state: *const u8) -> ${event.name}Iter {
3085
+ ${event.name}Iter {
3086
+ state,
3087
+ index: 0,
3088
+ count: get_${lower}_count(state),
3089
+ }
3090
+ }
3091
+ `;
3092
+ return out;
3093
+ }
3094
+ function isArrayType2(type) {
3095
+ return /^\w+\[\d+\]$/.test(type);
3096
+ }
3097
+ function validateInputSchemaForCodegen2(schema) {
3098
+ for (const ctrl of schema.controls) {
3099
+ if (isArrayType2(ctrl.type)) {
3100
+ throw new Error(
3101
+ `Input array types are not supported by codegen: control '${ctrl.name}' has type '${ctrl.type}'`
3102
+ );
3103
+ }
3104
+ }
3105
+ for (const cmd of schema.commands) {
3106
+ for (const arg of cmd.args) {
3107
+ if (isArrayType2(arg.type) || arg.arrayLength) {
3108
+ throw new Error(
3109
+ `Input array types are not supported by codegen: command '${cmd.name}' argument '${arg.name}' has type '${arg.type}'`
3110
+ );
3111
+ }
3112
+ }
3113
+ }
3114
+ }
3115
+ function generateInputHeader2() {
3116
+ return `// Language: Rust
3117
+ // Input Schema - Control and Command Accessors
3118
+ //
3119
+ // Controls are decoded by the Executor and written to the Player component.
3120
+ // These accessors read from the Player component fields.
3121
+ //
3122
+ // Commands are decoded by the Executor and pushed to command events.
3123
+ // Use the generated event iterators (e.g., iter_move_command) to read commands.
3124
+ `;
3125
+ }
3126
+ function generateControlConstants2(schema) {
3127
+ if (schema.controls.length === 0) {
3128
+ return "";
3129
+ }
3130
+ let out = `
3131
+ // =============================================================================
3132
+ // Control Index Constants (for reference)
3133
+ // =============================================================================
3134
+ `;
3135
+ for (const ctrl of schema.controls) {
3136
+ out += `pub const CONTROL_${toUpperSnake(ctrl.name)}: u8 = ${ctrl.index};
3137
+ `;
3138
+ }
3139
+ return out;
3140
+ }
3141
+ function generateFlagConstants2(schema) {
3142
+ let out = `
3143
+ // =============================================================================
3144
+ // Flag Bit Constants
3145
+ // =============================================================================
3146
+ `;
3147
+ for (const ctrl of schema.controls) {
3148
+ if (ctrl.options && ctrl.options.length > 0) {
3149
+ out += `// ${ctrl.name} flags
3150
+ `;
3151
+ for (let i = 0; i < ctrl.options.length; i++) {
3152
+ out += `pub const ${toUpperSnake(ctrl.name)}_${toUpperSnake(ctrl.options[i])}: u8 = ${i};
3153
+ `;
3154
+ }
3155
+ }
3156
+ }
3157
+ return out;
3158
+ }
3159
+ function generateControlAccessors2(ctrl) {
3160
+ const lower = toLowerSnake(ctrl.name);
3161
+ const UPPER = toUpperSnake(ctrl.name);
3162
+ let out = `
3163
+ // ---------- ${ctrl.name} (type ${ctrl.type}) ----------
3164
+ `;
3165
+ switch (ctrl.type) {
3166
+ case "flags8":
3167
+ out += `
3168
+ /// Get ${ctrl.name} flags byte from Player component
3169
+ #[inline]
3170
+ pub unsafe fn get_${lower}(state: *const u8, entity: EntityRef) -> u8 {
3171
+ get_player_${lower}(state, entity)
3172
+ }
3173
+
3174
+ /// Check if a specific bit is set in ${ctrl.name}
3175
+ #[inline]
3176
+ pub unsafe fn is_${lower}_set(state: *const u8, entity: EntityRef, bit: u8) -> bool {
3177
+ (get_${lower}(state, entity) & (1 << bit)) != 0
3178
+ }
3179
+ `;
3180
+ if (ctrl.options) {
3181
+ for (let i = 0; i < ctrl.options.length; i++) {
3182
+ const optLower = toLowerSnake(ctrl.options[i]);
3183
+ const optUpper = toUpperSnake(ctrl.options[i]);
3184
+ out += `
3185
+ /// Check if ${ctrl.name}.${ctrl.options[i]} is set
3186
+ #[inline]
3187
+ pub unsafe fn is_${lower}_${optLower}(state: *const u8, entity: EntityRef) -> bool {
3188
+ is_${lower}_set(state, entity, ${UPPER}_${optUpper})
3189
+ }
3190
+ `;
3191
+ }
3192
+ }
3193
+ break;
3194
+ case "flags16":
3195
+ out += `
3196
+ /// Get ${ctrl.name} flags word from Player component
3197
+ #[inline]
3198
+ pub unsafe fn get_${lower}(state: *const u8, entity: EntityRef) -> u16 {
3199
+ get_player_${lower}(state, entity)
3200
+ }
3201
+
3202
+ /// Check if a specific bit is set in ${ctrl.name}
3203
+ #[inline]
3204
+ pub unsafe fn is_${lower}_set(state: *const u8, entity: EntityRef, bit: u8) -> bool {
3205
+ (get_${lower}(state, entity) & (1 << bit)) != 0
3206
+ }
3207
+ `;
3208
+ break;
3209
+ case "flags32":
3210
+ out += `
3211
+ /// Get ${ctrl.name} flags dword from Player component
3212
+ #[inline]
3213
+ pub unsafe fn get_${lower}(state: *const u8, entity: EntityRef) -> u32 {
3214
+ get_player_${lower}(state, entity)
3215
+ }
3216
+
3217
+ /// Check if a specific bit is set in ${ctrl.name}
3218
+ #[inline]
3219
+ pub unsafe fn is_${lower}_set(state: *const u8, entity: EntityRef, bit: u8) -> bool {
3220
+ (get_${lower}(state, entity) & (1 << bit)) != 0
3221
+ }
3222
+ `;
3223
+ break;
3224
+ case "bool":
3225
+ out += `
3226
+ /// Get ${ctrl.name} boolean from Player component
3227
+ #[inline]
3228
+ pub unsafe fn get_${lower}(state: *const u8, entity: EntityRef) -> bool {
3229
+ get_player_${lower}(state, entity)
3230
+ }
3231
+ `;
3232
+ break;
3233
+ case "vec2":
3234
+ out += `
3235
+ /// Get ${ctrl.name}.x from Player component
3236
+ #[inline]
3237
+ pub unsafe fn get_${lower}_x(state: *const u8, entity: EntityRef) -> f32 {
3238
+ get_player_${lower}_x(state, entity)
3239
+ }
3240
+
3241
+ /// Get ${ctrl.name}.y from Player component
3242
+ #[inline]
3243
+ pub unsafe fn get_${lower}_y(state: *const u8, entity: EntityRef) -> f32 {
3244
+ get_player_${lower}_y(state, entity)
3245
+ }
3246
+ `;
3247
+ break;
3248
+ case "vec3":
3249
+ out += `
3250
+ /// Get ${ctrl.name}.x from Player component
3251
+ #[inline]
3252
+ pub unsafe fn get_${lower}_x(state: *const u8, entity: EntityRef) -> f32 {
3253
+ get_player_${lower}_x(state, entity)
3254
+ }
3255
+
3256
+ /// Get ${ctrl.name}.y from Player component
3257
+ #[inline]
3258
+ pub unsafe fn get_${lower}_y(state: *const u8, entity: EntityRef) -> f32 {
3259
+ get_player_${lower}_y(state, entity)
3260
+ }
3261
+
3262
+ /// Get ${ctrl.name}.z from Player component
3263
+ #[inline]
3264
+ pub unsafe fn get_${lower}_z(state: *const u8, entity: EntityRef) -> f32 {
3265
+ get_player_${lower}_z(state, entity)
3266
+ }
3267
+ `;
3268
+ break;
3269
+ case "quat":
3270
+ out += `
3271
+ /// Get ${ctrl.name}.x from Player component
3272
+ #[inline]
3273
+ pub unsafe fn get_${lower}_x(state: *const u8, entity: EntityRef) -> f32 {
3274
+ get_player_${lower}_x(state, entity)
3275
+ }
3276
+
3277
+ /// Get ${ctrl.name}.y from Player component
3278
+ #[inline]
3279
+ pub unsafe fn get_${lower}_y(state: *const u8, entity: EntityRef) -> f32 {
3280
+ get_player_${lower}_y(state, entity)
3281
+ }
3282
+
3283
+ /// Get ${ctrl.name}.z from Player component
3284
+ #[inline]
3285
+ pub unsafe fn get_${lower}_z(state: *const u8, entity: EntityRef) -> f32 {
3286
+ get_player_${lower}_z(state, entity)
3287
+ }
3288
+
3289
+ /// Get ${ctrl.name}.w from Player component
3290
+ #[inline]
3291
+ pub unsafe fn get_${lower}_w(state: *const u8, entity: EntityRef) -> f32 {
3292
+ get_player_${lower}_w(state, entity)
3293
+ }
3294
+ `;
3295
+ break;
3296
+ default:
3297
+ out += `
3298
+ /// Get ${ctrl.name} value from Player component
3299
+ #[inline]
3300
+ pub unsafe fn get_${lower}(state: *const u8, entity: EntityRef) -> ${rustInputType(ctrl.type)} {
3301
+ get_player_${lower}(state, entity)
3302
+ }
3303
+ `;
3304
+ break;
3305
+ }
3306
+ return out;
3307
+ }
3308
+ function generateCommandConstants(schema) {
3309
+ if (schema.commands.length === 0) {
3310
+ return "";
3311
+ }
3312
+ let out = `
3313
+ // =============================================================================
3314
+ // Command Index Constants (for reference)
3315
+ // =============================================================================
3316
+ `;
3317
+ for (const cmd of schema.commands) {
3318
+ out += `pub const COMMAND_${toUpperSnake(cmd.name)}: u8 = ${cmd.index};
3319
+ `;
3320
+ }
3321
+ return out;
3322
+ }
3323
+ function generateGameAllocator() {
3324
+ return `
3325
+ // =============================================================================
3326
+ // Global Allocator (host-managed for plugin architecture)
3327
+ // =============================================================================
3328
+
3329
+ use std::alloc::{GlobalAlloc, Layout};
3330
+
3331
+ // Host-provided allocator - enables shared memory across multiple WASM plugins.
3332
+ // The host maintains a single bump allocator and resets it between tick batches.
3333
+ extern "C" {
3334
+ /// Host-provided allocation function. Returns pointer to aligned memory.
3335
+ /// The host tracks arena position and handles memory growth.
3336
+ fn host_alloc(size: usize, align: usize) -> *mut u8;
3337
+ }
3338
+
3339
+ /// Allocator that delegates to host-provided \`host_alloc\` import.
3340
+ /// This enables multiple WASM modules to share the same memory without
3341
+ /// conflicting allocator state.
3342
+ struct HostAlloc;
3343
+
3344
+ unsafe impl GlobalAlloc for HostAlloc {
3345
+ unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
3346
+ host_alloc(layout.size(), layout.align())
3347
+ }
3348
+
3349
+ unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
3350
+ // No-op: arena is bulk-freed by host via reset_arena() between tick batches
3351
+ }
3352
+ }
3353
+
3354
+ #[global_allocator]
3355
+ static ALLOC: HostAlloc = HostAlloc;
3356
+ `;
3357
+ }
3358
+ var RustGenerator = class {
3359
+ language = "rust";
3360
+ opts;
3361
+ constructor(opts = {}) {
3362
+ this.opts = opts;
3363
+ }
3364
+ generateState(schema) {
3365
+ let out = generateStateHeader2(schema);
3366
+ out += generateComponentOffsets2(schema);
3367
+ out += generateMemoryHelpers();
3368
+ out += generateInternalHelpers();
3369
+ out += generateIterationHelpers2(schema);
3370
+ out += generateQueryHelpers2();
3371
+ out += generateLifecycle2(schema);
3372
+ out += `
3373
+ // =============================================================================
3374
+ // Component Operations
3375
+ // =============================================================================
3376
+ `;
3377
+ for (const comp of schema.entityComponents) {
3378
+ out += generateComponentAccessors2(comp);
3379
+ }
3380
+ if (schema.singletonComponents.length > 0) {
3381
+ out += `
3382
+ // =============================================================================
3383
+ // Singleton Components
3384
+ // =============================================================================
3385
+ `;
3386
+ for (const comp of schema.singletonComponents) {
3387
+ out += generateSingletonAccessors2(comp);
3388
+ }
3389
+ }
3390
+ if (schema.events && schema.events.length > 0) {
3391
+ out += `
3392
+ // =============================================================================
3393
+ // Events
3394
+ // =============================================================================
3395
+ `;
3396
+ for (const event of schema.events) {
3397
+ out += generateEventAccessors2(event);
3398
+ }
3399
+ }
3400
+ return out;
3401
+ }
3402
+ generateInput(schema) {
3403
+ validateInputSchemaForCodegen2(schema);
3404
+ let out = generateInputHeader2();
3405
+ if (schema.controls.length > 0) {
3406
+ out += generateControlConstants2(schema);
3407
+ out += generateFlagConstants2(schema);
3408
+ out += `
3409
+ // =============================================================================
3410
+ // Control Accessors (read from Player component)
3411
+ // =============================================================================
3412
+ `;
3413
+ for (const ctrl of schema.controls) {
3414
+ out += generateControlAccessors2(ctrl);
3415
+ }
3416
+ }
3417
+ if (schema.commands.length > 0) {
3418
+ out += generateCommandConstants(schema);
3419
+ }
3420
+ return out;
3421
+ }
3422
+ generateGame(state, input) {
3423
+ let out = `// Combined Rust module: state accessors + input helpers + transition
3424
+ //
3425
+ // Generated code - do not edit manually.
3426
+
3427
+ // --- State accessors --------------------------------------------------------
3428
+ `;
3429
+ out += this.generateState(state);
3430
+ out += `
3431
+ // --- Input helpers ---------------------------------------------------------
3432
+ `;
3433
+ out += this.generateInput(input);
3434
+ out += generateGameAllocator();
3435
+ return out;
3436
+ }
3437
+ };
3438
+
3439
+ // src/schema.ts
3440
+ var HEADER_SIZE = 44;
3441
+ var TYPE_SIZES = {
3442
+ bool: 1,
3443
+ uint8: 1,
3444
+ int8: 1,
3445
+ uint16: 2,
3446
+ int16: 2,
3447
+ uint32: 4,
3448
+ int32: 4,
3449
+ entityRef: 4,
3450
+ f32: 4,
3451
+ vec2: 8,
3452
+ vec3: 12,
3453
+ quat: 16,
3454
+ enum: 1
3455
+ };
3456
+ var INPUT_TYPE_SIZES = {
3457
+ bool: 1,
3458
+ uint8: 1,
3459
+ int8: 1,
3460
+ uint16: 2,
3461
+ int16: 2,
3462
+ uint32: 4,
3463
+ int32: 4,
3464
+ f32: 4,
3465
+ flags8: 1,
3466
+ flags16: 2,
3467
+ flags32: 4,
3468
+ vec2: 8,
3469
+ vec3: 12,
3470
+ quat: 16
3471
+ };
3472
+ var PRIMITIVE_TYPES = /* @__PURE__ */ new Set([
3473
+ "bool",
3474
+ "uint8",
3475
+ "int8",
3476
+ "uint16",
3477
+ "int16",
3478
+ "uint32",
3479
+ "int32",
3480
+ "entityRef",
3481
+ "f32"
3482
+ ]);
3483
+ var INPUT_PRIMITIVE_SCALAR_TYPES = /* @__PURE__ */ new Set([
3484
+ "bool",
3485
+ "uint8",
3486
+ "int8",
3487
+ "uint16",
3488
+ "int16",
3489
+ "uint32",
3490
+ "int32",
3491
+ "f32"
3492
+ ]);
3493
+ var ENUM_VARIANT_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
3494
+ function parseArrayType2(type) {
3495
+ const match = type.match(/^(\w+)\[(\d+)\]$/);
3496
+ if (!match || !match[1] || !match[2]) return null;
3497
+ const elementType = match[1];
3498
+ const length = parseInt(match[2], 10);
3499
+ if (!PRIMITIVE_TYPES.has(elementType)) return null;
3500
+ return { elementType, length };
3501
+ }
3502
+ function parseInputArrayType(type) {
3503
+ const match = type.match(/^(\w+)\[(\d+)\]$/);
3504
+ if (!match || !match[1] || !match[2]) return null;
3505
+ const elementType = match[1];
3506
+ const length = parseInt(match[2], 10);
3507
+ if (!INPUT_PRIMITIVE_SCALAR_TYPES.has(elementType)) {
3508
+ throw new Error(
3509
+ `Invalid array element type '${elementType}': arrays only support primitive scalar types (bool, uint8, int8, uint16, int16, uint32, int32, f32)`
3510
+ );
3511
+ }
3512
+ return { elementType, length };
3513
+ }
3514
+ function getTypeSize(type) {
3515
+ const arrayInfo = parseArrayType2(type);
3516
+ if (arrayInfo) {
3517
+ const elementSize = TYPE_SIZES[arrayInfo.elementType];
3518
+ if (elementSize === void 0) {
3519
+ throw new Error(`Unknown element type: ${arrayInfo.elementType}`);
3520
+ }
3521
+ return elementSize * arrayInfo.length;
3522
+ }
3523
+ const size = TYPE_SIZES[type];
3524
+ if (size === void 0) {
3525
+ throw new Error(`Unknown type: ${type}`);
3526
+ }
3527
+ return size;
3528
+ }
3529
+ function getInputTypeSize(type) {
3530
+ const arrayInfo = parseInputArrayType(type);
3531
+ if (arrayInfo) {
3532
+ const elementSize = INPUT_TYPE_SIZES[arrayInfo.elementType];
3533
+ if (elementSize === void 0) {
3534
+ throw new Error(`Unknown element type: ${arrayInfo.elementType}`);
3535
+ }
3536
+ return elementSize * arrayInfo.length;
3537
+ }
3538
+ const size = INPUT_TYPE_SIZES[type];
3539
+ if (size === void 0) {
3540
+ throw new Error(`Unknown type: ${type}`);
3541
+ }
3542
+ return size;
3543
+ }
3544
+ function getValidatedTypeSize(type, componentName, fieldName) {
3545
+ try {
3546
+ return getTypeSize(type);
3547
+ } catch (error) {
3548
+ const location = fieldName ? `${componentName}.${fieldName}` : componentName;
3549
+ const message = error instanceof Error ? error.message : String(error);
3550
+ throw new Error(`Invalid type '${type}' for '${location}': ${message}`);
3551
+ }
3552
+ }
3553
+ function validateEnumVariants(rawVariants, subject) {
3554
+ const variants = rawVariants ?? [];
3555
+ if (!Array.isArray(variants)) {
3556
+ throw new Error(`enum ${subject} must have at least one variant`);
3557
+ }
3558
+ if (variants.length === 0) {
3559
+ throw new Error(`enum ${subject} must have at least one variant`);
3560
+ }
3561
+ if (variants.length > 256) {
3562
+ throw new Error(`enum ${subject} has ${variants.length} variants, maximum is 256`);
3563
+ }
3564
+ const seen = /* @__PURE__ */ new Set();
3565
+ const seenNormalized = /* @__PURE__ */ new Map();
3566
+ for (const v of variants) {
3567
+ if (typeof v !== "string" || v.trim().length === 0) {
3568
+ throw new Error(`enum ${subject} has empty variant name`);
3569
+ }
3570
+ if (!ENUM_VARIANT_NAME_RE.test(v)) {
3571
+ throw new Error(
3572
+ `enum ${subject} has invalid variant name ${JSON.stringify(v)} (must match ${ENUM_VARIANT_NAME_RE})`
3573
+ );
3574
+ }
3575
+ if (seen.has(v)) {
3576
+ throw new Error(`enum ${subject} has duplicate variant '${v}'`);
3577
+ }
3578
+ seen.add(v);
3579
+ const normalized = toUpperSnake2(v);
3580
+ const prev = seenNormalized.get(normalized);
3581
+ if (prev !== void 0) {
3582
+ throw new Error(
3583
+ `enum ${subject} has variants ${JSON.stringify(prev)} and ${JSON.stringify(v)} which collide as ${JSON.stringify(normalized)}`
3584
+ );
3585
+ }
3586
+ seenNormalized.set(normalized, v);
3587
+ }
3588
+ return variants;
3589
+ }
3590
+ function toUpperSnake2(s) {
3591
+ let result = "";
3592
+ for (let i = 0; i < s.length; i++) {
3593
+ const c = s[i];
3594
+ if (i > 0 && c >= "A" && c <= "Z") {
3595
+ result += "_";
3596
+ }
3597
+ result += c.toUpperCase();
3598
+ }
3599
+ return result;
3600
+ }
3601
+ function compileStateSchema(schema) {
3602
+ const { maxEntities, components: componentDefs, events: eventDefs } = schema;
3603
+ if (maxEntities <= 0 || maxEntities > 65535) {
3604
+ throw new Error(`maxEntities must be between 1 and 65535, got ${maxEntities}`);
3605
+ }
3606
+ if (!Array.isArray(componentDefs)) {
3607
+ throw new Error("schema.components must be an array to preserve deterministic ordering");
3608
+ }
3609
+ const numEntityComponents = componentDefs.filter((componentDef) => {
3610
+ const singletonFlag = componentDef.singleton;
3611
+ return singletonFlag !== true;
3612
+ }).length;
3613
+ const bitmaskSize = Math.ceil(numEntityComponents / 8);
3614
+ const entityRecordSize = 2 + bitmaskSize;
3615
+ const entityTableOffset = HEADER_SIZE;
3616
+ const entityTableSize = maxEntities * entityRecordSize;
3617
+ const freeStackOffset = entityTableOffset + entityTableSize;
3618
+ const freeStackSize = 2 + maxEntities * 2;
3619
+ const componentStorageOffset = freeStackOffset + freeStackSize;
3620
+ const components = [];
3621
+ const entityComponents = [];
3622
+ const singletonComponents = [];
3623
+ const componentByName = /* @__PURE__ */ new Map();
3624
+ const seenComponentNames = /* @__PURE__ */ new Set();
3625
+ let storageOffset = componentStorageOffset;
3626
+ const pendingSingletons = [];
3627
+ let bitIndex = 0;
3628
+ for (let i = 0; i < componentDefs.length; i++) {
3629
+ const componentDef = componentDefs[i];
3630
+ if (componentDef === void 0 || componentDef === null || typeof componentDef !== "object") {
3631
+ throw new Error(`Component at index ${i} must be an object`);
3632
+ }
3633
+ const rawName = componentDef.name;
3634
+ if (typeof rawName !== "string" || rawName.trim().length === 0) {
3635
+ throw new Error(`Component at index ${i} must have a non-empty string 'name'`);
3636
+ }
3637
+ const name = rawName;
3638
+ if (seenComponentNames.has(name)) {
3639
+ throw new Error(`Duplicate component name '${name}' in schema`);
3640
+ }
3641
+ seenComponentNames.add(name);
3642
+ const rawType = componentDef.type;
3643
+ if (typeof rawType !== "string" || rawType.trim().length === 0) {
3644
+ throw new Error(`Component '${name}' must have a string 'type'`);
3645
+ }
3646
+ const componentType = rawType;
3647
+ const fields = [];
3648
+ let componentSize = 0;
3649
+ let isTag = false;
3650
+ const isSingleton = componentDef.singleton === true;
3651
+ if (componentType === "tag") {
3652
+ isTag = true;
3653
+ if (isSingleton) {
3654
+ throw new Error(`Component '${name}' cannot be a singleton tag`);
3655
+ }
3656
+ } else if (componentType === "compound") {
3657
+ const rawFields = componentDef.fields;
3658
+ if (!Array.isArray(rawFields)) {
3659
+ throw new Error(`Component '${name}' with type 'compound' must declare a 'fields' array`);
3660
+ }
3661
+ const seenFieldNames = /* @__PURE__ */ new Set();
3662
+ let fieldOffset = 0;
3663
+ for (let fieldIndex = 0; fieldIndex < rawFields.length; fieldIndex++) {
3664
+ const fieldDef = rawFields[fieldIndex];
3665
+ if (fieldDef === void 0 || fieldDef === null || typeof fieldDef !== "object") {
3666
+ throw new Error(`Component '${name}' has an invalid field at index ${fieldIndex}`);
3667
+ }
3668
+ const rawFieldName = fieldDef.name;
3669
+ if (typeof rawFieldName !== "string" || rawFieldName.trim().length === 0) {
3670
+ throw new Error(`Component '${name}' has a field with a missing or empty 'name'`);
3671
+ }
3672
+ if (seenFieldNames.has(rawFieldName)) {
3673
+ throw new Error(`Component '${name}' has duplicate field '${rawFieldName}'`);
3674
+ }
3675
+ seenFieldNames.add(rawFieldName);
3676
+ const rawFieldType = fieldDef.type;
3677
+ if (typeof rawFieldType !== "string" || rawFieldType.trim().length === 0) {
3678
+ throw new Error(`Field '${name}.${rawFieldName}' must have a string 'type'`);
3679
+ }
3680
+ const size = getValidatedTypeSize(rawFieldType, name, rawFieldName);
3681
+ const arrayInfo = parseArrayType2(rawFieldType);
3682
+ const compiledField = {
3683
+ name: rawFieldName,
3684
+ type: rawFieldType,
3685
+ size,
3686
+ offset: fieldOffset
3687
+ };
3688
+ if (arrayInfo) {
3689
+ compiledField.arrayLength = arrayInfo.length;
3690
+ compiledField.arrayElementType = arrayInfo.elementType;
3691
+ }
3692
+ if (rawFieldType === "enum") {
3693
+ compiledField.variants = validateEnumVariants(
3694
+ fieldDef.variants,
3695
+ `field '${name}.${rawFieldName}'`
3696
+ );
3697
+ }
3698
+ fields.push(compiledField);
3699
+ fieldOffset += size;
3700
+ componentSize += size;
3701
+ }
3702
+ } else {
3703
+ const fieldType = componentType;
3704
+ const size = getValidatedTypeSize(fieldType, name);
3705
+ const arrayInfo = parseArrayType2(fieldType);
3706
+ const compiledField = {
3707
+ name: "",
3708
+ type: fieldType,
3709
+ size,
3710
+ offset: 0
3711
+ };
3712
+ if (arrayInfo) {
3713
+ compiledField.arrayLength = arrayInfo.length;
3714
+ compiledField.arrayElementType = arrayInfo.elementType;
3715
+ }
3716
+ if (fieldType === "enum") {
3717
+ compiledField.variants = validateEnumVariants(
3718
+ componentDef.variants,
3719
+ `component '${name}'`
3720
+ );
3721
+ }
3722
+ fields.push(compiledField);
3723
+ componentSize = size;
3724
+ }
3725
+ const compiled = {
3726
+ name,
3727
+ index: isSingleton ? -1 : bitIndex,
3728
+ isTag,
3729
+ isSingleton,
3730
+ size: componentSize,
3731
+ storageOffset: 0,
3732
+ fields
3733
+ };
3734
+ components.push(compiled);
3735
+ componentByName.set(name, compiled);
3736
+ if (isSingleton) {
3737
+ pendingSingletons.push(compiled);
3738
+ singletonComponents.push(compiled);
3739
+ } else {
3740
+ if (!isTag && componentSize > 0) {
3741
+ compiled.storageOffset = storageOffset;
3742
+ storageOffset += maxEntities * componentSize;
3743
+ } else {
3744
+ compiled.storageOffset = 0;
3745
+ }
3746
+ entityComponents.push(compiled);
3747
+ bitIndex += 1;
3748
+ }
3749
+ }
3750
+ const singletonStorageOffset = storageOffset;
3751
+ let singletonCursor = singletonStorageOffset;
3752
+ for (const singleton of pendingSingletons) {
3753
+ singleton.storageOffset = singletonCursor;
3754
+ singletonCursor += singleton.size;
3755
+ }
3756
+ const singletonStorageSize = singletonCursor - singletonStorageOffset;
3757
+ let totalSize = singletonCursor;
3758
+ let events;
3759
+ let eventByName;
3760
+ let eventStorageOffset;
3761
+ let eventStorageSize;
3762
+ if (eventDefs && eventDefs.length > 0) {
3763
+ events = [];
3764
+ eventByName = /* @__PURE__ */ new Map();
3765
+ eventStorageOffset = totalSize;
3766
+ let eventCursor = eventStorageOffset;
3767
+ const seenEventNames = /* @__PURE__ */ new Set();
3768
+ for (let i = 0; i < eventDefs.length; i++) {
3769
+ const eventDef = eventDefs[i];
3770
+ if (typeof eventDef.name !== "string" || eventDef.name.trim().length === 0) {
3771
+ throw new Error(`Event at index ${i} must have a non-empty string 'name'`);
3772
+ }
3773
+ if (seenEventNames.has(eventDef.name)) {
3774
+ throw new Error(`Duplicate event name '${eventDef.name}' in schema`);
3775
+ }
3776
+ seenEventNames.add(eventDef.name);
3777
+ if (seenComponentNames.has(eventDef.name)) {
3778
+ throw new Error(`Event name '${eventDef.name}' conflicts with component name`);
3779
+ }
3780
+ if (!Array.isArray(eventDef.fields) || eventDef.fields.length === 0) {
3781
+ throw new Error(`Event '${eventDef.name}' must have at least one field`);
3782
+ }
3783
+ const maxEvents = eventDef.maxEvents && eventDef.maxEvents > 0 ? eventDef.maxEvents : 64;
3784
+ const eventFields = [];
3785
+ let fieldOffset = 0;
3786
+ const seenFieldNames = /* @__PURE__ */ new Set();
3787
+ for (let fi = 0; fi < eventDef.fields.length; fi++) {
3788
+ const fieldDef = eventDef.fields[fi];
3789
+ if (typeof fieldDef.name !== "string" || fieldDef.name.trim().length === 0) {
3790
+ throw new Error(`Event '${eventDef.name}' has a field with a missing or empty 'name' at index ${fi}`);
3791
+ }
3792
+ if (seenFieldNames.has(fieldDef.name)) {
3793
+ throw new Error(`Event '${eventDef.name}' has duplicate field '${fieldDef.name}'`);
3794
+ }
3795
+ seenFieldNames.add(fieldDef.name);
3796
+ if (typeof fieldDef.type !== "string" || fieldDef.type.trim().length === 0) {
3797
+ throw new Error(`Event field '${eventDef.name}.${fieldDef.name}' must have a string 'type'`);
3798
+ }
3799
+ const arrayInfo = parseArrayType2(fieldDef.type);
3800
+ if (arrayInfo) {
3801
+ throw new Error(
3802
+ `Event field '${eventDef.name}.${fieldDef.name}' has array type '${fieldDef.type}'; events do not support array types`
3803
+ );
3804
+ }
3805
+ const size = getValidatedTypeSize(fieldDef.type, eventDef.name, fieldDef.name);
3806
+ const compiledField = {
3807
+ name: fieldDef.name,
3808
+ type: fieldDef.type,
3809
+ size,
3810
+ offset: fieldOffset
3811
+ };
3812
+ if (fieldDef.type === "enum" && fieldDef.variants) {
3813
+ compiledField.variants = validateEnumVariants(fieldDef.variants, `event field '${eventDef.name}.${fieldDef.name}'`);
3814
+ }
3815
+ eventFields.push(compiledField);
3816
+ fieldOffset += size;
3817
+ }
3818
+ const recordSize = fieldOffset;
3819
+ const bufferSize = 2 + maxEvents * recordSize;
3820
+ const compiledEvent = {
3821
+ name: eventDef.name,
3822
+ index: i,
3823
+ maxEvents,
3824
+ recordSize,
3825
+ storageOffset: eventCursor,
3826
+ bufferSize,
3827
+ fields: eventFields
3828
+ };
3829
+ events.push(compiledEvent);
3830
+ eventByName.set(eventDef.name, compiledEvent);
3831
+ eventCursor += bufferSize;
3832
+ }
3833
+ eventStorageSize = eventCursor - eventStorageOffset;
3834
+ totalSize = eventCursor;
3835
+ }
3836
+ return {
3837
+ maxEntities,
3838
+ components,
3839
+ entityComponents,
3840
+ singletonComponents,
3841
+ componentByName,
3842
+ definition: schema,
3843
+ events,
3844
+ eventByName,
3845
+ headerOffset: 0,
3846
+ headerSize: HEADER_SIZE,
3847
+ entityTableOffset,
3848
+ entityRecordSize,
3849
+ freeStackOffset,
3850
+ freeStackSize,
3851
+ componentStorageOffset,
3852
+ singletonStorageOffset,
3853
+ singletonStorageSize,
3854
+ eventStorageOffset,
3855
+ eventStorageSize,
3856
+ totalSize,
3857
+ bitmaskSize
3858
+ };
3859
+ }
3860
+ function compileInputSchema(schema) {
3861
+ const controls = [];
3862
+ const commands = [];
3863
+ const controlByName = /* @__PURE__ */ new Map();
3864
+ const commandByName = /* @__PURE__ */ new Map();
3865
+ const seenNames = /* @__PURE__ */ new Set();
3866
+ let index = 0;
3867
+ for (let i = 0; i < schema.controls.length; i++) {
3868
+ const controlDef = schema.controls[i];
3869
+ if (typeof controlDef.name !== "string" || controlDef.name.trim().length === 0) {
3870
+ throw new Error(`Control at index ${i} must have a non-empty string 'name'`);
3871
+ }
3872
+ if (seenNames.has(controlDef.name)) {
3873
+ throw new Error(`Duplicate control/command name '${controlDef.name}' in schema`);
3874
+ }
3875
+ seenNames.add(controlDef.name);
3876
+ if (typeof controlDef.type !== "string" || controlDef.type.trim().length === 0) {
3877
+ throw new Error(`Control '${controlDef.name}' must have a string 'type'`);
3878
+ }
3879
+ const size = getInputTypeSize(controlDef.type);
3880
+ const compiled = {
3881
+ name: controlDef.name,
3882
+ index: index++,
3883
+ type: controlDef.type,
3884
+ size,
3885
+ options: controlDef.options,
3886
+ hint: controlDef.hint,
3887
+ retain: controlDef.retain === "always" ? "always" : "tick"
3888
+ };
3889
+ controls.push(compiled);
3890
+ controlByName.set(controlDef.name, compiled);
3891
+ if (index > 255) {
3892
+ throw new Error(`Too many controls/commands: index ${index} exceeds u8 limit (255)`);
3893
+ }
3894
+ }
3895
+ for (let i = 0; i < schema.commands.length; i++) {
3896
+ const commandDef = schema.commands[i];
3897
+ if (typeof commandDef.name !== "string" || commandDef.name.trim().length === 0) {
3898
+ throw new Error(`Command at index ${i} must have a non-empty string 'name'`);
3899
+ }
3900
+ if (seenNames.has(commandDef.name)) {
3901
+ throw new Error(`Duplicate control/command name '${commandDef.name}' in schema`);
3902
+ }
3903
+ seenNames.add(commandDef.name);
3904
+ const args = [];
3905
+ let offset = 0;
3906
+ for (let ai = 0; ai < commandDef.args.length; ai++) {
3907
+ const argDef = commandDef.args[ai];
3908
+ if (typeof argDef.name !== "string" || argDef.name.trim().length === 0) {
3909
+ throw new Error(`Command '${commandDef.name}' has an argument with missing 'name' at index ${ai}`);
3910
+ }
3911
+ const size = getInputTypeSize(argDef.type);
3912
+ const arrayInfo = parseInputArrayType(argDef.type);
3913
+ const arg = {
3914
+ name: argDef.name,
3915
+ type: argDef.type,
3916
+ size,
3917
+ offset
3918
+ };
3919
+ if (arrayInfo) {
3920
+ arg.arrayLength = arrayInfo.length;
3921
+ arg.arrayElementType = arrayInfo.elementType;
3922
+ }
3923
+ args.push(arg);
3924
+ offset += size;
3925
+ }
3926
+ const compiled = {
3927
+ name: commandDef.name,
3928
+ index: index++,
3929
+ args,
3930
+ totalSize: offset
3931
+ };
3932
+ commands.push(compiled);
3933
+ commandByName.set(commandDef.name, compiled);
3934
+ if (index > 255) {
3935
+ throw new Error(`Too many controls/commands: index ${index} exceeds u8 limit (255)`);
3936
+ }
3937
+ }
3938
+ return { controls, commands, controlByName, commandByName };
3939
+ }
3940
+
3941
+ // src/vite/codegen-runner.ts
3942
+ async function runCodegen(configPath, options) {
3943
+ const { generatedDir, verbose } = options;
3944
+ const project = loadProject(configPath);
3945
+ const graph = loadConfigGraph(project);
3946
+ const merged = mergeConfigs(graph);
3947
+ if (verbose) {
3948
+ console.log(`[multitap] Loaded ${graph.order.length} plugin(s)`);
3949
+ }
3950
+ const compiledInput = compileInputSchema(merged.config.input);
3951
+ const preparedState = prepareStateSchema(merged.config.state, compiledInput);
3952
+ const compiledState = compileStateSchema(preparedState);
3953
+ if (verbose) {
3954
+ console.log(`[multitap] State buffer size: ${compiledState.totalSize} bytes`);
3955
+ }
3956
+ const tsGen = new TypeScriptGenerator();
3957
+ const tsCode = tsGen.generateGame(compiledState, compiledInput);
3958
+ await mkdir(generatedDir, { recursive: true });
3959
+ const tsPath = join2(generatedDir, "state.ts");
3960
+ await writeFile(tsPath, tsCode, "utf-8");
3961
+ if (verbose) {
3962
+ console.log(`[multitap] Generated ${tsPath}`);
3963
+ }
3964
+ const rustGen = new RustGenerator();
3965
+ const rustCode = rustGen.generateGame(compiledState, compiledInput);
3966
+ for (const pluginPath of merged.pluginOrder) {
3967
+ const rustDir = join2(pluginPath, "src");
3968
+ await mkdir(rustDir, { recursive: true });
3969
+ const rustPath = join2(rustDir, "generated.rs");
3970
+ await writeFile(rustPath, rustCode, "utf-8");
3971
+ if (verbose) {
3972
+ console.log(`[multitap] Generated ${rustPath}`);
3973
+ }
3974
+ }
3975
+ return {
3976
+ compiledState,
3977
+ compiledInput,
3978
+ mergedConfig: merged,
3979
+ graph
3980
+ };
3981
+ }
3982
+
3983
+ // src/vite/wasm-compiler.ts
3984
+ import { spawn } from "node:child_process";
3985
+ import { readFile, readdir, stat } from "node:fs/promises";
3986
+ import { join as join3, basename, dirname as dirname2 } from "node:path";
3987
+ var WASM_PAGE_SIZE = 65536;
3988
+ async function findCargoToml(startDir) {
3989
+ let dir = startDir;
3990
+ while (true) {
3991
+ const cargoPath = join3(dir, "Cargo.toml");
3992
+ try {
3993
+ await stat(cargoPath);
3994
+ return dir;
3995
+ } catch {
3996
+ const parent = dirname2(dir);
3997
+ if (parent === dir) {
3998
+ throw new Error(`Cargo.toml not found starting from ${startDir}`);
3999
+ }
4000
+ dir = parent;
4001
+ }
4002
+ }
4003
+ }
4004
+ async function findWasmFile(targetDir) {
4005
+ const wasmDir = join3(targetDir, "wasm32-unknown-unknown", "release");
4006
+ const entries = await readdir(wasmDir);
4007
+ for (const entry of entries) {
4008
+ if (entry.endsWith(".wasm")) {
4009
+ return join3(wasmDir, entry);
4010
+ }
4011
+ }
4012
+ throw new Error(`No .wasm file found in ${wasmDir}`);
4013
+ }
4014
+ function runCommand(command, args, options) {
4015
+ return new Promise((resolve3, reject) => {
4016
+ const proc = spawn(command, args, {
4017
+ cwd: options.cwd,
4018
+ env: options.env,
4019
+ stdio: options.verbose ? "inherit" : "pipe"
4020
+ });
4021
+ let stderr = "";
4022
+ if (!options.verbose && proc.stderr) {
4023
+ proc.stderr.on("data", (data) => {
4024
+ stderr += data.toString();
4025
+ });
4026
+ }
4027
+ proc.on("error", (err) => {
4028
+ reject(new Error(`Failed to spawn ${command}: ${err.message}`));
4029
+ });
4030
+ proc.on("close", (code) => {
4031
+ if (code !== 0) {
4032
+ reject(new Error(`${command} exited with code ${code}
4033
+ ${stderr}`));
4034
+ } else {
4035
+ resolve3();
4036
+ }
4037
+ });
4038
+ });
4039
+ }
4040
+ async function compilePluginWasm(pluginPath, globalBase, verbose = false) {
4041
+ const cargoDir = await findCargoToml(pluginPath);
4042
+ const targetDir = join3(cargoDir, "target");
4043
+ const rustFlags = `-C link-args=--import-memory -C link-args=--global-base=${globalBase} -C link-args=--stack-first`;
4044
+ await runCommand(
4045
+ "cargo",
4046
+ ["build", "--release", "--target", "wasm32-unknown-unknown", "--target-dir", targetDir],
4047
+ {
4048
+ cwd: cargoDir,
4049
+ env: { ...process.env, RUSTFLAGS: rustFlags },
4050
+ verbose
4051
+ }
4052
+ );
4053
+ const wasmPath = await findWasmFile(targetDir);
4054
+ const wasmBytes = await readFile(wasmPath);
4055
+ return {
4056
+ name: basename(pluginPath),
4057
+ wasmBytes: new Uint8Array(wasmBytes),
4058
+ reservedBytes: 0
4059
+ // Will be set by caller from config
4060
+ };
4061
+ }
4062
+ function calculateGlobalBases(pluginOrder, reservedBytesMap) {
4063
+ const bases = /* @__PURE__ */ new Map();
4064
+ let offset = 0;
4065
+ for (const pluginPath of pluginOrder) {
4066
+ bases.set(pluginPath, offset);
4067
+ const reservedBytes = reservedBytesMap.get(pluginPath) ?? 0;
4068
+ const alignedSize = Math.ceil(reservedBytes / WASM_PAGE_SIZE) * WASM_PAGE_SIZE;
4069
+ offset += alignedSize;
4070
+ }
4071
+ return bases;
4072
+ }
4073
+
4074
+ // src/vite/module-builder.ts
4075
+ function bytesToBase64(data) {
4076
+ return Buffer.from(data).toString("base64");
4077
+ }
4078
+ function prepareStateSchemaForSerialization(schema) {
4079
+ return {
4080
+ maxEntities: schema.maxEntities,
4081
+ components: schema.components,
4082
+ entityComponents: schema.entityComponents,
4083
+ singletonComponents: schema.singletonComponents,
4084
+ definition: schema.definition,
4085
+ events: schema.events,
4086
+ headerOffset: schema.headerOffset,
4087
+ headerSize: schema.headerSize,
4088
+ entityTableOffset: schema.entityTableOffset,
4089
+ entityRecordSize: schema.entityRecordSize,
4090
+ freeStackOffset: schema.freeStackOffset,
4091
+ freeStackSize: schema.freeStackSize,
4092
+ componentStorageOffset: schema.componentStorageOffset,
4093
+ singletonStorageOffset: schema.singletonStorageOffset,
4094
+ singletonStorageSize: schema.singletonStorageSize,
4095
+ eventStorageOffset: schema.eventStorageOffset,
4096
+ eventStorageSize: schema.eventStorageSize,
4097
+ totalSize: schema.totalSize,
4098
+ bitmaskSize: schema.bitmaskSize
4099
+ };
4100
+ }
4101
+ function buildVirtualModule(options) {
4102
+ const { config, plugins, compiledState } = options;
4103
+ const lines = [];
4104
+ lines.push("function base64ToUint8Array(b64) {");
4105
+ lines.push(" const binary = atob(b64);");
4106
+ lines.push(" const bytes = new Uint8Array(binary.length);");
4107
+ lines.push(" for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);");
4108
+ lines.push(" return bytes;");
4109
+ lines.push("}");
4110
+ lines.push("");
4111
+ lines.push("const plugins = [");
4112
+ for (const p of plugins) {
4113
+ lines.push(
4114
+ ` { name: ${JSON.stringify(p.name)}, wasmBytes: base64ToUint8Array(${JSON.stringify(bytesToBase64(p.wasmBytes))}), reservedBytes: ${p.reservedBytes} },`
4115
+ );
4116
+ }
4117
+ lines.push("];");
4118
+ lines.push("");
4119
+ const stateJSON = JSON.stringify(prepareStateSchemaForSerialization(compiledState));
4120
+ const inputJSON = JSON.stringify(config.input);
4121
+ lines.push(`const schema = { state: ${stateJSON}, input: ${inputJSON} };`);
4122
+ lines.push("");
4123
+ lines.push("export default {");
4124
+ lines.push(` appID: ${JSON.stringify(config.appID)},`);
4125
+ lines.push(` tickRate: ${config.tickRate},`);
4126
+ lines.push(` maxTicks: ${config.maxTicks},`);
4127
+ lines.push(` maxParticipants: ${config.maxParticipants},`);
4128
+ lines.push(" plugins,");
4129
+ lines.push(" schema,");
4130
+ lines.push("};");
4131
+ return lines.join("\n");
4132
+ }
4133
+
4134
+ // src/vite/plugin.ts
4135
+ var VIRTUAL_PREFIX = "\0multitap:";
4136
+ var IMPORT_PATTERN = /^multitap:(.+\.json)$/;
4137
+ function multitapPlugin(options = {}) {
4138
+ const {
4139
+ generatedDir = "./generated",
4140
+ verbose = false,
4141
+ skipWasm = false
4142
+ } = options;
4143
+ let resolvedConfig;
4144
+ return {
4145
+ name: "vite-plugin-multitap",
4146
+ // Run before other plugins (especially vite:json) to intercept multitap: imports
4147
+ enforce: "pre",
4148
+ configResolved(config) {
4149
+ resolvedConfig = config;
4150
+ },
4151
+ resolveId(source, importer) {
4152
+ const match = source.match(IMPORT_PATTERN);
4153
+ if (!match || !match[1]) return null;
4154
+ const relativePath = match[1];
4155
+ const resolveDir = importer ? dirname3(importer) : resolvedConfig.root;
4156
+ const absolutePath = resolve2(resolveDir, relativePath);
4157
+ return VIRTUAL_PREFIX + absolutePath.replace(/\.json$/, "");
4158
+ },
4159
+ async load(id) {
4160
+ if (!id.startsWith(VIRTUAL_PREFIX)) return null;
4161
+ const absoluteConfigPath = id.slice(VIRTUAL_PREFIX.length) + ".json";
4162
+ if (verbose) {
4163
+ console.log(`[multitap] Building config: ${absoluteConfigPath}`);
4164
+ }
4165
+ const codegenResult = await runCodegen(absoluteConfigPath, {
4166
+ generatedDir: resolve2(resolvedConfig.root, generatedDir),
4167
+ verbose
4168
+ });
4169
+ const plugins = [];
4170
+ if (!skipWasm) {
4171
+ const reservedBytesMap = /* @__PURE__ */ new Map();
4172
+ for (const [pluginPath, node] of codegenResult.graph.plugins) {
4173
+ reservedBytesMap.set(pluginPath, node.config.wasm.reservedBytes);
4174
+ }
4175
+ const globalBases = calculateGlobalBases(codegenResult.mergedConfig.pluginOrder, reservedBytesMap);
4176
+ for (const pluginPath of codegenResult.mergedConfig.pluginOrder) {
4177
+ const globalBase = globalBases.get(pluginPath);
4178
+ const reservedBytes = reservedBytesMap.get(pluginPath);
4179
+ if (verbose) {
4180
+ console.log(`[multitap] Compiling ${pluginPath} (globalBase=${globalBase})`);
4181
+ }
4182
+ const result = await compilePluginWasm(pluginPath, globalBase, verbose);
4183
+ result.reservedBytes = reservedBytes;
4184
+ plugins.push(result);
4185
+ }
4186
+ }
4187
+ const moduleCode = buildVirtualModule({
4188
+ config: codegenResult.mergedConfig.config,
4189
+ plugins,
4190
+ compiledState: codegenResult.compiledState
4191
+ });
4192
+ if (verbose) {
4193
+ console.log(`[multitap] Build complete`);
4194
+ }
4195
+ return moduleCode;
4196
+ }
4197
+ };
4198
+ }
4199
+ export {
4200
+ multitapPlugin as default,
4201
+ multitapPlugin
4202
+ };