@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,544 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE loop gate for an ingested game — the SimCity ledger's S-5 ("the loop
|
|
3
|
+
* gate silently no-ops: after pause, the gated game still advances").
|
|
4
|
+
*
|
|
5
|
+
* ## What was actually wrong
|
|
6
|
+
*
|
|
7
|
+
* An ingested game runs in the EDITOR'S OWN page, where nothing can be replaced
|
|
8
|
+
* wholesale without collateral on the editor itself, so the entire "gate" was
|
|
9
|
+
* `createIngestLoopGate`: `renderer.setAnimationLoop(null)`, guarded by a check
|
|
10
|
+
* that the game had ever CALLED `setAnimationLoop`.
|
|
11
|
+
*
|
|
12
|
+
* That check is a declaration, not a measurement, and it covers exactly one
|
|
13
|
+
* loop driver. A game that renders through `setAnimationLoop` (so the check
|
|
14
|
+
* passed and the gate reported success) and simulates through
|
|
15
|
+
* `setInterval(this.simulate, 1000)` parks nothing when
|
|
16
|
+
* `setAnimationLoop(null)` runs. Pause froze the picture and the city kept
|
|
17
|
+
* building.
|
|
18
|
+
*
|
|
19
|
+
* ## The cure: the `gated-globals` precedent, applied to scheduling
|
|
20
|
+
*
|
|
21
|
+
* The dev server already prepends a lexical shadow to every project/game module
|
|
22
|
+
* so `window`/`document` resolve to editor-owned proxies
|
|
23
|
+
* (`game-globals-prelude.ts`). The same prelude now shadows the SCHEDULING
|
|
24
|
+
* functions — `setTimeout`, `setInterval`, `requestAnimationFrame` and their
|
|
25
|
+
* cancels — onto the gate this module builds. Only game modules see them; the
|
|
26
|
+
* editor's own timers are untouched, which is precisely why patching the real
|
|
27
|
+
* globals was never an option.
|
|
28
|
+
*
|
|
29
|
+
* A held gate PARKS callbacks, it does not drop them: a one-shot timer fires on
|
|
30
|
+
* release, an interval fires once (coalesced — a held gate must not release a
|
|
31
|
+
* burst of missed ticks), and frame callbacks replay in order. While OPEN the
|
|
32
|
+
* wrappers are straight pass-throughs that run the callback synchronously in
|
|
33
|
+
* the real timer's own tick, so an ungated session's timing is unchanged.
|
|
34
|
+
*
|
|
35
|
+
* ## Recorded limitation (stated, not hidden)
|
|
36
|
+
*
|
|
37
|
+
* This gate does not freeze `performance.now()`/`Date.now()` — those are the
|
|
38
|
+
* editor's own clocks and it shares them with the game, so a game CAN tell it
|
|
39
|
+
* was held. Parked frame callbacks are handed the gate's virtual timestamp (which
|
|
40
|
+
* is what a `dt` is normally computed from), but a game reading `Date.now()`
|
|
41
|
+
* directly will see the pause. That is a real gap in this rung, and it is
|
|
42
|
+
* better stated than papered over.
|
|
43
|
+
*
|
|
44
|
+
* Everything here is injectable (`createSameRealmLoopGate` takes its
|
|
45
|
+
* schedulers; `verifySameRealmLoopControl` takes the gate, a progress reader
|
|
46
|
+
* and a sleep) so the whole decision is unit-testable with no browser.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The vocabulary a loop verdict speaks.
|
|
51
|
+
*
|
|
52
|
+
* `'unmeasured'` is a first-class answer, not a failure: the probe only ever
|
|
53
|
+
* concludes from something it OBSERVED, and with nothing due while the gate was
|
|
54
|
+
* held it withheld nothing and therefore learned nothing about who drives this
|
|
55
|
+
* loop. On a hidden or backgrounded tab there is routinely nothing due, which
|
|
56
|
+
* would otherwise make the probe report TAB VISIBILITY as a property of the
|
|
57
|
+
* game.
|
|
58
|
+
*/
|
|
59
|
+
export type LoopVerdictValue = 'gated' | 'self-driven' | 'unmeasured';
|
|
60
|
+
|
|
61
|
+
/** What a verdict was concluded FROM — see {@link verifySameRealmLoopControl}
|
|
62
|
+
* for the rule each number participates in. */
|
|
63
|
+
export interface LoopEvidence {
|
|
64
|
+
readonly pendingWhileHeld: number;
|
|
65
|
+
readonly firedWhileHeld: number;
|
|
66
|
+
readonly progressBefore: number;
|
|
67
|
+
readonly progressAfter: number;
|
|
68
|
+
readonly windowMs: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* THE PAIR the coverage report and the status facet speak: a verdict WITH the
|
|
73
|
+
* evidence it was concluded from, or `null` for no verdict at all.
|
|
74
|
+
*
|
|
75
|
+
* ## Why it is a pair and not the word
|
|
76
|
+
*
|
|
77
|
+
* `'gated'` and `'self-driven'` are also an AUTHOR'S INTENT WORD — the manifest
|
|
78
|
+
* declares `loop: 'gated' | 'self-driven'` per root
|
|
79
|
+
* (`@volter/editor-project/manifest/schema`). While status and coverage emitted the bare
|
|
80
|
+
* string, a declared intent and a measured verdict were the same two bytes, and
|
|
81
|
+
* two mount routes duly asserted `'gated'` with no probe behind them: the report
|
|
82
|
+
* printed `loop ✓ gated` about games nothing had ever held.
|
|
83
|
+
*
|
|
84
|
+
* The provenance therefore lives in the VALUE'S SHAPE, not beside it. A verdict
|
|
85
|
+
* cannot be spelled without {@link LoopEvidence}, and evidence is producible
|
|
86
|
+
* only by {@link verifySameRealmLoopControl} — so "this was measured" is a
|
|
87
|
+
* structural property a stub cannot accidentally satisfy, and a route with no
|
|
88
|
+
* probe has exactly one thing it can say: `null`, plus its own reason.
|
|
89
|
+
*
|
|
90
|
+
* `readiness.ts`'s `RootReadiness.source: 'declared' | 'measured'` is the
|
|
91
|
+
* precedent for making provenance visible, and this is deliberately the
|
|
92
|
+
* STRONGER form of it rather than a transcription: readiness has two real
|
|
93
|
+
* answerers to tell apart, whereas the manifest's `loop` is
|
|
94
|
+
* `.default('gated')` — an unwritten field and an authored one are the same
|
|
95
|
+
* value by the time anything can read it, so a `source: 'declared'` member here
|
|
96
|
+
* would have to FABRICATE a declaration nobody made (the anti-shim rule). The
|
|
97
|
+
* declared word stays where it is read (mount machinery); it never enters this
|
|
98
|
+
* type.
|
|
99
|
+
*/
|
|
100
|
+
export interface MeasuredLoop {
|
|
101
|
+
readonly verdict: 'gated' | 'self-driven';
|
|
102
|
+
readonly evidence: LoopEvidence;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Fold a probe answer into {@link MeasuredLoop}: a MEASURED verdict keeps its
|
|
107
|
+
* evidence, and `'unmeasured'` (or no probe at all) becomes `null` — the same
|
|
108
|
+
* "not measured yet" every other unprobed seam reports. The probe's own
|
|
109
|
+
* `reason` travels alongside it either way, so a reader always learns why there
|
|
110
|
+
* is no verdict.
|
|
111
|
+
*/
|
|
112
|
+
export function measuredLoop(
|
|
113
|
+
verdict: SameRealmLoopVerdict | null | undefined,
|
|
114
|
+
): MeasuredLoop | null {
|
|
115
|
+
if (!verdict) return null;
|
|
116
|
+
if (verdict.loop !== 'gated' && verdict.loop !== 'self-driven') return null;
|
|
117
|
+
return { verdict: verdict.loop, evidence: verdict.evidence };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The real scheduling functions the gate wraps. */
|
|
121
|
+
export interface RealSchedulers {
|
|
122
|
+
setTimeout(callback: () => void, ms?: number): number;
|
|
123
|
+
clearTimeout(id: number): void;
|
|
124
|
+
setInterval(callback: () => void, ms?: number): number;
|
|
125
|
+
clearInterval(id: number): void;
|
|
126
|
+
requestAnimationFrame(callback: (time: number) => void): number;
|
|
127
|
+
cancelAnimationFrame(id: number): void;
|
|
128
|
+
now(): number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The gated replacements, in the shape game code expects. */
|
|
132
|
+
export interface GatedSchedulers {
|
|
133
|
+
setTimeout(callback: (...args: unknown[]) => void, ms?: number, ...args: unknown[]): number;
|
|
134
|
+
clearTimeout(id: number): void;
|
|
135
|
+
setInterval(callback: (...args: unknown[]) => void, ms?: number, ...args: unknown[]): number;
|
|
136
|
+
clearInterval(id: number): void;
|
|
137
|
+
requestAnimationFrame(callback: (time: number) => void): number;
|
|
138
|
+
cancelAnimationFrame(id: number): void;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** A snapshot of the gate's counters — everything the probe reads. */
|
|
142
|
+
export interface SameRealmLoopGateStats {
|
|
143
|
+
/** `'open'` (callbacks run) · `'held'` (callbacks parked) · `'step'` (one batch, then held). */
|
|
144
|
+
readonly mode: 'open' | 'held' | 'step';
|
|
145
|
+
/** Callbacks currently parked by the gate. */
|
|
146
|
+
readonly pending: number;
|
|
147
|
+
/** Total scheduling calls the game has made through the shadowed functions. */
|
|
148
|
+
readonly requests: number;
|
|
149
|
+
/** Total callbacks the gate has actually dispatched. */
|
|
150
|
+
readonly fired: number;
|
|
151
|
+
/**
|
|
152
|
+
* Callbacks that ran WHILE THE GATE WAS HELD. Structurally always 0 — it is a
|
|
153
|
+
* falsifier, not a statistic: a non-zero value means the gate leaked, and the
|
|
154
|
+
* probe reports `self-driven` rather than trusting itself.
|
|
155
|
+
*/
|
|
156
|
+
readonly firedWhileHeld: number;
|
|
157
|
+
/** Real scheduler handles still owned by this gate. Optional for older
|
|
158
|
+
* adapter-provided test doubles; the first-party gate always reports them. */
|
|
159
|
+
readonly activeTimeouts?: number;
|
|
160
|
+
readonly activeIntervals?: number;
|
|
161
|
+
readonly activeAnimationFrames?: number;
|
|
162
|
+
/** Input events observed by the realm's window/document proxies. A blocked
|
|
163
|
+
* event is counted in both `input` and `inputBlocked`. */
|
|
164
|
+
readonly input?: number;
|
|
165
|
+
readonly inputBlocked?: number;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface SameRealmLoopDisposal {
|
|
169
|
+
readonly timeouts: number;
|
|
170
|
+
readonly intervals: number;
|
|
171
|
+
readonly animationFrames: number;
|
|
172
|
+
readonly parkedCallbacks: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface SameRealmLoopGate {
|
|
176
|
+
/** The functions the prelude shadows onto. */
|
|
177
|
+
readonly schedulers: GatedSchedulers;
|
|
178
|
+
hold(): void;
|
|
179
|
+
release(): void;
|
|
180
|
+
/** Run exactly `n` parked batches, then hold again (editor Step). */
|
|
181
|
+
step(n?: number): void;
|
|
182
|
+
stats(): SameRealmLoopGateStats;
|
|
183
|
+
recordInput?(blocked: boolean): void;
|
|
184
|
+
/** Cancel every callback still owned by the mount. Optional only so
|
|
185
|
+
* third-party/test doubles written before mount-scoped realms remain valid. */
|
|
186
|
+
dispose?(): SameRealmLoopDisposal;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface Parked {
|
|
190
|
+
run(): void;
|
|
191
|
+
/** Set for one-shot timers, so `clearTimeout` can unpark an already-fired one. */
|
|
192
|
+
timerId?: number;
|
|
193
|
+
/** Set for frame callbacks, so `cancelAnimationFrame` can unpark one. */
|
|
194
|
+
frameId?: number;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Build a loop gate over `real`. The gate starts OPEN — a mount must be allowed
|
|
199
|
+
* to construct and draw before anything holds it.
|
|
200
|
+
*/
|
|
201
|
+
export function createSameRealmLoopGate(real: RealSchedulers): SameRealmLoopGate {
|
|
202
|
+
let mode: 'open' | 'held' | 'step' = 'open';
|
|
203
|
+
let stepsLeft = 0;
|
|
204
|
+
let requests = 0;
|
|
205
|
+
let fired = 0;
|
|
206
|
+
let firedWhileHeld = 0;
|
|
207
|
+
let input = 0;
|
|
208
|
+
let inputBlocked = 0;
|
|
209
|
+
let heldAt = 0;
|
|
210
|
+
let offset = 0;
|
|
211
|
+
let disposed = false;
|
|
212
|
+
const parked: Parked[] = [];
|
|
213
|
+
const activeTimeouts = new Set<number>();
|
|
214
|
+
const activeIntervals = new Set<number>();
|
|
215
|
+
const activeAnimationFrames = new Set<number>();
|
|
216
|
+
/** Interval id → its parked entry, so a held interval coalesces to ONE
|
|
217
|
+
* pending invocation instead of releasing a burst of missed ticks. */
|
|
218
|
+
const parkedIntervals = new Map<number, Parked>();
|
|
219
|
+
|
|
220
|
+
/** The gate's virtual time — frozen while held. Handed to parked frame
|
|
221
|
+
* callbacks so their `dt` does not swallow the whole pause. */
|
|
222
|
+
const virtualNow = (): number => (mode === 'held' ? heldAt : real.now()) - offset;
|
|
223
|
+
|
|
224
|
+
const dispatch = (entry: Parked): void => {
|
|
225
|
+
fired++;
|
|
226
|
+
if (mode === 'held') firedWhileHeld++;
|
|
227
|
+
try {
|
|
228
|
+
entry.run();
|
|
229
|
+
} catch (err) {
|
|
230
|
+
// A throwing game callback must not swallow the rest of the batch or
|
|
231
|
+
// wedge the gate; rethrow asynchronously so window.onerror still sees it.
|
|
232
|
+
real.setTimeout(() => {
|
|
233
|
+
throw err;
|
|
234
|
+
}, 0);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/** Run one parked batch. Callbacks scheduled BY the batch run normally (the
|
|
239
|
+
* gate is open by then). */
|
|
240
|
+
const flush = (): void => {
|
|
241
|
+
const batch = parked.splice(0, parked.length);
|
|
242
|
+
parkedIntervals.clear();
|
|
243
|
+
for (const entry of batch) dispatch(entry);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const park = (entry: Parked): void => {
|
|
247
|
+
parked.push(entry);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/** The one place "held?" is decided, for every kind of callback. */
|
|
251
|
+
const gated = (entry: Omit<Parked, 'run'> & { run(): void }) => (): void => {
|
|
252
|
+
if (mode === 'held') {
|
|
253
|
+
park(entry);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
dispatch(entry);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const hold = (): void => {
|
|
260
|
+
if (mode === 'held') return;
|
|
261
|
+
heldAt = real.now();
|
|
262
|
+
mode = 'held';
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const reopen = (): void => {
|
|
266
|
+
if (mode !== 'held') return;
|
|
267
|
+
offset += real.now() - heldAt;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const schedulers: GatedSchedulers = {
|
|
271
|
+
setTimeout(callback, ms, ...args) {
|
|
272
|
+
requests++;
|
|
273
|
+
if (disposed) return -1;
|
|
274
|
+
const entry: Parked = { run: () => callback(...args) };
|
|
275
|
+
let id = -1;
|
|
276
|
+
const run = gated(entry);
|
|
277
|
+
id = real.setTimeout(() => {
|
|
278
|
+
activeTimeouts.delete(id);
|
|
279
|
+
run();
|
|
280
|
+
}, ms);
|
|
281
|
+
activeTimeouts.add(id);
|
|
282
|
+
entry.timerId = id;
|
|
283
|
+
return id;
|
|
284
|
+
},
|
|
285
|
+
clearTimeout(id) {
|
|
286
|
+
real.clearTimeout(id);
|
|
287
|
+
activeTimeouts.delete(id);
|
|
288
|
+
const index = parked.findIndex((entry) => entry.timerId === id);
|
|
289
|
+
if (index !== -1) parked.splice(index, 1);
|
|
290
|
+
},
|
|
291
|
+
setInterval(callback, ms, ...args) {
|
|
292
|
+
requests++;
|
|
293
|
+
if (disposed) return -1;
|
|
294
|
+
let id = -1;
|
|
295
|
+
const tick = (): void => {
|
|
296
|
+
const entry: Parked = { run: () => callback(...args) };
|
|
297
|
+
if (mode === 'held') {
|
|
298
|
+
// Coalesce: replace this interval's pending invocation rather than
|
|
299
|
+
// stacking one per missed tick.
|
|
300
|
+
const previous = parkedIntervals.get(id);
|
|
301
|
+
if (previous) {
|
|
302
|
+
const index = parked.indexOf(previous);
|
|
303
|
+
if (index !== -1) parked.splice(index, 1);
|
|
304
|
+
}
|
|
305
|
+
parkedIntervals.set(id, entry);
|
|
306
|
+
park(entry);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
dispatch(entry);
|
|
310
|
+
};
|
|
311
|
+
id = real.setInterval(tick, ms);
|
|
312
|
+
activeIntervals.add(id);
|
|
313
|
+
return id;
|
|
314
|
+
},
|
|
315
|
+
clearInterval(id) {
|
|
316
|
+
real.clearInterval(id);
|
|
317
|
+
activeIntervals.delete(id);
|
|
318
|
+
const entry = parkedIntervals.get(id);
|
|
319
|
+
if (entry) {
|
|
320
|
+
parkedIntervals.delete(id);
|
|
321
|
+
const index = parked.indexOf(entry);
|
|
322
|
+
if (index !== -1) parked.splice(index, 1);
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
requestAnimationFrame(callback) {
|
|
326
|
+
requests++;
|
|
327
|
+
if (disposed) return -1;
|
|
328
|
+
const entry: Parked = { run: () => callback(virtualNow()) };
|
|
329
|
+
let id = -1;
|
|
330
|
+
const run = gated(entry);
|
|
331
|
+
id = real.requestAnimationFrame(() => {
|
|
332
|
+
activeAnimationFrames.delete(id);
|
|
333
|
+
run();
|
|
334
|
+
});
|
|
335
|
+
activeAnimationFrames.add(id);
|
|
336
|
+
entry.frameId = id;
|
|
337
|
+
return id;
|
|
338
|
+
},
|
|
339
|
+
cancelAnimationFrame(id) {
|
|
340
|
+
real.cancelAnimationFrame(id);
|
|
341
|
+
activeAnimationFrames.delete(id);
|
|
342
|
+
const index = parked.findIndex((entry) => entry.frameId === id);
|
|
343
|
+
if (index !== -1) parked.splice(index, 1);
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
schedulers,
|
|
349
|
+
hold,
|
|
350
|
+
release() {
|
|
351
|
+
if (disposed) return;
|
|
352
|
+
reopen();
|
|
353
|
+
mode = 'open';
|
|
354
|
+
flush();
|
|
355
|
+
},
|
|
356
|
+
step(n = 1) {
|
|
357
|
+
if (disposed) return;
|
|
358
|
+
reopen();
|
|
359
|
+
stepsLeft = n > 0 ? n : 1;
|
|
360
|
+
mode = 'step';
|
|
361
|
+
while (stepsLeft > 0 && parked.length > 0) {
|
|
362
|
+
stepsLeft--;
|
|
363
|
+
flush();
|
|
364
|
+
}
|
|
365
|
+
mode = 'held';
|
|
366
|
+
heldAt = real.now();
|
|
367
|
+
},
|
|
368
|
+
stats() {
|
|
369
|
+
return {
|
|
370
|
+
mode,
|
|
371
|
+
pending: parked.length,
|
|
372
|
+
requests,
|
|
373
|
+
fired,
|
|
374
|
+
firedWhileHeld,
|
|
375
|
+
activeTimeouts: activeTimeouts.size,
|
|
376
|
+
activeIntervals: activeIntervals.size,
|
|
377
|
+
activeAnimationFrames: activeAnimationFrames.size,
|
|
378
|
+
input,
|
|
379
|
+
inputBlocked,
|
|
380
|
+
};
|
|
381
|
+
},
|
|
382
|
+
recordInput(blocked) {
|
|
383
|
+
input++;
|
|
384
|
+
if (blocked) inputBlocked++;
|
|
385
|
+
},
|
|
386
|
+
dispose() {
|
|
387
|
+
if (disposed) {
|
|
388
|
+
return { timeouts: 0, intervals: 0, animationFrames: 0, parkedCallbacks: 0 };
|
|
389
|
+
}
|
|
390
|
+
disposed = true;
|
|
391
|
+
const report: SameRealmLoopDisposal = {
|
|
392
|
+
timeouts: activeTimeouts.size,
|
|
393
|
+
intervals: activeIntervals.size,
|
|
394
|
+
animationFrames: activeAnimationFrames.size,
|
|
395
|
+
parkedCallbacks: parked.length,
|
|
396
|
+
};
|
|
397
|
+
for (const id of activeTimeouts) real.clearTimeout(id);
|
|
398
|
+
for (const id of activeIntervals) real.clearInterval(id);
|
|
399
|
+
for (const id of activeAnimationFrames) real.cancelAnimationFrame(id);
|
|
400
|
+
activeTimeouts.clear();
|
|
401
|
+
activeIntervals.clear();
|
|
402
|
+
activeAnimationFrames.clear();
|
|
403
|
+
parked.length = 0;
|
|
404
|
+
parkedIntervals.clear();
|
|
405
|
+
stepsLeft = 0;
|
|
406
|
+
mode = 'held';
|
|
407
|
+
heldAt = real.now();
|
|
408
|
+
return report;
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** The measured verdict for a mount's loop. */
|
|
414
|
+
export interface SameRealmLoopVerdict {
|
|
415
|
+
readonly loop: LoopVerdictValue;
|
|
416
|
+
readonly reason: string;
|
|
417
|
+
readonly evidence: LoopEvidence;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* THE PROBE. Precondition: the caller has already held the gate.
|
|
422
|
+
*
|
|
423
|
+
* `loop: 'gated'` is a claim that something here actually
|
|
424
|
+
* controls this game's frames, so it must be MEASURED:
|
|
425
|
+
*
|
|
426
|
+
* - **Are we withholding anything?** `pending > 0` means the game asked for a
|
|
427
|
+
* frame or a timer and the gate is sitting on it. With nothing parked there
|
|
428
|
+
* is nothing to prove control OVER. This is the rule the old
|
|
429
|
+
* `getAnimationLoop(renderer) !== null` check lacked: it asked whether the
|
|
430
|
+
* game had ever *declared* a loop, not whether we were holding one.
|
|
431
|
+
* - **Did anything run anyway?** `firedWhileHeld` must be 0 (the gate leaked
|
|
432
|
+
* if not), and the injected `progress` reader — the game renderer's own
|
|
433
|
+
* frame counter, which is independent of this gate — must not advance.
|
|
434
|
+
*
|
|
435
|
+
* All three must hold to report `gated`.
|
|
436
|
+
*
|
|
437
|
+
* The window is SAMPLED, not measured end-to-end, and it is long by default.
|
|
438
|
+
* A 250ms window is sized for `requestAnimationFrame`, which fires
|
|
439
|
+
* every ~16ms; the slowest driver here is a `setInterval` a game chooses the
|
|
440
|
+
* period of, and a city-builder's simulation tick is 1000ms. Measured live: a 250ms
|
|
441
|
+
* single-sample window saw `pending: 0` and honestly-but-uselessly reported
|
|
442
|
+
* "the gate is withholding nothing" about a gate that had, moments later, both
|
|
443
|
+
* of that game's intervals parked. A probe reporting its own impatience as the
|
|
444
|
+
* game's shape is the same class of error as the declaration check it replaced,
|
|
445
|
+
* so the window covers a 1Hz driver and every sample counts.
|
|
446
|
+
*/
|
|
447
|
+
export async function verifySameRealmLoopControl(opts: {
|
|
448
|
+
gate: SameRealmLoopGate | null;
|
|
449
|
+
/** An independent, monotonically-advancing progress signal for this game —
|
|
450
|
+
* in practice `renderer.info.render.frame` on the CAPTURED game renderer. */
|
|
451
|
+
progress: () => number;
|
|
452
|
+
/** Total real time to watch. Default 1500ms — long enough for a 1Hz
|
|
453
|
+
* `setInterval` simulation tick to become observable (see the doc comment). */
|
|
454
|
+
windowMs?: number;
|
|
455
|
+
/** How often to sample within the window. */
|
|
456
|
+
sampleMs?: number;
|
|
457
|
+
sleep?: (ms: number) => Promise<void>;
|
|
458
|
+
}): Promise<SameRealmLoopVerdict> {
|
|
459
|
+
const windowMs = opts.windowMs ?? 1500;
|
|
460
|
+
const sampleMs = Math.max(1, Math.min(opts.sampleMs ?? 50, windowMs));
|
|
461
|
+
const sleep = opts.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
|
|
462
|
+
const gate = opts.gate;
|
|
463
|
+
if (!gate) {
|
|
464
|
+
return {
|
|
465
|
+
loop: 'self-driven',
|
|
466
|
+
reason:
|
|
467
|
+
'no same-realm loop gate is installed, so nothing here controls this game’s scheduling',
|
|
468
|
+
evidence: {
|
|
469
|
+
pendingWhileHeld: 0,
|
|
470
|
+
firedWhileHeld: 0,
|
|
471
|
+
progressBefore: 0,
|
|
472
|
+
progressAfter: 0,
|
|
473
|
+
windowMs,
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
const before = gate.stats();
|
|
478
|
+
const progressBefore = opts.progress();
|
|
479
|
+
let after = before;
|
|
480
|
+
let progressAfter = progressBefore;
|
|
481
|
+
// The PEAK parked count across the window, not the endpoint: a one-shot timer
|
|
482
|
+
// parked at 200ms and released by a Step at 900ms would read 0 at the end and
|
|
483
|
+
// erase the very evidence it is.
|
|
484
|
+
let pendingWhileHeld = before.pending;
|
|
485
|
+
let heldThroughout = before.mode === 'held';
|
|
486
|
+
for (let elapsed = 0; elapsed < windowMs; elapsed += sampleMs) {
|
|
487
|
+
await sleep(Math.min(sampleMs, windowMs - elapsed));
|
|
488
|
+
after = gate.stats();
|
|
489
|
+
progressAfter = opts.progress();
|
|
490
|
+
pendingWhileHeld = Math.max(pendingWhileHeld, after.pending);
|
|
491
|
+
if (after.mode !== 'held') {
|
|
492
|
+
heldThroughout = false;
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const firedWhileHeld = after.firedWhileHeld - before.firedWhileHeld;
|
|
497
|
+
const evidence = { pendingWhileHeld, firedWhileHeld, progressBefore, progressAfter, windowMs };
|
|
498
|
+
|
|
499
|
+
if (!heldThroughout || after.mode !== 'held') {
|
|
500
|
+
return {
|
|
501
|
+
loop: 'unmeasured',
|
|
502
|
+
reason:
|
|
503
|
+
'the gate was not held for the whole probe window — something else released it, so ' +
|
|
504
|
+
'control was not measured',
|
|
505
|
+
evidence,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
if (firedWhileHeld > 0) {
|
|
509
|
+
return {
|
|
510
|
+
loop: 'self-driven',
|
|
511
|
+
reason:
|
|
512
|
+
`${firedWhileHeld} gated callback(s) ran while the gate was held — the gate leaked, ` +
|
|
513
|
+
'so it cannot claim control',
|
|
514
|
+
evidence,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
if (progressAfter > progressBefore) {
|
|
518
|
+
return {
|
|
519
|
+
loop: 'self-driven',
|
|
520
|
+
reason:
|
|
521
|
+
`the game kept rendering while its scheduling was withheld ` +
|
|
522
|
+
`(frame ${progressBefore} → ${progressAfter} in ${windowMs}ms) — something outside this ` +
|
|
523
|
+
'gate drives its loop',
|
|
524
|
+
evidence,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
if (pendingWhileHeld === 0) {
|
|
528
|
+
return {
|
|
529
|
+
loop: 'unmeasured',
|
|
530
|
+
reason:
|
|
531
|
+
'nothing was due while the gate was held, so it withheld nothing and the probe learned ' +
|
|
532
|
+
'nothing about this loop — note a hidden or backgrounded tab schedules no frames at ' +
|
|
533
|
+
'all, which is a fact about the TAB, never about the game',
|
|
534
|
+
evidence,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
loop: 'gated',
|
|
539
|
+
reason:
|
|
540
|
+
`verified: ${pendingWhileHeld} scheduled callback(s) withheld, none ran, and no frame ` +
|
|
541
|
+
`advanced in ${windowMs}ms of held time`,
|
|
542
|
+
evidence,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHEN THE EDITOR'S OWN RENDERER CANNOT DRAW AN ADOPTED WORLD.
|
|
3
|
+
*
|
|
4
|
+
* The Scene view draws whatever world the editor has adopted with the EDITOR's
|
|
5
|
+
* `three`. For a first-party project, and for an ingested game that shares the
|
|
6
|
+
* host's instance, that is the same three and the question never arises. It
|
|
7
|
+
* arises for a game captured through three's devtools seam, which runs its own
|
|
8
|
+
* pinned revision: `cuberun` pins r129, and r180's `WebGLRenderer` calls
|
|
9
|
+
* `material.onBeforeRender()` — a method r129 materials do not have.
|
|
10
|
+
*
|
|
11
|
+
* Measured, before this guard: `TypeError: material.onBeforeRender is not a
|
|
12
|
+
* function`, ×8955 and climbing, one per frame, from a mount that had otherwise
|
|
13
|
+
* succeeded completely. The unresolved console set is meant to be a work list;
|
|
14
|
+
* a per-frame throw turns it into noise and buries everything else.
|
|
15
|
+
*
|
|
16
|
+
* THE DISCRIMINATOR IS THE FAILURE, NOT THE INSTANCE. `scene instanceof
|
|
17
|
+
* THREE.Scene` would have been the tidy test and it is the WRONG one: `tanks`
|
|
18
|
+
* also runs its own three (r170) and the host renderer draws its world
|
|
19
|
+
* perfectly — measured, in the editor viewport. Revision distance is not a
|
|
20
|
+
* predicate anyone can evaluate ahead of time, so this waits for the actual
|
|
21
|
+
* throw, attributes it to that scene, and stops re-throwing it forever.
|
|
22
|
+
*
|
|
23
|
+
* The game is NOT lost when this fires: its own renderer keeps drawing into its
|
|
24
|
+
* own canvas, which the host adopted, so the Game tab and `vgai screenshot`
|
|
25
|
+
* show the real thing. What is lost is the editor-camera Scene view of it,
|
|
26
|
+
* which is exactly what the message says.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { commandLine } from '@volter/editor-core/product-command';
|
|
30
|
+
import { editorConsole } from '@volter/editor-core/editor-console';
|
|
31
|
+
|
|
32
|
+
const undrawable = new WeakMap<object, string>();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Draw `scene`, unless a previous frame proved the host renderer cannot.
|
|
36
|
+
* Returns `true` when the draw ran.
|
|
37
|
+
*/
|
|
38
|
+
export function drawSceneUnlessRefused(
|
|
39
|
+
scene: object | null | undefined,
|
|
40
|
+
draw: () => void,
|
|
41
|
+
): boolean {
|
|
42
|
+
if (!scene) {
|
|
43
|
+
draw();
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (undrawable.has(scene)) return false;
|
|
47
|
+
try {
|
|
48
|
+
draw();
|
|
49
|
+
return true;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
52
|
+
undrawable.set(scene, message);
|
|
53
|
+
// WARN, not error: this is a standing capability gap with its closing
|
|
54
|
+
// condition named, the shape every other unreached-capability line here
|
|
55
|
+
// takes — not a failure the reader is being asked to go fix in the editor.
|
|
56
|
+
editorConsole.warn(
|
|
57
|
+
"The Scene view cannot draw this world with the editor's own renderer, and has stopped " +
|
|
58
|
+
`trying: ${message}. This world was built by a copy of three the editor did not make ` +
|
|
59
|
+
'(a vendored bundle pins its own revision), and the editor draws it with ITS three — ' +
|
|
60
|
+
'across a wide enough revision gap the two disagree about the renderer/material ' +
|
|
61
|
+
'contract. The game itself is unaffected and still drawing: its own canvas is what the ' +
|
|
62
|
+
`Game tab and ${commandLine('screenshot')} show, and hierarchy, selection and inspection all still ` +
|
|
63
|
+
"read the live world. It closes when the game resolves `three` to the host's instance " +
|
|
64
|
+
"— a bare, un-rewritten `import 'three'` in its own source.",
|
|
65
|
+
'ingest',
|
|
66
|
+
);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public project-tool contract.
|
|
3
|
+
*
|
|
4
|
+
* A tool is an ordinary registered function with a WIRE CONTRACT: declared
|
|
5
|
+
* input/result schemas, structured error codes, and the metadata a caller reads
|
|
6
|
+
* before invoking (`permission`, `host`, `mutates`, `supportsDryRun`). The
|
|
7
|
+
* contract exists because a tool is called across a process boundary — CLI to
|
|
8
|
+
* editor to node host, and `vgai mcp` to an external agent over stdio — where
|
|
9
|
+
* you cannot throw. `ToolRegistry.dispatch` turns every expected failure
|
|
10
|
+
* (unknown name, bad input, an `impl` throw, a bad return) into a typed outcome
|
|
11
|
+
* instead.
|
|
12
|
+
*
|
|
13
|
+
* This module is the surface every consumer imports. It re-exports rather than
|
|
14
|
+
* renames: there is ONE vocabulary, and it is this one.
|
|
15
|
+
*/
|
|
16
|
+
export { ToolError } from '@volter/editor-sdk/tools/errors';
|
|
17
|
+
export {
|
|
18
|
+
defineTool,
|
|
19
|
+
type ToolDefinition,
|
|
20
|
+
type ToolErrorDefinition,
|
|
21
|
+
type ToolOutcome,
|
|
22
|
+
ToolRegistry,
|
|
23
|
+
type ToolSummary,
|
|
24
|
+
} from '@volter/editor-sdk/tools/registry';
|
|
25
|
+
export type {
|
|
26
|
+
ExecutionHost as ToolHost,
|
|
27
|
+
ExecutionRequirements as ToolRequirements,
|
|
28
|
+
PermissionMetadata as ToolPermission,
|
|
29
|
+
PermissionRisk as ToolPermissionRisk,
|
|
30
|
+
ToolContext,
|
|
31
|
+
} from '@volter/editor-sdk/tools/types';
|