@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,119 @@
1
+ export interface DeterministicAnimationClip {
2
+ readonly id: string;
3
+ readonly durationFrames: number;
4
+ readonly loop: boolean;
5
+ readonly events?: readonly {
6
+ readonly frame: number;
7
+ readonly id: string;
8
+ }[];
9
+ readonly tracks?: readonly DeterministicAnimationTrack[];
10
+ readonly rootMotionTrackId?: string;
11
+ }
12
+ export interface DeterministicAnimationTrack {
13
+ readonly id: string;
14
+ readonly keyframes: readonly {
15
+ readonly frame: number;
16
+ readonly value: number;
17
+ }[];
18
+ }
19
+ export interface DeterministicAnimationTransition {
20
+ readonly from: string;
21
+ readonly to: string;
22
+ readonly when: {
23
+ readonly parameter: string;
24
+ readonly equals: boolean | number | string;
25
+ };
26
+ readonly priority?: number;
27
+ readonly consumeTrigger?: string;
28
+ readonly exitFrame?: number;
29
+ readonly durationFrames?: number;
30
+ }
31
+ export interface DeterministicAnimationState {
32
+ readonly id: string;
33
+ readonly clipId: string;
34
+ readonly blendTree?: {
35
+ readonly parameter: string;
36
+ readonly clips: readonly {
37
+ readonly clipId: string;
38
+ readonly threshold: number;
39
+ }[];
40
+ };
41
+ }
42
+ export interface DeterministicAnimationDescriptor {
43
+ readonly id: string;
44
+ readonly initialState: string;
45
+ readonly clips: readonly DeterministicAnimationClip[];
46
+ readonly states: readonly DeterministicAnimationState[];
47
+ readonly transitions: readonly DeterministicAnimationTransition[];
48
+ readonly layers?: readonly DeterministicAnimationLayer[];
49
+ readonly eventReconstruction?: 'synchronized' | 'non-synchronized';
50
+ }
51
+ export interface DeterministicAnimationLayer {
52
+ readonly id: string;
53
+ readonly stateId: string;
54
+ readonly weight: number;
55
+ readonly additive?: boolean;
56
+ }
57
+ export interface DeterministicAnimationRuntimeState {
58
+ readonly stateId: string;
59
+ readonly clipFrame: number;
60
+ readonly parameters: Readonly<Record<string, boolean | number | string>>;
61
+ readonly consumedTriggers: readonly string[];
62
+ readonly transitionBlend?: {
63
+ readonly from: string;
64
+ readonly to: string;
65
+ readonly fromClipFrame: number;
66
+ readonly elapsedFrames: number;
67
+ readonly durationFrames: number;
68
+ };
69
+ }
70
+ export interface DeterministicAnimationStepResult {
71
+ readonly state: DeterministicAnimationRuntimeState;
72
+ readonly events: readonly {
73
+ readonly id: string;
74
+ readonly clipId: string;
75
+ readonly frame: number;
76
+ }[];
77
+ readonly transitionCount: number;
78
+ readonly exitFrameTransitionCount: number;
79
+ readonly transitionBlendActive: boolean;
80
+ readonly sampledPose: Readonly<Record<string, number>>;
81
+ readonly layerPoses: Readonly<Record<string, Readonly<Record<string, number>>>>;
82
+ readonly rootMotionDelta: number;
83
+ readonly blendWeight: number;
84
+ readonly checksum: string;
85
+ }
86
+ export interface CookedDeterministicAnimation {
87
+ readonly id: string;
88
+ readonly bytes: string;
89
+ readonly hash: string;
90
+ }
91
+ export declare function cookDeterministicAnimation(descriptor: DeterministicAnimationDescriptor): CookedDeterministicAnimation;
92
+ export declare function createDeterministicAnimationRuntimeState(descriptor: DeterministicAnimationDescriptor, parameters?: Readonly<Record<string, boolean | number | string>>): DeterministicAnimationRuntimeState;
93
+ export declare function stepDeterministicAnimation(descriptor: DeterministicAnimationDescriptor, state: DeterministicAnimationRuntimeState, parameterUpdates?: Readonly<Record<string, boolean | number | string>>): DeterministicAnimationStepResult;
94
+ export declare function runDeterministicAnimationFixture(frames: number): {
95
+ readonly checksum: string;
96
+ readonly cookedAssetHash: string;
97
+ readonly transitions: number;
98
+ readonly exitFrameTransitions: number;
99
+ readonly transitionBlendFrames: number;
100
+ readonly triggerLatchCount: number;
101
+ readonly emittedEvents: number;
102
+ readonly clipTimeHash: string;
103
+ readonly normalizedTimeHash: string;
104
+ readonly poseHash: string;
105
+ readonly blendWeightHash: string;
106
+ readonly layerPoseHash: string;
107
+ readonly rootMotionHash: string;
108
+ readonly reconstructedEventHash: string;
109
+ readonly blendTreeFrames: number;
110
+ readonly layerPoseFrames: number;
111
+ readonly rootMotionFrames: number;
112
+ readonly reconstructedEvents: number;
113
+ readonly rollbackEquivalent: boolean;
114
+ };
115
+ export declare function reconstructDeterministicAnimationEvents(descriptor: DeterministicAnimationDescriptor, snapshot: DeterministicAnimationRuntimeState, startFrame: number, endFrame: number): {
116
+ readonly events: number;
117
+ readonly eventHash: string;
118
+ readonly checksum: string;
119
+ };
@@ -0,0 +1,488 @@
1
+ import { canonicalStringify, cloneCanonical, defaultChecksum } from './canonical.js';
2
+ export function cookDeterministicAnimation(descriptor) {
3
+ validateAnimationDescriptor(descriptor);
4
+ const cooked = {
5
+ id: descriptor.id,
6
+ initialState: descriptor.initialState,
7
+ clips: [...descriptor.clips].sort((left, right) => compareStrings(left.id, right.id)),
8
+ states: [...descriptor.states].sort((left, right) => compareStrings(left.id, right.id)),
9
+ transitions: [...descriptor.transitions].sort(compareTransitions),
10
+ layers: [...(descriptor.layers ?? [])].sort((left, right) => compareStrings(left.id, right.id)),
11
+ eventReconstruction: descriptor.eventReconstruction ?? 'synchronized',
12
+ };
13
+ const bytes = canonicalStringify(cooked);
14
+ return { id: descriptor.id, bytes, hash: defaultChecksum(bytes) };
15
+ }
16
+ export function createDeterministicAnimationRuntimeState(descriptor, parameters = {}) {
17
+ validateAnimationDescriptor(descriptor);
18
+ return {
19
+ stateId: descriptor.initialState,
20
+ clipFrame: 0,
21
+ parameters: cloneCanonical(parameters),
22
+ consumedTriggers: [],
23
+ transitionBlend: undefined,
24
+ };
25
+ }
26
+ export function stepDeterministicAnimation(descriptor, state, parameterUpdates = {}) {
27
+ const states = new Map(descriptor.states.map((animationState) => [animationState.id, animationState]));
28
+ const clips = new Map(descriptor.clips.map((clip) => [clip.id, clip]));
29
+ const parameters = { ...state.parameters, ...parameterUpdates };
30
+ const currentAnimationState = states.get(state.stateId);
31
+ if (currentAnimationState === undefined) {
32
+ throw new Error(`Unknown animation state ${state.stateId}`);
33
+ }
34
+ if (!clips.has(currentAnimationState.clipId)) {
35
+ throw new Error(`Unknown animation clip ${currentAnimationState.clipId}`);
36
+ }
37
+ const consumedTriggers = clearReleasedTriggers(descriptor, parameters, state.consumedTriggers);
38
+ const transition = descriptor.transitions
39
+ .filter((candidate) => candidate.from === state.stateId)
40
+ .sort(compareTransitions)
41
+ .find((candidate) => isAnimationTransitionEligible(candidate, parameters, state.clipFrame, consumedTriggers));
42
+ const nextStateId = transition?.to ?? state.stateId;
43
+ const animationState = states.get(nextStateId);
44
+ if (animationState === undefined) {
45
+ throw new Error(`Unknown animation state ${nextStateId}`);
46
+ }
47
+ const clip = clips.get(animationState.clipId);
48
+ if (clip === undefined) {
49
+ throw new Error(`Unknown animation clip ${animationState.clipId}`);
50
+ }
51
+ const clipFrame = transition === undefined ? advanceClipFrame(clip, state.clipFrame) : 0;
52
+ const nextConsumedTriggers = transition?.consumeTrigger === undefined ? consumedTriggers : [...consumedTriggers, transition.consumeTrigger].sort(compareStrings);
53
+ const transitionBlend = advanceAnimationBlend(state.transitionBlend, transition, state.clipFrame);
54
+ const previousPose = transition === undefined
55
+ ? sampleAnimationPose(descriptor, clips, currentAnimationState, state.clipFrame, parameters, undefined)
56
+ : {};
57
+ const sampledPose = sampleAnimationPose(descriptor, clips, animationState, clipFrame, parameters, transitionBlend);
58
+ const layerPoses = sampleAnimationLayers(descriptor, clips, clip, clipFrame, parameters);
59
+ const rootMotionDelta = rootMotionDeltaForState(descriptor, clips, animationState, previousPose, sampledPose);
60
+ const blendWeight = transitionBlend === undefined ? 1_000 : Math.trunc((transitionBlend.elapsedFrames * 1_000) / Math.max(1, transitionBlend.durationFrames));
61
+ const events = (clip.events ?? [])
62
+ .filter((event) => event.frame === clipFrame)
63
+ .sort((left, right) => compareStrings(left.id, right.id))
64
+ .map((event) => ({ id: event.id, clipId: clip.id, frame: clipFrame }));
65
+ const next = { stateId: nextStateId, clipFrame, parameters, consumedTriggers: nextConsumedTriggers, transitionBlend };
66
+ return {
67
+ state: next,
68
+ events,
69
+ transitionCount: transition === undefined ? 0 : 1,
70
+ exitFrameTransitionCount: transition?.exitFrame === undefined ? 0 : 1,
71
+ transitionBlendActive: transitionBlend !== undefined,
72
+ sampledPose,
73
+ layerPoses,
74
+ rootMotionDelta,
75
+ blendWeight,
76
+ checksum: defaultChecksum({ next, events, sampledPose, layerPoses, rootMotionDelta, blendWeight }),
77
+ };
78
+ }
79
+ export function runDeterministicAnimationFixture(frames) {
80
+ const descriptor = defaultAnimationDescriptor();
81
+ const cooked = cookDeterministicAnimation(descriptor);
82
+ let state = createDeterministicAnimationRuntimeState(descriptor, { moving: false, fire: false });
83
+ let snapshot = state;
84
+ let transitions = 0;
85
+ let exitFrameTransitions = 0;
86
+ let transitionBlendFrames = 0;
87
+ let triggerLatchCount = 0;
88
+ let emittedEvents = 0;
89
+ const clipFrames = [];
90
+ const normalizedTimes = [];
91
+ const poses = [];
92
+ const layerPoses = [];
93
+ const blendWeights = [];
94
+ const rootMotionDeltas = [];
95
+ for (let frame = 0; frame < frames; frame += 1) {
96
+ if (frame === Math.floor(frames / 2)) {
97
+ snapshot = cloneCanonical(state);
98
+ }
99
+ const result = stepDeterministicAnimation(descriptor, state, animationFixtureParameters(frame));
100
+ state = result.state;
101
+ transitions += result.transitionCount;
102
+ exitFrameTransitions += result.exitFrameTransitionCount;
103
+ transitionBlendFrames += result.transitionBlendActive ? 1 : 0;
104
+ triggerLatchCount += result.state.consumedTriggers.length;
105
+ emittedEvents += result.events.length;
106
+ clipFrames.push(state.clipFrame);
107
+ normalizedTimes.push(normalizeClipTime(descriptor, state));
108
+ poses.push(result.sampledPose);
109
+ layerPoses.push(result.layerPoses);
110
+ blendWeights.push(result.blendWeight);
111
+ rootMotionDeltas.push(result.rootMotionDelta);
112
+ }
113
+ const checksum = defaultChecksum(state);
114
+ let restored = snapshot;
115
+ for (let frame = Math.floor(frames / 2); frame < frames; frame += 1) {
116
+ restored = stepDeterministicAnimation(descriptor, restored, animationFixtureParameters(frame)).state;
117
+ }
118
+ const reconstructed = reconstructDeterministicAnimationEvents(descriptor, snapshot, Math.floor(frames / 2), frames);
119
+ return {
120
+ checksum,
121
+ cookedAssetHash: cooked.hash,
122
+ transitions,
123
+ exitFrameTransitions,
124
+ transitionBlendFrames,
125
+ triggerLatchCount,
126
+ emittedEvents,
127
+ clipTimeHash: defaultChecksum(clipFrames),
128
+ normalizedTimeHash: defaultChecksum(normalizedTimes),
129
+ poseHash: defaultChecksum(poses),
130
+ blendWeightHash: defaultChecksum(blendWeights),
131
+ layerPoseHash: defaultChecksum(layerPoses),
132
+ rootMotionHash: defaultChecksum(rootMotionDeltas),
133
+ reconstructedEventHash: reconstructed.eventHash,
134
+ blendTreeFrames: poses.filter((pose) => Object.prototype.hasOwnProperty.call(pose, 'upper-aim')).length,
135
+ layerPoseFrames: layerPoses.filter((pose) => stableLayerIds(pose).length > 0).length,
136
+ rootMotionFrames: rootMotionDeltas.filter((delta) => delta !== 0).length,
137
+ reconstructedEvents: reconstructed.events,
138
+ rollbackEquivalent: checksum === defaultChecksum(restored),
139
+ };
140
+ }
141
+ export function reconstructDeterministicAnimationEvents(descriptor, snapshot, startFrame, endFrame) {
142
+ let state = cloneCanonical(snapshot);
143
+ const events = [];
144
+ for (let frame = startFrame; frame < endFrame; frame += 1) {
145
+ const result = stepDeterministicAnimation(descriptor, state, animationFixtureParameters(frame));
146
+ state = result.state;
147
+ events.push(...result.events.map((event) => ({ frame, id: event.id, clipId: event.clipId })));
148
+ }
149
+ return {
150
+ events: events.length,
151
+ eventHash: defaultChecksum(events),
152
+ checksum: defaultChecksum({ state, events }),
153
+ };
154
+ }
155
+ function animationFixtureParameters(frame) {
156
+ return {
157
+ moving: frame % 40 > 5,
158
+ fire: frame % 31 <= 1,
159
+ aim: (frame * 7) % 101,
160
+ };
161
+ }
162
+ function defaultAnimationDescriptor() {
163
+ return {
164
+ id: 'humanoid.headless',
165
+ initialState: 'idle',
166
+ clips: [
167
+ { id: 'idle', durationFrames: 60, loop: true, events: [{ frame: 0, id: 'idle-loop' }], rootMotionTrackId: 'root-x', tracks: [{ id: 'root-x', keyframes: [{ frame: 0, value: 0 }, { frame: 59, value: 0 }] }, { id: 'upper-aim', keyframes: [{ frame: 0, value: 0 }, { frame: 59, value: 0 }] }] },
168
+ { id: 'run', durationFrames: 30, loop: true, events: [{ frame: 15, id: 'footstep' }], rootMotionTrackId: 'root-x', tracks: [{ id: 'root-x', keyframes: [{ frame: 0, value: 0 }, { frame: 29, value: 290 }] }, { id: 'upper-aim', keyframes: [{ frame: 0, value: 50 }, { frame: 29, value: 150 }] }] },
169
+ { id: 'fire', durationFrames: 12, loop: false, events: [{ frame: 4, id: 'muzzle' }], rootMotionTrackId: 'root-x', tracks: [{ id: 'root-x', keyframes: [{ frame: 0, value: 100 }, { frame: 11, value: 100 }] }, { id: 'upper-aim', keyframes: [{ frame: 0, value: 900 }, { frame: 11, value: 900 }] }] },
170
+ { id: 'aim-low', durationFrames: 30, loop: true, tracks: [{ id: 'upper-aim', keyframes: [{ frame: 0, value: 100 }, { frame: 29, value: 100 }] }] },
171
+ { id: 'aim-high', durationFrames: 30, loop: true, tracks: [{ id: 'upper-aim', keyframes: [{ frame: 0, value: 700 }, { frame: 29, value: 700 }] }] },
172
+ ],
173
+ states: [
174
+ { id: 'idle', clipId: 'idle' },
175
+ {
176
+ id: 'run',
177
+ clipId: 'run',
178
+ blendTree: {
179
+ parameter: 'aim',
180
+ clips: [
181
+ { clipId: 'aim-low', threshold: 0 },
182
+ { clipId: 'aim-high', threshold: 100 },
183
+ ],
184
+ },
185
+ },
186
+ { id: 'fire', clipId: 'fire' },
187
+ ],
188
+ transitions: [
189
+ { from: 'idle', to: 'run', when: { parameter: 'moving', equals: true }, priority: 1, durationFrames: 3 },
190
+ { from: 'run', to: 'idle', when: { parameter: 'moving', equals: false }, priority: 1, durationFrames: 3 },
191
+ { from: 'idle', to: 'fire', when: { parameter: 'fire', equals: true }, priority: 0, consumeTrigger: 'fire', durationFrames: 2 },
192
+ { from: 'run', to: 'fire', when: { parameter: 'fire', equals: true }, priority: 0, consumeTrigger: 'fire', durationFrames: 2 },
193
+ { from: 'fire', to: 'idle', when: { parameter: 'fire', equals: false }, priority: 9, exitFrame: 11, durationFrames: 1 },
194
+ ],
195
+ layers: [{ id: 'upper-body', stateId: 'run', weight: 500, additive: true }],
196
+ eventReconstruction: 'synchronized',
197
+ };
198
+ }
199
+ function validateAnimationDescriptor(descriptor) {
200
+ const clipIds = uniqueIds(descriptor.clips.map((clip) => clip.id), 'animation clip');
201
+ const stateIds = uniqueIds(descriptor.states.map((state) => state.id), 'animation state');
202
+ if (!stateIds.has(descriptor.initialState)) {
203
+ throw new Error(`Unknown initial animation state ${descriptor.initialState}`);
204
+ }
205
+ for (const clip of descriptor.clips) {
206
+ if (!Number.isInteger(clip.durationFrames) || clip.durationFrames <= 0) {
207
+ throw new Error(`Animation clip ${clip.id} has invalid duration`);
208
+ }
209
+ for (const event of clip.events ?? []) {
210
+ if (!Number.isInteger(event.frame) || event.frame < 0 || event.frame >= clip.durationFrames) {
211
+ throw new Error(`Animation clip ${clip.id} has event ${event.id} outside clip duration`);
212
+ }
213
+ }
214
+ uniqueIds((clip.events ?? []).map((event) => event.id), `animation clip ${clip.id} event`);
215
+ uniqueIds((clip.tracks ?? []).map((track) => track.id), `animation clip ${clip.id} track`);
216
+ for (const track of clip.tracks ?? []) {
217
+ if (track.keyframes.length === 0) {
218
+ throw new Error(`Animation track ${track.id} requires keyframes`);
219
+ }
220
+ uniqueIds(track.keyframes.map((keyframe) => String(keyframe.frame)), `animation track ${track.id} keyframe`);
221
+ for (const keyframe of track.keyframes) {
222
+ if (!Number.isInteger(keyframe.frame) || keyframe.frame < 0 || keyframe.frame >= clip.durationFrames) {
223
+ throw new Error(`Animation track ${track.id} has keyframe outside clip duration`);
224
+ }
225
+ if (!Number.isInteger(keyframe.value)) {
226
+ throw new Error(`Animation track ${track.id} has unquantized value`);
227
+ }
228
+ }
229
+ }
230
+ if (clip.rootMotionTrackId !== undefined && !(clip.tracks ?? []).some((track) => track.id === clip.rootMotionTrackId)) {
231
+ throw new Error(`Animation clip ${clip.id} references missing root motion track ${clip.rootMotionTrackId}`);
232
+ }
233
+ }
234
+ for (const state of descriptor.states) {
235
+ if (!clipIds.has(state.clipId)) {
236
+ throw new Error(`Animation state ${state.id} references missing clip ${state.clipId}`);
237
+ }
238
+ if (state.blendTree !== undefined) {
239
+ if (state.blendTree.clips.length < 2) {
240
+ throw new Error(`Animation state ${state.id} blend tree requires at least two clips`);
241
+ }
242
+ for (const entry of state.blendTree.clips) {
243
+ if (!clipIds.has(entry.clipId)) {
244
+ throw new Error(`Animation state ${state.id} blend tree references missing clip ${entry.clipId}`);
245
+ }
246
+ if (!Number.isInteger(entry.threshold)) {
247
+ throw new Error(`Animation state ${state.id} blend tree threshold must be quantized integer`);
248
+ }
249
+ }
250
+ }
251
+ }
252
+ for (const transition of descriptor.transitions) {
253
+ if (!stateIds.has(transition.from) || !stateIds.has(transition.to)) {
254
+ throw new Error(`Animation transition ${transition.from}->${transition.to} references missing state`);
255
+ }
256
+ if (transition.exitFrame !== undefined && !Number.isInteger(transition.exitFrame)) {
257
+ throw new Error(`Animation transition ${transition.from}->${transition.to} has invalid exit frame`);
258
+ }
259
+ const fromState = descriptor.states.find((state) => state.id === transition.from);
260
+ const fromClip = descriptor.clips.find((clip) => clip.id === fromState?.clipId);
261
+ if (transition.exitFrame !== undefined && (transition.exitFrame < 0 || fromClip === undefined || transition.exitFrame >= fromClip.durationFrames)) {
262
+ throw new Error(`Animation transition ${transition.from}->${transition.to} has invalid exit frame`);
263
+ }
264
+ if (transition.durationFrames !== undefined && (!Number.isInteger(transition.durationFrames) || transition.durationFrames < 0)) {
265
+ throw new Error(`Animation transition ${transition.from}->${transition.to} has invalid duration`);
266
+ }
267
+ }
268
+ uniqueIds((descriptor.layers ?? []).map((candidate) => candidate.id), 'animation layer');
269
+ for (const layer of descriptor.layers ?? []) {
270
+ if (!stateIds.has(layer.stateId)) {
271
+ throw new Error(`Animation layer ${layer.id} references missing state ${layer.stateId}`);
272
+ }
273
+ if (!Number.isInteger(layer.weight) || layer.weight < 0 || layer.weight > 1_000) {
274
+ throw new Error(`Animation layer ${layer.id} has invalid weight`);
275
+ }
276
+ }
277
+ rejectAmbiguousAnimationTransitions(descriptor.transitions);
278
+ }
279
+ function rejectAmbiguousAnimationTransitions(transitions) {
280
+ const seen = new Set();
281
+ for (const transition of [...transitions].sort(compareTransitions)) {
282
+ const key = [
283
+ transition.from,
284
+ String(transition.priority ?? 0),
285
+ transition.when.parameter,
286
+ transitionWhenValueKey(transition.when.equals),
287
+ String(transition.exitFrame ?? -1),
288
+ transition.consumeTrigger ?? '',
289
+ ].join('|');
290
+ if (seen.has(key)) {
291
+ throw new Error(`Ambiguous animation transition from ${transition.from} on ${transition.when.parameter}`);
292
+ }
293
+ seen.add(key);
294
+ }
295
+ }
296
+ function transitionWhenValueKey(value) {
297
+ return `${typeof value}:${String(value)}`;
298
+ }
299
+ function isAnimationTransitionEligible(transition, parameters, clipFrame, consumedTriggers) {
300
+ if (parameters[transition.when.parameter] !== transition.when.equals) {
301
+ return false;
302
+ }
303
+ if (transition.exitFrame !== undefined && clipFrame < transition.exitFrame) {
304
+ return false;
305
+ }
306
+ return transition.consumeTrigger === undefined || !consumedTriggers.includes(transition.consumeTrigger);
307
+ }
308
+ function uniqueIds(ids, label) {
309
+ const sorted = [...ids].sort(compareStrings);
310
+ const duplicate = sorted.find((id, index) => index > 0 && id === sorted[index - 1]);
311
+ if (duplicate !== undefined) {
312
+ throw new Error(`Duplicate ${label} id ${duplicate}`);
313
+ }
314
+ return new Set(sorted);
315
+ }
316
+ function clearReleasedTriggers(descriptor, parameters, consumedTriggers) {
317
+ return consumedTriggers
318
+ .filter((trigger) => descriptor.transitions.some((transition) => (transition.consumeTrigger === trigger && parameters[transition.when.parameter] === transition.when.equals)))
319
+ .sort(compareStrings);
320
+ }
321
+ function advanceAnimationBlend(current, transition, fromClipFrame) {
322
+ if (transition !== undefined) {
323
+ const durationFrames = transition.durationFrames ?? 0;
324
+ if (durationFrames <= 0) {
325
+ return undefined;
326
+ }
327
+ return {
328
+ from: transition.from,
329
+ to: transition.to,
330
+ fromClipFrame,
331
+ elapsedFrames: 0,
332
+ durationFrames,
333
+ };
334
+ }
335
+ if (current === undefined) {
336
+ return undefined;
337
+ }
338
+ const elapsedFrames = current.elapsedFrames + 1;
339
+ return elapsedFrames >= current.durationFrames ? undefined : { ...current, elapsedFrames };
340
+ }
341
+ function sampleAnimationPose(descriptor, clips, state, clipFrame, parameters, blend) {
342
+ const currentPose = state.blendTree === undefined
343
+ ? sampleClipPose(clips.get(state.clipId), clipFrame)
344
+ : sampleBlendTreePose(clips, state.blendTree, parameters, clipFrame);
345
+ if (blend === undefined) {
346
+ return currentPose;
347
+ }
348
+ const fromState = descriptor.states.find((candidate) => candidate.id === blend.from);
349
+ if (fromState === undefined) {
350
+ throw new Error(`Animation transition blend references missing state ${blend.from}`);
351
+ }
352
+ const fromClip = clips.get(fromState.clipId);
353
+ if (fromClip === undefined) {
354
+ throw new Error(`Animation transition blend references missing clip ${fromState.clipId}`);
355
+ }
356
+ const fromPose = sampleClipPose(fromClip, blend.fromClipFrame);
357
+ const weight = Math.trunc((blend.elapsedFrames * 1_000) / Math.max(1, blend.durationFrames));
358
+ const keys = [...new Set([...stablePoseKeys(fromPose), ...stablePoseKeys(currentPose)])].sort(compareStrings);
359
+ return Object.fromEntries(keys.map((key) => [
360
+ key,
361
+ Math.trunc(((fromPose[key] ?? 0) * (1_000 - weight) + (currentPose[key] ?? 0) * weight) / 1_000),
362
+ ]));
363
+ }
364
+ function sampleAnimationLayers(descriptor, clips, sourceClip, clipFrame, parameters) {
365
+ const states = new Map(descriptor.states.map((state) => [state.id, state]));
366
+ const entries = [...(descriptor.layers ?? [])]
367
+ .sort((left, right) => compareStrings(left.id, right.id))
368
+ .map((layer) => {
369
+ const state = states.get(layer.stateId);
370
+ if (state === undefined) {
371
+ throw new Error(`Animation layer ${layer.id} references missing state ${layer.stateId}`);
372
+ }
373
+ const layerClip = clips.get(state.clipId);
374
+ if (layerClip === undefined) {
375
+ throw new Error(`Animation layer ${layer.id} references missing clip ${state.clipId}`);
376
+ }
377
+ const layerFrame = mapClipFrameToLayer(sourceClip, layerClip, clipFrame);
378
+ const pose = sampleAnimationPose(descriptor, clips, state, layerFrame, parameters, undefined);
379
+ const weighted = Object.fromEntries(stablePoseKeys(pose).map((key) => [
380
+ key,
381
+ Math.trunc((pose[key] * layer.weight) / 1_000),
382
+ ]));
383
+ return [layer.id, weighted];
384
+ });
385
+ return Object.fromEntries(entries);
386
+ }
387
+ function mapClipFrameToLayer(sourceClip, layerClip, clipFrame) {
388
+ const sourceDuration = Math.max(1, sourceClip.durationFrames);
389
+ const layerDuration = Math.max(1, layerClip.durationFrames);
390
+ return Math.min(layerDuration - 1, Math.trunc((clipFrame * layerDuration) / sourceDuration));
391
+ }
392
+ function sampleBlendTreePose(clips, blendTree, parameters, clipFrame) {
393
+ const sorted = [...blendTree.clips].sort((left, right) => left.threshold - right.threshold || compareStrings(left.clipId, right.clipId));
394
+ const first = sorted[0];
395
+ const last = sorted.at(-1);
396
+ const rawParameter = parameters[blendTree.parameter];
397
+ if (typeof rawParameter === 'number' && !Number.isFinite(rawParameter)) {
398
+ throw new Error(`Animation blend parameter ${blendTree.parameter} must be finite`);
399
+ }
400
+ const parameterValue = typeof rawParameter === 'number'
401
+ ? Math.max(first.threshold, Math.min(last.threshold, rawParameter))
402
+ : first.threshold;
403
+ const upperIndex = sorted.findIndex((entry) => entry.threshold >= parameterValue);
404
+ const lower = upperIndex <= 0 ? first : sorted[upperIndex - 1];
405
+ const upper = sorted[upperIndex];
406
+ const weight = Math.trunc(((parameterValue - lower.threshold) * 1_000) / Math.max(1, upper.threshold - lower.threshold));
407
+ const lowerClip = clips.get(lower.clipId);
408
+ if (lowerClip === undefined) {
409
+ throw new Error(`Animation blend tree references missing clip ${lower.clipId}`);
410
+ }
411
+ const upperClip = clips.get(upper.clipId);
412
+ if (upperClip === undefined) {
413
+ throw new Error(`Animation blend tree references missing clip ${upper.clipId}`);
414
+ }
415
+ const lowerPose = sampleClipPose(lowerClip, clipFrame);
416
+ const upperPose = sampleClipPose(upperClip, clipFrame);
417
+ const keys = [...new Set([...stablePoseKeys(lowerPose), ...stablePoseKeys(upperPose)])].sort(compareStrings);
418
+ return Object.fromEntries(keys.map((key) => [
419
+ key,
420
+ Math.trunc(((lowerPose[key] ?? 0) * (1_000 - weight) + (upperPose[key] ?? 0) * weight) / 1_000),
421
+ ]));
422
+ }
423
+ function rootMotionDeltaForState(descriptor, clips, state, previousPose, currentPose) {
424
+ const clip = clips.get(state.clipId);
425
+ const trackId = clip?.rootMotionTrackId;
426
+ if (trackId === undefined) {
427
+ return 0;
428
+ }
429
+ const delta = (currentPose[trackId] ?? 0) - (previousPose[trackId] ?? currentPose[trackId] ?? 0);
430
+ return descriptor.eventReconstruction === 'non-synchronized' ? 0 : delta;
431
+ }
432
+ function stablePoseKeys(pose) {
433
+ return Reflect.ownKeys(pose)
434
+ .filter((key) => typeof key === 'string')
435
+ .sort(compareStrings);
436
+ }
437
+ function stableLayerIds(layers) {
438
+ return Reflect.ownKeys(layers)
439
+ .filter((key) => typeof key === 'string')
440
+ .sort(compareStrings);
441
+ }
442
+ function sampleClipPose(clip, clipFrame) {
443
+ const entries = [...(clip.tracks ?? [])]
444
+ .sort((left, right) => compareStrings(left.id, right.id))
445
+ .map((track) => [track.id, sampleTrack(track, clipFrame)]);
446
+ return Object.fromEntries(entries);
447
+ }
448
+ function sampleTrack(track, clipFrame) {
449
+ const keyframes = [...track.keyframes].sort((left, right) => left.frame - right.frame);
450
+ let previous = keyframes[0];
451
+ let next = keyframes.at(-1) ?? previous;
452
+ for (const keyframe of keyframes) {
453
+ if (keyframe.frame <= clipFrame) {
454
+ previous = keyframe;
455
+ }
456
+ if (keyframe.frame >= clipFrame) {
457
+ next = keyframe;
458
+ break;
459
+ }
460
+ }
461
+ if (previous.frame === next.frame) {
462
+ return previous.value;
463
+ }
464
+ const numerator = clipFrame - previous.frame;
465
+ const denominator = Math.max(1, next.frame - previous.frame);
466
+ return Math.trunc(previous.value + ((next.value - previous.value) * numerator) / denominator);
467
+ }
468
+ function normalizeClipTime(descriptor, state) {
469
+ const animationState = descriptor.states.find((candidate) => candidate.id === state.stateId);
470
+ const clip = descriptor.clips.find((candidate) => candidate.id === animationState?.clipId);
471
+ return Math.trunc((state.clipFrame * 1_000_000) / Math.max(1, clip.durationFrames));
472
+ }
473
+ function advanceClipFrame(clip, frame) {
474
+ const next = frame + 1;
475
+ if (next < clip.durationFrames) {
476
+ return next;
477
+ }
478
+ return clip.loop ? 0 : clip.durationFrames - 1;
479
+ }
480
+ function compareTransitions(left, right) {
481
+ return (left.priority ?? 0) - (right.priority ?? 0)
482
+ || compareStrings(left.from, right.from)
483
+ || compareStrings(left.to, right.to)
484
+ || compareStrings(left.when.parameter, right.when.parameter);
485
+ }
486
+ function compareStrings(left, right) {
487
+ return left < right ? -1 : left > right ? 1 : 0;
488
+ }