@series-inc/rundot-syncplay 5.23.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/README.md +610 -0
  2. package/dist/animation.d.ts +119 -0
  3. package/dist/animation.js +488 -0
  4. package/dist/asset-cooking.d.ts +82 -0
  5. package/dist/asset-cooking.js +364 -0
  6. package/dist/authority-room.d.ts +217 -0
  7. package/dist/authority-room.js +701 -0
  8. package/dist/bot-documents.d.ts +115 -0
  9. package/dist/bot-documents.js +549 -0
  10. package/dist/bot-primitives.d.ts +152 -0
  11. package/dist/bot-primitives.js +472 -0
  12. package/dist/browser.d.ts +57 -0
  13. package/dist/browser.js +53 -0
  14. package/dist/canonical.d.ts +12 -0
  15. package/dist/canonical.js +288 -0
  16. package/dist/certification.d.ts +455 -0
  17. package/dist/certification.js +1855 -0
  18. package/dist/chaos-certification.d.ts +56 -0
  19. package/dist/chaos-certification.js +1397 -0
  20. package/dist/cjs/animation.js +495 -0
  21. package/dist/cjs/asset-cooking.js +372 -0
  22. package/dist/cjs/authority-room.js +704 -0
  23. package/dist/cjs/bot-documents.js +556 -0
  24. package/dist/cjs/bot-primitives.js +482 -0
  25. package/dist/cjs/browser.js +96 -0
  26. package/dist/cjs/canonical.js +299 -0
  27. package/dist/cjs/certification.js +1860 -0
  28. package/dist/cjs/chaos-certification.js +1402 -0
  29. package/dist/cjs/collections.js +36 -0
  30. package/dist/cjs/collision.js +96 -0
  31. package/dist/cjs/commands.js +987 -0
  32. package/dist/cjs/config-bundle.js +491 -0
  33. package/dist/cjs/creator.js +30 -0
  34. package/dist/cjs/deterministic-primitives.js +134 -0
  35. package/dist/cjs/deterministic-session.js +1410 -0
  36. package/dist/cjs/ecs-lite.js +155 -0
  37. package/dist/cjs/effects-adapter.js +97 -0
  38. package/dist/cjs/engine-complete.js +2297 -0
  39. package/dist/cjs/engine-core-proof.js +70 -0
  40. package/dist/cjs/engine-parity-matrix.js +874 -0
  41. package/dist/cjs/engine-parity.js +3183 -0
  42. package/dist/cjs/engine-runtime.js +3141 -0
  43. package/dist/cjs/errors.js +22 -0
  44. package/dist/cjs/events.js +247 -0
  45. package/dist/cjs/examples/counter-duel.js +157 -0
  46. package/dist/cjs/examples/fixed-arena.js +368 -0
  47. package/dist/cjs/index.js +229 -0
  48. package/dist/cjs/input-authority.js +150 -0
  49. package/dist/cjs/input-button.js +256 -0
  50. package/dist/cjs/input-timeline.js +418 -0
  51. package/dist/cjs/instant-replay.js +66 -0
  52. package/dist/cjs/lag-compensation3d.js +837 -0
  53. package/dist/cjs/local-authority-server-runner.js +1121 -0
  54. package/dist/cjs/local-authority-wire.js +1497 -0
  55. package/dist/cjs/local-authority.js +817 -0
  56. package/dist/cjs/local-ws-adapter.js +150 -0
  57. package/dist/cjs/match-log.js +61 -0
  58. package/dist/cjs/math.js +157 -0
  59. package/dist/cjs/mock-network-adapter.js +26 -0
  60. package/dist/cjs/movement.js +703 -0
  61. package/dist/cjs/movement3d.js +1246 -0
  62. package/dist/cjs/multiclient-authority.js +308 -0
  63. package/dist/cjs/multiplayer-service.js +434 -0
  64. package/dist/cjs/navigation.js +347 -0
  65. package/dist/cjs/navmesh.js +1437 -0
  66. package/dist/cjs/networked-client.js +691 -0
  67. package/dist/cjs/networked-input-authority.js +443 -0
  68. package/dist/cjs/node.js +47 -0
  69. package/dist/cjs/number-lab.js +78 -0
  70. package/dist/cjs/offline-session.js +370 -0
  71. package/dist/cjs/package.json +1 -0
  72. package/dist/cjs/pause-resume.js +43 -0
  73. package/dist/cjs/physics-cert.js +426 -0
  74. package/dist/cjs/physics2d.js +751 -0
  75. package/dist/cjs/physics3d.js +1996 -0
  76. package/dist/cjs/prediction-culling.js +203 -0
  77. package/dist/cjs/protocol-lab.js +1299 -0
  78. package/dist/cjs/r3f-render-adapter.js +209 -0
  79. package/dist/cjs/random.js +59 -0
  80. package/dist/cjs/replay-bundle.js +1598 -0
  81. package/dist/cjs/replay-inspect.js +236 -0
  82. package/dist/cjs/replay-scoring.js +68 -0
  83. package/dist/cjs/replay.js +356 -0
  84. package/dist/cjs/reports.js +181 -0
  85. package/dist/cjs/rollback-history.js +251 -0
  86. package/dist/cjs/runtime-guards.js +111 -0
  87. package/dist/cjs/sample-lab.js +145 -0
  88. package/dist/cjs/sample-scenes.js +752 -0
  89. package/dist/cjs/schema-artifacts.js +359 -0
  90. package/dist/cjs/schema-codegen.js +544 -0
  91. package/dist/cjs/schema-dsl-binary.js +655 -0
  92. package/dist/cjs/schema.js +319 -0
  93. package/dist/cjs/sdk-offline.js +99 -0
  94. package/dist/cjs/session-promotion.js +74 -0
  95. package/dist/cjs/session-snapshot.js +265 -0
  96. package/dist/cjs/session-wire.js +216 -0
  97. package/dist/cjs/signals.js +103 -0
  98. package/dist/cjs/sparse-set.js +169 -0
  99. package/dist/cjs/synctest.js +136 -0
  100. package/dist/cjs/system-lifecycle.js +286 -0
  101. package/dist/cjs/timers.js +61 -0
  102. package/dist/cjs/tools/static-checker.js +1468 -0
  103. package/dist/cjs/types.js +2 -0
  104. package/dist/cjs/venus-mp-budget.js +125 -0
  105. package/dist/cjs/ws-frame.js +255 -0
  106. package/dist/cli.d.ts +2 -0
  107. package/dist/cli.js +11379 -0
  108. package/dist/collections.d.ts +9 -0
  109. package/dist/collections.js +30 -0
  110. package/dist/collision.d.ts +25 -0
  111. package/dist/collision.js +85 -0
  112. package/dist/commands.d.ts +137 -0
  113. package/dist/commands.js +977 -0
  114. package/dist/config-bundle.d.ts +89 -0
  115. package/dist/config-bundle.js +483 -0
  116. package/dist/creator.d.ts +20 -0
  117. package/dist/creator.js +11 -0
  118. package/dist/deterministic-primitives.d.ts +59 -0
  119. package/dist/deterministic-primitives.js +126 -0
  120. package/dist/deterministic-session.d.ts +197 -0
  121. package/dist/deterministic-session.js +1401 -0
  122. package/dist/ecs-lite.d.ts +26 -0
  123. package/dist/ecs-lite.js +152 -0
  124. package/dist/effects-adapter.d.ts +21 -0
  125. package/dist/effects-adapter.js +94 -0
  126. package/dist/engine-complete.d.ts +45 -0
  127. package/dist/engine-complete.js +2292 -0
  128. package/dist/engine-core-proof.d.ts +26 -0
  129. package/dist/engine-core-proof.js +66 -0
  130. package/dist/engine-parity-matrix.d.ts +300 -0
  131. package/dist/engine-parity-matrix.js +869 -0
  132. package/dist/engine-parity.d.ts +74 -0
  133. package/dist/engine-parity.js +3178 -0
  134. package/dist/engine-runtime.d.ts +351 -0
  135. package/dist/engine-runtime.js +3123 -0
  136. package/dist/errors.d.ts +11 -0
  137. package/dist/errors.js +17 -0
  138. package/dist/events.d.ts +45 -0
  139. package/dist/events.js +242 -0
  140. package/dist/examples/counter-duel.d.ts +43 -0
  141. package/dist/examples/counter-duel.js +150 -0
  142. package/dist/examples/fixed-arena.d.ts +110 -0
  143. package/dist/examples/fixed-arena.js +356 -0
  144. package/dist/index.d.ts +101 -0
  145. package/dist/index.js +66 -0
  146. package/dist/input-authority.d.ts +82 -0
  147. package/dist/input-authority.js +147 -0
  148. package/dist/input-button.d.ts +36 -0
  149. package/dist/input-button.js +249 -0
  150. package/dist/input-timeline.d.ts +65 -0
  151. package/dist/input-timeline.js +412 -0
  152. package/dist/instant-replay.d.ts +59 -0
  153. package/dist/instant-replay.js +62 -0
  154. package/dist/lag-compensation3d.d.ts +146 -0
  155. package/dist/lag-compensation3d.js +827 -0
  156. package/dist/local-authority-server-runner.d.ts +79 -0
  157. package/dist/local-authority-server-runner.js +1118 -0
  158. package/dist/local-authority-wire.d.ts +142 -0
  159. package/dist/local-authority-wire.js +1477 -0
  160. package/dist/local-authority.d.ts +160 -0
  161. package/dist/local-authority.js +810 -0
  162. package/dist/local-ws-adapter.d.ts +14 -0
  163. package/dist/local-ws-adapter.js +146 -0
  164. package/dist/match-log.d.ts +74 -0
  165. package/dist/match-log.js +57 -0
  166. package/dist/math.d.ts +41 -0
  167. package/dist/math.js +154 -0
  168. package/dist/mock-network-adapter.d.ts +13 -0
  169. package/dist/mock-network-adapter.js +23 -0
  170. package/dist/movement.d.ts +155 -0
  171. package/dist/movement.js +694 -0
  172. package/dist/movement3d.d.ts +195 -0
  173. package/dist/movement3d.js +1238 -0
  174. package/dist/multiclient-authority.d.ts +30 -0
  175. package/dist/multiclient-authority.js +305 -0
  176. package/dist/multiplayer-service.d.ts +153 -0
  177. package/dist/multiplayer-service.js +429 -0
  178. package/dist/navigation.d.ts +91 -0
  179. package/dist/navigation.js +338 -0
  180. package/dist/navmesh.d.ts +236 -0
  181. package/dist/navmesh.js +1423 -0
  182. package/dist/networked-client.d.ts +124 -0
  183. package/dist/networked-client.js +688 -0
  184. package/dist/networked-input-authority.d.ts +51 -0
  185. package/dist/networked-input-authority.js +439 -0
  186. package/dist/node.d.ts +22 -0
  187. package/dist/node.js +19 -0
  188. package/dist/number-lab.d.ts +15 -0
  189. package/dist/number-lab.js +75 -0
  190. package/dist/offline-session.d.ts +2 -0
  191. package/dist/offline-session.js +367 -0
  192. package/dist/pause-resume.d.ts +48 -0
  193. package/dist/pause-resume.js +40 -0
  194. package/dist/physics-cert.d.ts +40 -0
  195. package/dist/physics-cert.js +389 -0
  196. package/dist/physics2d.d.ts +142 -0
  197. package/dist/physics2d.js +741 -0
  198. package/dist/physics3d.d.ts +285 -0
  199. package/dist/physics3d.js +1979 -0
  200. package/dist/prediction-culling.d.ts +34 -0
  201. package/dist/prediction-culling.js +200 -0
  202. package/dist/protocol-lab.d.ts +115 -0
  203. package/dist/protocol-lab.js +1295 -0
  204. package/dist/r3f-render-adapter.d.ts +61 -0
  205. package/dist/r3f-render-adapter.js +203 -0
  206. package/dist/random.d.ts +13 -0
  207. package/dist/random.js +55 -0
  208. package/dist/replay-bundle.d.ts +150 -0
  209. package/dist/replay-bundle.js +1586 -0
  210. package/dist/replay-inspect.d.ts +60 -0
  211. package/dist/replay-inspect.js +228 -0
  212. package/dist/replay-scoring.d.ts +28 -0
  213. package/dist/replay-scoring.js +65 -0
  214. package/dist/replay.d.ts +17 -0
  215. package/dist/replay.js +351 -0
  216. package/dist/reports.d.ts +176 -0
  217. package/dist/reports.js +174 -0
  218. package/dist/rollback-history.d.ts +46 -0
  219. package/dist/rollback-history.js +247 -0
  220. package/dist/runtime-guards.d.ts +1 -0
  221. package/dist/runtime-guards.js +108 -0
  222. package/dist/sample-lab.d.ts +32 -0
  223. package/dist/sample-lab.js +134 -0
  224. package/dist/sample-scenes.d.ts +86 -0
  225. package/dist/sample-scenes.js +745 -0
  226. package/dist/schema-artifacts.d.ts +49 -0
  227. package/dist/schema-artifacts.js +356 -0
  228. package/dist/schema-codegen.d.ts +21 -0
  229. package/dist/schema-codegen.js +538 -0
  230. package/dist/schema-dsl-binary.d.ts +75 -0
  231. package/dist/schema-dsl-binary.js +648 -0
  232. package/dist/schema.d.ts +87 -0
  233. package/dist/schema.js +315 -0
  234. package/dist/sdk-offline.d.ts +34 -0
  235. package/dist/sdk-offline.js +96 -0
  236. package/dist/session-promotion.d.ts +26 -0
  237. package/dist/session-promotion.js +70 -0
  238. package/dist/session-snapshot.d.ts +95 -0
  239. package/dist/session-snapshot.js +255 -0
  240. package/dist/session-wire.d.ts +89 -0
  241. package/dist/session-wire.js +211 -0
  242. package/dist/signals.d.ts +31 -0
  243. package/dist/signals.js +99 -0
  244. package/dist/sparse-set.d.ts +34 -0
  245. package/dist/sparse-set.js +165 -0
  246. package/dist/synctest.d.ts +31 -0
  247. package/dist/synctest.js +133 -0
  248. package/dist/system-lifecycle.d.ts +72 -0
  249. package/dist/system-lifecycle.js +281 -0
  250. package/dist/timers.d.ts +17 -0
  251. package/dist/timers.js +53 -0
  252. package/dist/tools/index.d.ts +4 -0
  253. package/dist/tools/index.js +4 -0
  254. package/dist/tools/static-checker.d.ts +31 -0
  255. package/dist/tools/static-checker.js +1462 -0
  256. package/dist/tools/vite-plugin.d.ts +69 -0
  257. package/dist/tools/vite-plugin.js +203 -0
  258. package/dist/types.d.ts +148 -0
  259. package/dist/types.js +1 -0
  260. package/dist/venus-mp-budget.d.ts +29 -0
  261. package/dist/venus-mp-budget.js +122 -0
  262. package/dist/ws-frame.d.ts +49 -0
  263. package/dist/ws-frame.js +247 -0
  264. package/package.json +169 -0
