@volter/editor-game 0.5.66 → 0.5.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/contributions/audio-unlock.service.ts +2 -2
  2. package/contributions/autoplay.service.ts +1 -1
  3. package/contributions/bridge.command.ts +3 -3
  4. package/contributions/build-player.document.tsx +95 -0
  5. package/contributions/canvas/component-board.service.ts +16 -0
  6. package/contributions/canvas/design-time-mount.service.ts +45 -0
  7. package/contributions/canvas-story-capture.service.ts +12 -0
  8. package/contributions/edit-mode-audio.service.ts +2 -2
  9. package/contributions/edit-mode-networking.service.ts +1 -1
  10. package/contributions/gameplay.command.ts +4 -4
  11. package/contributions/generation.service.ts +1 -1
  12. package/contributions/godot.style.ts +26 -6
  13. package/contributions/godot.view.ts +6 -0
  14. package/contributions/ingest.service.ts +2 -2
  15. package/contributions/instances.command.ts +1 -1
  16. package/contributions/navmesh.menu.ts +1 -1
  17. package/contributions/network-observer.service.ts +14 -0
  18. package/contributions/play.command.ts +10 -4
  19. package/contributions/react/component-board.service.ts +1 -1
  20. package/contributions/react/design-time-mount.service.ts +1 -1
  21. package/contributions/scene-document.service.ts +3 -3
  22. package/contributions/state-watch.menu.ts +1 -1
  23. package/contributions/state-watch.utility.tsx +1 -1
  24. package/contributions/team-playtest.service.ts +4 -4
  25. package/contributions/three/component-board.service.ts +1 -1
  26. package/contributions/three/component-verbs.command.ts +6 -6
  27. package/contributions/three/three-authoring.service.ts +8 -5
  28. package/contributions/three-story-capture.service.ts +12 -0
  29. package/contributions/unity.style.ts +17 -7
  30. package/contributions/unity.view.ts +6 -0
  31. package/contributions/unreal.style.ts +10 -2
  32. package/contributions/unreal.view.ts +5 -0
  33. package/package.json +21 -10
  34. package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
  35. package/src/asset-budget/asset-budget-model.ts +1 -1
  36. package/src/audio/AudioDebuggerPanel.tsx +1 -1
  37. package/src/bridge/dispatch.ts +14 -14
  38. package/src/bridge/live-frames.ts +1 -1
  39. package/src/bridge/screenshot.ts +3 -3
  40. package/src/build/BuildProfilesPanel.tsx +14 -3
  41. package/src/build/build-session.ts +35 -0
  42. package/src/canvas/canvas-board/CanvasBoardDocument.tsx +749 -0
  43. package/src/canvas/canvas-board/canvas-board-model.ts +407 -0
  44. package/src/canvas/canvas-board/canvas-component-board.ts +56 -0
  45. package/src/canvas/canvas-design-mount.ts +524 -0
  46. package/src/canvas/design-time-canvas-mount.ts +79 -0
  47. package/src/coverage/live-authoring-surface.ts +5 -5
  48. package/src/coverage/live-project-verbs.ts +1 -1
  49. package/src/coverage/native-system-coverage.ts +5 -5
  50. package/src/coverage/root-coverage.ts +1 -1
  51. package/src/coverage/session-coverage.ts +3 -3
  52. package/src/design-system-stories/ApplicationChrome.stories.tsx +5 -5
  53. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +7 -7
  54. package/src/edit-mode/edit-mode-audio.ts +4 -4
  55. package/src/edit-mode/edit-mode-networking.ts +2 -2
  56. package/src/game-document/GameCaptureFrameButton.tsx +1 -1
  57. package/src/game-document/GameDocument.tsx +2 -2
  58. package/src/game-document/GamePanel.tsx +4 -4
  59. package/src/game-document/InstanceInspectorPicker.tsx +2 -2
  60. package/src/game-document/crowd-debug.ts +1 -1
  61. package/src/game-document/device-preview.ts +10 -11
  62. package/src/game-document/physics-debug.ts +1 -1
  63. package/src/generation/GenerationActivity.tsx +3 -3
  64. package/src/generation/generation-documents.tsx +5 -5
  65. package/src/generation/generation-jobs.ts +1 -1
  66. package/src/host/adapter-runtime-bindings.ts +96 -5
  67. package/src/host/authoring/babylon-authoring-adapter.ts +6 -6
  68. package/src/host/authoring/gesture-persist.ts +1 -1
  69. package/src/host/authoring/ingest-data-writer.ts +1 -1
  70. package/src/host/authoring/ingest-source-persistence.ts +4 -4
  71. package/src/host/authoring/mounted-authoring.ts +4 -4
  72. package/src/host/authoring/phaser-live-authoring-adapter.ts +4 -4
  73. package/src/host/authoring/pixi-authoring-adapter.ts +178 -31
  74. package/src/host/authoring/pixi-creatable-kinds.ts +62 -0
  75. package/src/host/authoring/pixi-creation-site-write-target.ts +2 -2
  76. package/src/host/authoring/pixi-live-write-target.ts +4 -4
  77. package/src/host/authoring/pixi-source-identity.ts +3 -3
  78. package/src/host/authoring/pixi-source-write-target.ts +1709 -0
  79. package/src/host/authoring/pixi-still-presentation.ts +1 -1
  80. package/src/host/authoring/pixi-structure-history.ts +2 -2
  81. package/src/host/authoring/pixi-transform-channels.ts +16 -14
  82. package/src/host/authoring/source-persistence-backend.ts +3 -3
  83. package/src/host/authoring/struct-write-pipe.ts +1 -1
  84. package/src/host/binding-resolver.ts +8 -9
  85. package/src/host/browser-transpile.ts +1 -1
  86. package/src/host/canvas-entry-runtime.ts +58 -47
  87. package/src/host/canvas-preview-frames.ts +482 -0
  88. package/src/host/components/CameraAuthoringOverlay.tsx +1 -1
  89. package/src/host/components/HeaderTelemetry.tsx +4 -4
  90. package/src/host/components/PixiIsolationSceneContent.tsx +11 -11
  91. package/src/host/components/ThreeIsolationSceneContent.tsx +3 -3
  92. package/src/host/components/frame-debugger-model.ts +2 -2
  93. package/src/host/components/header-telemetry-model.ts +2 -2
  94. package/src/host/components/scene-document.tsx +14 -14
  95. package/src/host/components/utility-view-state.ts +1 -1
  96. package/src/host/components/world-root-stage-binding.tsx +12 -12
  97. package/src/host/components/world-root-stage.ts +70 -49
  98. package/src/host/coverage/system-adapter-coverage.ts +3 -4
  99. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +9 -11
  100. package/src/host/document-preview-three.ts +1 -1
  101. package/src/host/entry-adjudication.ts +6 -6
  102. package/src/host/game-css-scope-transform.ts +4 -0
  103. package/src/host/game-realm-page.ts +1 -1
  104. package/src/host/gameplay-export.ts +25 -14
  105. package/src/host/gameplay-recording.ts +5 -5
  106. package/src/host/gated-globals.ts +2 -2
  107. package/src/host/history/json-history-resource.ts +1 -1
  108. package/src/host/projection/pixi.ts +24 -2
  109. package/src/host/r3f-entry-runtime.ts +65 -34
  110. package/src/host/react-mount-runtime.ts +7 -48
  111. package/src/host/realm-services.ts +1 -1
  112. package/src/host/roots/canvas-root.tsx +373 -0
  113. package/src/host/roots/r3f-root.tsx +473 -0
  114. package/src/host/roots/react-root.ts +9 -43
  115. package/src/host/served-bundle-runtime-modules.ts +3 -19
  116. package/src/host/server-log-bridge.ts +2 -2
  117. package/src/host/stories/mounted-story-viewport-source.ts +1 -1
  118. package/src/host/stories/pixi-story-model.ts +30 -0
  119. package/src/host/stories/story-media-captures.ts +46 -0
  120. package/src/host/stories/story-media-presence.ts +3 -3
  121. package/src/host/stories/story-pixi-preview.ts +408 -0
  122. package/src/host/stories/story-three-preview.ts +806 -0
  123. package/src/host/stories/three-story-captures.ts +35 -0
  124. package/src/host/story-three-preview-runtime.ts +56 -0
  125. package/src/host/use-active-performance-source.ts +2 -2
  126. package/src/host/viewport-pose-memory.ts +1 -1
  127. package/src/host/viewport-root-presentation.ts +6 -5
  128. package/src/ingest/active-ingest.ts +1 -1
  129. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
  130. package/src/ingest/authoring/ingest-root-adapter.ts +8 -8
  131. package/src/ingest/deferred-ingest-play.ts +6 -6
  132. package/src/ingest/discovery-public-ingest.ts +2 -2
  133. package/src/ingest/ingest-boot-viewport.ts +2 -2
  134. package/src/ingest/ingest-canvas-scene-document.tsx +9 -9
  135. package/src/ingest/ingest-canvas-scene.ts +3 -3
  136. package/src/ingest/ingest-evidence-hook.ts +1 -1
  137. package/src/ingest/ingest-frame-snapshot.ts +1 -1
  138. package/src/ingest/ingest-play-control.ts +1 -1
  139. package/src/ingest/ingest-render-debug.ts +10 -10
  140. package/src/ingest/ingest-siblings.ts +13 -25
  141. package/src/ingest/module-mode.ts +14 -14
  142. package/src/ingest/mount-canvas-ingest-root.ts +21 -21
  143. package/src/ingest/mount-coverage.ts +2 -2
  144. package/src/ingest/mount-dom-ingest-root.ts +11 -11
  145. package/src/ingest/mount-ingest-root.ts +8 -8
  146. package/src/ingest/mount-three-ingest-root.ts +8 -8
  147. package/src/ingest/resolve-canvas.ts +1 -1
  148. package/src/ingest/served-html-boot.ts +1 -1
  149. package/src/ingest/surface-canvas.ts +1 -1
  150. package/src/ingest/unmount-ingest-root.ts +5 -5
  151. package/src/navmesh/navmesh-handler.ts +24 -16
  152. package/src/network/NetworkInspectorPanel.tsx +939 -37
  153. package/src/network/network-inspector-model.ts +20 -2
  154. package/src/play/play-log-events.ts +1 -1
  155. package/src/play/play-mode.ts +76 -133
  156. package/src/play/play-recording.ts +1 -1
  157. package/src/play/react-play-live-authoring.ts +3 -3
  158. package/src/play/run-selection.ts +93 -0
  159. package/src/play-bar/PlayBar.tsx +20 -39
  160. package/src/profiler/FrameDebuggerPanel.tsx +1 -1
  161. package/src/profiler/PerformancePanel.tsx +3 -3
  162. package/src/react/design-time-react-mount.ts +23 -65
  163. package/src/react/dom-authoring-adapter.ts +9 -9
  164. package/src/react/react-inspector-section.tsx +5 -5
  165. package/src/react/react-world-authoring-adapter.ts +11 -11
  166. package/src/react/story-documents/story-documents.tsx +9 -9
  167. package/src/react/ui-board-document.tsx +7 -7
  168. package/src/react/ui-component-board.ts +2 -2
  169. package/src/runtime/adapter/audio-meter.ts +21 -0
  170. package/src/runtime/adapter/first-party-audio-system.ts +230 -0
  171. package/src/runtime/adapter/ingest/contract-debug-adapter.ts +114 -0
  172. package/src/runtime/adapter/ingest/contract-system-adapters.ts +256 -0
  173. package/src/runtime/adapter/ingest/merge-debug-adapters.ts +197 -0
  174. package/src/runtime/adapter/ingest/observation-debug-adapter.ts +162 -0
  175. package/src/runtime/adapter/ingest/upstream-pin.ts +51 -0
  176. package/src/runtime/adapter/native-debug-module.ts +498 -0
  177. package/src/runtime/audio/bus-mixer.ts +161 -0
  178. package/src/runtime/audio/pose-guard.ts +80 -0
  179. package/src/runtime/core/frame-pacing.ts +126 -0
  180. package/src/runtime/core/game-loop.ts +225 -0
  181. package/src/runtime/core/game-scoped-slot.ts +28 -0
  182. package/src/runtime/core/seeded-random.ts +162 -0
  183. package/src/runtime/core/sim-clock.ts +391 -0
  184. package/src/runtime/core/system-runner.ts +269 -0
  185. package/src/runtime/core/types.ts +104 -0
  186. package/src/runtime/create-runtime.ts +1128 -0
  187. package/src/runtime/debug-bridge.ts +570 -0
  188. package/src/runtime/debug-registry.ts +899 -0
  189. package/src/runtime/dev/chrome-trace.ts +153 -0
  190. package/src/runtime/dev/instruments.ts +403 -0
  191. package/src/runtime/dev/logger.ts +119 -0
  192. package/src/runtime/dev/performance-profiler.ts +367 -0
  193. package/src/runtime/dev/register-render-vitals.ts +276 -0
  194. package/src/runtime/dev/render-census.ts +354 -0
  195. package/src/runtime/dev/render-debug-adapter.ts +218 -0
  196. package/src/runtime/dev/render-memory.ts +226 -0
  197. package/src/runtime/dev/render-vitals.ts +338 -0
  198. package/src/runtime/dev/static-batch-advisor.ts +188 -0
  199. package/src/runtime/dev/webgl-frame-capture.ts +366 -0
  200. package/src/runtime/dev/webgl-gpu-timer.ts +53 -0
  201. package/src/runtime/dev-build.ts +47 -0
  202. package/src/runtime/game.ts +1636 -0
  203. package/src/runtime/gameplay-rng-trap.ts +135 -0
  204. package/src/runtime/host-context.ts +64 -0
  205. package/src/runtime/input-router.ts +182 -0
  206. package/src/runtime/mount-manifest.ts +480 -0
  207. package/src/runtime/pixi/authoring.ts +706 -0
  208. package/src/runtime/pixi/ingest.ts +116 -0
  209. package/src/runtime/pixi/physics-registry.ts +49 -0
  210. package/src/runtime/pixi/render-pass-bracket.ts +117 -0
  211. package/src/runtime/pixi/scene-capture.ts +179 -0
  212. package/src/runtime/pixi/system-adapters.ts +69 -0
  213. package/src/runtime/playtest.ts +22 -0
  214. package/src/runtime/presentation.ts +141 -0
  215. package/src/runtime/render-control.ts +642 -0
  216. package/src/runtime/render-seed.ts +77 -0
  217. package/src/runtime/run-ticks-settled.ts +73 -0
  218. package/src/runtime/setup/setup-audio.ts +72 -0
  219. package/src/services/audio-pose-guard.ts +2 -2
  220. package/src/services/game-audio.ts +152 -0
  221. package/src/services/game-network.ts +767 -0
  222. package/src/services/game-physics.ts +334 -0
  223. package/src/state-watch/StateWatchPanel.tsx +1 -1
  224. package/src/three/authoring/camera-runtime-inspector-section.tsx +3 -2
  225. package/src/three/authoring/constraint-inspector-section.tsx +6 -5
  226. package/src/three/authoring/model-asset-inspector-section.tsx +7 -6
  227. package/src/three/authoring/oid-source-persistence.ts +7 -7
  228. package/src/three/authoring/r3f-design-session.ts +58 -54
  229. package/src/three/authoring/r3f-source-authoring-adapter.ts +95 -91
  230. package/src/three/authoring/reflection-probe-inspector-section.tsx +3 -2
  231. package/src/three/authoring/three-authoring-adapter.ts +29 -29
  232. package/src/three/component-verbs/extract-menu.ts +7 -6
  233. package/src/three/component-verbs/fork-menu.ts +7 -6
  234. package/src/three/component-verbs/internals-menu.ts +2 -2
  235. package/src/three/story-documents/three-story-documents.tsx +13 -13
  236. package/src/three/three-board/ThreeBoardDocument.tsx +13 -12
  237. package/src/three/three-board/board-scene.ts +6 -6
  238. package/src/three/three-board/three-component-board.ts +2 -2
  239. package/src/services/game-audio-unlock.ts +0 -48
