@volter/editor-game 0.5.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +23 -0
  3. package/contributions/asset-budget-asset.menu.ts +26 -0
  4. package/contributions/asset-budget.action.ts +18 -0
  5. package/contributions/asset-budget.document.tsx +21 -0
  6. package/contributions/asset-budget.menu.ts +22 -0
  7. package/contributions/audio-unlock.service.ts +17 -0
  8. package/contributions/audio.utility.tsx +15 -0
  9. package/contributions/autoplay.service.ts +48 -0
  10. package/contributions/bridge.command.ts +172 -0
  11. package/contributions/build-profiles.document.tsx +21 -0
  12. package/contributions/build-progress.status.tsx +45 -0
  13. package/contributions/build.action.ts +26 -0
  14. package/contributions/build.command.ts +27 -0
  15. package/contributions/build.header.tsx +37 -0
  16. package/contributions/build.menu.ts +31 -0
  17. package/contributions/build.service.ts +31 -0
  18. package/contributions/connection.status.tsx +43 -0
  19. package/contributions/coverage.service.ts +118 -0
  20. package/contributions/edit-mode-audio.service.ts +23 -0
  21. package/contributions/edit-mode-networking.service.ts +30 -0
  22. package/contributions/game-document.service.ts +25 -0
  23. package/contributions/game-eval.command.ts +210 -0
  24. package/contributions/game.layout.ts +19 -0
  25. package/contributions/gameplay.command.ts +255 -0
  26. package/contributions/generation.service.ts +96 -0
  27. package/contributions/generations.status.tsx +53 -0
  28. package/contributions/ingest.service.ts +80 -0
  29. package/contributions/instances.command.ts +75 -0
  30. package/contributions/navmesh.menu.ts +39 -0
  31. package/contributions/navmesh.service.ts +19 -0
  32. package/contributions/network.utility.tsx +17 -0
  33. package/contributions/play.command.ts +355 -0
  34. package/contributions/profiler.action.ts +16 -0
  35. package/contributions/profiler.menu.ts +22 -0
  36. package/contributions/profiler.utility.tsx +17 -0
  37. package/contributions/react/component-board.service.ts +21 -0
  38. package/contributions/react/design-time-mount.service.ts +60 -0
  39. package/contributions/react/pasteboard.action.ts +41 -0
  40. package/contributions/react/react-inspector.service.ts +85 -0
  41. package/contributions/react/story-documents.service.ts +44 -0
  42. package/contributions/scene-document.service.ts +32 -0
  43. package/contributions/state-watch.action.ts +17 -0
  44. package/contributions/state-watch.menu.ts +23 -0
  45. package/contributions/state-watch.utility.tsx +20 -0
  46. package/contributions/team-playtest.service.ts +124 -0
  47. package/contributions/three/camera-runtime.inspector.tsx +34 -0
  48. package/contributions/three/component-board.service.ts +24 -0
  49. package/contributions/three/component-verbs.command.ts +110 -0
  50. package/contributions/three/component-verbs.service.ts +92 -0
  51. package/contributions/three/constraints.inspector.tsx +34 -0
  52. package/contributions/three/model-asset-sections.service.ts +26 -0
  53. package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
  54. package/contributions/three/story-documents.service.ts +31 -0
  55. package/contributions/three/three-authoring.service.ts +66 -0
  56. package/contributions/transport.header.tsx +19 -0
  57. package/contributions/xstate-behavior.action.ts +42 -0
  58. package/contributions/xstate-behavior.document.tsx +73 -0
  59. package/contributions/xstate-behavior.inspector.tsx +28 -0
  60. package/contributions/xstate-behavior.menu.ts +23 -0
  61. package/package.json +144 -0
  62. package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
  63. package/src/asset-budget/asset-budget-model.ts +799 -0
  64. package/src/asset-budget/basis-encoder.ts +165 -0
  65. package/src/asset-budget/gltf-io.ts +154 -0
  66. package/src/asset-budget/gltf-optimize.ts +384 -0
  67. package/src/asset-budget/image-dims.ts +78 -0
  68. package/src/asset-budget/optimize-apply.ts +221 -0
  69. package/src/audio/AudioDebuggerPanel.tsx +457 -0
  70. package/src/audio/audio-debugger-model.ts +87 -0
  71. package/src/bridge/call.ts +60 -0
  72. package/src/bridge/dispatch.ts +459 -0
  73. package/src/bridge/live-frames.ts +25 -0
  74. package/src/bridge/screenshot.ts +316 -0
  75. package/src/build/BuildProfilesPanel.tsx +476 -0
  76. package/src/build/build-session.ts +247 -0
  77. package/src/build/format-bytes.ts +14 -0
  78. package/src/command-results.ts +36 -0
  79. package/src/coverage/live-authoring-surface.ts +30 -0
  80. package/src/coverage/live-project-verbs.ts +162 -0
  81. package/src/coverage/native-system-coverage.ts +143 -0
  82. package/src/coverage/root-coverage.ts +79 -0
  83. package/src/coverage/session-coverage.ts +193 -0
  84. package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
  85. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
  86. package/src/edit-mode/edit-mode-audio.ts +56 -0
  87. package/src/edit-mode/edit-mode-networking.ts +112 -0
  88. package/src/game-document/DevicePresetPicker.tsx +84 -0
  89. package/src/game-document/GameCaptureFrameButton.tsx +45 -0
  90. package/src/game-document/GameDocument.tsx +215 -0
  91. package/src/game-document/GamePanel.tsx +662 -0
  92. package/src/game-document/InstanceInspectorPicker.tsx +183 -0
  93. package/src/game-document/crowd-debug.ts +183 -0
  94. package/src/game-document/device-preview.ts +336 -0
  95. package/src/game-document/game-view-store.ts +107 -0
  96. package/src/game-document/physics-debug.ts +187 -0
  97. package/src/generation/GenerationActivity.tsx +421 -0
  98. package/src/generation/GenerationGallery.css +231 -0
  99. package/src/generation/generation-documents.tsx +338 -0
  100. package/src/generation/generation-jobs.ts +128 -0
  101. package/src/generation/generation-presentation.ts +257 -0
  102. package/src/host/adapter-reach.ts +445 -0
  103. package/src/host/adapter-runtime-bindings.ts +303 -0
  104. package/src/host/after-paint.ts +112 -0
  105. package/src/host/api/configurations.ts +66 -0
  106. package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
  107. package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
  108. package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
  109. package/src/host/authoring/contract-scenes-stories.ts +204 -0
  110. package/src/host/authoring/creation-site-related.ts +55 -0
  111. package/src/host/authoring/ephemeral-persistence.ts +29 -0
  112. package/src/host/authoring/gesture-persist.ts +84 -0
  113. package/src/host/authoring/ingest-data-writer.ts +232 -0
  114. package/src/host/authoring/ingest-source-persistence.ts +826 -0
  115. package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
  116. package/src/host/authoring/mounted-authoring.ts +41 -0
  117. package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
  118. package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
  119. package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
  120. package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
  121. package/src/host/authoring/pixi-isolation-assets.ts +25 -0
  122. package/src/host/authoring/pixi-live-write-target.ts +979 -0
  123. package/src/host/authoring/pixi-source-identity.ts +141 -0
  124. package/src/host/authoring/pixi-still-presentation.ts +71 -0
  125. package/src/host/authoring/pixi-structure-history.ts +237 -0
  126. package/src/host/authoring/pixi-transform-channels.ts +205 -0
  127. package/src/host/authoring/selection-remount-handoff.ts +23 -0
  128. package/src/host/authoring/source-persistence-backend.ts +373 -0
  129. package/src/host/authoring/source-refresh-revisions.ts +81 -0
  130. package/src/host/authoring/struct-write-pipe.ts +143 -0
  131. package/src/host/auto-frame-window.ts +89 -0
  132. package/src/host/binding-resolver.ts +393 -0
  133. package/src/host/browser-transpile.ts +631 -0
  134. package/src/host/canvas-entry-runtime.ts +95 -0
  135. package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
  136. package/src/host/components/HeaderTelemetry.tsx +341 -0
  137. package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
  138. package/src/host/components/ResolutionPicker.tsx +89 -0
  139. package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
  140. package/src/host/components/frame-debugger-model.ts +579 -0
  141. package/src/host/components/header-telemetry-model.ts +74 -0
  142. package/src/host/components/scene-document.tsx +447 -0
  143. package/src/host/components/utility-view-state.ts +87 -0
  144. package/src/host/components/world-root-stage-binding.tsx +133 -0
  145. package/src/host/components/world-root-stage.ts +1046 -0
  146. package/src/host/coverage/authoring-read-probe.ts +583 -0
  147. package/src/host/coverage/capability-coverage.ts +1587 -0
  148. package/src/host/coverage/coverage-accounting.ts +261 -0
  149. package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
  150. package/src/host/coverage/project-verb-coverage.ts +148 -0
  151. package/src/host/coverage/system-adapter-coverage.ts +373 -0
  152. package/src/host/design-system-stories/StoryLayout.tsx +104 -0
  153. package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
  154. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
  155. package/src/host/document-preview-three.ts +110 -0
  156. package/src/host/entry-adjudication.ts +89 -0
  157. package/src/host/game-location-guard.ts +150 -0
  158. package/src/host/game-module-access.ts +196 -0
  159. package/src/host/game-realm-page.ts +358 -0
  160. package/src/host/game-realm-reclaim.ts +55 -0
  161. package/src/host/game-realm-storage.ts +104 -0
  162. package/src/host/gameplay-export.ts +288 -0
  163. package/src/host/gameplay-recording.ts +717 -0
  164. package/src/host/gated-globals.ts +1511 -0
  165. package/src/host/history/json-history-resource.ts +254 -0
  166. package/src/host/ingest/registry.ts +261 -0
  167. package/src/host/instance-extract-actions.ts +120 -0
  168. package/src/host/instance-fork-actions.ts +120 -0
  169. package/src/host/play-control-hook.ts +26 -0
  170. package/src/host/playwright-shim.ts +479 -0
  171. package/src/host/projection/dom.ts +295 -0
  172. package/src/host/projection/pixi.ts +288 -0
  173. package/src/host/r3f-entry-runtime.ts +77 -0
  174. package/src/host/react-mount-runtime.ts +162 -0
  175. package/src/host/realm-services.ts +148 -0
  176. package/src/host/recording-preview.ts +106 -0
  177. package/src/host/roots/module-root.ts +203 -0
  178. package/src/host/roots/react-root.ts +181 -0
  179. package/src/host/same-realm-loop-gate.ts +544 -0
  180. package/src/host/scene-view-drawability.ts +69 -0
  181. package/src/host/sdk/tools.ts +31 -0
  182. package/src/host/served-bundle-runtime-modules.ts +331 -0
  183. package/src/host/server-log-bridge.ts +60 -0
  184. package/src/host/staged-projects.ts +24 -0
  185. package/src/host/stories/mounted-story-viewport-source.ts +64 -0
  186. package/src/host/stories/story-arg-descriptors.ts +50 -0
  187. package/src/host/stories/story-media-presence.ts +152 -0
  188. package/src/host/surface-content.ts +87 -0
  189. package/src/host/take-named-export.ts +23 -0
  190. package/src/host/three-ingest-runtime.ts +76 -0
  191. package/src/host/types-fastnoise-lite.d.ts +7 -0
  192. package/src/host/types-mikktspace.d.ts +20 -0
  193. package/src/host/types-troika-three-text.d.ts +7 -0
  194. package/src/host/use-active-performance-source.ts +53 -0
  195. package/src/host/viewport-pose-memory.ts +48 -0
  196. package/src/host/viewport-root-presentation.ts +40 -0
  197. package/src/ingest/active-ingest.ts +251 -0
  198. package/src/ingest/active-scene-navigation.ts +43 -0
  199. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
  200. package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
  201. package/src/ingest/capture-wait-report.ts +122 -0
  202. package/src/ingest/deferred-ingest-play.ts +216 -0
  203. package/src/ingest/deferred-ingest-session.ts +23 -0
  204. package/src/ingest/discovery-public-ingest.ts +242 -0
  205. package/src/ingest/dom-stub-mark.ts +7 -0
  206. package/src/ingest/entry-load.ts +56 -0
  207. package/src/ingest/game-contract-realm.ts +34 -0
  208. package/src/ingest/game-pointer-lock.ts +89 -0
  209. package/src/ingest/held-scene-repaint.ts +73 -0
  210. package/src/ingest/host-surface-box.ts +174 -0
  211. package/src/ingest/ingest-boot-viewport.ts +54 -0
  212. package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
  213. package/src/ingest/ingest-canvas-scene.ts +50 -0
  214. package/src/ingest/ingest-evidence-hook.ts +131 -0
  215. package/src/ingest/ingest-frame-snapshot.ts +183 -0
  216. package/src/ingest/ingest-play-commands.ts +134 -0
  217. package/src/ingest/ingest-play-control.ts +299 -0
  218. package/src/ingest/ingest-render-debug.ts +320 -0
  219. package/src/ingest/ingest-siblings.ts +487 -0
  220. package/src/ingest/ingest-status.ts +62 -0
  221. package/src/ingest/live-ingest-facet.ts +46 -0
  222. package/src/ingest/module-mode.ts +229 -0
  223. package/src/ingest/mount-canvas-ingest-root.ts +899 -0
  224. package/src/ingest/mount-coverage.ts +296 -0
  225. package/src/ingest/mount-dom-ingest-root.ts +282 -0
  226. package/src/ingest/mount-ingest-root.ts +744 -0
  227. package/src/ingest/mount-three-ingest-root.ts +366 -0
  228. package/src/ingest/resolve-canvas.ts +47 -0
  229. package/src/ingest/resolve-three.ts +123 -0
  230. package/src/ingest/served-bundle.ts +109 -0
  231. package/src/ingest/served-html-boot.ts +292 -0
  232. package/src/ingest/surface-canvas.ts +84 -0
  233. package/src/ingest/surface-dom.ts +84 -0
  234. package/src/ingest/surface-three.ts +128 -0
  235. package/src/ingest/types.ts +76 -0
  236. package/src/ingest/unmount-ingest-root.ts +224 -0
  237. package/src/navmesh/navmesh-actions.ts +27 -0
  238. package/src/navmesh/navmesh-handler.ts +237 -0
  239. package/src/navmesh/navmesh-workflow-store.ts +78 -0
  240. package/src/network/NetworkInspectorPanel.tsx +644 -0
  241. package/src/network/network-inspector-model.ts +225 -0
  242. package/src/play/play-boot-stall.ts +118 -0
  243. package/src/play/play-log-events.ts +26 -0
  244. package/src/play/play-mode.ts +2553 -0
  245. package/src/play/play-recording.ts +335 -0
  246. package/src/play/react-play-live-authoring.ts +165 -0
  247. package/src/play-bar/PlayBar.tsx +488 -0
  248. package/src/play-bar/PlayerCountPicker.tsx +100 -0
  249. package/src/profiler/FrameDebuggerPanel.tsx +458 -0
  250. package/src/profiler/PerformancePanel.tsx +1068 -0
  251. package/src/profiler/ProfilerPanel.tsx +53 -0
  252. package/src/profiler/frame-debugger-store.ts +97 -0
  253. package/src/profiler/main-thread-busy.ts +90 -0
  254. package/src/react/design-time-react-mount.ts +782 -0
  255. package/src/react/dom-authoring-adapter.ts +764 -0
  256. package/src/react/pasteboard-materialize.ts +154 -0
  257. package/src/react/react-inspector-section.tsx +2496 -0
  258. package/src/react/react-world-authoring-adapter.ts +3795 -0
  259. package/src/react/story-documents/story-args-section.ts +19 -0
  260. package/src/react/story-documents/story-documents.tsx +1380 -0
  261. package/src/react/story-paint-bounds.ts +94 -0
  262. package/src/react/ui-board-document.tsx +101 -0
  263. package/src/react/ui-board-title.ts +9 -0
  264. package/src/react/ui-component-board.ts +72 -0
  265. package/src/services/audio-pose-guard.ts +81 -0
  266. package/src/services/game-audio-unlock.ts +48 -0
  267. package/src/state-watch/StateWatchPanel.tsx +535 -0
  268. package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
  269. package/src/three/authoring/constraint-inspector-section.tsx +189 -0
  270. package/src/three/authoring/design-time-renderer.ts +189 -0
  271. package/src/three/authoring/model-asset-inspector-section.css +41 -0
  272. package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
  273. package/src/three/authoring/oid-source-persistence.ts +557 -0
  274. package/src/three/authoring/r3f-design-session.ts +1174 -0
  275. package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
  276. package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
  277. package/src/three/authoring/spatial-audio-handles.ts +215 -0
  278. package/src/three/authoring/spatial-collider-handles.ts +229 -0
  279. package/src/three/authoring/spatial-joint-handles.ts +114 -0
  280. package/src/three/authoring/spatial-light-handles.ts +178 -0
  281. package/src/three/authoring/spatial-lod-handles.ts +93 -0
  282. package/src/three/authoring/spatial-particle-handles.ts +368 -0
  283. package/src/three/authoring/three-authoring-adapter.ts +1953 -0
  284. package/src/three/authoring/three-scene-identity.ts +19 -0
  285. package/src/three/authoring/three-spatial-handles.ts +161 -0
  286. package/src/three/authoring/typed-three-inspector.ts +528 -0
  287. package/src/three/component-verbs/extract-menu.ts +71 -0
  288. package/src/three/component-verbs/fork-menu.ts +78 -0
  289. package/src/three/component-verbs/internals-menu.ts +91 -0
  290. package/src/three/story-documents/three-story-documents.tsx +607 -0
  291. package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
  292. package/src/three/three-board/board-framing.ts +412 -0
  293. package/src/three/three-board/board-layout.ts +401 -0
  294. package/src/three/three-board/board-scene.ts +901 -0
  295. package/src/three/three-board/three-component-board.ts +62 -0
  296. package/src/xstate/XStateBehaviorSection.tsx +130 -0
  297. package/src/xstate/XStateMachineInspector.tsx +566 -0
  298. package/src/xstate/character-animation-machine.fixture.ts +74 -0
  299. package/src/xstate/live-behaviors.ts +106 -0
  300. package/src/xstate/use-live-actor-state.ts +44 -0
  301. package/src/xstate/xstate-graph.ts +235 -0
  302. package/src/xstate/xstate-layout.ts +76 -0