@@ -0,0 +1,319 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defineSyncplayGame = defineSyncplayGame;
4
+ exports.stableDescriptorId = stableDescriptorId;
5
+ const canonical_js_1 = require("./canonical.js");
6
+ const errors_js_1 = require("./errors.js");
7
+ const tickRates = [10, 20, 30, 60];
8
+ const idPattern = /^[a-zA-Z0-9][a-zA-Z0-9._/@-]*$/;
9
+ const numericModes = ['integer', 'fixed-q16', 'quantized-float', 'render-only-float'];
10
+ const numericFieldKinds = ['integer', 'fixed-q16', 'bitfield', 'player-slot'];
11
+ const collectionOrderings = ['indexed', 'sorted-key', 'stable-id'];
12
+ function defineSyncplayGame(descriptor) {
13
+ validateDescriptor(descriptor);
14
+ const eventSchemaId = descriptor.events === undefined ? 'events/none' : stableDescriptorId('events', descriptor.events);
15
+ const assetSchemaId = descriptor.assets === undefined ? 'assets/none' : stableDescriptorId('assets', descriptor.assets);
16
+ const commandSchemaId = descriptor.commandChannels === undefined ? 'commands/none' : stableDescriptorId('commands', descriptor.commandChannels);
17
+ const prototypeSchemaId = descriptor.prototypes === undefined ? 'prototypes/none' : stableDescriptorId('prototypes', descriptor.prototypes);
18
+ // Content-hash the input/state/config sub-ids like every other identity
19
+ // component: the author-declared label rides along, but editing the FIELDS
20
+ // without bumping the label must change the replay identity — otherwise a
21
+ // schema edit produces replays that verify as compatible and then diverge.
22
+ const configSchemaId = descriptor.config === undefined
23
+ ? 'config/none'
24
+ : `${descriptor.config.id}#${stableDescriptorId('config', descriptor.config)}`;
25
+ const inputSchemaId = `${descriptor.input.id}#${stableDescriptorId('input', descriptor.input)}`;
26
+ const stateSchemaId = `${descriptor.state.id}#${stableDescriptorId('state', descriptor.state)}`;
27
+ const systemOrderId = stableDescriptorId('systems', descriptor.systems.map((system) => system.id));
28
+ const identity = {
29
+ deterministicVersion: descriptor.deterministicVersion,
30
+ tickRate: descriptor.tickRate,
31
+ maxPlayers: descriptor.maxPlayers,
32
+ inputSchemaId,
33
+ stateSchemaId,
34
+ commandSchemaId,
35
+ eventSchemaId,
36
+ assetSchemaId,
37
+ prototypeSchemaId,
38
+ configSchemaId,
39
+ systemOrderId,
40
+ artifactId: '',
41
+ };
42
+ const artifactId = stableDescriptorId('game', {
43
+ ...descriptor,
44
+ commandSchemaId,
45
+ eventSchemaId,
46
+ assetSchemaId,
47
+ prototypeSchemaId,
48
+ configSchemaId,
49
+ systemOrderId,
50
+ });
51
+ return {
52
+ ...descriptor,
53
+ artifactId,
54
+ replayIdentity: {
55
+ ...identity,
56
+ artifactId,
57
+ },
58
+ };
59
+ }
60
+ function stableDescriptorId(prefix, value) {
61
+ return `${prefix}/${(0, canonical_js_1.defaultChecksum)(value)}`;
62
+ }
63
+ function validateDescriptor(descriptor) {
64
+ validateId('deterministicVersion', descriptor.deterministicVersion);
65
+ if (!tickRates.includes(descriptor.tickRate)) {
66
+ throwSchemaError('tickRate must be one of 10, 20, 30, or 60', 'schema.tick-rate');
67
+ }
68
+ if (!Number.isInteger(descriptor.maxPlayers) || descriptor.maxPlayers < 1 || descriptor.maxPlayers > 128) {
69
+ throwSchemaError('maxPlayers must be an integer between 1 and 128', 'schema.max-players');
70
+ }
71
+ validateId('input.id', descriptor.input.id);
72
+ validateInputFields(descriptor.input.fields, descriptor.maxPlayers);
73
+ validateId('state.id', descriptor.state.id);
74
+ validateStateFields(descriptor.state.fields);
75
+ validateCommandChannels(descriptor.commandChannels ?? []);
76
+ validateEvents(descriptor.events ?? []);
77
+ validateAssets(descriptor.assets ?? []);
78
+ validatePrototypes(descriptor.prototypes ?? []);
79
+ if (descriptor.config !== undefined) {
80
+ validateId('config.id', descriptor.config.id);
81
+ validateFields('config.fields', descriptor.config.fields);
82
+ rejectButtonFields('config.fields', descriptor.config.fields);
83
+ }
84
+ validateNamedDescriptors('systems', descriptor.systems);
85
+ if (descriptor.systems.length === 0) {
86
+ throwSchemaError('At least one stateless system id is required', 'schema.systems-empty');
87
+ }
88
+ (0, canonical_js_1.canonicalStringify)(descriptor);
89
+ }
90
+ function validateCommandChannels(channels) {
91
+ validateNamedDescriptors('commandChannels', channels);
92
+ for (const channel of channels) {
93
+ if (channel.delivery !== 'reliable' && channel.delivery !== 'unreliable') {
94
+ throwSchemaError(`Command channel ${channel.id} has invalid delivery`, 'schema.command-delivery');
95
+ }
96
+ validateFields(`commandChannels.${channel.id}.fields`, channel.fields);
97
+ rejectButtonFields(`commandChannels.${channel.id}.fields`, channel.fields);
98
+ }
99
+ }
100
+ function validateEvents(events) {
101
+ validateNamedDescriptors('events', events);
102
+ for (const event of events) {
103
+ if (event.rollback !== 'predicted' && event.rollback !== 'verified-only') {
104
+ throwSchemaError(`Event ${event.id} has invalid rollback mode`, 'schema.event-rollback');
105
+ }
106
+ validateFields(`events.${event.id}.payloadFields`, event.payloadFields);
107
+ rejectButtonFields(`events.${event.id}.payloadFields`, event.payloadFields);
108
+ }
109
+ }
110
+ function validateAssets(assets) {
111
+ validateNamedDescriptors('assets', assets);
112
+ for (const asset of assets) {
113
+ validateFields(`assets.${asset.id}.fields`, asset.fields);
114
+ rejectButtonFields(`assets.${asset.id}.fields`, asset.fields);
115
+ }
116
+ }
117
+ function validatePrototypes(prototypes) {
118
+ validateNamedDescriptors('prototypes', prototypes);
119
+ for (const prototype of prototypes) {
120
+ if (prototype.components.length === 0) {
121
+ throwSchemaError(`Prototype ${prototype.id} must include at least one component`, 'schema.prototype-components');
122
+ }
123
+ const components = new Set();
124
+ for (const component of prototype.components) {
125
+ validateId(`prototypes.${prototype.id}.component`, component);
126
+ if (components.has(component)) {
127
+ throwSchemaError(`Prototype ${prototype.id} contains duplicate component ${component}`, 'schema.prototype-duplicate-component');
128
+ }
129
+ components.add(component);
130
+ }
131
+ }
132
+ }
133
+ function validateInputFields(fields, maxPlayers) {
134
+ validateFields('input.fields', fields);
135
+ for (const field of fields) {
136
+ if (field.policy !== 'repeat' && field.policy !== 'command') {
137
+ throwSchemaError(`Input field ${field.name} has invalid policy`, 'schema.input-policy');
138
+ }
139
+ if (field.kind === 'bitfield'
140
+ && (field.bitPacking === undefined
141
+ || !Number.isInteger(field.bitPacking.bits)
142
+ || field.bitPacking.bits < 1
143
+ || field.bitPacking.bits > 32)) {
144
+ throwSchemaError(`Input bitfield ${field.name} requires 1 to 32 packing bits`, 'schema.bit-packing');
145
+ }
146
+ if (field.kind === 'button' && field.bitPacking !== undefined && (field.bitPacking.bits !== 1 || field.bitPacking.signed === true)) {
147
+ throwSchemaError(`Input button ${field.name} bitPacking must be one unsigned bit`, 'schema.bit-packing');
148
+ }
149
+ if (field.kind !== 'button'
150
+ && field.kind !== 'bitfield'
151
+ && field.kind !== 'integer'
152
+ && field.kind !== 'player-slot'
153
+ && field.bitPacking !== undefined) {
154
+ throwSchemaError(`Input ${field.name} bitPacking is only valid on numeric or button fields`, 'schema.bit-packing');
155
+ }
156
+ if (field.bitPacking !== undefined && typeof field.bitPacking.signed !== 'boolean' && field.bitPacking.signed !== undefined) {
157
+ throwSchemaError(`Input bitfield ${field.name} signed packing flag must be boolean`, 'schema.bit-packing');
158
+ }
159
+ validateInputDefaultValue(field);
160
+ const playerSlotDefault = Number(field.defaultValue);
161
+ // Validate against the game's actual maxPlayers, not the platform cap of
162
+ // 128 — the runtime canonicalizer rejects `value >= maxPlayers`
163
+ // (schema-artifacts.player-slot), so a default that only passes the 128
164
+ // check here would throw in the hot path the first time it is substituted.
165
+ if (field.kind === 'player-slot' && (playerSlotDefault < 0 || playerSlotDefault >= maxPlayers)) {
166
+ throwSchemaError(`Input player-slot ${field.name} defaultValue must be within [0, maxPlayers)`, 'schema.input-default');
167
+ }
168
+ }
169
+ }
170
+ function validateStateFields(fields) {
171
+ validateFields('state.fields', fields);
172
+ for (const field of fields) {
173
+ if (field.checksummed !== true && field.checksummed !== false) {
174
+ throwSchemaError(`State field ${field.name} must declare checksum participation`, 'schema.state-checksum');
175
+ }
176
+ if (field.numericMode === 'render-only-float' && field.checksummed) {
177
+ throwSchemaError(`Render-only float field ${field.name} cannot be checksummed`, 'schema.render-float-checksum');
178
+ }
179
+ if (field.kind === 'button') {
180
+ throwSchemaError(`State field ${field.name} cannot use input-only button kind`, 'schema.button-state');
181
+ }
182
+ }
183
+ // With zero checksummed fields the state projection is {} for every frame,
184
+ // so checksums are a single constant — desync detection and replay
185
+ // verification would silently compare always-equal values. Reject at
186
+ // definition time (after per-field validation, so specific field errors
187
+ // surface first): a determinism contract needs at least one observed field.
188
+ if (!fields.some((field) => field.checksummed === true)) {
189
+ throwSchemaError('At least one state field must be checksummed', 'schema.state-no-checksum');
190
+ }
191
+ }
192
+ function rejectButtonFields(label, fields) {
193
+ for (const field of fields) {
194
+ if (field.kind === 'button') {
195
+ throwSchemaError(`${label}.${field.name} cannot use input-only button kind`, 'schema.button-non-input');
196
+ }
197
+ }
198
+ }
199
+ function validateFields(label, fields) {
200
+ if (fields.length === 0) {
201
+ throwSchemaError(`${label} must not be empty`, 'schema.empty-fields');
202
+ }
203
+ const names = new Set();
204
+ for (const field of fields) {
205
+ validateId(`${label}.name`, field.name);
206
+ if (names.has(field.name)) {
207
+ throwSchemaError(`${label} contains duplicate field ${field.name}`, 'schema.duplicate-field');
208
+ }
209
+ names.add(field.name);
210
+ validateFieldKind(field);
211
+ validateNumericMode(field);
212
+ validateCollectionOrdering(field);
213
+ validateRange(field);
214
+ if (field.min !== undefined && field.max !== undefined && field.max < field.min) {
215
+ throwSchemaError(`Field ${field.name} has max below min`, 'schema.invalid-range');
216
+ }
217
+ }
218
+ }
219
+ function validateRange(field) {
220
+ if (field.min !== undefined && (typeof field.min !== 'number' || !Number.isFinite(field.min))) {
221
+ throwSchemaError(`Field ${field.name} min must be numeric`, 'schema.invalid-range');
222
+ }
223
+ if (field.max !== undefined && (typeof field.max !== 'number' || !Number.isFinite(field.max))) {
224
+ throwSchemaError(`Field ${field.name} max must be numeric`, 'schema.invalid-range');
225
+ }
226
+ if ((field.min !== undefined || field.max !== undefined) && !numericFieldKinds.includes(field.kind)) {
227
+ throwSchemaError(`Field ${field.name} range is only valid on numeric fields`, 'schema.invalid-range');
228
+ }
229
+ }
230
+ function validateNumericMode(field) {
231
+ if (field.numericMode === undefined) {
232
+ return;
233
+ }
234
+ if (!numericModes.includes(field.numericMode)) {
235
+ throwSchemaError(`Field ${field.name} has invalid numericMode`, 'schema.numeric-mode');
236
+ }
237
+ if (!numericFieldKinds.includes(field.kind)) {
238
+ throwSchemaError(`Field ${field.name} numericMode is only valid on numeric fields`, 'schema.numeric-mode');
239
+ }
240
+ if (field.kind === 'fixed-q16' && field.numericMode !== 'fixed-q16' && field.numericMode !== 'quantized-float' && field.numericMode !== 'render-only-float') {
241
+ throwSchemaError(`Field ${field.name} fixed-q16 requires fixed numericMode`, 'schema.numeric-mode');
242
+ }
243
+ if ((field.kind === 'integer' || field.kind === 'bitfield' || field.kind === 'player-slot') && field.numericMode !== 'integer') {
244
+ throwSchemaError(`Field ${field.name} integer field requires integer numericMode`, 'schema.numeric-mode');
245
+ }
246
+ }
247
+ function validateCollectionOrdering(field) {
248
+ if (field.collectionOrdering === undefined) {
249
+ return;
250
+ }
251
+ if (!collectionOrderings.includes(field.collectionOrdering)) {
252
+ throwSchemaError(`Field ${field.name} has invalid collectionOrdering`, 'schema.collection-ordering');
253
+ }
254
+ // Scalar collection fields have no record ids to sort by — accepting a
255
+ // contract the canonicalizer cannot enforce is worse than rejecting it.
256
+ // Reintroduce alongside record-item collections.
257
+ if (field.collectionOrdering === 'stable-id') {
258
+ throwSchemaError(`Field ${field.name} collectionOrdering 'stable-id' is not supported on scalar collections (use 'indexed' or 'sorted-key')`, 'schema.collection-ordering');
259
+ }
260
+ }
261
+ function validateFieldKind(field) {
262
+ switch (field.kind) {
263
+ case 'boolean':
264
+ case 'button':
265
+ case 'integer':
266
+ case 'fixed-q16':
267
+ case 'string':
268
+ case 'bitfield':
269
+ case 'player-slot':
270
+ case 'asset-ref':
271
+ return;
272
+ default:
273
+ throwSchemaError(`Unsupported field kind ${field.kind}`, 'schema.field-kind');
274
+ }
275
+ }
276
+ function validateInputDefaultValue(field) {
277
+ if (field.kind === 'boolean' || field.kind === 'button') {
278
+ if (typeof field.defaultValue !== 'boolean') {
279
+ throwSchemaError(`Input ${field.kind} ${field.name} requires boolean defaultValue`, 'schema.input-default');
280
+ }
281
+ return;
282
+ }
283
+ if (field.kind === 'integer' || field.kind === 'bitfield' || field.kind === 'player-slot') {
284
+ if (typeof field.defaultValue !== 'number' || !Number.isInteger(field.defaultValue)) {
285
+ throwSchemaError(`Input ${field.kind} ${field.name} requires integer defaultValue`, 'schema.input-default');
286
+ }
287
+ return;
288
+ }
289
+ if (field.kind === 'fixed-q16') {
290
+ if (typeof field.defaultValue !== 'number' || !Number.isInteger(field.defaultValue)) {
291
+ throwSchemaError(`Input fixed-q16 ${field.name} requires quantized integer defaultValue`, 'schema.input-default');
292
+ }
293
+ return;
294
+ }
295
+ /* istanbul ignore else -- validateFieldKind rejects unsupported input field kinds before default validation. */
296
+ if (field.kind === 'string' || field.kind === 'asset-ref') {
297
+ if (typeof field.defaultValue !== 'string') {
298
+ throwSchemaError(`Input ${field.kind} ${field.name} requires string defaultValue`, 'schema.input-default');
299
+ }
300
+ }
301
+ }
302
+ function validateNamedDescriptors(label, descriptors) {
303
+ const ids = new Set();
304
+ for (const descriptor of descriptors) {
305
+ validateId(`${label}.id`, descriptor.id);
306
+ if (ids.has(descriptor.id)) {
307
+ throwSchemaError(`${label} contains duplicate id ${descriptor.id}`, 'schema.duplicate-id');
308
+ }
309
+ ids.add(descriptor.id);
310
+ }
311
+ }
312
+ function validateId(label, value) {
313
+ if (typeof value !== 'string' || value.length === 0 || !idPattern.test(value)) {
314
+ throwSchemaError(`${label} must be a stable non-empty id`, 'schema.invalid-id');
315
+ }
316
+ }
317
+ function throwSchemaError(message, code) {
318
+ throw new errors_js_1.SyncplayError(message, { code });
319
+ }
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSyncplayOfflineRuntime = createSyncplayOfflineRuntime;
4
+ const offline_session_js_1 = require("./offline-session.js");
5
+ const schema_artifacts_js_1 = require("./schema-artifacts.js");
6
+ function createSyncplayOfflineRuntime(config) {
7
+ const artifacts = (0, schema_artifacts_js_1.createSchemaArtifacts)(config.descriptor);
8
+ const listeners = new Set();
9
+ const playerCount = config.playerCount ?? config.descriptor.maxPlayers;
10
+ if (playerCount > config.descriptor.maxPlayers) {
11
+ throw new Error(`Offline runtime playerCount ${playerCount} exceeds descriptor maxPlayers ${config.descriptor.maxPlayers}`);
12
+ }
13
+ const session = (0, offline_session_js_1.createOfflineSession)({
14
+ tickRate: config.descriptor.tickRate,
15
+ playerCount,
16
+ initialState: config.initialState,
17
+ step: config.step,
18
+ defaultInput: config.defaultInput,
19
+ // Use the content-hashed identity ids, not the author labels — replay
20
+ // compatibility must track the actual field shapes.
21
+ inputSchemaId: artifacts.descriptor.replayIdentity.inputSchemaId,
22
+ stateSchemaId: artifacts.descriptor.replayIdentity.stateSchemaId,
23
+ deterministicVersion: config.descriptor.deterministicVersion,
24
+ canonicalizeInput(input) {
25
+ return artifacts.canonicalizeInput(input);
26
+ },
27
+ // FULL declared state for replay initial-state and snapshots: the session
28
+ // RESTORES from these (rollback, late-join hydration), so they must carry
29
+ // every declared field. The checksum projection stays checksum-only below —
30
+ // wiring it here wiped every checksummed:false field to undefined on the
31
+ // first rollback, silently diverging from peers that never rolled back.
32
+ canonicalizeState(state) {
33
+ return artifacts.canonicalizeFullState(state);
34
+ },
35
+ serializeState(state) {
36
+ return artifacts.serializeFullState(state);
37
+ },
38
+ deserializeState(bytes) {
39
+ return artifacts.deserializeFullState(bytes);
40
+ },
41
+ checksum(state) {
42
+ return artifacts.checksumState(state);
43
+ },
44
+ randomSeed: config.randomSeed,
45
+ snapshotBufferSize: config.snapshotBufferSize,
46
+ checksumIntervalFrames: config.checksumIntervalFrames,
47
+ maxCatchUpFrames: config.maxCatchUpFrames,
48
+ replayRecordingMode: config.replayRecordingMode,
49
+ runtimeGuards: config.runtimeGuards,
50
+ });
51
+ const runtime = {
52
+ mode: 'offline',
53
+ session,
54
+ getFrame() {
55
+ const state = session.getState();
56
+ return {
57
+ mode: 'offline',
58
+ frame: session.currentFrame,
59
+ predictedFrame: session.currentFrame,
60
+ verifiedFrame: session.currentFrame,
61
+ state,
62
+ checksum: artifacts.checksumState(state),
63
+ };
64
+ },
65
+ setInput(slot, input) {
66
+ session.setInput(slot, input);
67
+ },
68
+ update(deltaMs) {
69
+ const advanced = session.update(deltaMs);
70
+ if (advanced > 0) {
71
+ notify();
72
+ }
73
+ return advanced;
74
+ },
75
+ stepFrames(count) {
76
+ session.stepFrames(count);
77
+ if (count > 0) {
78
+ notify();
79
+ }
80
+ },
81
+ subscribe(listener) {
82
+ listeners.add(listener);
83
+ listener(runtime.getFrame());
84
+ return () => {
85
+ listeners.delete(listener);
86
+ };
87
+ },
88
+ exportReplay() {
89
+ return session.exportReplay();
90
+ },
91
+ };
92
+ function notify() {
93
+ const frame = runtime.getFrame();
94
+ for (const listener of listeners) {
95
+ listener(frame);
96
+ }
97
+ }
98
+ return runtime;
99
+ }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.exportPromotionSnapshot = exportPromotionSnapshot;
4
+ exports.createSessionFromPromotionSnapshot = createSessionFromPromotionSnapshot;
5
+ const canonical_js_1 = require("./canonical.js");
6
+ const errors_js_1 = require("./errors.js");
7
+ const offline_session_js_1 = require("./offline-session.js");
8
+ const session_snapshot_js_1 = require("./session-snapshot.js");
9
+ /**
10
+ * Start-from-snapshot session promotion (practice → online, Quantum 3.0.5
11
+ * parity): freeze a running offline session into a self-describing blob, then
12
+ * hydrate a FRESH session from it with frame numbering continuing where the
13
+ * source left off.
14
+ *
15
+ * The blob is the same versioned `DeterministicStateSnapshot` envelope
16
+ * (`encodeDeterministicStateSnapshot`, `v: 1`) that the snapshot late-join
17
+ * transfer path ships, so it is wire-compatible with the existing
18
+ * snapshot-offer/chunk/complete envelopes.
19
+ *
20
+ * HONESTY NOTE — the online half is protocol work that is NOT done here.
21
+ * Seeding a NETWORKED match from a promoted snapshot requires the authority to
22
+ * accept an initial snapshot at session start (today the authority only brokers
23
+ * snapshots for late-joiners into an already-running match, and networked
24
+ * clients always start from tick 0 of the room descriptor). Until that
25
+ * protocol lands, promotion works offline-to-offline: practice session →
26
+ * `exportPromotionSnapshot` → `createSessionFromPromotionSnapshot` on any peer
27
+ * running the same game build.
28
+ *
29
+ * The hydrated session cannot export a replay (its history does not start at
30
+ * tick 0) — `exportReplay()` throws, per the `restoreSnapshot` contract.
31
+ */
32
+ function exportPromotionSnapshot(session, frame) {
33
+ const targetFrame = frame ?? session.currentFrame;
34
+ (0, errors_js_1.invariant)(Number.isInteger(targetFrame) && targetFrame >= 1, 'Promotion export requires a stepped frame (>= 1): the v1 snapshot envelope encodes tick = frame - 1', 'promotion.invalid-frame');
35
+ const snapshot = session.getSnapshot(targetFrame);
36
+ if (snapshot === undefined) {
37
+ throw new errors_js_1.SyncplayError(`No snapshot buffered for frame ${targetFrame} — it may have been evicted from the rolling buffer`, { code: 'promotion.missing-snapshot', frame: targetFrame });
38
+ }
39
+ return (0, session_snapshot_js_1.encodeDeterministicStateSnapshot)({
40
+ v: 1,
41
+ tick: targetFrame - 1,
42
+ sessionFrame: targetFrame,
43
+ stateBytes: (0, session_snapshot_js_1.bytesToBase64)(snapshot.stateBytes),
44
+ randomState: snapshot.randomState,
45
+ stateChecksum: snapshot.checksum,
46
+ // Session inputs are canonical by construction (cloneCanonical on setInput).
47
+ confirmedInputs: snapshot.inputs.map((input) => (0, canonical_js_1.canonicalize)(input)),
48
+ });
49
+ }
50
+ function createSessionFromPromotionSnapshot(config, blob) {
51
+ const decoded = (0, session_snapshot_js_1.decodeDeterministicStateSnapshot)(blob);
52
+ if (!decoded.ok) {
53
+ throw new errors_js_1.SyncplayError(`Promotion snapshot rejected: ${decoded.reason}`, {
54
+ code: 'promotion.invalid-snapshot',
55
+ });
56
+ }
57
+ const snapshot = decoded.snapshot;
58
+ (0, errors_js_1.invariant)(snapshot.confirmedInputs.length === config.playerCount, `Promotion snapshot carries ${snapshot.confirmedInputs.length} input slots but config.playerCount is ${config.playerCount}`, 'promotion.player-count-mismatch');
59
+ const session = (0, offline_session_js_1.createOfflineSession)(config);
60
+ session.restoreSnapshot({
61
+ frame: snapshot.sessionFrame,
62
+ stateBytes: (0, session_snapshot_js_1.base64ToBytes)(snapshot.stateBytes),
63
+ randomState: snapshot.randomState,
64
+ inputs: snapshot.confirmedInputs.map(
65
+ // NOT `??`: a hydrator returning null (the canonical neutral) must win
66
+ // over the raw recorded value (adversarial-review LOW).
67
+ (recorded) => (config.hydrateInput !== undefined ? config.hydrateInput(recorded) : recorded)),
68
+ });
69
+ const restored = session.getSnapshot(session.currentFrame);
70
+ if (restored === undefined || restored.checksum !== snapshot.stateChecksum) {
71
+ throw new errors_js_1.SyncplayError('Promotion snapshot checksum mismatch after hydration — the game build or state bytes do not match the exporter', { code: 'promotion.checksum-mismatch', frame: snapshot.sessionFrame });
72
+ }
73
+ return session;
74
+ }