@@ -0,0 +1,373 @@
1
+ /**
2
+ * The editor's mount of a `canvas` root whose entry module DEFAULT-EXPORTS a
3
+ * React component — the world a game's own boot renders with `@pixi/react`:
4
+ *
5
+ * ```tsx
6
+ * export default function World() {
7
+ * return <pixiContainer><pixiSprite texture={Texture.WHITE} /></pixiContainer>;
8
+ * }
9
+ * ```
10
+ *
11
+ * The world is an ordinary `@pixi/react` app. The editor renders it bare and
12
+ * advances Pixi's real (never-started) ticker on GAME time. A game's input is
13
+ * its own: the session's input door reaches it through the entry's `debug.input`
14
+ * export.
15
+ *
16
+ * WHOSE REACT, RECONCILER AND PIXI. The entry's hooks resolve `react`,
17
+ * `@pixi/react` and `pixi.js` through the project's module graph, so the root
18
+ * that renders it must be built with the same instances: {@link CanvasRuntime}
19
+ * is handed in by `canvas-entry-runtime.ts`, which takes them from the canvas
20
+ * doorway under the packaged runtime and loads its own otherwise. Only types
21
+ * are imported here, so a three-only project's editor bundle never carries
22
+ * the Pixi renderer.
23
+ *
24
+ * ## The loop contract
25
+ *
26
+ * `Application` is initialized with `autoStart: false` and `sharedTicker:
27
+ * false`, so it owns a native Pixi ticker that is NEVER started and therefore
28
+ * never requests an animation frame. `mounted.update(dt)` is the only thing
29
+ * that advances that ticker, and it feeds GAME time, not wall time — which is
30
+ * what makes the world freeze exactly when the host freezes it, and what makes
31
+ * `game.waitSimTime` work in a hidden tab (a hidden tab has no rAF; the engine
32
+ * loop's own catch-up still calls `update`, and Pixi has no independent clock
33
+ * to disagree with it).
34
+ *
35
+ * ## Component catalogue
36
+ *
37
+ * `@pixi/react` resolves `<pixiSprite>` etc. through a module-global catalogue
38
+ * that starts EMPTY — the first intrinsic in an un-extended tree throws
39
+ * "X is not part of the PIXI namespace! Did you forget to extend?". This
40
+ * module extends it with the whole `pixi.js` namespace once per mount
41
+ * (idempotent `Object.assign`). A world that adds its own classes
42
+ * (`@pixi/tilemap`, a custom `Container` subclass) calls `extend`/`useExtend`
43
+ * itself.
44
+ */
45
+
46
+ import type * as PixiReact from '@pixi/react';
47
+ import type { MountedCanvasRoot, MountedCanvasSubstrate, RootAdapter } from '@volter/editor-project/adapter';
48
+ import type { SystemAdapters } from '@volter/editor-project/adapter/system-adapter';
49
+ import { getDebugRegistry } from '../../runtime/debug-registry';
50
+ import type { GameCanvasHostContext } from '../../runtime/host-context';
51
+ import type * as PIXI from 'pixi.js';
52
+ import type { Application, ApplicationOptions, Container } from 'pixi.js';
53
+ import type * as React from 'react';
54
+ import type { ComponentType } from 'react';
55
+
56
+ /** The React, `@pixi/react` and `pixi.js` a canvas world is mounted with — the ones its own
57
+ * hooks resolve. */
58
+ export interface CanvasRuntime {
59
+ readonly createElement: typeof React.createElement;
60
+ readonly Fragment: typeof React.Fragment;
61
+ readonly useEffect: typeof React.useEffect;
62
+ readonly useLayoutEffect: typeof React.useLayoutEffect;
63
+ readonly createRoot: typeof PixiReact.createRoot;
64
+ readonly extend: typeof PixiReact.extend;
65
+ readonly pixi: typeof PIXI;
66
+ }
67
+
68
+ /** How long `mount()` waits for the tree's first commit before failing loudly
69
+ * rather than hanging (and wedging every root declared after this one, since
70
+ * roots mount sequentially). */
71
+ const FIRST_COMMIT_TIMEOUT_MS = 10_000;
72
+
73
+ /**
74
+ * `@pixi/react` keys its reconciler roots off the canvas element in a
75
+ * module-global map, and its own unmount path (which would clear that key) is
76
+ * not exported. So a canvas this lane has already mounted must never be handed
77
+ * to a second mount: `createRoot` would silently hand back the FIRST root,
78
+ * whose `Application` this lane already destroyed. Every host in this repo
79
+ * builds a fresh canvas per mount, so this only ever fires on a genuine bug —
80
+ * and it fires as a named error rather than as a dead world.
81
+ */
82
+ const MOUNTED_CANVASES = new WeakSet<HTMLCanvasElement>();
83
+
84
+ /** `@pixi/react`'s reconciler-root handle. Its `Root` type is internal (the
85
+ * package exports the FUNCTION, not the type), so name it off the function. */
86
+ type PixiReactRoot = ReturnType<typeof PixiReact.createRoot>;
87
+
88
+ interface CanvasEntryModuleExports {
89
+ /** The one shape: the world IS a component. */
90
+ readonly default?: ComponentType;
91
+ /** Imperative canvas substrates export their own host adapter. This is the
92
+ * same explicit boundary the three entry adjudicator accepts; gameplay
93
+ * modules remain ecosystem-native. */
94
+ readonly adapter?: unknown;
95
+ }
96
+
97
+ /** Pixi's substrate-narrowed mounted shape: generic canvas hosts depend on
98
+ * {@link MountedCanvasRoot}. */
99
+ export interface MountedPixiRoot extends MountedCanvasRoot {
100
+ readonly substrate: MountedCanvasSubstrate<Container> & { readonly name: 'pixi' };
101
+ }
102
+
103
+ function isCanvasRootAdapter(candidate: unknown): candidate is RootAdapter<'canvas'> {
104
+ if (candidate === null || typeof candidate !== 'object') return false;
105
+ const value = candidate as Record<string, unknown>;
106
+ return (
107
+ typeof value['id'] === 'string' &&
108
+ value['id'].length > 0 &&
109
+ typeof value['mount'] === 'function'
110
+ );
111
+ }
112
+
113
+ /**
114
+ * Build the `RootAdapter<'canvas'>` for one default-exported Pixi world.
115
+ * Mounts through `@pixi/react`, gated entirely by the host's own loop and
116
+ * rendering into the host's own canvas — never a second canvas, never a
117
+ * second `requestAnimationFrame` loop.
118
+ */
119
+ function canvasWorldAdapter(
120
+ id: string,
121
+ component: ComponentType,
122
+ runtime: CanvasRuntime,
123
+ ): RootAdapter<'canvas'> {
124
+ const { createElement, Fragment, useEffect, useLayoutEffect, createRoot, extend } = runtime;
125
+ const content = createElement(component);
126
+
127
+ return {
128
+ id,
129
+
130
+ async mount(host: GameCanvasHostContext): Promise<MountedPixiRoot> {
131
+ const registry = host.game ? getDebugRegistry(host.game) : null;
132
+ const canvas = host.canvas;
133
+ if (MOUNTED_CANVASES.has(canvas)) {
134
+ throw new Error(
135
+ `canvas world "${id}": this lane has already mounted this canvas. @pixi/react keys ` +
136
+ 'its reconciler roots off the canvas element, so re-using one would resurrect a ' +
137
+ 'root whose Application is destroyed. Hand each mount a fresh canvas (every vgai ' +
138
+ 'host already does).',
139
+ );
140
+ }
141
+ MOUNTED_CANVASES.add(canvas);
142
+
143
+ // The catalogue is module-global and starts empty; this is idempotent.
144
+ extend(runtime.pixi as unknown as Parameters<typeof extend>[0]);
145
+
146
+ let resolveCommitted!: () => void;
147
+ const committed = new Promise<void>((resolve) => {
148
+ resolveCommitted = resolve;
149
+ });
150
+ let resolveEffectsReady!: () => void;
151
+ const effectsReady = new Promise<void>((resolve) => {
152
+ resolveEffectsReady = resolve;
153
+ });
154
+ let resolveUnmounted!: () => void;
155
+ const unmounted = new Promise<void>((resolve) => {
156
+ resolveUnmounted = resolve;
157
+ });
158
+ let resolveEffectsUnmounted!: () => void;
159
+ const effectsUnmounted = new Promise<void>((resolve) => {
160
+ resolveEffectsUnmounted = resolve;
161
+ });
162
+ /**
163
+ * Signals the tree's FIRST COMMIT and both unmount cleanup phases. Rendered as the LAST
164
+ * child deliberately:
165
+ *
166
+ * - mount: layout effects run synchronously inside the commit, before
167
+ * any passive effect and before any tick can be scheduled;
168
+ * - unmount: the layout cleanup fences `useTick` and the passive cleanup fences ordinary
169
+ * project effects. Native destruction waits for BOTH; resolving only from layout left
170
+ * passive listener/resource cleanup racing an Application whose renderer was null.
171
+ */
172
+ function Lifecycle(): null {
173
+ useLayoutEffect(() => {
174
+ resolveCommitted();
175
+ return resolveUnmounted;
176
+ }, []);
177
+ // This component is the last sibling. Reaching its passive effect
178
+ // means the initial tree's provider/command effects have registered,
179
+ // so mount() can truthfully back Play's ack.
180
+ useEffect(() => {
181
+ resolveEffectsReady();
182
+ return resolveEffectsUnmounted;
183
+ }, []);
184
+ return null;
185
+ }
186
+
187
+ const element = createElement(Fragment, null, content, createElement(Lifecycle));
188
+
189
+ const root: PixiReactRoot = createRoot(canvas);
190
+
191
+ // The host owns size, dpr, transparency and capture cost; the loop
192
+ // contract owns `autoStart`/`sharedTicker`.
193
+ const initOptions: Partial<ApplicationOptions> = {
194
+ antialias: true,
195
+ width: host.width,
196
+ height: host.height,
197
+ resolution: host.dpr ?? globalThis.devicePixelRatio ?? 1,
198
+ autoDensity: true,
199
+ // Stacked ABOVE a sibling root -> clear with alpha 0 so the layer below
200
+ // shows through (COMPOSITION-DESIGN D5 §1).
201
+ backgroundAlpha: host.transparent ? 0 : 1,
202
+ preserveDrawingBuffer: host.preserveDrawingBuffer ?? false,
203
+ autoStart: false,
204
+ sharedTicker: false,
205
+ };
206
+
207
+ // Install the reconcile-error listener BEFORE `root.render`: Pixi hands
208
+ // the Application back after `init()` but React can already have thrown
209
+ // during that same turn. Attaching after the await loses the real error
210
+ // and turns it into a misleading ten-second timeout.
211
+ let reportReconcileFailure!: (why: string) => void;
212
+ const reconcileFailure = new Promise<string>((resolve) => {
213
+ reportReconcileFailure = resolve;
214
+ });
215
+ const onError = (event: ErrorEvent): void => {
216
+ reportReconcileFailure(
217
+ `the Pixi tree crashed before its first commit — ${event.message} ` +
218
+ '(mount() fails loudly instead of hanging)',
219
+ );
220
+ };
221
+ globalThis.addEventListener?.('error', onError);
222
+
223
+ let app: Application | undefined;
224
+ let timer: ReturnType<typeof setTimeout> | undefined;
225
+ try {
226
+ // `root.render` is async: it awaits `Application.init()` (which is what
227
+ // creates the renderer) and only then hands the tree to the reconciler.
228
+ // A crash inside init rejects here, which is the loud failure we want.
229
+ const renderedApp = await root.render(element, initOptions);
230
+ app = renderedApp;
231
+
232
+ // A reconcile-time crash (e.g. an un-extended component) surfaces as
233
+ // an uncaught window error and the first commit never lands. Race that
234
+ // exact signal against the commit and the last-resort ceiling.
235
+ const timeout = new Promise<string>((resolve) => {
236
+ timer = setTimeout(
237
+ () =>
238
+ resolve(
239
+ `no first commit within ${FIRST_COMMIT_TIMEOUT_MS / 1000}s ` +
240
+ '(mount() fails loudly instead of hanging)',
241
+ ),
242
+ FIRST_COMMIT_TIMEOUT_MS,
243
+ );
244
+ });
245
+ const failure = await Promise.race([
246
+ Promise.all([committed, effectsReady]).then(() => null),
247
+ reconcileFailure,
248
+ timeout,
249
+ ]);
250
+ if (failure !== null) {
251
+ throw new Error(`canvas world "${id}": ${failure}`);
252
+ }
253
+ } catch (error) {
254
+ try {
255
+ app?.destroy(false, { children: true });
256
+ } catch {
257
+ /* the world never fully came up; teardown failure must not mask why */
258
+ }
259
+ throw error;
260
+ } finally {
261
+ if (timer !== undefined) clearTimeout(timer);
262
+ globalThis.removeEventListener?.('error', onError);
263
+ }
264
+
265
+ // `app` is assigned before the first-commit race. Reaching here proves
266
+ // both initialization and commit succeeded, so narrow once for the live
267
+ // mounted-root methods below.
268
+ if (app === undefined) {
269
+ throw new Error(`canvas world "${id}": initialized without an Application`);
270
+ }
271
+ const mountedApp = app;
272
+
273
+ // Pin the ticker's clock to GAME time. `Ticker.lastTime` starts at -1, so
274
+ // without this the first `update()` would report a 1ms phantom delta on
275
+ // top of the real one. From here every `deltaMS` a `useTick` callback
276
+ // sees is exactly the `dt` the host handed us — no wall clock anywhere,
277
+ // which is what makes a paused world's resume frame ordinary rather than
278
+ // one giant catch-up step.
279
+ mountedApp.ticker.lastTime = 0;
280
+ let elapsedMs = 0;
281
+ let disposed = false;
282
+ // Adapter surface: the host's shared debug registry. The game's own
283
+ // capabilities arrive as the entry module's declared `systems`.
284
+ const systemAdapters: SystemAdapters = registry ? { debug: registry.adapter } : {};
285
+ const disposeComplete = Promise.all([unmounted, effectsUnmounted]).then(() => {
286
+ // React can tear the root down itself after a fatal layout/effect error. In that path the
287
+ // host has not called dispose() yet, but Application.destroy() still clears Pixi's ticker
288
+ // and renderer. Wait for both layout and passive cleanup first: project effects may own
289
+ // window/document listeners and can still read the Pixi Application while React drains
290
+ // passive deletion effects. Fence the mounted root before clearing the native slots so
291
+ // subsequent host frames and the eventual host teardown cannot re-enter them.
292
+ disposed = true;
293
+ mountedApp.destroy(false, { children: true });
294
+ });
295
+
296
+ return {
297
+ kind: 'canvas',
298
+ canvas,
299
+ substrate: { name: 'pixi', root: mountedApp.stage },
300
+ drivesOwnLoop: false,
301
+ systems: systemAdapters,
302
+ disposeComplete,
303
+ update(dt: number): void {
304
+ if (disposed) return;
305
+ elapsedMs += dt * 1000;
306
+ // The real Pixi ticker remains the one scheduler `useTick` observes;
307
+ // the host only gates/advances it.
308
+ mountedApp.ticker.update(elapsedMs);
309
+ },
310
+ // Pixi's own hit test finds only interactive objects (`eventMode` static or dynamic), so a
311
+ // HUD claims its buttons and lets every other point fall through to the world beneath.
312
+ hitTest(x: number, y: number): boolean {
313
+ if (disposed) return false;
314
+ // Pixi points its boundary at the last rendered scene inside each of its own pointer
315
+ // handlers; a hit test asked from outside them does the same, or finds no root.
316
+ const boundary = mountedApp.renderer.events.rootBoundary;
317
+ const rendered = mountedApp.renderer.lastObjectRendered;
318
+ if (!rendered) return false;
319
+ boundary.rootTarget = rendered;
320
+ return boundary.hitTest(x, y) !== null;
321
+ },
322
+ resize(width: number, height: number): void {
323
+ if (disposed) return;
324
+ // `autoDensity: true` makes Pixi re-stamp `canvas.style.width`/
325
+ // `.height` from this call, so the on-screen box follows the logical
326
+ // size with no explicit CSS write from the host.
327
+ mountedApp.renderer.resize(width, height);
328
+ },
329
+ dispose(): void {
330
+ if (disposed) return;
331
+ disposed = true;
332
+ // Pixi's EventSystem owns document.pointermove + window.pointerup
333
+ // (and its event ticker's global listener). React's deletion commit
334
+ // is asynchronous, so waiting to destroy the Application leaves
335
+ // those listeners live when the host audits this game realm. Detach
336
+ // the input target synchronously; the later Application.destroy()
337
+ // repeats this idempotently after every useTick cleanup has landed.
338
+ mountedApp.renderer.events.setTargetElement(null as unknown as HTMLElement);
339
+ // Rendering `null` children is the only unmount path `@pixi/react`
340
+ // exposes publicly (its `unmountRoot` helper is internal), and React
341
+ // commits that deletion on its own schedule — NOT synchronously. So
342
+ // the Application is destroyed on the `Lifecycle` cleanup signal
343
+ // instead of inline: `useTick`'s cleanup calls `ticker.remove()`, and
344
+ // a ticker destroyed before it lands throws inside React's commit.
345
+ // `removeView` stays false — the canvas belongs to the HOST.
346
+ void root.render(null, initOptions);
347
+ },
348
+ };
349
+ },
350
+ };
351
+ }
352
+
353
+ /**
354
+ * What a canvas entry module MEANS, in one place.
355
+ *
356
+ * The editor resolves the same entry for play mode and for the design-time
357
+ * layer, through this one function. When only one side knew the shape, a
358
+ * world played fine and showed "(no scene loaded)" in edit mode.
359
+ *
360
+ * Returns `null` when the module has no default-exported component — the
361
+ * editor reports a mount failure on that root's boundary node and leaves every
362
+ * sibling working.
363
+ */
364
+ export function resolveCanvasEntryAdapter(
365
+ entryModule: unknown,
366
+ rootId: string,
367
+ runtime: CanvasRuntime,
368
+ ): RootAdapter<'canvas'> | null {
369
+ const mod = entryModule as CanvasEntryModuleExports | undefined;
370
+ if (isCanvasRootAdapter(mod?.adapter)) return mod.adapter;
371
+ if (typeof mod?.default === 'function') return canvasWorldAdapter(rootId, mod.default, runtime);
372
+ return null;
373
+ }