@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,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE SESSION-WIRE DISPATCH — `bridge-call`'s method table, moved out of the
|
|
3
|
+
* editor's `command-listener.ts` with the verb it serves (WORK.md §The
|
|
4
|
+
* workbench).
|
|
5
|
+
*
|
|
6
|
+
* The game skew's own host modules — play mode, the active-systems registry,
|
|
7
|
+
* the live-seam evidence recorders, the ingest contract — are reached through
|
|
8
|
+
* the `@editor/*` alias the editor's Vite serves to every contribution, and
|
|
9
|
+
* become this package's own imports when they move. Nothing here is host API.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
getActiveSystems,
|
|
14
|
+
InstanceResolutionError,
|
|
15
|
+
systemsForInstance,
|
|
16
|
+
} from '@volter/editor-core/authoring/active-systems';
|
|
17
|
+
import { collectPlayRunPageErrors } from '@volter/editor-core/command-listener';
|
|
18
|
+
import { gameContractEpoch } from '../host/coverage/game-contract-seam-evidence';
|
|
19
|
+
import { recordLiveSeamEvidence } from '@volter/editor-core/coverage/live-seam-evidence';
|
|
20
|
+
import { systemAdapterEpoch } from '@volter/editor-core/coverage/system-seam-evidence';
|
|
21
|
+
import {
|
|
22
|
+
HOLD_STARVED_NO_DRIVER_REASON,
|
|
23
|
+
waitForHoldBudget,
|
|
24
|
+
} from '@volter/game-runtime/runtime/debug-bridge';
|
|
25
|
+
import type { SystemAdapters } from '@volter/editor-project/adapter';
|
|
26
|
+
import { activeIngestContract } from '../ingest/ingest-play-control';
|
|
27
|
+
import { getPlayRuntimeAccess } from '../play/play-mode';
|
|
28
|
+
|
|
29
|
+
/** Is there ANY live debug plane to answer from?
|
|
30
|
+
*
|
|
31
|
+
* Not the same question as `isPlayModeActive()`: the edit-time R3F design
|
|
32
|
+
* session publishes the edit-mounted world's own registry through
|
|
33
|
+
* `setActiveSystems` while play is stopped (ARCHITECTURE-CORE §Editor chrome
|
|
34
|
+
* — "the edit-time design session PUBLISHES its debug plane"), so a stopped
|
|
35
|
+
* session with a mounted world has a real, honest plane to read: the game's
|
|
36
|
+
* declared providers/commands, values as the still world reports them.
|
|
37
|
+
* An INGESTED game reaches the same answer by a different route and is the
|
|
38
|
+
* second reason this predicate is provenance-free: its mount IS the running
|
|
39
|
+
* game, so it never creates a play session at all, and what it publishes
|
|
40
|
+
* through `setActiveSystems` is the debug adapter projected from its own
|
|
41
|
+
* declared contract (`ingest-root-adapter.ts` →
|
|
42
|
+
* `@volter/game-runtime/adapter/ingest/contract-debug-adapter`). The question asked here is
|
|
43
|
+
* only ever "has a live mount published a plane", never who mounted it.
|
|
44
|
+
* `notPlayingResult()` stays the answer for the genuinely-no-world case. */
|
|
45
|
+
export const hasLiveDebugPlane = (): boolean => getActiveSystems().debug != null;
|
|
46
|
+
|
|
47
|
+
/** Duck-typed structured error (see `structuredErrorResult` above) — a plain
|
|
48
|
+
* `Error` carrying a machine-readable `code` (and optional `data`), the same
|
|
49
|
+
* shape `DebugError`/`InputActionError` already throw. Used by
|
|
50
|
+
* `dispatchBridgeMethod` below for its OWN precondition failures (no debug
|
|
51
|
+
* adapter installed, no InputManager wired) so they survive
|
|
52
|
+
* `structuredErrorResult` exactly like an engine-thrown error would. */
|
|
53
|
+
export function bridgeError(code: string, message: string, data?: Record<string, unknown>): Error {
|
|
54
|
+
const err = new Error(message) as Error & { code: string; data?: unknown };
|
|
55
|
+
err.code = code;
|
|
56
|
+
if (data) err.data = data;
|
|
57
|
+
return err;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function returnedNames(value: unknown): ReadonlySet<string> {
|
|
61
|
+
if (!Array.isArray(value)) return new Set();
|
|
62
|
+
return new Set(
|
|
63
|
+
value
|
|
64
|
+
.filter((entry): entry is { name: string } =>
|
|
65
|
+
Boolean(entry && typeof entry === 'object' && typeof entry.name === 'string'),
|
|
66
|
+
)
|
|
67
|
+
.map((entry) => entry.name),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function debugUseReachesContract(
|
|
72
|
+
member: string,
|
|
73
|
+
value: unknown,
|
|
74
|
+
name: string | undefined,
|
|
75
|
+
commands: ReadonlySet<string>,
|
|
76
|
+
state: ReadonlySet<string>,
|
|
77
|
+
): boolean {
|
|
78
|
+
if (member === 'commands') {
|
|
79
|
+
const returned = returnedNames(value);
|
|
80
|
+
return Array.isArray(value) && [...commands].every((declared) => returned.has(declared));
|
|
81
|
+
}
|
|
82
|
+
if (member === 'providers') {
|
|
83
|
+
const returned = returnedNames(value);
|
|
84
|
+
return Array.isArray(value) && [...state].every((declared) => returned.has(declared));
|
|
85
|
+
}
|
|
86
|
+
if (!name) return false;
|
|
87
|
+
if (member === 'state') return state.has(name);
|
|
88
|
+
if (member === 'invoke') return commands.has(name);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function recordContractSystemUse(member: string, value: unknown, name?: string): void {
|
|
93
|
+
const contract = activeIngestContract();
|
|
94
|
+
const systems = contract?.systems;
|
|
95
|
+
if (!contract || !systems) return;
|
|
96
|
+
const commands = new Set((systems.commands ?? []).map((entry) => entry.name));
|
|
97
|
+
const state = new Set((systems.state ?? []).map((entry) => entry.name));
|
|
98
|
+
if (!debugUseReachesContract(member, value, name, commands, state)) return;
|
|
99
|
+
recordLiveSeamEvidence({
|
|
100
|
+
seam: 'contract.systems',
|
|
101
|
+
subject: 'game',
|
|
102
|
+
epoch: gameContractEpoch(contract),
|
|
103
|
+
stage: 'operation',
|
|
104
|
+
outcome: 'pass',
|
|
105
|
+
source: 'consumer',
|
|
106
|
+
detail: `the session wire consumed contract systems through debug.${member}`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* #140 — the generic session-wire dispatch table: the SAME method-name
|
|
112
|
+
* surface `window.__vgai` exposes (`@volter/game-runtime/runtime/debug-bridge`'s
|
|
113
|
+
* `VgaiDebugHandle` — `state`/`stateAll`/`providers`/`commands`/`events`/
|
|
114
|
+
* `snapshot`/`invoke`/`runTicks`/`input.*`), built from the SAME
|
|
115
|
+
* registry-backed accessors the sibling
|
|
116
|
+
* `gameplay.command.ts` verbs (`list-gameplay-state`/`inject-input`/`run-ticks`)
|
|
117
|
+
* already use. This is what makes the
|
|
118
|
+
* `bridge-call` relay op (below) a session-generic primitive rather than a
|
|
119
|
+
* test-specific one — any client of the live session (the `@volter/editor-live`
|
|
120
|
+
* `RelayTransport`, a future one-shot `vgai game state/call/hold` REPL) can
|
|
121
|
+
* issue `{method, callArgs}` against it with no dependency on a proof run's
|
|
122
|
+
* own lifecycle/env, and get byte-identical dispatch to the page-transport's
|
|
123
|
+
* `page.evaluate(bridgeCallInPage)` (D17) — without requiring the editor tab
|
|
124
|
+
* to be loaded with `?vgai-debug=1` (that gate exists for the bridge's own
|
|
125
|
+
* production exposure; this relay is already local-loopback-only — see
|
|
126
|
+
* `editor-server.ts`'s CSRF/origin guard).
|
|
127
|
+
*/
|
|
128
|
+
export async function dispatchBridgeMethod(
|
|
129
|
+
method: string,
|
|
130
|
+
callArgs: unknown[],
|
|
131
|
+
instance?: string,
|
|
132
|
+
): Promise<unknown> {
|
|
133
|
+
// ADDRESSED resolution, not editor focus. `getActiveSystems()` answers "the
|
|
134
|
+
// game the user is looking at", which is the right question for the panels
|
|
135
|
+
// and the wrong one here: a relayed command names an instance, and resolving
|
|
136
|
+
// it through focus would let a bot drive whichever game was last clicked.
|
|
137
|
+
// With one instance the two coincide, which is why this was invisible.
|
|
138
|
+
let debug: SystemAdapters['debug'];
|
|
139
|
+
try {
|
|
140
|
+
debug = systemsForInstance(instance).debug;
|
|
141
|
+
} catch (err) {
|
|
142
|
+
if (err instanceof InstanceResolutionError) {
|
|
143
|
+
throw bridgeError('INSTANCE_UNRESOLVED', `session wire: "${method}" — ${err.message}`, {
|
|
144
|
+
instances: err.instances,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
const requireDebug = () => {
|
|
150
|
+
if (!debug) {
|
|
151
|
+
throw bridgeError(
|
|
152
|
+
'DEBUG_ADAPTER_UNAVAILABLE',
|
|
153
|
+
`session wire: "${method}" — this game exposes no debug adapter`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return debug;
|
|
157
|
+
};
|
|
158
|
+
// Named `throughDebug`, not `useDebug`: a `use`-prefixed local reads as a
|
|
159
|
+
// React hook to the lint rules a package is checked under.
|
|
160
|
+
const throughDebug = <T>(member: string, stage: 'operation' | 'effect', run: () => T): T => {
|
|
161
|
+
const adapter = requireDebug();
|
|
162
|
+
const epoch = systemAdapterEpoch(adapter);
|
|
163
|
+
try {
|
|
164
|
+
const value = run();
|
|
165
|
+
recordLiveSeamEvidence({
|
|
166
|
+
seam: `system.debug.${member}`,
|
|
167
|
+
subject: 'game',
|
|
168
|
+
epoch,
|
|
169
|
+
stage,
|
|
170
|
+
outcome: 'pass',
|
|
171
|
+
source: 'consumer',
|
|
172
|
+
detail: `the session wire called debug.${member} and received its result`,
|
|
173
|
+
});
|
|
174
|
+
recordLiveSeamEvidence({
|
|
175
|
+
seam: 'system.debug',
|
|
176
|
+
subject: 'game',
|
|
177
|
+
epoch,
|
|
178
|
+
stage: 'operation',
|
|
179
|
+
outcome: 'pass',
|
|
180
|
+
source: 'consumer',
|
|
181
|
+
detail: 'the session wire consumed the mounted debug adapter',
|
|
182
|
+
});
|
|
183
|
+
recordContractSystemUse(member, value, callArgs[0] as string | undefined);
|
|
184
|
+
return value;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
recordLiveSeamEvidence({
|
|
187
|
+
seam: `system.debug.${member}`,
|
|
188
|
+
subject: 'game',
|
|
189
|
+
epoch,
|
|
190
|
+
stage,
|
|
191
|
+
outcome: 'fail',
|
|
192
|
+
source: 'consumer',
|
|
193
|
+
detail: `the session wire called debug.${member} and it failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
194
|
+
});
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
switch (method) {
|
|
199
|
+
case 'state':
|
|
200
|
+
return throughDebug('state', 'operation', () => requireDebug().state(callArgs[0] as string));
|
|
201
|
+
case 'stateAll':
|
|
202
|
+
return throughDebug('stateAll', 'operation', () => requireDebug().stateAll());
|
|
203
|
+
case 'providers':
|
|
204
|
+
return throughDebug('providers', 'operation', () => requireDebug().providers());
|
|
205
|
+
case 'commands':
|
|
206
|
+
return throughDebug('commands', 'operation', () => requireDebug().commands());
|
|
207
|
+
case 'events':
|
|
208
|
+
return throughDebug('events', 'operation', () =>
|
|
209
|
+
requireDebug().events(callArgs[0] as number | undefined),
|
|
210
|
+
);
|
|
211
|
+
case 'snapshot': {
|
|
212
|
+
const adapter = requireDebug();
|
|
213
|
+
return {
|
|
214
|
+
time: throughDebug('state', 'operation', () => adapter.state('time')) as {
|
|
215
|
+
simSeconds: number;
|
|
216
|
+
tick: number;
|
|
217
|
+
},
|
|
218
|
+
state: throughDebug('stateAll', 'operation', () => adapter.stateAll()),
|
|
219
|
+
// Run-4 friction #5 — same `sinceSeq` contract as the in-page
|
|
220
|
+
// bridge's `snapshot()` (`runtime/debug-bridge.ts`): both doors must
|
|
221
|
+
// fence identically, or a script driving the live session (this door)
|
|
222
|
+
// vs. standalone (that door) could disagree about which events are
|
|
223
|
+
// "new".
|
|
224
|
+
events: throughDebug('events', 'operation', () =>
|
|
225
|
+
adapter.events(callArgs[0] as number | undefined),
|
|
226
|
+
),
|
|
227
|
+
// #146 (closes the #140 honest-empty gap): the editor page already
|
|
228
|
+
// funnels window error/unhandledrejection into the editor console as
|
|
229
|
+
// level 'error' / source 'runtime' (`installEditorConsoleCapture`'s
|
|
230
|
+
// boot-time listeners) — the SAME events and message format
|
|
231
|
+
// `window.__vgai`'s own handle buffers. Fenced to the current play run
|
|
232
|
+
// via `inPlayRun` so a probe never reads a previous run's stale
|
|
233
|
+
// failures, capped at 100 for parity with the bridge's ring buffer.
|
|
234
|
+
pageErrors: collectPlayRunPageErrors(),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
case 'invoke': {
|
|
238
|
+
const name = callArgs[0] as string;
|
|
239
|
+
const invokeArgs = (callArgs[1] as unknown[]) ?? [];
|
|
240
|
+
try {
|
|
241
|
+
const result = await requireDebug().invoke(name, invokeArgs);
|
|
242
|
+
const adapter = requireDebug();
|
|
243
|
+
recordLiveSeamEvidence({
|
|
244
|
+
seam: 'system.debug.invoke',
|
|
245
|
+
subject: 'game',
|
|
246
|
+
epoch: systemAdapterEpoch(adapter),
|
|
247
|
+
stage: 'effect',
|
|
248
|
+
outcome: 'pass',
|
|
249
|
+
source: 'consumer',
|
|
250
|
+
detail: `the session wire invoked debug command "${name}" and received its result`,
|
|
251
|
+
});
|
|
252
|
+
recordLiveSeamEvidence({
|
|
253
|
+
seam: 'system.debug',
|
|
254
|
+
subject: 'game',
|
|
255
|
+
epoch: systemAdapterEpoch(adapter),
|
|
256
|
+
stage: 'operation',
|
|
257
|
+
outcome: 'pass',
|
|
258
|
+
source: 'consumer',
|
|
259
|
+
detail: 'the session wire consumed the mounted debug adapter',
|
|
260
|
+
});
|
|
261
|
+
recordContractSystemUse('invoke', result, name);
|
|
262
|
+
return result;
|
|
263
|
+
} catch (error) {
|
|
264
|
+
const adapter = requireDebug();
|
|
265
|
+
recordLiveSeamEvidence({
|
|
266
|
+
seam: 'system.debug.invoke',
|
|
267
|
+
subject: 'game',
|
|
268
|
+
epoch: systemAdapterEpoch(adapter),
|
|
269
|
+
stage: 'effect',
|
|
270
|
+
outcome: 'fail',
|
|
271
|
+
source: 'consumer',
|
|
272
|
+
detail: `the session wire invoked debug command "${name}" and it failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
273
|
+
});
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
case 'runTicks': {
|
|
278
|
+
const runtime = getPlayRuntimeAccess();
|
|
279
|
+
if (!runtime?.runTicks) {
|
|
280
|
+
throw bridgeError(
|
|
281
|
+
'RUN_TICKS_UNAVAILABLE',
|
|
282
|
+
'this play session has no first-party Game to run ticks on',
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const n = callArgs[0] as number;
|
|
286
|
+
const opts = callArgs[1] as { render?: 'last' | 'all' | 'none' } | undefined;
|
|
287
|
+
runtime.runTicks(n, opts);
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
case 'runTicksSettled': {
|
|
291
|
+
// The settled-aware driver (`engine/runtime/run-ticks-settled.ts`) — a tick never races
|
|
292
|
+
// a scene remount's async commit; byte-identical with the relay's `run-ticks` case and
|
|
293
|
+
// the page bridge's own `runTicksSettled` (D17).
|
|
294
|
+
const runtime = getPlayRuntimeAccess();
|
|
295
|
+
if (!runtime?.runTicksSettled) {
|
|
296
|
+
throw bridgeError(
|
|
297
|
+
'RUN_TICKS_UNAVAILABLE',
|
|
298
|
+
'this play session has no first-party Game to run ticks on',
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
const n = callArgs[0] as number;
|
|
302
|
+
const opts = callArgs[1] as { render?: 'last' | 'all' | 'none' } | undefined;
|
|
303
|
+
await runtime.runTicksSettled(n, opts);
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
case 'input.setVirtualAction':
|
|
307
|
+
case 'input.tapVirtualAction':
|
|
308
|
+
case 'input.clearVirtualActions':
|
|
309
|
+
// bridge↔wire coverage-parity gate (`bridge-wire-parity.test.ts`)
|
|
310
|
+
// closed this trio's gap: `startRecording`/`stopRecording`/`isRecording`
|
|
311
|
+
// exist on the page bridge's `VgaiDebugInputHandle` (`runtime/
|
|
312
|
+
// debug-bridge.ts`) but had no relay case at all — an agent driving the
|
|
313
|
+
// input-trace recorder over the session wire (RelayTransport) instead of
|
|
314
|
+
// `?vgai-debug=1` simply had no door to it. Same single optional
|
|
315
|
+
// trailing `worldId` shape as `clearVirtualActions`.
|
|
316
|
+
case 'input.startRecording':
|
|
317
|
+
case 'input.stopRecording':
|
|
318
|
+
case 'input.isRecording': {
|
|
319
|
+
const runtime = getPlayRuntimeAccess();
|
|
320
|
+
// D15/T-D15.5 (rebase composition with #140): this wire is a THIRD
|
|
321
|
+
// door to the virtual-input target — it must resolve through the SAME
|
|
322
|
+
// `DebugRegistry.getVirtualInputTarget(worldId?)` the page bridge
|
|
323
|
+
// (`window.__vgai.input.*`) and the `inject-input` relay case use, so
|
|
324
|
+
// no two doors can ever disagree about which world an unqualified
|
|
325
|
+
// actuation targets. Each method takes the page bridge's own optional
|
|
326
|
+
// trailing `worldId` (byte-parity with `VgaiDebugInputHandle`); an
|
|
327
|
+
// explicit, unregistered id throws the registry's structured
|
|
328
|
+
// `DEBUG_INPUT_WORLD_NOT_FOUND`, shipped back over the wire as-is.
|
|
329
|
+
const worldId = (
|
|
330
|
+
method === 'input.setVirtualAction'
|
|
331
|
+
? callArgs[2]
|
|
332
|
+
: method === 'input.tapVirtualAction'
|
|
333
|
+
? callArgs[1]
|
|
334
|
+
: callArgs[0]
|
|
335
|
+
) as string | undefined;
|
|
336
|
+
const inputTarget = runtime ? runtime.getInputTarget(worldId) : null;
|
|
337
|
+
if (!inputTarget) {
|
|
338
|
+
throw bridgeError(
|
|
339
|
+
'DEBUG_INPUT_UNAVAILABLE',
|
|
340
|
+
`session wire: ${method}() has no virtual-input target wired — no default three ` +
|
|
341
|
+
"world has mounted yet, or this project's mount path never wired one",
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
if (method === 'input.setVirtualAction') {
|
|
345
|
+
return inputTarget.setVirtualAction(
|
|
346
|
+
callArgs[0] as string,
|
|
347
|
+
callArgs[1] as boolean | number | { x: number; y: number },
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if (method === 'input.tapVirtualAction') {
|
|
351
|
+
return inputTarget.tapVirtualAction(callArgs[0] as string);
|
|
352
|
+
}
|
|
353
|
+
if (method === 'input.clearVirtualActions') {
|
|
354
|
+
inputTarget.clearVirtualActions();
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
if (method === 'input.startRecording') {
|
|
358
|
+
inputTarget.startInputRecording();
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
if (method === 'input.stopRecording') {
|
|
362
|
+
inputTarget.stopInputRecording();
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
return inputTarget.isInputRecording();
|
|
366
|
+
}
|
|
367
|
+
case 'input.scheduleActionAtTick': {
|
|
368
|
+
// bridge↔wire coverage-parity gate: `window.__vgai.input.
|
|
369
|
+
// scheduleActionAtTick` (`runtime/debug-bridge.ts`, D15/T-D15.5) had NO
|
|
370
|
+
// bridge-call case — it was reachable over the relay ONLY via the
|
|
371
|
+
// separate `inject-input` command's `atTick` field (see that case,
|
|
372
|
+
// above), never by this method name. Same `getInputTarget(worldId?)`
|
|
373
|
+
// resolution as the other `input.*` cases; forwards `tick`/`action`/
|
|
374
|
+
// `value` straight through and returns whatever the target returns
|
|
375
|
+
// (`void`, same as the page bridge's own `scheduleActionAtTick`).
|
|
376
|
+
const runtime = getPlayRuntimeAccess();
|
|
377
|
+
const tick = callArgs[0] as number;
|
|
378
|
+
const action = callArgs[1] as string;
|
|
379
|
+
const value = callArgs[2] as boolean | number | { x: number; y: number };
|
|
380
|
+
const worldId = callArgs[3] as string | undefined;
|
|
381
|
+
const inputTarget = runtime ? runtime.getInputTarget(worldId) : null;
|
|
382
|
+
if (!inputTarget) {
|
|
383
|
+
throw bridgeError(
|
|
384
|
+
'DEBUG_INPUT_UNAVAILABLE',
|
|
385
|
+
'session wire: input.scheduleActionAtTick() has no virtual-input target wired — no ' +
|
|
386
|
+
"default three world has mounted yet, or this project's mount path never wired one",
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
return inputTarget.scheduleActionAtTick(tick, action, value);
|
|
390
|
+
}
|
|
391
|
+
case 'input.injectPointerDelta':
|
|
392
|
+
case 'input.injectPointerPosition': {
|
|
393
|
+
// pointer-dispatch op: `window.__vgai.input.injectPointerDelta`/
|
|
394
|
+
// `injectPointerPosition` (`runtime/debug-bridge.ts`) had no relay case
|
|
395
|
+
// — this is the HONEST gameplay-seam reading of "pointer dispatch": the
|
|
396
|
+
// engine's existing virtual pointer-injection, never a synthesized DOM
|
|
397
|
+
// mouse event (that boundary belongs to the future playwright shim).
|
|
398
|
+
// Same `getInputTarget(worldId?)` resolution as every other `input.*`
|
|
399
|
+
// case.
|
|
400
|
+
const runtime = getPlayRuntimeAccess();
|
|
401
|
+
const sourceId = callArgs[0] as string;
|
|
402
|
+
const value = callArgs[1] as { x: number; y: number };
|
|
403
|
+
const worldId = callArgs[2] as string | undefined;
|
|
404
|
+
const inputTarget = runtime ? runtime.getInputTarget(worldId) : null;
|
|
405
|
+
if (!inputTarget) {
|
|
406
|
+
throw bridgeError(
|
|
407
|
+
'DEBUG_INPUT_UNAVAILABLE',
|
|
408
|
+
`session wire: ${method}() has no virtual-input target wired — no default three ` +
|
|
409
|
+
"world has mounted yet, or this project's mount path never wired one",
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
if (method === 'input.injectPointerDelta') {
|
|
413
|
+
inputTarget.injectPointerDelta(sourceId, value);
|
|
414
|
+
} else {
|
|
415
|
+
inputTarget.injectPointerPosition(sourceId, value);
|
|
416
|
+
}
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
case 'holdFor': {
|
|
420
|
+
// #140/D17: byte-identical semantics to `window.__vgai.holdFor` (door
|
|
421
|
+
// a, `runtime/debug-bridge.ts`) — same `getInputTarget(worldId?)`
|
|
422
|
+
// resolution as the `input.*` cases above, same `waitForHoldBudget`
|
|
423
|
+
// poll/stall loop (imported, not hand-copied, so the two doors can
|
|
424
|
+
// never silently drift apart), reached through `requireDebug().state
|
|
425
|
+
// ('time')` instead of a `DebugRegistry` (this relay only has
|
|
426
|
+
// `getActiveSystems().debug`, not the registry itself).
|
|
427
|
+
const runtime = getPlayRuntimeAccess();
|
|
428
|
+
const worldId = callArgs[2] as string | undefined;
|
|
429
|
+
const inputTarget = runtime ? runtime.getInputTarget(worldId) : null;
|
|
430
|
+
if (!inputTarget) {
|
|
431
|
+
throw bridgeError(
|
|
432
|
+
'DEBUG_INPUT_UNAVAILABLE',
|
|
433
|
+
'session wire: holdFor() has no virtual-input target wired — no default three ' +
|
|
434
|
+
"world has mounted yet, or this project's mount path never wired one",
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
const action = callArgs[0] as string;
|
|
438
|
+
const simSeconds = callArgs[1] as number;
|
|
439
|
+
const setResult = inputTarget.setVirtualAction(action, true);
|
|
440
|
+
if (!setResult.delivered) {
|
|
441
|
+
inputTarget.clearVirtualActions();
|
|
442
|
+
return { delivered: false, reason: setResult.reason };
|
|
443
|
+
}
|
|
444
|
+
const { stalled, starvedWithoutDriver } = await waitForHoldBudget(
|
|
445
|
+
requireDebug(),
|
|
446
|
+
simSeconds,
|
|
447
|
+
runtime?.runTicks ? (n) => runtime.runTicks?.(n, { render: 'last' }) : undefined,
|
|
448
|
+
);
|
|
449
|
+
inputTarget.clearVirtualActions();
|
|
450
|
+
if (!stalled) return { delivered: true };
|
|
451
|
+
return {
|
|
452
|
+
delivered: false,
|
|
453
|
+
reason: starvedWithoutDriver ? HOLD_STARVED_NO_DRIVER_REASON : 'play stopped during hold',
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
default:
|
|
457
|
+
throw bridgeError('UNKNOWN_BRIDGE_METHOD', `session wire: unknown bridge method "${method}"`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two frame indirections `bridge-screenshot` and `bridge-recording-start`
|
|
3
|
+
* need, copied out of the editor's `command-listener.ts` with them (WORK.md
|
|
4
|
+
* §The workbench). Each is a lazy hop, not logic: the host keeps its own
|
|
5
|
+
* copies for the verbs that stay there (the play boot and
|
|
6
|
+
* `bridge-recording-export`, which still reads the shell store).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { getPlayRuntimeAccess } from '../play/play-mode';
|
|
10
|
+
|
|
11
|
+
/** Pixel recovery is a command-time concern. Keeping this indirection here
|
|
12
|
+
* avoids loading the Pixi extraction stack merely because the control relay
|
|
13
|
+
* is listening for a future screenshot/recording command. */
|
|
14
|
+
export async function captureLiveCanvasFrame(
|
|
15
|
+
canvas: HTMLCanvasElement,
|
|
16
|
+
): Promise<CanvasImageSource | null> {
|
|
17
|
+
const { liveCanvasFrame } = await import('@volter/editor-core/live-canvas-frame');
|
|
18
|
+
return liveCanvasFrame(canvas);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Both recording entry points keep a hidden native game's pixels current. */
|
|
22
|
+
export function refreshRecordingFrame(): void {
|
|
23
|
+
const runtime = getPlayRuntimeAccess();
|
|
24
|
+
if (runtime?.loop.liveness === 'loop-starved') runtime.runTicks?.(1, { render: 'last' });
|
|
25
|
+
}
|