@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,82 @@
1
+ import { type DeterministicFrameRuntimeDescriptor } from './engine-runtime.js';
2
+ export interface DeterministicAssetSource {
3
+ readonly schemaVersion?: number;
4
+ readonly engineVersion?: string;
5
+ readonly dependencies?: readonly DeterministicAssetDependency[];
6
+ readonly assets: readonly DeterministicAssetRecord[];
7
+ }
8
+ export interface DeterministicAssetDependency {
9
+ readonly name: string;
10
+ readonly version: string;
11
+ }
12
+ export interface DeterministicAssetRecord {
13
+ readonly id: string;
14
+ readonly type?: string;
15
+ readonly references?: readonly string[];
16
+ readonly prototype?: boolean;
17
+ readonly [key: string]: unknown;
18
+ }
19
+ export interface DeterministicCookedAsset {
20
+ readonly id: string;
21
+ readonly type: string;
22
+ readonly hash: string;
23
+ readonly data: unknown;
24
+ }
25
+ export interface DeterministicAssetCookManifest {
26
+ readonly schemaVersion: 1;
27
+ readonly schemaHash: string;
28
+ readonly engineVersion: string;
29
+ readonly dependencyHash: string;
30
+ readonly dependencies: readonly DeterministicAssetDependency[];
31
+ readonly assetDbHash: string;
32
+ readonly lookupHash: string;
33
+ readonly prototypeIds: readonly string[];
34
+ readonly assetCount: number;
35
+ readonly assets: readonly {
36
+ readonly id: string;
37
+ readonly type: string;
38
+ readonly hash: string;
39
+ }[];
40
+ }
41
+ export interface DeterministicAssetCookResult {
42
+ readonly manifest: DeterministicAssetCookManifest;
43
+ readonly cookedAssets: readonly DeterministicCookedAsset[];
44
+ readonly bytes: string;
45
+ }
46
+ export interface DeterministicPrototypeEntity {
47
+ readonly entityId: number;
48
+ readonly prototypeId: string;
49
+ readonly assetDbHash: string;
50
+ readonly components: unknown;
51
+ }
52
+ export interface DeterministicPrototypeMaterializationOptions {
53
+ readonly startEntityId?: number;
54
+ readonly componentOverrides?: unknown;
55
+ }
56
+ export interface DeterministicMapMaterializationResult {
57
+ readonly mapId: string;
58
+ readonly assetDbHash: string;
59
+ readonly mapBakeHash: string;
60
+ readonly spawnCount: number;
61
+ readonly materializedEntityCount: number;
62
+ readonly prototypeIds: readonly string[];
63
+ readonly materializedEntities: readonly DeterministicPrototypeEntity[];
64
+ readonly checksum: string;
65
+ }
66
+ export interface DeterministicPrototypeRuntimeValidationResult {
67
+ readonly prototypeId: string;
68
+ readonly materializedEntityCount: number;
69
+ readonly injectedComponentCount: number;
70
+ readonly descriptorComponentCount: number;
71
+ readonly runtimeSnapshotChecksum: string;
72
+ readonly checksum: string;
73
+ }
74
+ export declare function cookDeterministicAssets(source: DeterministicAssetSource): DeterministicAssetCookResult;
75
+ export declare function stableAssetCookJson(result: DeterministicAssetCookResult): string;
76
+ export declare function materializePrototypeEntities(cooked: DeterministicAssetCookResult, prototypeId: string, count: number, options?: DeterministicPrototypeMaterializationOptions): readonly DeterministicPrototypeEntity[];
77
+ export declare function materializeDeterministicMapEntities(cooked: DeterministicAssetCookResult, mapId: string, options?: DeterministicPrototypeMaterializationOptions): DeterministicMapMaterializationResult;
78
+ export declare function validatePrototypeEntitiesAgainstFrameRuntime(cooked: DeterministicAssetCookResult, prototypeId: string, descriptor: DeterministicFrameRuntimeDescriptor, count?: number): DeterministicPrototypeRuntimeValidationResult;
79
+ export declare function assertAssetManifestMatches(manifest: DeterministicAssetCookManifest, expected: {
80
+ readonly engineVersion?: string;
81
+ readonly assetDbHash?: string;
82
+ }): void;
@@ -0,0 +1,364 @@
1
+ import { canonicalStringify, cloneCanonical, defaultChecksum } from './canonical.js';
2
+ import { createDeterministicFrameRuntime } from './engine-runtime.js';
3
+ export function cookDeterministicAssets(source) {
4
+ validateAssetSource(source);
5
+ const engineVersion = source.engineVersion ?? 'rdm-assets@1';
6
+ const dependencies = [...(source.dependencies ?? [])]
7
+ .map((dependency) => cloneCanonical(dependency))
8
+ .sort(compareDependencies);
9
+ const schemaHash = defaultChecksum({
10
+ kind: 'deterministic-asset-schema',
11
+ schemaVersion: 1,
12
+ requiredFields: ['id'],
13
+ optionalFields: ['type', 'references', 'prototype'],
14
+ numericPolicy: 'integer-only',
15
+ dependencyVersionPolicy: 'exact',
16
+ });
17
+ const dependencyHash = defaultChecksum(dependencies);
18
+ const assets = source.assets
19
+ .map((asset) => cloneCanonical(asset))
20
+ .sort((left, right) => compareStrings(left.id, right.id));
21
+ const cookedAssets = assets.map((asset) => {
22
+ const data = cloneCanonical(asset);
23
+ return {
24
+ id: asset.id,
25
+ type: asset.type ?? 'asset',
26
+ hash: defaultChecksum({ id: asset.id, data }),
27
+ data,
28
+ };
29
+ });
30
+ const lookup = Object.fromEntries(cookedAssets.map((asset, index) => [asset.id, index]));
31
+ const prototypeIds = cookedAssets
32
+ .filter((asset) => asset.data.prototype === true)
33
+ .map((asset) => asset.id)
34
+ .sort(compareStrings);
35
+ const withoutHash = {
36
+ schemaVersion: 1,
37
+ schemaHash,
38
+ engineVersion,
39
+ dependencyHash,
40
+ dependencies,
41
+ lookupHash: defaultChecksum(lookup),
42
+ prototypeIds,
43
+ assetCount: cookedAssets.length,
44
+ assets: cookedAssets.map((asset) => ({
45
+ id: asset.id,
46
+ type: asset.type,
47
+ hash: asset.hash,
48
+ })),
49
+ };
50
+ const manifest = {
51
+ ...withoutHash,
52
+ assetDbHash: defaultChecksum({ engineVersion, schemaHash, dependencyHash, dependencies, cookedAssets }),
53
+ };
54
+ const bytes = canonicalStringify({
55
+ manifest,
56
+ assets: cookedAssets,
57
+ });
58
+ return {
59
+ manifest,
60
+ cookedAssets,
61
+ bytes,
62
+ };
63
+ }
64
+ export function stableAssetCookJson(result) {
65
+ return `${canonicalStringify(result)}\n`;
66
+ }
67
+ export function materializePrototypeEntities(cooked, prototypeId, count, options = {}) {
68
+ if (!Number.isInteger(count) || count < 0) {
69
+ throw new Error('Prototype count must be a non-negative integer');
70
+ }
71
+ const startEntityId = options.startEntityId ?? 1;
72
+ if (!Number.isInteger(startEntityId) || startEntityId <= 0) {
73
+ throw new Error('Prototype start entity id must be a positive integer');
74
+ }
75
+ const prototype = cooked.cookedAssets.find((asset) => asset.id === prototypeId);
76
+ if (prototype === undefined) {
77
+ throw new Error(`Unknown prototype ${prototypeId}`);
78
+ }
79
+ if (!cooked.manifest.prototypeIds.includes(prototypeId)) {
80
+ throw new Error(`Asset ${prototypeId} is not a prototype`);
81
+ }
82
+ const components = resolvePrototypeComponents(cooked, prototypeId, new Set());
83
+ const overrides = options.componentOverrides === undefined
84
+ ? {}
85
+ : recordOrThrow(options.componentOverrides, `Prototype ${prototypeId} overrides`);
86
+ const resolvedComponents = mergeRecords(components, overrides);
87
+ return Array.from({ length: count }, (_, index) => ({
88
+ entityId: startEntityId + index,
89
+ prototypeId,
90
+ assetDbHash: cooked.manifest.assetDbHash,
91
+ components: materializeDynamicCollections(resolvedComponents),
92
+ }));
93
+ }
94
+ export function materializeDeterministicMapEntities(cooked, mapId, options = {}) {
95
+ const mapAsset = cooked.cookedAssets.find((asset) => asset.id === mapId);
96
+ if (mapAsset === undefined) {
97
+ throw new Error(`Unknown map ${mapId}`);
98
+ }
99
+ if (mapAsset.type !== 'map') {
100
+ throw new Error(`Asset ${mapId} is not a map`);
101
+ }
102
+ const mapData = recordOrThrow(mapAsset.data, `Map ${mapId}`);
103
+ const spawns = mapData.spawns;
104
+ if (!Array.isArray(spawns)) {
105
+ throw new Error(`Map ${mapId} requires a spawns array`);
106
+ }
107
+ const materialized = [];
108
+ const prototypeIds = [];
109
+ const usedEntityIds = new Set();
110
+ let nextEntityId = options.startEntityId ?? 1;
111
+ for (const [index, spawn] of spawns.entries()) {
112
+ const spawnRecord = recordOrThrow(spawn, `Map ${mapId}.spawns[${index}]`);
113
+ const prototypeRef = spawnRecord.prototypeRef;
114
+ if (typeof prototypeRef !== 'string') {
115
+ throw new Error(`Map ${mapId}.spawns[${index}].prototypeRef must be a string`);
116
+ }
117
+ const count = optionalInteger(spawnRecord.count, `Map ${mapId}.spawns[${index}].count`) ?? 1;
118
+ if (!Number.isInteger(count) || count < 0) {
119
+ throw new Error(`Map ${mapId}.spawns[${index}].count must be a non-negative integer`);
120
+ }
121
+ const startEntityId = optionalInteger(spawnRecord.startEntityId, `Map ${mapId}.spawns[${index}].startEntityId`) ?? nextEntityId;
122
+ if (!Number.isInteger(startEntityId) || startEntityId <= 0) {
123
+ throw new Error(`Map ${mapId}.spawns[${index}].startEntityId must be a positive integer`);
124
+ }
125
+ const entities = materializePrototypeEntities(cooked, prototypeRef, count, {
126
+ startEntityId,
127
+ componentOverrides: spawnRecord.overrides,
128
+ });
129
+ for (const entity of entities) {
130
+ if (usedEntityIds.has(entity.entityId)) {
131
+ throw new Error(`Map ${mapId}.spawns[${index}] duplicates entityId ${entity.entityId}`);
132
+ }
133
+ usedEntityIds.add(entity.entityId);
134
+ }
135
+ materialized.push(...entities);
136
+ prototypeIds.push(prototypeRef);
137
+ nextEntityId = Math.max(nextEntityId, startEntityId + count);
138
+ }
139
+ const withoutChecksum = {
140
+ mapId,
141
+ assetDbHash: cooked.manifest.assetDbHash,
142
+ mapBakeHash: defaultChecksum({ mapId, assetDbHash: cooked.manifest.assetDbHash, materialized }),
143
+ spawnCount: spawns.length,
144
+ materializedEntityCount: materialized.length,
145
+ prototypeIds: [...new Set(prototypeIds)].sort(compareStrings),
146
+ materializedEntities: materialized,
147
+ };
148
+ return {
149
+ ...withoutChecksum,
150
+ checksum: defaultChecksum(withoutChecksum),
151
+ };
152
+ }
153
+ export function validatePrototypeEntitiesAgainstFrameRuntime(cooked, prototypeId, descriptor, count = 1) {
154
+ const runtime = createDeterministicFrameRuntime(descriptor, 0);
155
+ const entities = materializePrototypeEntities(cooked, prototypeId, count);
156
+ let injectedComponentCount = 0;
157
+ for (const materialized of entities) {
158
+ const runtimeEntity = runtime.createEntity();
159
+ const components = recordOrThrow(materialized.components, `Prototype ${prototypeId} components`);
160
+ for (const componentId of Object.keys(components).sort(compareStrings)) {
161
+ const data = recordOrThrow(components[componentId], `Prototype ${prototypeId}.${componentId}`);
162
+ runtime.setComponent(runtimeEntity, componentId, cloneCanonical(data));
163
+ injectedComponentCount += 1;
164
+ }
165
+ }
166
+ const snapshot = runtime.snapshot();
167
+ const result = {
168
+ prototypeId,
169
+ materializedEntityCount: entities.length,
170
+ injectedComponentCount,
171
+ descriptorComponentCount: descriptor.components.filter((component) => component.singleton !== true).length,
172
+ runtimeSnapshotChecksum: snapshot.checksum,
173
+ };
174
+ return {
175
+ ...result,
176
+ checksum: defaultChecksum(result),
177
+ };
178
+ }
179
+ export function assertAssetManifestMatches(manifest, expected) {
180
+ if (expected.engineVersion !== undefined && manifest.engineVersion !== expected.engineVersion) {
181
+ throw new Error(`Stale deterministic asset engine: expected ${expected.engineVersion}, actual ${manifest.engineVersion}`);
182
+ }
183
+ if (expected.assetDbHash !== undefined && manifest.assetDbHash !== expected.assetDbHash) {
184
+ throw new Error(`Stale deterministic asset database hash: expected ${expected.assetDbHash}, actual ${manifest.assetDbHash}`);
185
+ }
186
+ }
187
+ function validateAssetSource(source) {
188
+ if (!Array.isArray(source.assets)) {
189
+ throw new Error('Deterministic asset source requires an assets array');
190
+ }
191
+ if (source.schemaVersion !== undefined && source.schemaVersion !== 1) {
192
+ throw new Error(`Unsupported deterministic asset schema ${source.schemaVersion}`);
193
+ }
194
+ validateDependencies(source.dependencies ?? []);
195
+ const ids = new Set();
196
+ for (const asset of source.assets) {
197
+ if (typeof asset.id !== 'string' || asset.id.length === 0) {
198
+ throw new Error('Every deterministic asset requires a non-empty string id');
199
+ }
200
+ if (ids.has(asset.id)) {
201
+ throw new Error(`Duplicate deterministic asset id ${asset.id}`);
202
+ }
203
+ ids.add(asset.id);
204
+ rejectUnquantizedNumbers(asset, asset.id);
205
+ }
206
+ for (const asset of source.assets) {
207
+ for (const reference of asset.references ?? []) {
208
+ if (!ids.has(reference)) {
209
+ throw new Error(`Unresolved deterministic asset reference ${asset.id} -> ${reference}`);
210
+ }
211
+ }
212
+ validateImplicitAssetRefs(asset, ids, asset.id);
213
+ }
214
+ }
215
+ function validateImplicitAssetRefs(value, ids, path) {
216
+ if (Array.isArray(value)) {
217
+ value.forEach((entry, index) => validateImplicitAssetRefs(entry, ids, `${path}[${index}]`));
218
+ return;
219
+ }
220
+ if (value === null || typeof value !== 'object') {
221
+ return;
222
+ }
223
+ for (const key of Object.keys(value).sort(compareStrings)) {
224
+ const entry = value[key];
225
+ const entryPath = `${path}.${key}`;
226
+ if (key.endsWith('Ref') && typeof entry === 'string' && !ids.has(entry)) {
227
+ throw new Error(`Unresolved deterministic asset ref ${entryPath} -> ${entry}`);
228
+ }
229
+ if (key.endsWith('Refs') && Array.isArray(entry)) {
230
+ for (const [index, ref] of entry.entries()) {
231
+ if (typeof ref === 'string' && !ids.has(ref)) {
232
+ throw new Error(`Unresolved deterministic asset ref ${entryPath}[${index}] -> ${ref}`);
233
+ }
234
+ }
235
+ }
236
+ validateImplicitAssetRefs(entry, ids, entryPath);
237
+ }
238
+ }
239
+ function recordOrThrow(value, label) {
240
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
241
+ throw new Error(`${label} must be an object`);
242
+ }
243
+ return value;
244
+ }
245
+ function resolvePrototypeComponents(cooked, prototypeId, seen) {
246
+ if (seen.has(prototypeId)) {
247
+ throw new Error(`Prototype inheritance cycle at ${prototypeId}`);
248
+ }
249
+ seen.add(prototypeId);
250
+ const prototype = cooked.cookedAssets.find((asset) => asset.id === prototypeId);
251
+ if (prototype === undefined) {
252
+ throw new Error(`Unknown prototype ${prototypeId}`);
253
+ }
254
+ const data = recordOrThrow(prototype.data, `Prototype ${prototypeId}`);
255
+ const parentRef = data.extendsRef;
256
+ const parent = parentRef === undefined
257
+ ? {}
258
+ : resolvePrototypeComponents(cooked, stringOrThrow(parentRef, `Prototype ${prototypeId}.extendsRef`), seen);
259
+ const own = data.components === undefined
260
+ ? {}
261
+ : recordOrThrow(data.components, `Prototype ${prototypeId} components`);
262
+ return mergeRecords(parent, own);
263
+ }
264
+ function mergeRecords(base, override) {
265
+ const merged = {};
266
+ for (const key of [...new Set([...stableRecordKeys(base), ...stableRecordKeys(override)])].sort(compareStrings)) {
267
+ const baseValue = base[key];
268
+ const overrideValue = override[key];
269
+ if (overrideValue === undefined) {
270
+ merged[key] = cloneCanonical(baseValue);
271
+ }
272
+ else if (isPlainRecord(baseValue) && isPlainRecord(overrideValue) && !isCollectionDescriptor(overrideValue)) {
273
+ merged[key] = mergeRecords(baseValue, overrideValue);
274
+ }
275
+ else {
276
+ merged[key] = cloneCanonical(overrideValue);
277
+ }
278
+ }
279
+ return merged;
280
+ }
281
+ function materializeDynamicCollections(value) {
282
+ if (Array.isArray(value)) {
283
+ return value.map(materializeDynamicCollections);
284
+ }
285
+ if (!isPlainRecord(value)) {
286
+ return cloneCanonical(value);
287
+ }
288
+ if (isCollectionDescriptor(value)) {
289
+ const kind = stringOrThrow(value.collection, 'collection');
290
+ if (kind === 'list' || kind === 'buffer') {
291
+ const values = Array.isArray(value.values) ? value.values : [];
292
+ return values.map(materializeDynamicCollections);
293
+ }
294
+ if (kind === 'set') {
295
+ const values = Array.isArray(value.values) ? value.values : [];
296
+ return [...new Set(values.map((entry) => canonicalStringify(materializeDynamicCollections(entry))))]
297
+ .sort(compareStrings)
298
+ .map((entry) => JSON.parse(entry));
299
+ }
300
+ if (kind === 'map' || kind === 'dictionary') {
301
+ const entries = recordOrThrow(value.entries ?? {}, `${kind} entries`);
302
+ return Object.fromEntries(Object.keys(entries).sort(compareStrings).map((key) => [key, materializeDynamicCollections(entries[key])]));
303
+ }
304
+ throw new Error(`Unsupported deterministic prototype collection ${kind}`);
305
+ }
306
+ return Object.fromEntries(Object.keys(value).sort(compareStrings).map((key) => [key, materializeDynamicCollections(value[key])]));
307
+ }
308
+ function isCollectionDescriptor(value) {
309
+ return typeof value.collection === 'string';
310
+ }
311
+ function isPlainRecord(value) {
312
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
313
+ }
314
+ function stringOrThrow(value, label) {
315
+ if (typeof value !== 'string' || value.length === 0) {
316
+ throw new Error(`${label} must be a non-empty string`);
317
+ }
318
+ return value;
319
+ }
320
+ function optionalInteger(value, label) {
321
+ if (value === undefined) {
322
+ return undefined;
323
+ }
324
+ if (!Number.isInteger(value)) {
325
+ throw new Error(`${label} must be an integer`);
326
+ }
327
+ return value;
328
+ }
329
+ function stableRecordKeys(record) {
330
+ return Reflect.ownKeys(record)
331
+ .filter((key) => typeof key === 'string')
332
+ .sort(compareStrings);
333
+ }
334
+ function validateDependencies(dependencies) {
335
+ for (const dependency of dependencies) {
336
+ if (/[\^~*xX]/.test(dependency.version) || dependency.version === 'latest') {
337
+ throw new Error(`Nondeterministic dependency version ${dependency.name}@${dependency.version}`);
338
+ }
339
+ }
340
+ }
341
+ function compareDependencies(left, right) {
342
+ return compareStrings(left.name, right.name) || compareStrings(left.version, right.version);
343
+ }
344
+ function rejectUnquantizedNumbers(value, path) {
345
+ if (typeof value === 'number') {
346
+ if (!Number.isInteger(value)) {
347
+ throw new Error(`Unquantized float at ${path}`);
348
+ }
349
+ return;
350
+ }
351
+ if (Array.isArray(value)) {
352
+ value.forEach((entry, index) => rejectUnquantizedNumbers(entry, `${path}[${index}]`));
353
+ return;
354
+ }
355
+ if (value !== null && typeof value === 'object') {
356
+ for (const key of Object.keys(value).sort(compareStrings)) {
357
+ const entry = value[key];
358
+ rejectUnquantizedNumbers(entry, `${path}.${key}`);
359
+ }
360
+ }
361
+ }
362
+ function compareStrings(left, right) {
363
+ return left < right ? -1 : left > right ? 1 : 0;
364
+ }
@@ -0,0 +1,217 @@
1
+ import { type ConfirmedInputFrame, type EncodedInput } from './input-authority.js';
2
+ import type { SyncplayMatchLog } from './match-log.js';
3
+ /**
4
+ * Transport-agnostic, game-agnostic deterministic authority room (§6 M2).
5
+ *
6
+ * This is the reusable core a host (mp-room-server's worker) wraps: it assigns slots, owns and
7
+ * distributes the RNG seed, orders/confirms inputs via {@link createInputAuthority} (running NO
8
+ * simulation), and broadcasts confirmed-input frames on a server tick. Every deterministic game
9
+ * uses this one component — there is no per-game server code. The host supplies only a transport
10
+ * (broadcast/sendTo) and drives the tick.
11
+ *
12
+ * It speaks the frozen `rdm-local-authority/v1` session protocol (`session-wire.ts`): players are
13
+ * greeted with `session-start`, send `input`, and receive `confirmed-input`.
14
+ */
15
+ export interface AuthorityRoomTransport {
16
+ broadcast(msgType: string, payload: string): void;
17
+ sendTo(playerId: string, msgType: string, payload: string): void;
18
+ }
19
+ export interface DeterministicAuthorityRoomConfig {
20
+ readonly playerCount: number;
21
+ readonly seed: number;
22
+ readonly hardToleranceTicks: number;
23
+ readonly tickRateHz: number;
24
+ readonly redundancyWindowTicks: number;
25
+ /**
26
+ * The game's canonical "no-op" input, used when a slot's real input never arrives and there is
27
+ * no prior confirmed input to repeat. Game-defined (the authority never interprets it), so it
28
+ * MUST be supplied — the room is game-agnostic and cannot assume an input shape.
29
+ */
30
+ readonly neutralInput: EncodedInput;
31
+ /** Reject inputs further ahead than `confirmedThrough + maxFutureTicks` (bounds buffering). Default 256. */
32
+ readonly maxFutureTicks?: number;
33
+ /**
34
+ * Hold the authority clock until EVERY slot has joined (lobby-style
35
+ * synchronized start). Default false: the clock runs from the first join and
36
+ * later players catch up via input-history replay (drop-in). Games that want
37
+ * a hard "everyone starts together" opt in.
38
+ */
39
+ readonly waitForFullRoom?: boolean;
40
+ /**
41
+ * How many confirmed frames to retain for late-join/rejoin catch-up.
42
+ * A joiner must replay the input history from tick 0 to reconstruct state
43
+ * deterministically (the authority never runs the sim, so history IS the
44
+ * state). Joins after tick 0 has been pruned are rejected with
45
+ * 'history-unavailable' — fail loud, never desync. Frames are tiny (encoded
46
+ * inputs), so the default retains a full hour at 60 Hz for a few MB.
47
+ * Default 216_000.
48
+ */
49
+ readonly historyRetentionTicks?: number;
50
+ /**
51
+ * Desync telemetry (§3): invoked when two clients report different state checksums for the same
52
+ * tick. Detection ONLY — the match is never halted (checksum cheat-gating is deliberately avoided).
53
+ */
54
+ readonly onDesync?: (info: AuthorityRoomDesyncInfo) => void;
55
+ /** msgType used for session-start / confirmed-input broadcasts (host wire envelope). Default 'rdm'. */
56
+ readonly sessionMsgType?: string;
57
+ /**
58
+ * How often (in authority ticks) to acquire a validated client-donated state
59
+ * snapshot (M5.5). Once one is stored, history below its tail is pruned and
60
+ * late joins are served snapshot + input tail regardless of match age.
61
+ * Default 3600 (1 min @ 60 Hz).
62
+ */
63
+ readonly snapshotCadenceTicks?: number;
64
+ /** Abandon a donor transfer that has not completed after this many ticks. Default 600. */
65
+ readonly transferTimeoutTicks?: number;
66
+ /** Snapshot acquisition telemetry: stored snapshots and rejected/abandoned transfers. */
67
+ readonly onSnapshotEvent?: (event: AuthorityRoomSnapshotEvent) => void;
68
+ /**
69
+ * Keep the confirmed-input history back to tick 0 even after a validated
70
+ * snapshot is stored (G3 — replay-verified matches). Normally the snapshot
71
+ * prunes everything below it; with full history the room can export a
72
+ * complete server-ordered match log (`matchLog()`) that an offline verifier
73
+ * re-simulates for score verification. Memory cost is bounded by
74
+ * `historyRetentionTicks` — a match longer than that loses tick 0 and stops
75
+ * being log-verifiable (matchLog() returns undefined; fail loud, never lie).
76
+ */
77
+ readonly preserveFullHistory?: boolean;
78
+ /**
79
+ * Catch-up telemetry (M8): fired whenever the room serves a player state
80
+ * reconstruction — late join, rejoin, reconnect refill, or post-restore
81
+ * resync — with which plan ran and how much tail was replayed.
82
+ */
83
+ readonly onCatchUpEvent?: (event: AuthorityRoomCatchUpEvent) => void;
84
+ /**
85
+ * Rebuild a crashed room from its persisted record (M6.5): frontier, rejoin
86
+ * map, seated players, the input tail, and the latest validated snapshot.
87
+ * Construction is fail-loud on any inconsistency — a silently-empty restore
88
+ * would strand every surviving client. After building the room the host
89
+ * MUST call `resyncSeatedPlayers()` so surviving clients re-seat.
90
+ */
91
+ readonly restoreFrom?: DeterministicAuthorityRoomRestoreState;
92
+ }
93
+ export interface DeterministicAuthorityRoomRestoreState {
94
+ readonly now: number;
95
+ readonly confirmedThrough: number;
96
+ readonly everSlots: Readonly<Record<string, number>>;
97
+ readonly seatedPlayers: readonly string[];
98
+ readonly tail: readonly ConfirmedInputFrame[];
99
+ readonly snapshotBytes?: string;
100
+ readonly schemaHash?: string;
101
+ }
102
+ export interface AuthorityRoomCatchUpEvent {
103
+ readonly playerId: string;
104
+ readonly kind: 'join' | 'rejoin' | 'reconnect' | 'resync';
105
+ readonly plan: 'snapshot' | 'history';
106
+ /** Confirmed frames sent for replay (frontier - tail start + 1). */
107
+ readonly tailFrames: number;
108
+ readonly snapshotTick?: number;
109
+ }
110
+ export interface AuthorityRoomSnapshotEvent {
111
+ readonly kind: 'stored' | 'rejected';
112
+ readonly donorId?: string;
113
+ readonly tick?: number;
114
+ readonly reason?: string;
115
+ }
116
+ export interface AuthorityRoomDesyncInfo {
117
+ readonly tick: number;
118
+ readonly slot: number;
119
+ readonly checksum: string;
120
+ readonly conflictingSlot: number;
121
+ readonly conflictingChecksum: string;
122
+ /**
123
+ * Every checksum reported for this tick so far (including the report that
124
+ * triggered detection), by slot. Lets the host pick the MINORITY client to
125
+ * resync instead of guessing between the two conflicting slots.
126
+ */
127
+ readonly reportsAtTick: Readonly<Record<number, string>>;
128
+ }
129
+ /** Latest client-reported netcode telemetry, by slot (G8). */
130
+ export interface AuthorityRoomClientNetStats {
131
+ readonly tick: number;
132
+ readonly rttMs: number;
133
+ readonly rollbacks: number;
134
+ readonly maxRollbackDepth: number;
135
+ readonly timescalePermille: number;
136
+ readonly inputDelayTicks: number;
137
+ }
138
+ export interface AuthorityRoomNetcodeStats {
139
+ readonly confirmedThrough: number;
140
+ /** Ticks in which each slot's input had to be substituted (cumulative). */
141
+ readonly substitutedTicksBySlot: Readonly<Record<number, number>>;
142
+ readonly clientStats: Readonly<Record<number, AuthorityRoomClientNetStats>>;
143
+ }
144
+ export interface AuthorityRoomJoinResult {
145
+ readonly accepted: boolean;
146
+ readonly slot?: number;
147
+ readonly reason?: 'room-full' | 'already-joined' | 'history-unavailable';
148
+ /** True when this join re-attached a previously-seen playerId to its original slot. */
149
+ readonly rejoined?: boolean;
150
+ }
151
+ export interface AuthorityRoomSnapshot {
152
+ readonly confirmedThrough: number;
153
+ readonly now: number;
154
+ readonly slots: Readonly<Record<string, number>>;
155
+ /** Every playerId ever seated, by slot — the rejoin reclaim map. */
156
+ readonly everSlots: Readonly<Record<string, number>>;
157
+ /** Lowest confirmed tick still retained (-1 when nothing is confirmed). */
158
+ readonly earliestRetainedTick: number;
159
+ /**
160
+ * Metadata of the latest VALIDATED state snapshot. The raw bytes are
161
+ * deliberately excluded — hosts persist this record (mp-room-server hard-caps
162
+ * persisted state at 1 MB) — and exposed via `latestSnapshotBytes()` for
163
+ * out-of-band persistence (M6.5).
164
+ */
165
+ readonly latestSnapshot?: AuthorityRoomStoredSnapshotMeta;
166
+ }
167
+ export interface AuthorityRoomStoredSnapshotMeta {
168
+ readonly tick: number;
169
+ readonly snapshotChecksum: string;
170
+ readonly descriptorHash: string;
171
+ readonly schemaHash?: string;
172
+ readonly byteLength: number;
173
+ }
174
+ export interface DeterministicAuthorityRoom {
175
+ readonly confirmedThrough: number;
176
+ readonly playerCount: number;
177
+ onPlayerJoin(playerId: string): AuthorityRoomJoinResult;
178
+ /** Seat a read-only spectator (G11): role 'spectator', no slot, full catch-up + live follow. */
179
+ onSpectatorJoin(playerId: string): boolean;
180
+ onPlayerMessage(playerId: string, envelope: string): void;
181
+ onPlayerLeave(playerId: string): void;
182
+ /**
183
+ * A player's transport re-attached inside the host's grace window (socket
184
+ * swap; no leave/join fired). Re-unicasts the confirmed-input history so any
185
+ * frames missed while offline are refilled — clients dedupe already-applied
186
+ * ticks, so this is idempotent.
187
+ */
188
+ onPlayerReconnected(playerId: string): void;
189
+ /** Advance the authority by one server tick and broadcast newly-confirmed frames. */
190
+ onTick(): void;
191
+ snapshot(): AuthorityRoomSnapshot;
192
+ /** Raw bytes of the latest validated state snapshot (see AuthorityRoomSnapshot.latestSnapshot). */
193
+ latestSnapshotBytes(): string | undefined;
194
+ /** The retained confirmed-input tail (earliest retained .. frontier), for persistence (M6.5). */
195
+ confirmedTail(): readonly ConfirmedInputFrame[];
196
+ /** After a restore: re-greet every seated player (session-start + snapshot + tail). */
197
+ resyncSeatedPlayers(): void;
198
+ /**
199
+ * Force one seated player through the full reset + re-hydrate path
200
+ * (session-start re-greet + snapshot/history catch-up) — the live-desync
201
+ * response (G4): the minority client of a checksum conflict is re-seated from
202
+ * the validated snapshot exactly like a late joiner instead of drifting for
203
+ * the rest of the match. Returns false when the player is not seated or no
204
+ * catch-up plan exists (nothing was sent).
205
+ */
206
+ resyncPlayer(playerId: string): boolean;
207
+ /** Cumulative substitution counters + latest per-client telemetry (G8). */
208
+ netcodeStats(): AuthorityRoomNetcodeStats;
209
+ /**
210
+ * The complete server-ordered match log (G3): every confirmed input frame
211
+ * from tick 0, plus the descriptor a verifier needs to re-simulate. Returns
212
+ * undefined when history no longer reaches tick 0 (snapshot-pruned without
213
+ * `preserveFullHistory`, or the match outlived `historyRetentionTicks`).
214
+ */
215
+ matchLog(): SyncplayMatchLog | undefined;
216
+ }
217
+ export declare function createDeterministicAuthorityRoom(transport: AuthorityRoomTransport, config: DeterministicAuthorityRoomConfig): DeterministicAuthorityRoom;