@@ -0,0 +1,644 @@
1
+ /**
2
+ * Network inspector (W3b, F9) — the full replicated-state introspection
3
+ * surface over `SystemAdapters.NetworkingAdapter`:
4
+ *
5
+ * - replicated-state TREE (live `getStateSnapshot()`; collapsed nodes mount
6
+ * no children, so a collapsed subtree costs one row per poll);
7
+ * - MESSAGE LOG (`messageEvents` seq-fenced ring → `MessageLogModel`;
8
+ * pausable, filterable by type);
9
+ * - rate SPARKLINES (`getRates()` sampled once per second into
10
+ * `RateHistory`; inline SVG polylines);
11
+ * - latency/loss CONDITIONER (`get/setConditioning` — applied at the
12
+ * adapter seam, M3).
13
+ *
14
+ * Degradation ladder rendered honestly (W3a contract, first system-adapter
15
+ * consumer): no adapter → the register-an-adapter notice; an adapter missing
16
+ * an optional capability → that section says "not provided by this adapter";
17
+ * nothing is ever fabricated. Editor code never imports Colyseus here — only
18
+ * the `NetworkingAdapter` interface (net-import-ban).
19
+ *
20
+ * Mounted as the `network` workspace utility (drawer tab beside
21
+ * Console/Profiler, `core-utilities.tsx`), available only while a
22
+ * real `NetworkingAdapter` is registered. Every caller that wants network
23
+ * diagnostics reveals that ONE utility.
24
+ */
25
+
26
+ import { editorHost } from '@volter/editor-sdk/host';
27
+ import {
28
+ Button,
29
+ DisclosureIcon,
30
+ fontSizeVar,
31
+ NumberInput,
32
+ spaceVar,
33
+ TextInput,
34
+ themeVars,
35
+ } from '@volter/editor-sdk/widgets';
36
+ import type {
37
+ ConnectionState,
38
+ NetConditioning,
39
+ NetMessageEvent,
40
+ NetworkingAdapter,
41
+ } from '@volter/editor-project/adapter';
42
+ import { memo, useEffect, useReducer, useRef, useState } from 'react';
43
+ import {
44
+ deriveNetworkCapabilities,
45
+ deriveNetworkView,
46
+ filterMessages,
47
+ MessageLogModel,
48
+ type NetworkCapabilityMap,
49
+ type NetworkView,
50
+ RateHistory,
51
+ type RateSeries,
52
+ sparklinePoints,
53
+ } from './network-inspector-model';
54
+
55
+ const STATE_COLORS: Record<ConnectionState, string> = {
56
+ disconnected: themeVars.content.muted,
57
+ connecting: themeVars.semantic.warning,
58
+ connected: themeVars.semantic.success,
59
+ error: themeVars.semantic.danger,
60
+ };
61
+
62
+ const MONO: React.CSSProperties = {
63
+ fontFamily: themeVars.typography.mono,
64
+ fontSize: fontSizeVar.base,
65
+ };
66
+
67
+ function AbsentNote({ children, testId }: { children: React.ReactNode; testId?: string }) {
68
+ return (
69
+ <div
70
+ data-testid={testId}
71
+ style={{
72
+ padding: spaceVar[4],
73
+ color: themeVars.content.muted,
74
+ fontSize: fontSizeVar.base,
75
+ fontStyle: 'italic',
76
+ }}
77
+ >
78
+ {children}
79
+ </div>
80
+ );
81
+ }
82
+
83
+ // --- Replicated-state tree ------------------------------------------------
84
+
85
+ function isBranch(value: unknown): value is object {
86
+ return value !== null && typeof value === 'object';
87
+ }
88
+
89
+ function formatLeaf(value: unknown): string {
90
+ if (typeof value === 'string') return `"${value}"`;
91
+ if (typeof value === 'number') {
92
+ return Number.isInteger(value) ? String(value) : value.toFixed(3);
93
+ }
94
+ return String(value);
95
+ }
96
+
97
+ /**
98
+ * One tree node. Expansion is per-node LOCAL state (stable across polls —
99
+ * React keeps state for a keyed position), and children of a collapsed node
100
+ * are NOT mounted: a fresh snapshot every poll re-renders only the visible
101
+ * rows, never a collapsed subtree.
102
+ */
103
+ function StateTreeNode({ name, value, depth }: { name: string; value: unknown; depth: number }) {
104
+ const [expanded, setExpanded] = useState(depth === 0);
105
+ const rowStyle: React.CSSProperties = {
106
+ ...MONO,
107
+ paddingLeft: 8 + depth * 14,
108
+ lineHeight: '18px',
109
+ whiteSpace: 'nowrap',
110
+ };
111
+
112
+ if (!isBranch(value)) {
113
+ return (
114
+ <div style={rowStyle} data-testid="net-tree-leaf">
115
+ <span style={{ color: themeVars.content.primary }}>{name}</span>
116
+ <span style={{ color: themeVars.content.muted }}>: </span>
117
+ <span style={{ color: themeVars.content.primary }}>{formatLeaf(value)}</span>
118
+ </div>
119
+ );
120
+ }
121
+
122
+ const entries = Object.entries(value as Record<string, unknown>);
123
+ return (
124
+ <div>
125
+ <div
126
+ style={{ ...rowStyle, cursor: 'pointer', userSelect: 'none' }}
127
+ data-testid="net-tree-branch"
128
+ data-expanded={expanded || undefined}
129
+ onClick={() => setExpanded((e) => !e)}
130
+ >
131
+ <span style={{ color: themeVars.content.muted, display: 'inline-block', width: 12 }}>
132
+ <DisclosureIcon direction={expanded ? 'down' : 'right'} />
133
+ </span>
134
+ <span style={{ color: themeVars.content.primary }}>{name}</span>
135
+ <span style={{ color: themeVars.content.dim }}>
136
+ {' '}
137
+ {Array.isArray(value) ? `[${entries.length}]` : `{${entries.length}}`}
138
+ </span>
139
+ </div>
140
+ {expanded &&
141
+ entries.map(([key, child]) => (
142
+ <StateTreeNode key={key} name={key} value={child} depth={depth + 1} />
143
+ ))}
144
+ </div>
145
+ );
146
+ }
147
+
148
+ // --- Sparklines -----------------------------------------------------------
149
+
150
+ const SPARK_W = 96;
151
+ const SPARK_H = 22;
152
+
153
+ /** Memoized on the DERIVED points string (computed by the caller each
154
+ * render): a sparkline redraws only when a new 1 Hz sample changes the
155
+ * geometry, not on every 250 ms panel poll. (The comparator must not read
156
+ * the mutable RateSeries itself — both props would alias the same mutated
157
+ * instance and compare equal forever, freezing the sparkline.) */
158
+ const Sparkline = memo(function Sparkline({ points, color }: { points: string; color: string }) {
159
+ return (
160
+ <svg
161
+ width={SPARK_W}
162
+ height={SPARK_H}
163
+ style={{
164
+ display: 'block',
165
+ background: themeVars.surface.inset,
166
+ borderRadius: themeVars.shape.small,
167
+ }}
168
+ role="img"
169
+ aria-label="rate sparkline"
170
+ >
171
+ {points && <polyline points={points} fill="none" stroke={color} strokeWidth={1.2} />}
172
+ </svg>
173
+ );
174
+ });
175
+
176
+ function RateCell({
177
+ label,
178
+ series,
179
+ unit,
180
+ color,
181
+ testId,
182
+ }: {
183
+ label: string;
184
+ series: RateSeries;
185
+ unit: string;
186
+ color: string;
187
+ testId: string;
188
+ }) {
189
+ const latest = series.latest;
190
+ return (
191
+ <div
192
+ data-testid={testId}
193
+ style={{ display: 'flex', flexDirection: 'column', gap: spaceVar[1] }}
194
+ >
195
+ <div style={{ fontSize: fontSizeVar.sm, color: themeVars.content.muted }}>
196
+ {label}{' '}
197
+ <span style={{ color: themeVars.content.primary, fontFamily: themeVars.typography.mono }}>
198
+ {/* An empty series = the adapter omits this direction (e.g. inbound
199
+ bytes aren't observable) — say n/a, never draw a fabricated 0. */}
200
+ {latest === null ? 'n/a' : `${latest}${unit}`}
201
+ </span>
202
+ </div>
203
+ <Sparkline points={sparklinePoints(series.data, SPARK_W, SPARK_H)} color={color} />
204
+ </div>
205
+ );
206
+ }
207
+
208
+ // --- Message log ----------------------------------------------------------
209
+
210
+ function MessageRow({ event }: { event: NetMessageEvent }) {
211
+ const dirColor = event.direction === 'in' ? themeVars.semantic.success : themeVars.accent.default;
212
+ return (
213
+ <div
214
+ data-testid="net-log-row"
215
+ data-direction={event.direction}
216
+ style={{
217
+ ...MONO,
218
+ display: 'grid',
219
+ gridTemplateColumns: '64px 24px 1fr 56px',
220
+ gap: spaceVar[2],
221
+ lineHeight: '17px',
222
+ padding: `0 ${spaceVar[4]}`,
223
+ opacity: event.dropped ? 0.55 : 1,
224
+ }}
225
+ >
226
+ <span style={{ color: themeVars.content.dim }}>{(event.time / 1000).toFixed(2)}s</span>
227
+ <span style={{ color: dirColor }}>{event.direction === 'in' ? '↓in' : '↑out'}</span>
228
+ <span
229
+ style={{
230
+ color: themeVars.content.primary,
231
+ textDecoration: event.dropped ? 'line-through' : 'none',
232
+ overflow: 'hidden',
233
+ textOverflow: 'ellipsis',
234
+ }}
235
+ >
236
+ {event.type}
237
+ {event.dropped ? ' (dropped)' : ''}
238
+ </span>
239
+ <span style={{ color: themeVars.content.muted, textAlign: 'right' }}>
240
+ {event.size !== undefined ? `${event.size} B` : '—'}
241
+ </span>
242
+ </div>
243
+ );
244
+ }
245
+
246
+ // --- Conditioner ----------------------------------------------------------
247
+
248
+ function ConditionerControls({ adapter }: { adapter: NetworkingAdapter }) {
249
+ // Local mirror initialized from the adapter; commits go through
250
+ // setConditioning on gesture end. Re-reading each render would fight the
251
+ // NumberInput drag, so the adapter's value seeds state once per mount.
252
+ const [cond, setCond] = useState<NetConditioning>(
253
+ () => adapter.getConditioning?.() ?? { latencyMs: 0, jitterMs: 0, packetLoss: 0 },
254
+ );
255
+ const apply = (next: NetConditioning) => {
256
+ setCond(next);
257
+ adapter.setConditioning?.(next);
258
+ };
259
+ return (
260
+ <div
261
+ data-testid="net-conditioner"
262
+ style={{
263
+ display: 'flex',
264
+ gap: spaceVar[4],
265
+ alignItems: 'center',
266
+ padding: `${spaceVar[2]} ${spaceVar[4]}`,
267
+ }}
268
+ >
269
+ <span style={{ fontSize: fontSizeVar.sm, color: themeVars.content.muted }}>Conditioner</span>
270
+ <NumberInput
271
+ label="latency ms"
272
+ value={cond.latencyMs}
273
+ step={10}
274
+ testId="net-cond-latency"
275
+ onChange={(v) => apply({ ...cond, latencyMs: Math.max(0, v) })}
276
+ />
277
+ <NumberInput
278
+ label="jitter ms"
279
+ value={cond.jitterMs}
280
+ step={5}
281
+ testId="net-cond-jitter"
282
+ onChange={(v) => apply({ ...cond, jitterMs: Math.max(0, v) })}
283
+ />
284
+ <NumberInput
285
+ label="loss %"
286
+ value={cond.packetLoss * 100}
287
+ step={5}
288
+ testId="net-cond-loss"
289
+ onChange={(v) => apply({ ...cond, packetLoss: Math.min(1, Math.max(0, v / 100)) })}
290
+ />
291
+ </div>
292
+ );
293
+ }
294
+
295
+ // --- Player identity ------------------------------------------------------
296
+
297
+ /** The local player's name: an EDITABLE field when the adapter can set it
298
+ * (`setPlayerIdentity`), a read-only label when it can only read it. Present
299
+ * in the header only when the game wired identity at all — never fabricated. */
300
+ function IdentityControls({
301
+ adapter,
302
+ settable,
303
+ }: {
304
+ adapter: NetworkingAdapter;
305
+ settable: boolean;
306
+ }) {
307
+ // Seed once per mount from the adapter (re-reading each render would fight
308
+ // typing), same as the conditioner. The live value is re-seeded when the
309
+ // adapter identity changes via `key` on the parent.
310
+ const [name, setName] = useState(() => adapter.getPlayerIdentity?.()?.name ?? '');
311
+ const commit = () => {
312
+ const next = name.trim();
313
+ if (next) void adapter.setPlayerIdentity?.({ name: next });
314
+ };
315
+ return (
316
+ <div
317
+ data-testid="net-identity"
318
+ style={{
319
+ display: 'flex',
320
+ gap: spaceVar[4],
321
+ alignItems: 'center',
322
+ padding: `${spaceVar[2]} ${spaceVar[4]}`,
323
+ }}
324
+ >
325
+ <span style={{ fontSize: fontSizeVar.sm, color: themeVars.content.muted }}>Player name</span>
326
+ {settable ? (
327
+ <TextInput
328
+ data-testid="net-identity-name"
329
+ value={name}
330
+ onChange={(e) => setName(e.target.value)}
331
+ onBlur={commit}
332
+ onKeyDown={(e) => {
333
+ if (e.key === 'Enter') e.currentTarget.blur();
334
+ }}
335
+ style={{ width: 140 }}
336
+ />
337
+ ) : (
338
+ <span data-testid="net-identity-name" style={{ ...MONO, color: themeVars.content.primary }}>
339
+ {name || '—'}
340
+ </span>
341
+ )}
342
+ </div>
343
+ );
344
+ }
345
+
346
+ // --- Header ---------------------------------------------------------------
347
+
348
+ /** Connection / room / replication-stat / rate row. Extracted from the panel
349
+ * so each half stays readable: every cell here is a present-or-absent
350
+ * branch, and the panel below owns state and the body layout. */
351
+ function NetworkHeader({
352
+ view,
353
+ caps,
354
+ rates,
355
+ }: {
356
+ view: NetworkView;
357
+ caps: NetworkCapabilityMap;
358
+ rates: RateHistory;
359
+ }) {
360
+ return (
361
+ <div
362
+ style={{
363
+ display: 'flex',
364
+ flexWrap: 'wrap',
365
+ gap: spaceVar[6],
366
+ alignItems: 'center',
367
+ padding: `${spaceVar[2]} ${spaceVar[4]}`,
368
+ borderBottom: `1px solid ${themeVars.boundary.default}`,
369
+ }}
370
+ >
371
+ {view.connectionState !== undefined ? (
372
+ <span
373
+ data-testid="net-connection-state"
374
+ style={{ color: STATE_COLORS[view.connectionState] }}
375
+ >
376
+ ● {view.connectionState}
377
+ </span>
378
+ ) : (
379
+ <AbsentNote testId="net-connection-state-absent">
380
+ Connection state not provided by this adapter.
381
+ </AbsentNote>
382
+ )}
383
+ {view.roomInfo === undefined ? (
384
+ <AbsentNote testId="net-room-absent">Room info not provided by this adapter.</AbsentNote>
385
+ ) : view.roomInfo ? (
386
+ <span style={{ ...MONO, color: themeVars.content.primary }}>
387
+ {view.roomInfo.roomName} · {view.roomInfo.roomId} · session {view.roomInfo.sessionId}
388
+ </span>
389
+ ) : (
390
+ <span style={{ color: themeVars.content.muted }}>no room</span>
391
+ )}
392
+ {view.stats ? (
393
+ <span style={{ color: themeVars.content.muted }}>
394
+ entities <span style={{ color: themeVars.content.primary }}>{view.stats.entities}</span>
395
+ </span>
396
+ ) : (
397
+ <AbsentNote testId="net-stats-absent">
398
+ Replication stats not provided by this adapter.
399
+ </AbsentNote>
400
+ )}
401
+ {caps.rates ? (
402
+ <div data-testid="net-sparklines" style={{ display: 'flex', gap: spaceVar[5] }}>
403
+ <RateCell
404
+ label="msgs in"
405
+ unit="/s"
406
+ series={rates.msgsIn}
407
+ color={themeVars.semantic.success}
408
+ testId="net-rate-msgs-in"
409
+ />
410
+ <RateCell
411
+ label="msgs out"
412
+ unit="/s"
413
+ series={rates.msgsOut}
414
+ color={themeVars.accent.default}
415
+ testId="net-rate-msgs-out"
416
+ />
417
+ <RateCell
418
+ label="bytes in"
419
+ unit=" B/s"
420
+ series={rates.bytesIn}
421
+ color={themeVars.semantic.success}
422
+ testId="net-rate-bytes-in"
423
+ />
424
+ <RateCell
425
+ label="bytes out"
426
+ unit=" B/s"
427
+ series={rates.bytesOut}
428
+ color={themeVars.accent.default}
429
+ testId="net-rate-bytes-out"
430
+ />
431
+ </div>
432
+ ) : (
433
+ <AbsentNote testId="net-rates-absent">Rates not provided by this adapter.</AbsentNote>
434
+ )}
435
+ </div>
436
+ );
437
+ }
438
+
439
+ // --- The panel ------------------------------------------------------------
440
+
441
+ export function NetworkInspectorPanel() {
442
+ const [, force] = useReducer((c: number) => c + 1, 0);
443
+ const logRef = useRef(new MessageLogModel());
444
+ const ratesRef = useRef(new RateHistory());
445
+ const adapterRef = useRef<NetworkingAdapter | null>(null);
446
+ const [paused, setPaused] = useState(false);
447
+ const [filter, setFilter] = useState('');
448
+
449
+ useEffect(() => {
450
+ const syncAdapter = () => {
451
+ const adapter = editorHost().systems.inspectedNetworking() ?? null;
452
+ if (adapter !== adapterRef.current) {
453
+ // New mount (or unmount): the old game's log/rate history is stale.
454
+ adapterRef.current = adapter;
455
+ logRef.current = new MessageLogModel();
456
+ logRef.current.paused = paused;
457
+ ratesRef.current = new RateHistory();
458
+ }
459
+ return adapter;
460
+ };
461
+ // 250 ms consume/render poll — also covers the
462
+ // adapter's IDENTITY changing when a game mounts/unmounts)…
463
+ const tick = () => {
464
+ const adapter = syncAdapter();
465
+ logRef.current.pull(adapter);
466
+ force();
467
+ };
468
+ tick();
469
+ const pollId = setInterval(tick, 250);
470
+ // …and a 1 Hz sparkline sampler (rates are whole-second windows).
471
+ const rateId = setInterval(() => {
472
+ const adapter = syncAdapter();
473
+ const rates = adapter?.getRates?.();
474
+ if (rates) ratesRef.current.sample(rates);
475
+ }, 1000);
476
+ const unsubscribe = editorHost()
477
+ .systems.inspectedNetworking()
478
+ ?.subscribe(() => force());
479
+ return () => {
480
+ clearInterval(pollId);
481
+ clearInterval(rateId);
482
+ unsubscribe?.();
483
+ };
484
+ }, [paused]);
485
+
486
+ const adapter = adapterRef.current ?? editorHost().systems.inspectedNetworking() ?? null;
487
+ const caps = deriveNetworkCapabilities(adapter);
488
+
489
+ if (!adapter || !caps) {
490
+ return (
491
+ <div
492
+ data-testid="network-inspector-empty"
493
+ style={{
494
+ padding: spaceVar[8],
495
+ color: themeVars.content.muted,
496
+ fontSize: fontSizeVar.md,
497
+ lineHeight: '18px',
498
+ }}
499
+ >
500
+ <div style={{ color: themeVars.content.primary, marginBottom: spaceVar[2] }}>
501
+ No networking adapter is registered.
502
+ </div>
503
+ Games expose replication state to this inspector by registering a{' '}
504
+ <code style={MONO}>NetworkingAdapter</code> during setup:{' '}
505
+ <code style={MONO}>{"ctx.registerSystemAdapter?.('networking', adapter)"}</code> — see the
506
+ third-person-arena example. Single-player games register none; there is nothing to inspect.
507
+ </div>
508
+ );
509
+ }
510
+
511
+ // `undefined` here means NO READER, which the header renders as an absent
512
+ // note — never as `disconnected` / `no room` / `entities 0`, each of which is
513
+ // a measurement claim the adapter never made. See `deriveNetworkView`.
514
+ const view = deriveNetworkView(adapter);
515
+ const snapshot = caps.stateTree ? adapter.getStateSnapshot?.() : undefined;
516
+ const messages = filterMessages(logRef.current.all, filter);
517
+
518
+ return (
519
+ <div
520
+ data-testid="network-inspector"
521
+ // §2.31 P2 amendment: text-dense output region reads over a local
522
+ // frosted layer (inert for non-frost themes).
523
+ className="vgai-content-frost"
524
+ style={{
525
+ flex: 1,
526
+ minHeight: 0,
527
+ display: 'flex',
528
+ flexDirection: 'column',
529
+ fontSize: fontSizeVar.md,
530
+ }}
531
+ >
532
+ {/* Header: connection / room / rates / conditioner */}
533
+ <NetworkHeader view={view} caps={caps} rates={ratesRef.current} />
534
+
535
+ {caps.identity ? (
536
+ // `key` re-seeds the field when the game reports a new name (e.g. the
537
+ // server assigned one on join) so the input isn't pinned to a stale mount value.
538
+ <IdentityControls
539
+ key={adapter.getPlayerIdentity?.()?.name ?? ''}
540
+ adapter={adapter}
541
+ settable={caps.identitySettable}
542
+ />
543
+ ) : null}
544
+
545
+ {caps.conditioning ? (
546
+ <ConditionerControls adapter={adapter} />
547
+ ) : (
548
+ <AbsentNote testId="net-conditioner-absent">
549
+ Link conditioning not provided by this adapter.
550
+ </AbsentNote>
551
+ )}
552
+
553
+ {/* Body: state tree | message log */}
554
+ <div style={{ flex: 1, minHeight: 0, display: 'flex' }}>
555
+ <div
556
+ data-testid="net-state-tree"
557
+ style={{
558
+ flex: 1,
559
+ minWidth: 0,
560
+ overflow: 'auto',
561
+ borderRight: `1px solid ${themeVars.boundary.default}`,
562
+ padding: `${spaceVar[2]} 0`,
563
+ }}
564
+ >
565
+ {caps.stateTree ? (
566
+ snapshot != null ? (
567
+ <StateTreeNode name="state" value={snapshot} depth={0} />
568
+ ) : (
569
+ <AbsentNote>Not connected — no replicated state to show.</AbsentNote>
570
+ )
571
+ ) : (
572
+ <AbsentNote testId="net-tree-absent">
573
+ Replicated-state snapshot not provided by this adapter.
574
+ </AbsentNote>
575
+ )}
576
+ </div>
577
+
578
+ <div
579
+ data-testid="net-message-log"
580
+ style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }}
581
+ >
582
+ {caps.messageLog ? (
583
+ <>
584
+ <div
585
+ style={{
586
+ display: 'flex',
587
+ gap: spaceVar[3],
588
+ alignItems: 'center',
589
+ padding: spaceVar[2],
590
+ }}
591
+ >
592
+ <Button
593
+ type="button"
594
+ variant="ghost"
595
+ data-testid="net-log-pause"
596
+ onClick={() => {
597
+ const next = !paused;
598
+ logRef.current.paused = next;
599
+ setPaused(next);
600
+ }}
601
+ >
602
+ {paused ? 'Resume' : 'Pause'}
603
+ </Button>
604
+ <Button
605
+ type="button"
606
+ variant="ghost"
607
+ data-testid="net-log-clear"
608
+ onClick={() => {
609
+ logRef.current.clear();
610
+ force();
611
+ }}
612
+ >
613
+ Clear
614
+ </Button>
615
+ <TextInput
616
+ data-testid="net-log-filter"
617
+ placeholder="Filter by type"
618
+ value={filter}
619
+ onChange={(e) => setFilter(e.target.value)}
620
+ style={{ flex: 1, minWidth: 60 }}
621
+ />
622
+ <span style={{ fontSize: fontSizeVar.sm, color: themeVars.content.muted }}>
623
+ {messages.length}/{logRef.current.all.length}
624
+ </span>
625
+ </div>
626
+ <div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
627
+ {messages.length === 0 ? (
628
+ <AbsentNote>No messages captured yet.</AbsentNote>
629
+ ) : (
630
+ // Newest last, natural reading order for a tail-follow log.
631
+ messages.map((event) => <MessageRow key={event.seq} event={event} />)
632
+ )}
633
+ </div>
634
+ </>
635
+ ) : (
636
+ <AbsentNote testId="net-log-absent">
637
+ Message log not provided by this adapter.
638
+ </AbsentNote>
639
+ )}
640
+ </div>
641
+ </div>
642
+ </div>
643
+ );
644
+ }