@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.
- package/LICENSE +661 -0
- package/NOTICE +23 -0
- package/contributions/asset-budget-asset.menu.ts +26 -0
- package/contributions/asset-budget.action.ts +18 -0
- package/contributions/asset-budget.document.tsx +21 -0
- package/contributions/asset-budget.menu.ts +22 -0
- package/contributions/audio-unlock.service.ts +17 -0
- package/contributions/audio.utility.tsx +15 -0
- package/contributions/autoplay.service.ts +48 -0
- package/contributions/bridge.command.ts +172 -0
- package/contributions/build-profiles.document.tsx +21 -0
- package/contributions/build-progress.status.tsx +45 -0
- package/contributions/build.action.ts +26 -0
- package/contributions/build.command.ts +27 -0
- package/contributions/build.header.tsx +37 -0
- package/contributions/build.menu.ts +31 -0
- package/contributions/build.service.ts +31 -0
- package/contributions/connection.status.tsx +43 -0
- package/contributions/coverage.service.ts +118 -0
- package/contributions/edit-mode-audio.service.ts +23 -0
- package/contributions/edit-mode-networking.service.ts +30 -0
- package/contributions/game-document.service.ts +25 -0
- package/contributions/game-eval.command.ts +210 -0
- package/contributions/game.layout.ts +19 -0
- package/contributions/gameplay.command.ts +255 -0
- package/contributions/generation.service.ts +96 -0
- package/contributions/generations.status.tsx +53 -0
- package/contributions/ingest.service.ts +80 -0
- package/contributions/instances.command.ts +75 -0
- package/contributions/navmesh.menu.ts +39 -0
- package/contributions/navmesh.service.ts +19 -0
- package/contributions/network.utility.tsx +17 -0
- package/contributions/play.command.ts +355 -0
- package/contributions/profiler.action.ts +16 -0
- package/contributions/profiler.menu.ts +22 -0
- package/contributions/profiler.utility.tsx +17 -0
- package/contributions/react/component-board.service.ts +21 -0
- package/contributions/react/design-time-mount.service.ts +60 -0
- package/contributions/react/pasteboard.action.ts +41 -0
- package/contributions/react/react-inspector.service.ts +85 -0
- package/contributions/react/story-documents.service.ts +44 -0
- package/contributions/scene-document.service.ts +32 -0
- package/contributions/state-watch.action.ts +17 -0
- package/contributions/state-watch.menu.ts +23 -0
- package/contributions/state-watch.utility.tsx +20 -0
- package/contributions/team-playtest.service.ts +124 -0
- package/contributions/three/camera-runtime.inspector.tsx +34 -0
- package/contributions/three/component-board.service.ts +24 -0
- package/contributions/three/component-verbs.command.ts +110 -0
- package/contributions/three/component-verbs.service.ts +92 -0
- package/contributions/three/constraints.inspector.tsx +34 -0
- package/contributions/three/model-asset-sections.service.ts +26 -0
- package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
- package/contributions/three/story-documents.service.ts +31 -0
- package/contributions/three/three-authoring.service.ts +66 -0
- package/contributions/transport.header.tsx +19 -0
- package/contributions/xstate-behavior.action.ts +42 -0
- package/contributions/xstate-behavior.document.tsx +73 -0
- package/contributions/xstate-behavior.inspector.tsx +28 -0
- package/contributions/xstate-behavior.menu.ts +23 -0
- package/package.json +144 -0
- package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
- package/src/asset-budget/asset-budget-model.ts +799 -0
- package/src/asset-budget/basis-encoder.ts +165 -0
- package/src/asset-budget/gltf-io.ts +154 -0
- package/src/asset-budget/gltf-optimize.ts +384 -0
- package/src/asset-budget/image-dims.ts +78 -0
- package/src/asset-budget/optimize-apply.ts +221 -0
- package/src/audio/AudioDebuggerPanel.tsx +457 -0
- package/src/audio/audio-debugger-model.ts +87 -0
- package/src/bridge/call.ts +60 -0
- package/src/bridge/dispatch.ts +459 -0
- package/src/bridge/live-frames.ts +25 -0
- package/src/bridge/screenshot.ts +316 -0
- package/src/build/BuildProfilesPanel.tsx +476 -0
- package/src/build/build-session.ts +247 -0
- package/src/build/format-bytes.ts +14 -0
- package/src/command-results.ts +36 -0
- package/src/coverage/live-authoring-surface.ts +30 -0
- package/src/coverage/live-project-verbs.ts +162 -0
- package/src/coverage/native-system-coverage.ts +143 -0
- package/src/coverage/root-coverage.ts +79 -0
- package/src/coverage/session-coverage.ts +193 -0
- package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
- package/src/edit-mode/edit-mode-audio.ts +56 -0
- package/src/edit-mode/edit-mode-networking.ts +112 -0
- package/src/game-document/DevicePresetPicker.tsx +84 -0
- package/src/game-document/GameCaptureFrameButton.tsx +45 -0
- package/src/game-document/GameDocument.tsx +215 -0
- package/src/game-document/GamePanel.tsx +662 -0
- package/src/game-document/InstanceInspectorPicker.tsx +183 -0
- package/src/game-document/crowd-debug.ts +183 -0
- package/src/game-document/device-preview.ts +336 -0
- package/src/game-document/game-view-store.ts +107 -0
- package/src/game-document/physics-debug.ts +187 -0
- package/src/generation/GenerationActivity.tsx +421 -0
- package/src/generation/GenerationGallery.css +231 -0
- package/src/generation/generation-documents.tsx +338 -0
- package/src/generation/generation-jobs.ts +128 -0
- package/src/generation/generation-presentation.ts +257 -0
- package/src/host/adapter-reach.ts +445 -0
- package/src/host/adapter-runtime-bindings.ts +303 -0
- package/src/host/after-paint.ts +112 -0
- package/src/host/api/configurations.ts +66 -0
- package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
- package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
- package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
- package/src/host/authoring/contract-scenes-stories.ts +204 -0
- package/src/host/authoring/creation-site-related.ts +55 -0
- package/src/host/authoring/ephemeral-persistence.ts +29 -0
- package/src/host/authoring/gesture-persist.ts +84 -0
- package/src/host/authoring/ingest-data-writer.ts +232 -0
- package/src/host/authoring/ingest-source-persistence.ts +826 -0
- package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
- package/src/host/authoring/mounted-authoring.ts +41 -0
- package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
- package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
- package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
- package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
- package/src/host/authoring/pixi-isolation-assets.ts +25 -0
- package/src/host/authoring/pixi-live-write-target.ts +979 -0
- package/src/host/authoring/pixi-source-identity.ts +141 -0
- package/src/host/authoring/pixi-still-presentation.ts +71 -0
- package/src/host/authoring/pixi-structure-history.ts +237 -0
- package/src/host/authoring/pixi-transform-channels.ts +205 -0
- package/src/host/authoring/selection-remount-handoff.ts +23 -0
- package/src/host/authoring/source-persistence-backend.ts +373 -0
- package/src/host/authoring/source-refresh-revisions.ts +81 -0
- package/src/host/authoring/struct-write-pipe.ts +143 -0
- package/src/host/auto-frame-window.ts +89 -0
- package/src/host/binding-resolver.ts +393 -0
- package/src/host/browser-transpile.ts +631 -0
- package/src/host/canvas-entry-runtime.ts +95 -0
- package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
- package/src/host/components/HeaderTelemetry.tsx +341 -0
- package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
- package/src/host/components/ResolutionPicker.tsx +89 -0
- package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
- package/src/host/components/frame-debugger-model.ts +579 -0
- package/src/host/components/header-telemetry-model.ts +74 -0
- package/src/host/components/scene-document.tsx +447 -0
- package/src/host/components/utility-view-state.ts +87 -0
- package/src/host/components/world-root-stage-binding.tsx +133 -0
- package/src/host/components/world-root-stage.ts +1046 -0
- package/src/host/coverage/authoring-read-probe.ts +583 -0
- package/src/host/coverage/capability-coverage.ts +1587 -0
- package/src/host/coverage/coverage-accounting.ts +261 -0
- package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
- package/src/host/coverage/project-verb-coverage.ts +148 -0
- package/src/host/coverage/system-adapter-coverage.ts +373 -0
- package/src/host/design-system-stories/StoryLayout.tsx +104 -0
- package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
- package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
- package/src/host/document-preview-three.ts +110 -0
- package/src/host/entry-adjudication.ts +89 -0
- package/src/host/game-location-guard.ts +150 -0
- package/src/host/game-module-access.ts +196 -0
- package/src/host/game-realm-page.ts +358 -0
- package/src/host/game-realm-reclaim.ts +55 -0
- package/src/host/game-realm-storage.ts +104 -0
- package/src/host/gameplay-export.ts +288 -0
- package/src/host/gameplay-recording.ts +717 -0
- package/src/host/gated-globals.ts +1511 -0
- package/src/host/history/json-history-resource.ts +254 -0
- package/src/host/ingest/registry.ts +261 -0
- package/src/host/instance-extract-actions.ts +120 -0
- package/src/host/instance-fork-actions.ts +120 -0
- package/src/host/play-control-hook.ts +26 -0
- package/src/host/playwright-shim.ts +479 -0
- package/src/host/projection/dom.ts +295 -0
- package/src/host/projection/pixi.ts +288 -0
- package/src/host/r3f-entry-runtime.ts +77 -0
- package/src/host/react-mount-runtime.ts +162 -0
- package/src/host/realm-services.ts +148 -0
- package/src/host/recording-preview.ts +106 -0
- package/src/host/roots/module-root.ts +203 -0
- package/src/host/roots/react-root.ts +181 -0
- package/src/host/same-realm-loop-gate.ts +544 -0
- package/src/host/scene-view-drawability.ts +69 -0
- package/src/host/sdk/tools.ts +31 -0
- package/src/host/served-bundle-runtime-modules.ts +331 -0
- package/src/host/server-log-bridge.ts +60 -0
- package/src/host/staged-projects.ts +24 -0
- package/src/host/stories/mounted-story-viewport-source.ts +64 -0
- package/src/host/stories/story-arg-descriptors.ts +50 -0
- package/src/host/stories/story-media-presence.ts +152 -0
- package/src/host/surface-content.ts +87 -0
- package/src/host/take-named-export.ts +23 -0
- package/src/host/three-ingest-runtime.ts +76 -0
- package/src/host/types-fastnoise-lite.d.ts +7 -0
- package/src/host/types-mikktspace.d.ts +20 -0
- package/src/host/types-troika-three-text.d.ts +7 -0
- package/src/host/use-active-performance-source.ts +53 -0
- package/src/host/viewport-pose-memory.ts +48 -0
- package/src/host/viewport-root-presentation.ts +40 -0
- package/src/ingest/active-ingest.ts +251 -0
- package/src/ingest/active-scene-navigation.ts +43 -0
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
- package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
- package/src/ingest/capture-wait-report.ts +122 -0
- package/src/ingest/deferred-ingest-play.ts +216 -0
- package/src/ingest/deferred-ingest-session.ts +23 -0
- package/src/ingest/discovery-public-ingest.ts +242 -0
- package/src/ingest/dom-stub-mark.ts +7 -0
- package/src/ingest/entry-load.ts +56 -0
- package/src/ingest/game-contract-realm.ts +34 -0
- package/src/ingest/game-pointer-lock.ts +89 -0
- package/src/ingest/held-scene-repaint.ts +73 -0
- package/src/ingest/host-surface-box.ts +174 -0
- package/src/ingest/ingest-boot-viewport.ts +54 -0
- package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
- package/src/ingest/ingest-canvas-scene.ts +50 -0
- package/src/ingest/ingest-evidence-hook.ts +131 -0
- package/src/ingest/ingest-frame-snapshot.ts +183 -0
- package/src/ingest/ingest-play-commands.ts +134 -0
- package/src/ingest/ingest-play-control.ts +299 -0
- package/src/ingest/ingest-render-debug.ts +320 -0
- package/src/ingest/ingest-siblings.ts +487 -0
- package/src/ingest/ingest-status.ts +62 -0
- package/src/ingest/live-ingest-facet.ts +46 -0
- package/src/ingest/module-mode.ts +229 -0
- package/src/ingest/mount-canvas-ingest-root.ts +899 -0
- package/src/ingest/mount-coverage.ts +296 -0
- package/src/ingest/mount-dom-ingest-root.ts +282 -0
- package/src/ingest/mount-ingest-root.ts +744 -0
- package/src/ingest/mount-three-ingest-root.ts +366 -0
- package/src/ingest/resolve-canvas.ts +47 -0
- package/src/ingest/resolve-three.ts +123 -0
- package/src/ingest/served-bundle.ts +109 -0
- package/src/ingest/served-html-boot.ts +292 -0
- package/src/ingest/surface-canvas.ts +84 -0
- package/src/ingest/surface-dom.ts +84 -0
- package/src/ingest/surface-three.ts +128 -0
- package/src/ingest/types.ts +76 -0
- package/src/ingest/unmount-ingest-root.ts +224 -0
- package/src/navmesh/navmesh-actions.ts +27 -0
- package/src/navmesh/navmesh-handler.ts +237 -0
- package/src/navmesh/navmesh-workflow-store.ts +78 -0
- package/src/network/NetworkInspectorPanel.tsx +644 -0
- package/src/network/network-inspector-model.ts +225 -0
- package/src/play/play-boot-stall.ts +118 -0
- package/src/play/play-log-events.ts +26 -0
- package/src/play/play-mode.ts +2553 -0
- package/src/play/play-recording.ts +335 -0
- package/src/play/react-play-live-authoring.ts +165 -0
- package/src/play-bar/PlayBar.tsx +488 -0
- package/src/play-bar/PlayerCountPicker.tsx +100 -0
- package/src/profiler/FrameDebuggerPanel.tsx +458 -0
- package/src/profiler/PerformancePanel.tsx +1068 -0
- package/src/profiler/ProfilerPanel.tsx +53 -0
- package/src/profiler/frame-debugger-store.ts +97 -0
- package/src/profiler/main-thread-busy.ts +90 -0
- package/src/react/design-time-react-mount.ts +782 -0
- package/src/react/dom-authoring-adapter.ts +764 -0
- package/src/react/pasteboard-materialize.ts +154 -0
- package/src/react/react-inspector-section.tsx +2496 -0
- package/src/react/react-world-authoring-adapter.ts +3795 -0
- package/src/react/story-documents/story-args-section.ts +19 -0
- package/src/react/story-documents/story-documents.tsx +1380 -0
- package/src/react/story-paint-bounds.ts +94 -0
- package/src/react/ui-board-document.tsx +101 -0
- package/src/react/ui-board-title.ts +9 -0
- package/src/react/ui-component-board.ts +72 -0
- package/src/services/audio-pose-guard.ts +81 -0
- package/src/services/game-audio-unlock.ts +48 -0
- package/src/state-watch/StateWatchPanel.tsx +535 -0
- package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
- package/src/three/authoring/constraint-inspector-section.tsx +189 -0
- package/src/three/authoring/design-time-renderer.ts +189 -0
- package/src/three/authoring/model-asset-inspector-section.css +41 -0
- package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
- package/src/three/authoring/oid-source-persistence.ts +557 -0
- package/src/three/authoring/r3f-design-session.ts +1174 -0
- package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
- package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
- package/src/three/authoring/spatial-audio-handles.ts +215 -0
- package/src/three/authoring/spatial-collider-handles.ts +229 -0
- package/src/three/authoring/spatial-joint-handles.ts +114 -0
- package/src/three/authoring/spatial-light-handles.ts +178 -0
- package/src/three/authoring/spatial-lod-handles.ts +93 -0
- package/src/three/authoring/spatial-particle-handles.ts +368 -0
- package/src/three/authoring/three-authoring-adapter.ts +1953 -0
- package/src/three/authoring/three-scene-identity.ts +19 -0
- package/src/three/authoring/three-spatial-handles.ts +161 -0
- package/src/three/authoring/typed-three-inspector.ts +528 -0
- package/src/three/component-verbs/extract-menu.ts +71 -0
- package/src/three/component-verbs/fork-menu.ts +78 -0
- package/src/three/component-verbs/internals-menu.ts +91 -0
- package/src/three/story-documents/three-story-documents.tsx +607 -0
- package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
- package/src/three/three-board/board-framing.ts +412 -0
- package/src/three/three-board/board-layout.ts +401 -0
- package/src/three/three-board/board-scene.ts +901 -0
- package/src/three/three-board/three-component-board.ts +62 -0
- package/src/xstate/XStateBehaviorSection.tsx +130 -0
- package/src/xstate/XStateMachineInspector.tsx +566 -0
- package/src/xstate/character-animation-machine.fixture.ts +74 -0
- package/src/xstate/live-behaviors.ts +106 -0
- package/src/xstate/use-live-actor-state.ts +44 -0
- package/src/xstate/xstate-graph.ts +235 -0
- 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;
|