@volter/editor-game 0.5.65

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 (302) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +23 -0
  3. package/contributions/asset-budget-asset.menu.ts +26 -0
  4. package/contributions/asset-budget.action.ts +18 -0
  5. package/contributions/asset-budget.document.tsx +21 -0
  6. package/contributions/asset-budget.menu.ts +22 -0
  7. package/contributions/audio-unlock.service.ts +17 -0
  8. package/contributions/audio.utility.tsx +15 -0
  9. package/contributions/autoplay.service.ts +48 -0
  10. package/contributions/bridge.command.ts +172 -0
  11. package/contributions/build-profiles.document.tsx +21 -0
  12. package/contributions/build-progress.status.tsx +45 -0
  13. package/contributions/build.action.ts +26 -0
  14. package/contributions/build.command.ts +27 -0
  15. package/contributions/build.header.tsx +37 -0
  16. package/contributions/build.menu.ts +31 -0
  17. package/contributions/build.service.ts +31 -0
  18. package/contributions/connection.status.tsx +43 -0
  19. package/contributions/coverage.service.ts +118 -0
  20. package/contributions/edit-mode-audio.service.ts +23 -0
  21. package/contributions/edit-mode-networking.service.ts +30 -0
  22. package/contributions/game-document.service.ts +25 -0
  23. package/contributions/game-eval.command.ts +210 -0
  24. package/contributions/game.layout.ts +19 -0
  25. package/contributions/gameplay.command.ts +255 -0
  26. package/contributions/generation.service.ts +96 -0
  27. package/contributions/generations.status.tsx +53 -0
  28. package/contributions/ingest.service.ts +80 -0
  29. package/contributions/instances.command.ts +75 -0
  30. package/contributions/navmesh.menu.ts +39 -0
  31. package/contributions/navmesh.service.ts +19 -0
  32. package/contributions/network.utility.tsx +17 -0
  33. package/contributions/play.command.ts +355 -0
  34. package/contributions/profiler.action.ts +16 -0
  35. package/contributions/profiler.menu.ts +22 -0
  36. package/contributions/profiler.utility.tsx +17 -0
  37. package/contributions/react/component-board.service.ts +21 -0
  38. package/contributions/react/design-time-mount.service.ts +60 -0
  39. package/contributions/react/pasteboard.action.ts +41 -0
  40. package/contributions/react/react-inspector.service.ts +85 -0
  41. package/contributions/react/story-documents.service.ts +44 -0
  42. package/contributions/scene-document.service.ts +32 -0
  43. package/contributions/state-watch.action.ts +17 -0
  44. package/contributions/state-watch.menu.ts +23 -0
  45. package/contributions/state-watch.utility.tsx +20 -0
  46. package/contributions/team-playtest.service.ts +124 -0
  47. package/contributions/three/camera-runtime.inspector.tsx +34 -0
  48. package/contributions/three/component-board.service.ts +24 -0
  49. package/contributions/three/component-verbs.command.ts +110 -0
  50. package/contributions/three/component-verbs.service.ts +92 -0
  51. package/contributions/three/constraints.inspector.tsx +34 -0
  52. package/contributions/three/model-asset-sections.service.ts +26 -0
  53. package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
  54. package/contributions/three/story-documents.service.ts +31 -0
  55. package/contributions/three/three-authoring.service.ts +66 -0
  56. package/contributions/transport.header.tsx +19 -0
  57. package/contributions/xstate-behavior.action.ts +42 -0
  58. package/contributions/xstate-behavior.document.tsx +73 -0
  59. package/contributions/xstate-behavior.inspector.tsx +28 -0
  60. package/contributions/xstate-behavior.menu.ts +23 -0
  61. package/package.json +144 -0
  62. package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
  63. package/src/asset-budget/asset-budget-model.ts +799 -0
  64. package/src/asset-budget/basis-encoder.ts +165 -0
  65. package/src/asset-budget/gltf-io.ts +154 -0
  66. package/src/asset-budget/gltf-optimize.ts +384 -0
  67. package/src/asset-budget/image-dims.ts +78 -0
  68. package/src/asset-budget/optimize-apply.ts +221 -0
  69. package/src/audio/AudioDebuggerPanel.tsx +457 -0
  70. package/src/audio/audio-debugger-model.ts +87 -0
  71. package/src/bridge/call.ts +60 -0
  72. package/src/bridge/dispatch.ts +459 -0
  73. package/src/bridge/live-frames.ts +25 -0
  74. package/src/bridge/screenshot.ts +316 -0
  75. package/src/build/BuildProfilesPanel.tsx +476 -0
  76. package/src/build/build-session.ts +247 -0
  77. package/src/build/format-bytes.ts +14 -0
  78. package/src/command-results.ts +36 -0
  79. package/src/coverage/live-authoring-surface.ts +30 -0
  80. package/src/coverage/live-project-verbs.ts +162 -0
  81. package/src/coverage/native-system-coverage.ts +143 -0
  82. package/src/coverage/root-coverage.ts +79 -0
  83. package/src/coverage/session-coverage.ts +193 -0
  84. package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
  85. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
  86. package/src/edit-mode/edit-mode-audio.ts +56 -0
  87. package/src/edit-mode/edit-mode-networking.ts +112 -0
  88. package/src/game-document/DevicePresetPicker.tsx +84 -0
  89. package/src/game-document/GameCaptureFrameButton.tsx +45 -0
  90. package/src/game-document/GameDocument.tsx +215 -0
  91. package/src/game-document/GamePanel.tsx +662 -0
  92. package/src/game-document/InstanceInspectorPicker.tsx +183 -0
  93. package/src/game-document/crowd-debug.ts +183 -0
  94. package/src/game-document/device-preview.ts +336 -0
  95. package/src/game-document/game-view-store.ts +107 -0
  96. package/src/game-document/physics-debug.ts +187 -0
  97. package/src/generation/GenerationActivity.tsx +421 -0
  98. package/src/generation/GenerationGallery.css +231 -0
  99. package/src/generation/generation-documents.tsx +338 -0
  100. package/src/generation/generation-jobs.ts +128 -0
  101. package/src/generation/generation-presentation.ts +257 -0
  102. package/src/host/adapter-reach.ts +445 -0
  103. package/src/host/adapter-runtime-bindings.ts +303 -0
  104. package/src/host/after-paint.ts +112 -0
  105. package/src/host/api/configurations.ts +66 -0
  106. package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
  107. package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
  108. package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
  109. package/src/host/authoring/contract-scenes-stories.ts +204 -0
  110. package/src/host/authoring/creation-site-related.ts +55 -0
  111. package/src/host/authoring/ephemeral-persistence.ts +29 -0
  112. package/src/host/authoring/gesture-persist.ts +84 -0
  113. package/src/host/authoring/ingest-data-writer.ts +232 -0
  114. package/src/host/authoring/ingest-source-persistence.ts +826 -0
  115. package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
  116. package/src/host/authoring/mounted-authoring.ts +41 -0
  117. package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
  118. package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
  119. package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
  120. package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
  121. package/src/host/authoring/pixi-isolation-assets.ts +25 -0
  122. package/src/host/authoring/pixi-live-write-target.ts +979 -0
  123. package/src/host/authoring/pixi-source-identity.ts +141 -0
  124. package/src/host/authoring/pixi-still-presentation.ts +71 -0
  125. package/src/host/authoring/pixi-structure-history.ts +237 -0
  126. package/src/host/authoring/pixi-transform-channels.ts +205 -0
  127. package/src/host/authoring/selection-remount-handoff.ts +23 -0
  128. package/src/host/authoring/source-persistence-backend.ts +373 -0
  129. package/src/host/authoring/source-refresh-revisions.ts +81 -0
  130. package/src/host/authoring/struct-write-pipe.ts +143 -0
  131. package/src/host/auto-frame-window.ts +89 -0
  132. package/src/host/binding-resolver.ts +393 -0
  133. package/src/host/browser-transpile.ts +631 -0
  134. package/src/host/canvas-entry-runtime.ts +95 -0
  135. package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
  136. package/src/host/components/HeaderTelemetry.tsx +341 -0
  137. package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
  138. package/src/host/components/ResolutionPicker.tsx +89 -0
  139. package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
  140. package/src/host/components/frame-debugger-model.ts +579 -0
  141. package/src/host/components/header-telemetry-model.ts +74 -0
  142. package/src/host/components/scene-document.tsx +447 -0
  143. package/src/host/components/utility-view-state.ts +87 -0
  144. package/src/host/components/world-root-stage-binding.tsx +133 -0
  145. package/src/host/components/world-root-stage.ts +1046 -0
  146. package/src/host/coverage/authoring-read-probe.ts +583 -0
  147. package/src/host/coverage/capability-coverage.ts +1587 -0
  148. package/src/host/coverage/coverage-accounting.ts +261 -0
  149. package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
  150. package/src/host/coverage/project-verb-coverage.ts +148 -0
  151. package/src/host/coverage/system-adapter-coverage.ts +373 -0
  152. package/src/host/design-system-stories/StoryLayout.tsx +104 -0
  153. package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
  154. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
  155. package/src/host/document-preview-three.ts +110 -0
  156. package/src/host/entry-adjudication.ts +89 -0
  157. package/src/host/game-location-guard.ts +150 -0
  158. package/src/host/game-module-access.ts +196 -0
  159. package/src/host/game-realm-page.ts +358 -0
  160. package/src/host/game-realm-reclaim.ts +55 -0
  161. package/src/host/game-realm-storage.ts +104 -0
  162. package/src/host/gameplay-export.ts +288 -0
  163. package/src/host/gameplay-recording.ts +717 -0
  164. package/src/host/gated-globals.ts +1511 -0
  165. package/src/host/history/json-history-resource.ts +254 -0
  166. package/src/host/ingest/registry.ts +261 -0
  167. package/src/host/instance-extract-actions.ts +120 -0
  168. package/src/host/instance-fork-actions.ts +120 -0
  169. package/src/host/play-control-hook.ts +26 -0
  170. package/src/host/playwright-shim.ts +479 -0
  171. package/src/host/projection/dom.ts +295 -0
  172. package/src/host/projection/pixi.ts +288 -0
  173. package/src/host/r3f-entry-runtime.ts +77 -0
  174. package/src/host/react-mount-runtime.ts +162 -0
  175. package/src/host/realm-services.ts +148 -0
  176. package/src/host/recording-preview.ts +106 -0
  177. package/src/host/roots/module-root.ts +203 -0
  178. package/src/host/roots/react-root.ts +181 -0
  179. package/src/host/same-realm-loop-gate.ts +544 -0
  180. package/src/host/scene-view-drawability.ts +69 -0
  181. package/src/host/sdk/tools.ts +31 -0
  182. package/src/host/served-bundle-runtime-modules.ts +331 -0
  183. package/src/host/server-log-bridge.ts +60 -0
  184. package/src/host/staged-projects.ts +24 -0
  185. package/src/host/stories/mounted-story-viewport-source.ts +64 -0
  186. package/src/host/stories/story-arg-descriptors.ts +50 -0
  187. package/src/host/stories/story-media-presence.ts +152 -0
  188. package/src/host/surface-content.ts +87 -0
  189. package/src/host/take-named-export.ts +23 -0
  190. package/src/host/three-ingest-runtime.ts +76 -0
  191. package/src/host/types-fastnoise-lite.d.ts +7 -0
  192. package/src/host/types-mikktspace.d.ts +20 -0
  193. package/src/host/types-troika-three-text.d.ts +7 -0
  194. package/src/host/use-active-performance-source.ts +53 -0
  195. package/src/host/viewport-pose-memory.ts +48 -0
  196. package/src/host/viewport-root-presentation.ts +40 -0
  197. package/src/ingest/active-ingest.ts +251 -0
  198. package/src/ingest/active-scene-navigation.ts +43 -0
  199. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
  200. package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
  201. package/src/ingest/capture-wait-report.ts +122 -0
  202. package/src/ingest/deferred-ingest-play.ts +216 -0
  203. package/src/ingest/deferred-ingest-session.ts +23 -0
  204. package/src/ingest/discovery-public-ingest.ts +242 -0
  205. package/src/ingest/dom-stub-mark.ts +7 -0
  206. package/src/ingest/entry-load.ts +56 -0
  207. package/src/ingest/game-contract-realm.ts +34 -0
  208. package/src/ingest/game-pointer-lock.ts +89 -0
  209. package/src/ingest/held-scene-repaint.ts +73 -0
  210. package/src/ingest/host-surface-box.ts +174 -0
  211. package/src/ingest/ingest-boot-viewport.ts +54 -0
  212. package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
  213. package/src/ingest/ingest-canvas-scene.ts +50 -0
  214. package/src/ingest/ingest-evidence-hook.ts +131 -0
  215. package/src/ingest/ingest-frame-snapshot.ts +183 -0
  216. package/src/ingest/ingest-play-commands.ts +134 -0
  217. package/src/ingest/ingest-play-control.ts +299 -0
  218. package/src/ingest/ingest-render-debug.ts +320 -0
  219. package/src/ingest/ingest-siblings.ts +487 -0
  220. package/src/ingest/ingest-status.ts +62 -0
  221. package/src/ingest/live-ingest-facet.ts +46 -0
  222. package/src/ingest/module-mode.ts +229 -0
  223. package/src/ingest/mount-canvas-ingest-root.ts +899 -0
  224. package/src/ingest/mount-coverage.ts +296 -0
  225. package/src/ingest/mount-dom-ingest-root.ts +282 -0
  226. package/src/ingest/mount-ingest-root.ts +744 -0
  227. package/src/ingest/mount-three-ingest-root.ts +366 -0
  228. package/src/ingest/resolve-canvas.ts +47 -0
  229. package/src/ingest/resolve-three.ts +123 -0
  230. package/src/ingest/served-bundle.ts +109 -0
  231. package/src/ingest/served-html-boot.ts +292 -0
  232. package/src/ingest/surface-canvas.ts +84 -0
  233. package/src/ingest/surface-dom.ts +84 -0
  234. package/src/ingest/surface-three.ts +128 -0
  235. package/src/ingest/types.ts +76 -0
  236. package/src/ingest/unmount-ingest-root.ts +224 -0
  237. package/src/navmesh/navmesh-actions.ts +27 -0
  238. package/src/navmesh/navmesh-handler.ts +237 -0
  239. package/src/navmesh/navmesh-workflow-store.ts +78 -0
  240. package/src/network/NetworkInspectorPanel.tsx +644 -0
  241. package/src/network/network-inspector-model.ts +225 -0
  242. package/src/play/play-boot-stall.ts +118 -0
  243. package/src/play/play-log-events.ts +26 -0
  244. package/src/play/play-mode.ts +2553 -0
  245. package/src/play/play-recording.ts +335 -0
  246. package/src/play/react-play-live-authoring.ts +165 -0
  247. package/src/play-bar/PlayBar.tsx +488 -0
  248. package/src/play-bar/PlayerCountPicker.tsx +100 -0
  249. package/src/profiler/FrameDebuggerPanel.tsx +458 -0
  250. package/src/profiler/PerformancePanel.tsx +1068 -0
  251. package/src/profiler/ProfilerPanel.tsx +53 -0
  252. package/src/profiler/frame-debugger-store.ts +97 -0
  253. package/src/profiler/main-thread-busy.ts +90 -0
  254. package/src/react/design-time-react-mount.ts +782 -0
  255. package/src/react/dom-authoring-adapter.ts +764 -0
  256. package/src/react/pasteboard-materialize.ts +154 -0
  257. package/src/react/react-inspector-section.tsx +2496 -0
  258. package/src/react/react-world-authoring-adapter.ts +3795 -0
  259. package/src/react/story-documents/story-args-section.ts +19 -0
  260. package/src/react/story-documents/story-documents.tsx +1380 -0
  261. package/src/react/story-paint-bounds.ts +94 -0
  262. package/src/react/ui-board-document.tsx +101 -0
  263. package/src/react/ui-board-title.ts +9 -0
  264. package/src/react/ui-component-board.ts +72 -0
  265. package/src/services/audio-pose-guard.ts +81 -0
  266. package/src/services/game-audio-unlock.ts +48 -0
  267. package/src/state-watch/StateWatchPanel.tsx +535 -0
  268. package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
  269. package/src/three/authoring/constraint-inspector-section.tsx +189 -0
  270. package/src/three/authoring/design-time-renderer.ts +189 -0
  271. package/src/three/authoring/model-asset-inspector-section.css +41 -0
  272. package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
  273. package/src/three/authoring/oid-source-persistence.ts +557 -0
  274. package/src/three/authoring/r3f-design-session.ts +1174 -0
  275. package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
  276. package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
  277. package/src/three/authoring/spatial-audio-handles.ts +215 -0
  278. package/src/three/authoring/spatial-collider-handles.ts +229 -0
  279. package/src/three/authoring/spatial-joint-handles.ts +114 -0
  280. package/src/three/authoring/spatial-light-handles.ts +178 -0
  281. package/src/three/authoring/spatial-lod-handles.ts +93 -0
  282. package/src/three/authoring/spatial-particle-handles.ts +368 -0
  283. package/src/three/authoring/three-authoring-adapter.ts +1953 -0
  284. package/src/three/authoring/three-scene-identity.ts +19 -0
  285. package/src/three/authoring/three-spatial-handles.ts +161 -0
  286. package/src/three/authoring/typed-three-inspector.ts +528 -0
  287. package/src/three/component-verbs/extract-menu.ts +71 -0
  288. package/src/three/component-verbs/fork-menu.ts +78 -0
  289. package/src/three/component-verbs/internals-menu.ts +91 -0
  290. package/src/three/story-documents/three-story-documents.tsx +607 -0
  291. package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
  292. package/src/three/three-board/board-framing.ts +412 -0
  293. package/src/three/three-board/board-layout.ts +401 -0
  294. package/src/three/three-board/board-scene.ts +901 -0
  295. package/src/three/three-board/three-component-board.ts +62 -0
  296. package/src/xstate/XStateBehaviorSection.tsx +130 -0
  297. package/src/xstate/XStateMachineInspector.tsx +566 -0
  298. package/src/xstate/character-animation-machine.fixture.ts +74 -0
  299. package/src/xstate/live-behaviors.ts +106 -0
  300. package/src/xstate/use-live-actor-state.ts +44 -0
  301. package/src/xstate/xstate-graph.ts +235 -0
  302. package/src/xstate/xstate-layout.ts +76 -0
@@ -0,0 +1,488 @@
1
+ import {
2
+ type ConfigurationStatus,
3
+ listConfigurations,
4
+ startConfiguration,
5
+ stopConfiguration,
6
+ } from '../host/api/configurations';
7
+ import { isGameplayExportActive, subscribeGameplayExport } from '@volter/editor-core/gameplay-export-state';
8
+ import { PLAY_CONTROL_TEST_ID } from '../host/play-control-hook';
9
+ import type { IconDefinition } from '@fortawesome/fontawesome-svg-core';
10
+ import { faChevronDown } from '@fortawesome/free-solid-svg-icons';
11
+ import { editorHost } from '@volter/editor-sdk/host';
12
+ import {
13
+ AnchoredMenu,
14
+ Button,
15
+ EditorIcon,
16
+ editorIcons,
17
+ IconButton,
18
+ Inline,
19
+ MenuItem,
20
+ Text,
21
+ ToolbarDivider,
22
+ } from '@volter/editor-sdk/widgets';
23
+ import type React from 'react';
24
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
25
+ import { isIngestActive } from '../ingest/active-ingest';
26
+ import { deferredIngestPlayActive } from '../ingest/deferred-ingest-play';
27
+ import { getIngestPlayControl } from '../ingest/ingest-play-control';
28
+ import {
29
+ enterPlayMode,
30
+ exitPlayMode,
31
+ getRestartRequiredReason,
32
+ pausePlayMode,
33
+ resumePlayMode,
34
+ setDesiredExtraInstances,
35
+ stepPlayMode,
36
+ subscribeRestartRequired,
37
+ } from '../play/play-mode';
38
+ import { InstanceCountPicker } from './PlayerCountPicker';
39
+
40
+ export { type Resolution, ResolutionPicker } from '../host/components/ResolutionPicker';
41
+
42
+ export interface PlayBarViewProps {
43
+ readonly placement?: 'launcher' | 'runtime';
44
+ readonly exporting?: boolean;
45
+ readonly state: 'stopped' | 'playing' | 'paused';
46
+ readonly restartReason?: string | null | undefined;
47
+ readonly playEditRegime?: 'ephemeral' | null | undefined;
48
+ readonly onPlay: () => void;
49
+ readonly onPause: () => void;
50
+ readonly onStep: () => void;
51
+ readonly onRestart: () => void;
52
+ readonly onStop: () => void;
53
+ /** The Instances picker is a PARAMETER of a compound configuration that
54
+ * declares it (ARCHITECTURE-CORE §The project model); absent otherwise. */
55
+ readonly instances?: number | null | undefined;
56
+ /** The picker over the project's run configurations, when it declares any. */
57
+ readonly picker?: React.ReactNode;
58
+ readonly options?: React.ReactNode;
59
+ }
60
+
61
+ function SaveRegimeIndicator({ regime }: { regime: 'ephemeral' | null }) {
62
+ if (!regime) return null;
63
+ const label = 'Play edits are temporary unless explicitly committed to source.';
64
+ return (
65
+ <span
66
+ role="img"
67
+ aria-label={label}
68
+ className="vgai-play-edit-regime"
69
+ data-testid="play-edit-regime"
70
+ data-regime={regime}
71
+ title={label}
72
+ >
73
+ <EditorIcon icon={editorIcons.status.warning} size="sm" />
74
+ </span>
75
+ );
76
+ }
77
+
78
+ function TransportButton({
79
+ label,
80
+ icon,
81
+ onClick,
82
+ title = label,
83
+ disabled = false,
84
+ pressed,
85
+ variant = 'ghost',
86
+ testId,
87
+ }: {
88
+ label: string;
89
+ icon: IconDefinition;
90
+ onClick: () => void;
91
+ title?: string;
92
+ disabled?: boolean;
93
+ pressed?: boolean;
94
+ variant?: 'primary' | 'ghost';
95
+ testId?: string;
96
+ }) {
97
+ return (
98
+ <IconButton
99
+ aria-label={label}
100
+ aria-pressed={pressed}
101
+ data-testid={testId}
102
+ disabled={disabled}
103
+ size="comfortable"
104
+ variant={variant}
105
+ // A transport button never KEEPS the keyboard: after Play the next
106
+ // Space or Enter must reach the game, not re-activate this button and
107
+ // stop the session (runhuman pass 145; traced on production build 70).
108
+ onClick={(event) => {
109
+ event.currentTarget.blur();
110
+ onClick();
111
+ }}
112
+ title={title}
113
+ >
114
+ <EditorIcon icon={icon} size="md" />
115
+ </IconButton>
116
+ );
117
+ }
118
+
119
+ /** Store-free production transport shared by the connected app and bounded hosts. */
120
+ export function PlayBarView({
121
+ state,
122
+ exporting = false,
123
+ restartReason = null,
124
+ playEditRegime = null,
125
+ onPlay,
126
+ onPause,
127
+ onStep,
128
+ onRestart,
129
+ onStop,
130
+ instances = null,
131
+ picker = null,
132
+ options = null,
133
+ placement = 'launcher',
134
+ }: PlayBarViewProps) {
135
+ const active = state !== 'stopped';
136
+ const paused = state === 'paused';
137
+ if (placement === 'runtime' && !active) return null;
138
+ return (
139
+ <Inline
140
+ role="toolbar"
141
+ aria-label="Game transport"
142
+ className="vgai-playbar"
143
+ gap={2}
144
+ align="center"
145
+ >
146
+ {placement === 'launcher' && (
147
+ <>
148
+ {picker}
149
+ {options}
150
+ {/* Runtime cardinality sits beside transport, like the local-client
151
+ settings in established game editors — for a compound that declares it. */}
152
+ {instances !== null && <InstanceCountPicker />}
153
+ <TransportButton
154
+ label="Play"
155
+ title={active ? 'Exit Play mode' : 'Play (run game)'}
156
+ icon={editorIcons.transport.play}
157
+ pressed={active}
158
+ testId={PLAY_CONTROL_TEST_ID}
159
+ onClick={onPlay}
160
+ />
161
+ </>
162
+ )}
163
+ {placement === 'runtime' && (
164
+ <>
165
+ <TransportButton
166
+ label="Pause"
167
+ title={paused ? 'Resume' : 'Pause'}
168
+ icon={editorIcons.transport.pause}
169
+ disabled={!active || exporting}
170
+ pressed={paused}
171
+ onClick={onPause}
172
+ />
173
+ <TransportButton
174
+ label="Step one frame"
175
+ icon={editorIcons.transport.step}
176
+ disabled={!paused || exporting}
177
+ onClick={onStep}
178
+ />
179
+ <ToolbarDivider />
180
+ <TransportButton
181
+ label="Restart Game"
182
+ icon={editorIcons.transport.restart}
183
+ disabled={!active || exporting}
184
+ variant={restartReason ? 'primary' : 'ghost'}
185
+ testId="restart-game"
186
+ onClick={onRestart}
187
+ title={
188
+ restartReason ??
189
+ 'Restart game (dispose and remount every root without reloading the editor)'
190
+ }
191
+ />
192
+ <TransportButton
193
+ label="Stop"
194
+ icon={editorIcons.transport.stop}
195
+ disabled={!active}
196
+ onClick={onStop}
197
+ />
198
+ {active && (
199
+ <>
200
+ <ToolbarDivider />
201
+ <SaveRegimeIndicator regime={playEditRegime} />
202
+ </>
203
+ )}
204
+ </>
205
+ )}
206
+ </Inline>
207
+ );
208
+ }
209
+
210
+ /** The global launcher and the live Game document's transport share session actions. */
211
+ export function PlayBar({ placement = 'launcher' }: { placement?: 'launcher' | 'runtime' }) {
212
+ const exporting = useSyncExternalStore(subscribeGameplayExport, isGameplayExportActive);
213
+ const { session } = editorHost();
214
+ useSyncExternalStore(session.subscribe, session.version, session.version);
215
+ const state = session.playState();
216
+ const restartReason = useSyncExternalStore(
217
+ subscribeRestartRequired,
218
+ getRestartRequiredReason,
219
+ getRestartRequiredReason,
220
+ );
221
+ // F17/F21: ingest sessions get a real play surface instead of a silent
222
+ // no-op. Mounts are cold (getIngestPlayControl's doc comment); ▶ starts or
223
+ // resumes the ingested game's own loop/session, ⏸ freezes it.
224
+ const [, forceIngestRender] = useState(0);
225
+ const run = useRunConfigurations();
226
+ const ingest = isIngestActive() ? getIngestPlayControl() : null;
227
+ if (ingest) {
228
+ return (
229
+ <Inline
230
+ role="toolbar"
231
+ aria-label="Game transport"
232
+ className="vgai-playbar"
233
+ gap={2}
234
+ align="center"
235
+ >
236
+ <TransportButton
237
+ label={
238
+ placement === 'runtime'
239
+ ? ingest.playing
240
+ ? 'Pause ingested game'
241
+ : 'Resume ingested game'
242
+ : 'Run ingested game'
243
+ }
244
+ title={ingest.playing ? 'Pause ingested game' : 'Run ingested game'}
245
+ icon={ingest.playing ? editorIcons.transport.pause : editorIcons.transport.play}
246
+ pressed={ingest.playing}
247
+ // S-6: the SAME hook the native transport carries — this surface's
248
+ // label differs, and callers must not have to know that.
249
+ testId={PLAY_CONTROL_TEST_ID}
250
+ onClick={() => {
251
+ if (ingest.playing) ingest.pause();
252
+ else ingest.play();
253
+ forceIngestRender((value) => value + 1);
254
+ }}
255
+ />
256
+ {/* Stop exists only where it can honestly END something. A boot-mounted
257
+ ingest IS the open document, so there is nothing to stop (that is why
258
+ ⏹ maps to pause on the relay — `ingest/ingest-play-commands.ts`). A
259
+ root that declares its own world component mounted this game BECAUSE
260
+ Play started (`ingest/deferred-ingest-play.ts`), so ⏹ tears the run
261
+ down and hands the surface back to the design-time Scene. */}
262
+ {placement === 'runtime' && deferredIngestPlayActive() && (
263
+ <TransportButton
264
+ label="Stop"
265
+ title="Stop the game and return to the scene"
266
+ icon={editorIcons.transport.stop}
267
+ onClick={() => exitPlayMode()}
268
+ />
269
+ )}
270
+ {/* Step exists only where the serve-time loop gate MEASURED
271
+ control of this game's frames — offering it otherwise would be a
272
+ button that cannot do what it says. */}
273
+ {placement === 'runtime' && ingest.canStep && (
274
+ <TransportButton
275
+ label="Step ingested game"
276
+ title="Advance the ingested game one frame"
277
+ icon={editorIcons.transport.step}
278
+ onClick={() => {
279
+ ingest.step();
280
+ forceIngestRender((value) => value + 1);
281
+ }}
282
+ />
283
+ )}
284
+ </Inline>
285
+ );
286
+ }
287
+
288
+ const selected = run.configurations.find((c) => c.id === run.selectedId) ?? null;
289
+ const instances = selected?.kind === 'compound' ? (selected.instances ?? null) : null;
290
+ const play = async () => {
291
+ if (selected && selected.kind !== PLAY_ID) {
292
+ const outcome = await startConfiguration(selected.id).catch((error: unknown) => {
293
+ editorHost().notify({
294
+ tone: 'error',
295
+ title: `Run "${selected.id}"`,
296
+ detail: error instanceof Error ? error.message : String(error),
297
+ });
298
+ return null;
299
+ });
300
+ if (!outcome) return;
301
+ if (!outcome.ready) {
302
+ editorHost().notify({
303
+ tone: 'error',
304
+ title: `Run "${selected.id}" is not ready`,
305
+ detail: Object.entries(outcome.logs)
306
+ .map(([id, lines]) => `${id}: ${lines.slice(-3).join(' | ') || 'no output'}`)
307
+ .join('\n'),
308
+ });
309
+ return;
310
+ }
311
+ if (instances !== null) setDesiredExtraInstances(Math.max(0, instances - 1));
312
+ void run.refresh();
313
+ }
314
+ await enterPlayMode();
315
+ };
316
+ const stop = () => {
317
+ exitPlayMode();
318
+ if (selected && selected.kind !== PLAY_ID) {
319
+ void stopConfiguration(selected.id).then(() => run.refresh());
320
+ }
321
+ };
322
+ return (
323
+ <PlayBarView
324
+ placement={placement}
325
+ exporting={exporting}
326
+ state={state}
327
+ restartReason={restartReason}
328
+ playEditRegime={session.playEditRegime()}
329
+ instances={instances}
330
+ options={<PlayOptions />}
331
+ picker={
332
+ run.configurations.length > 1 ? (
333
+ <RunPicker run={run} disabled={state !== 'stopped'} />
334
+ ) : null
335
+ }
336
+ onPlay={() => {
337
+ if (state !== 'stopped') stop();
338
+ else void play();
339
+ }}
340
+ onPause={() => {
341
+ if (state === 'paused') resumePlayMode();
342
+ else pausePlayMode();
343
+ }}
344
+ onStep={() => stepPlayMode()}
345
+ onRestart={() => void play()}
346
+ onStop={() => stop()}
347
+ />
348
+ );
349
+ }
350
+
351
+ /** A user preference, carried by the workbench settings like every other editor choice. */
352
+ function PlayOptions() {
353
+ const { settings } = editorHost();
354
+ const keepPanelsVisible = useSyncExternalStore(
355
+ settings.subscribe,
356
+ () => settings.get('vgai.play.keepPanelsVisible') === true,
357
+ );
358
+ const [open, setOpen] = useState(false);
359
+ const anchor = useRef<HTMLButtonElement>(null);
360
+ return (
361
+ <>
362
+ <IconButton
363
+ ref={anchor}
364
+ aria-label="Play options"
365
+ title="Play options"
366
+ aria-expanded={open}
367
+ aria-haspopup="menu"
368
+ onClick={() => setOpen(!open)}
369
+ >
370
+ <EditorIcon icon={faChevronDown} />
371
+ </IconButton>
372
+ {open && (
373
+ <AnchoredMenu anchorRef={anchor} onDismiss={() => setOpen(false)}>
374
+ <MenuItem
375
+ role="menuitemcheckbox"
376
+ aria-checked={keepPanelsVisible}
377
+ onSelect={() => {
378
+ settings.set('vgai.play.keepPanelsVisible', !keepPanelsVisible);
379
+ setOpen(false);
380
+ }}
381
+ >
382
+ Keep panels visible during Play
383
+ </MenuItem>
384
+ </AnchoredMenu>
385
+ )}
386
+ </>
387
+ );
388
+ }
389
+
390
+ /** The implicit configuration: mount every root in the host. */
391
+ const PLAY_ID = 'play';
392
+ const RUN_SELECTION_SECTION = 'runSelection';
393
+
394
+ interface RunState {
395
+ readonly configurations: readonly ConfigurationStatus[];
396
+ readonly selectedId: string;
397
+ readonly select: (id: string) => void;
398
+ readonly refresh: () => Promise<void>;
399
+ }
400
+
401
+ /** The project's declared configurations (plus the implicit `play`), their
402
+ * status, and the selection — kept per checkout in the project-local
403
+ * document, the way an IDE remembers the chosen launch configuration. */
404
+ function useRunConfigurations(): RunState {
405
+ const [declared, setDeclared] = useState<readonly ConfigurationStatus[]>([]);
406
+ const [selectedId, setSelectedId] = useState<string>(
407
+ () => editorHost().projectLocalState.read<string>(RUN_SELECTION_SECTION) ?? PLAY_ID,
408
+ );
409
+ const refresh = useCallback(async () => {
410
+ const list = await listConfigurations().catch(() => []);
411
+ setDeclared(list);
412
+ }, []);
413
+ useEffect(() => {
414
+ void refresh();
415
+ }, [refresh]);
416
+ const configurations = useMemo<readonly ConfigurationStatus[]>(
417
+ () => [
418
+ {
419
+ id: PLAY_ID,
420
+ kind: PLAY_ID,
421
+ role: 'run',
422
+ describe: 'Mount every root in the host',
423
+ status: 'stopped',
424
+ },
425
+ ...declared.filter((c) => c.role === 'run'),
426
+ ],
427
+ [declared],
428
+ );
429
+ const select = useCallback((id: string) => {
430
+ setSelectedId(id);
431
+ editorHost().projectLocalState.write(RUN_SELECTION_SECTION, id);
432
+ }, []);
433
+ return { configurations, selectedId, select, refresh };
434
+ }
435
+
436
+ function RunPicker({ run, disabled }: { readonly run: RunState; readonly disabled: boolean }) {
437
+ const ref = useRef<HTMLButtonElement | null>(null);
438
+ const [open, setOpen] = useState(false);
439
+ const selected = run.configurations.find((c) => c.id === run.selectedId);
440
+ const label = selected?.id === PLAY_ID ? 'Play' : (selected?.id ?? 'Play');
441
+ return (
442
+ <div className="vgai-playbar-popover-anchor">
443
+ <Button
444
+ ref={ref}
445
+ variant="ghost"
446
+ size="compact"
447
+ aria-haspopup="menu"
448
+ aria-expanded={open}
449
+ aria-controls={open ? 'vgai-run-configuration-menu' : undefined}
450
+ data-testid="run-configuration-picker"
451
+ disabled={disabled}
452
+ onClick={() => setOpen((value) => !value)}
453
+ title="Which configuration Play starts (the manifest's run-role configurations)"
454
+ >
455
+ <Text variant="code">{label}</Text>
456
+ <EditorIcon icon={faChevronDown} size="sm" />
457
+ </Button>
458
+ {open && (
459
+ <AnchoredMenu
460
+ id="vgai-run-configuration-menu"
461
+ anchorRef={ref}
462
+ align="start"
463
+ gap={4}
464
+ onDismiss={() => setOpen(false)}
465
+ >
466
+ {run.configurations.map((configuration) => (
467
+ <MenuItem
468
+ key={configuration.id}
469
+ role="menuitemradio"
470
+ aria-checked={configuration.id === run.selectedId}
471
+ data-testid={`run-configuration-${configuration.id}`}
472
+ title={configuration.describe ?? undefined}
473
+ onSelect={() => {
474
+ run.select(configuration.id);
475
+ setOpen(false);
476
+ }}
477
+ >
478
+ {configuration.id === PLAY_ID
479
+ ? 'Play'
480
+ : `${configuration.id} · ${configuration.kind}`}
481
+ {configuration.status === 'running' ? ' ●' : ''}
482
+ </MenuItem>
483
+ ))}
484
+ </AnchoredMenu>
485
+ )}
486
+ </div>
487
+ );
488
+ }
@@ -0,0 +1,100 @@
1
+ import { faUsers } from '@fortawesome/free-solid-svg-icons';
2
+ import {
3
+ AnchoredMenu,
4
+ Button,
5
+ DisclosureIcon,
6
+ EditorIcon,
7
+ MenuItem,
8
+ Text,
9
+ } from '@volter/editor-sdk/widgets';
10
+ import { useRef, useState, useSyncExternalStore } from 'react';
11
+ import {
12
+ desiredExtraInstances,
13
+ setDesiredExtraInstances,
14
+ subscribeExtraInstances,
15
+ } from '../play/play-mode';
16
+
17
+ /** The split view caps at four side-by-side seats — beyond that a single
18
+ * editor viewport is unreadable, and the multiplayer-verification path is
19
+ * bot-driven anyway (no human needs five keyboards). */
20
+ export const MAX_INSTANCES = 4;
21
+ /** Compatibility for the existing pure mapping tests; product vocabulary is
22
+ * instance because a mount need not represent a player. */
23
+ export const MAX_PLAYERS = MAX_INSTANCES;
24
+
25
+ /** Total player seats → EXTRA instances beside the primary (clamped to
26
+ * 1..MAX_PLAYERS). Pure so the mapping is unit-tested without React. */
27
+ export function extrasForPlayerCount(count: number): number {
28
+ const clamped = Math.min(MAX_INSTANCES, Math.max(1, Math.floor(count)));
29
+ return clamped - 1;
30
+ }
31
+
32
+ /** EXTRA instances → total player seats. */
33
+ export function playerCountFromExtras(extra: number): number {
34
+ return extra + 1;
35
+ }
36
+
37
+ function instanceLabel(count: number): string {
38
+ return count === 1 ? '1 instance' : `${count} instances`;
39
+ }
40
+
41
+ /**
42
+ * Pick how many local runtime instances Play mounts side by side. Multiplayer
43
+ * is the common use, but the same mounts also support seed/camera A/B work; the
44
+ * editor therefore never calls them players or hides the mechanism behind a
45
+ * networking capability.
46
+ */
47
+ export function InstanceCountPicker() {
48
+ const extra = useSyncExternalStore(
49
+ subscribeExtraInstances,
50
+ desiredExtraInstances,
51
+ desiredExtraInstances,
52
+ );
53
+ const [open, setOpen] = useState(false);
54
+ const ref = useRef<HTMLButtonElement>(null);
55
+
56
+ const count = playerCountFromExtras(extra);
57
+ return (
58
+ <div className="vgai-playbar-popover-anchor">
59
+ <Button
60
+ ref={ref}
61
+ variant="ghost"
62
+ size="comfortable"
63
+ aria-haspopup="menu"
64
+ aria-expanded={open}
65
+ aria-controls={open ? 'vgai-player-count-menu' : undefined}
66
+ onClick={() => setOpen((value) => !value)}
67
+ title={`${instanceLabel(count)} — choose how many runtimes Play mounts side by side`}
68
+ >
69
+ <EditorIcon icon={faUsers} size="md" />
70
+ <Text>Instances</Text>
71
+ <Text variant="code">{count}</Text>
72
+ <DisclosureIcon direction="down" />
73
+ </Button>
74
+ {open && (
75
+ <AnchoredMenu
76
+ id="vgai-player-count-menu"
77
+ anchorRef={ref}
78
+ gap={4}
79
+ onDismiss={() => setOpen(false)}
80
+ >
81
+ {Array.from({ length: MAX_INSTANCES }, (_v, i) => i + 1).map((n) => (
82
+ <MenuItem
83
+ key={n}
84
+ role="menuitemradio"
85
+ aria-checked={n === count}
86
+ onSelect={() => {
87
+ setDesiredExtraInstances(extrasForPlayerCount(n));
88
+ setOpen(false);
89
+ }}
90
+ >
91
+ {instanceLabel(n)}
92
+ </MenuItem>
93
+ ))}
94
+ </AnchoredMenu>
95
+ )}
96
+ </div>
97
+ );
98
+ }
99
+
100
+ export const PlayerCountPicker = InstanceCountPicker;