@volter/editor-game 0.5.66 → 0.5.67

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 (236) 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/canvas/component-board.service.ts +16 -0
  5. package/contributions/canvas/design-time-mount.service.ts +45 -0
  6. package/contributions/canvas-story-capture.service.ts +12 -0
  7. package/contributions/edit-mode-audio.service.ts +2 -2
  8. package/contributions/edit-mode-networking.service.ts +1 -1
  9. package/contributions/gameplay.command.ts +4 -4
  10. package/contributions/generation.service.ts +1 -1
  11. package/contributions/godot.style.ts +26 -6
  12. package/contributions/godot.view.ts +6 -0
  13. package/contributions/ingest.service.ts +2 -2
  14. package/contributions/instances.command.ts +1 -1
  15. package/contributions/navmesh.menu.ts +1 -1
  16. package/contributions/network-observer.service.ts +14 -0
  17. package/contributions/play.command.ts +10 -4
  18. package/contributions/react/component-board.service.ts +1 -1
  19. package/contributions/react/design-time-mount.service.ts +1 -1
  20. package/contributions/scene-document.service.ts +3 -3
  21. package/contributions/state-watch.menu.ts +1 -1
  22. package/contributions/state-watch.utility.tsx +1 -1
  23. package/contributions/team-playtest.service.ts +4 -4
  24. package/contributions/three/component-board.service.ts +1 -1
  25. package/contributions/three/component-verbs.command.ts +6 -6
  26. package/contributions/three/three-authoring.service.ts +8 -5
  27. package/contributions/three-story-capture.service.ts +12 -0
  28. package/contributions/unity.style.ts +17 -7
  29. package/contributions/unity.view.ts +6 -0
  30. package/contributions/unreal.style.ts +10 -2
  31. package/contributions/unreal.view.ts +5 -0
  32. package/package.json +20 -10
  33. package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
  34. package/src/asset-budget/asset-budget-model.ts +1 -1
  35. package/src/audio/AudioDebuggerPanel.tsx +1 -1
  36. package/src/bridge/dispatch.ts +14 -14
  37. package/src/bridge/live-frames.ts +1 -1
  38. package/src/bridge/screenshot.ts +3 -3
  39. package/src/build/BuildProfilesPanel.tsx +3 -3
  40. package/src/canvas/canvas-board/CanvasBoardDocument.tsx +676 -0
  41. package/src/canvas/canvas-board/canvas-board-model.ts +407 -0
  42. package/src/canvas/canvas-board/canvas-component-board.ts +56 -0
  43. package/src/canvas/canvas-design-mount.ts +524 -0
  44. package/src/canvas/design-time-canvas-mount.ts +79 -0
  45. package/src/coverage/live-authoring-surface.ts +5 -5
  46. package/src/coverage/live-project-verbs.ts +1 -1
  47. package/src/coverage/native-system-coverage.ts +5 -5
  48. package/src/coverage/root-coverage.ts +1 -1
  49. package/src/coverage/session-coverage.ts +3 -3
  50. package/src/design-system-stories/ApplicationChrome.stories.tsx +5 -5
  51. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +7 -7
  52. package/src/edit-mode/edit-mode-audio.ts +4 -4
  53. package/src/edit-mode/edit-mode-networking.ts +2 -2
  54. package/src/game-document/GameCaptureFrameButton.tsx +1 -1
  55. package/src/game-document/GameDocument.tsx +2 -2
  56. package/src/game-document/GamePanel.tsx +4 -4
  57. package/src/game-document/InstanceInspectorPicker.tsx +2 -2
  58. package/src/game-document/crowd-debug.ts +1 -1
  59. package/src/game-document/device-preview.ts +10 -11
  60. package/src/game-document/physics-debug.ts +1 -1
  61. package/src/generation/GenerationActivity.tsx +3 -3
  62. package/src/generation/generation-documents.tsx +5 -5
  63. package/src/generation/generation-jobs.ts +1 -1
  64. package/src/host/adapter-runtime-bindings.ts +96 -5
  65. package/src/host/authoring/babylon-authoring-adapter.ts +6 -6
  66. package/src/host/authoring/gesture-persist.ts +1 -1
  67. package/src/host/authoring/ingest-data-writer.ts +1 -1
  68. package/src/host/authoring/ingest-source-persistence.ts +4 -4
  69. package/src/host/authoring/mounted-authoring.ts +4 -4
  70. package/src/host/authoring/phaser-live-authoring-adapter.ts +4 -4
  71. package/src/host/authoring/pixi-authoring-adapter.ts +40 -15
  72. package/src/host/authoring/pixi-creation-site-write-target.ts +2 -2
  73. package/src/host/authoring/pixi-live-write-target.ts +4 -4
  74. package/src/host/authoring/pixi-source-identity.ts +3 -3
  75. package/src/host/authoring/pixi-source-write-target.ts +1614 -0
  76. package/src/host/authoring/pixi-still-presentation.ts +1 -1
  77. package/src/host/authoring/pixi-structure-history.ts +2 -2
  78. package/src/host/authoring/pixi-transform-channels.ts +16 -14
  79. package/src/host/authoring/source-persistence-backend.ts +3 -3
  80. package/src/host/authoring/struct-write-pipe.ts +1 -1
  81. package/src/host/binding-resolver.ts +8 -9
  82. package/src/host/browser-transpile.ts +1 -1
  83. package/src/host/canvas-entry-runtime.ts +58 -47
  84. package/src/host/canvas-preview-frames.ts +482 -0
  85. package/src/host/components/CameraAuthoringOverlay.tsx +1 -1
  86. package/src/host/components/HeaderTelemetry.tsx +4 -4
  87. package/src/host/components/PixiIsolationSceneContent.tsx +10 -10
  88. package/src/host/components/ThreeIsolationSceneContent.tsx +3 -3
  89. package/src/host/components/frame-debugger-model.ts +2 -2
  90. package/src/host/components/header-telemetry-model.ts +2 -2
  91. package/src/host/components/scene-document.tsx +14 -14
  92. package/src/host/components/utility-view-state.ts +1 -1
  93. package/src/host/components/world-root-stage-binding.tsx +12 -12
  94. package/src/host/components/world-root-stage.ts +70 -49
  95. package/src/host/coverage/system-adapter-coverage.ts +3 -4
  96. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +9 -11
  97. package/src/host/document-preview-three.ts +1 -1
  98. package/src/host/entry-adjudication.ts +6 -6
  99. package/src/host/game-css-scope-transform.ts +4 -0
  100. package/src/host/game-realm-page.ts +1 -1
  101. package/src/host/gameplay-export.ts +25 -14
  102. package/src/host/gameplay-recording.ts +5 -5
  103. package/src/host/gated-globals.ts +2 -2
  104. package/src/host/history/json-history-resource.ts +1 -1
  105. package/src/host/projection/pixi.ts +24 -2
  106. package/src/host/r3f-entry-runtime.ts +65 -34
  107. package/src/host/react-mount-runtime.ts +7 -48
  108. package/src/host/realm-services.ts +1 -1
  109. package/src/host/roots/canvas-root.tsx +361 -0
  110. package/src/host/roots/r3f-root.tsx +473 -0
  111. package/src/host/roots/react-root.ts +9 -43
  112. package/src/host/served-bundle-runtime-modules.ts +1 -19
  113. package/src/host/server-log-bridge.ts +2 -2
  114. package/src/host/stories/mounted-story-viewport-source.ts +1 -1
  115. package/src/host/stories/pixi-story-model.ts +30 -0
  116. package/src/host/stories/story-media-captures.ts +46 -0
  117. package/src/host/stories/story-media-presence.ts +3 -3
  118. package/src/host/stories/story-pixi-preview.ts +408 -0
  119. package/src/host/stories/story-three-preview.ts +806 -0
  120. package/src/host/stories/three-story-captures.ts +35 -0
  121. package/src/host/story-three-preview-runtime.ts +56 -0
  122. package/src/host/use-active-performance-source.ts +2 -2
  123. package/src/host/viewport-pose-memory.ts +1 -1
  124. package/src/host/viewport-root-presentation.ts +6 -5
  125. package/src/ingest/active-ingest.ts +1 -1
  126. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
  127. package/src/ingest/authoring/ingest-root-adapter.ts +8 -8
  128. package/src/ingest/deferred-ingest-play.ts +6 -6
  129. package/src/ingest/discovery-public-ingest.ts +2 -2
  130. package/src/ingest/ingest-boot-viewport.ts +2 -2
  131. package/src/ingest/ingest-canvas-scene-document.tsx +8 -8
  132. package/src/ingest/ingest-canvas-scene.ts +3 -3
  133. package/src/ingest/ingest-evidence-hook.ts +1 -1
  134. package/src/ingest/ingest-frame-snapshot.ts +1 -1
  135. package/src/ingest/ingest-play-control.ts +1 -1
  136. package/src/ingest/ingest-render-debug.ts +10 -10
  137. package/src/ingest/ingest-siblings.ts +13 -25
  138. package/src/ingest/module-mode.ts +14 -14
  139. package/src/ingest/mount-canvas-ingest-root.ts +21 -21
  140. package/src/ingest/mount-coverage.ts +2 -2
  141. package/src/ingest/mount-dom-ingest-root.ts +11 -11
  142. package/src/ingest/mount-ingest-root.ts +8 -8
  143. package/src/ingest/mount-three-ingest-root.ts +8 -8
  144. package/src/ingest/resolve-canvas.ts +1 -1
  145. package/src/ingest/served-html-boot.ts +1 -1
  146. package/src/ingest/surface-canvas.ts +1 -1
  147. package/src/ingest/unmount-ingest-root.ts +5 -5
  148. package/src/navmesh/navmesh-handler.ts +24 -16
  149. package/src/network/NetworkInspectorPanel.tsx +449 -14
  150. package/src/network/network-inspector-model.ts +14 -2
  151. package/src/play/play-log-events.ts +1 -1
  152. package/src/play/play-mode.ts +76 -133
  153. package/src/play/play-recording.ts +1 -1
  154. package/src/play/react-play-live-authoring.ts +3 -3
  155. package/src/play/run-selection.ts +93 -0
  156. package/src/play-bar/PlayBar.tsx +20 -39
  157. package/src/profiler/FrameDebuggerPanel.tsx +1 -1
  158. package/src/profiler/PerformancePanel.tsx +3 -3
  159. package/src/react/design-time-react-mount.ts +23 -65
  160. package/src/react/dom-authoring-adapter.ts +9 -9
  161. package/src/react/react-inspector-section.tsx +5 -5
  162. package/src/react/react-world-authoring-adapter.ts +11 -11
  163. package/src/react/story-documents/story-documents.tsx +9 -9
  164. package/src/react/ui-board-document.tsx +7 -7
  165. package/src/react/ui-component-board.ts +2 -2
  166. package/src/runtime/adapter/audio-meter.ts +21 -0
  167. package/src/runtime/adapter/first-party-audio-system.ts +230 -0
  168. package/src/runtime/adapter/ingest/contract-debug-adapter.ts +114 -0
  169. package/src/runtime/adapter/ingest/contract-system-adapters.ts +256 -0
  170. package/src/runtime/adapter/ingest/merge-debug-adapters.ts +197 -0
  171. package/src/runtime/adapter/ingest/observation-debug-adapter.ts +162 -0
  172. package/src/runtime/adapter/ingest/upstream-pin.ts +51 -0
  173. package/src/runtime/adapter/native-debug-module.ts +498 -0
  174. package/src/runtime/audio/bus-mixer.ts +161 -0
  175. package/src/runtime/audio/pose-guard.ts +80 -0
  176. package/src/runtime/core/frame-pacing.ts +126 -0
  177. package/src/runtime/core/game-loop.ts +225 -0
  178. package/src/runtime/core/game-scoped-slot.ts +28 -0
  179. package/src/runtime/core/seeded-random.ts +162 -0
  180. package/src/runtime/core/sim-clock.ts +391 -0
  181. package/src/runtime/core/system-runner.ts +269 -0
  182. package/src/runtime/core/types.ts +104 -0
  183. package/src/runtime/create-runtime.ts +1128 -0
  184. package/src/runtime/debug-bridge.ts +570 -0
  185. package/src/runtime/debug-registry.ts +899 -0
  186. package/src/runtime/dev/chrome-trace.ts +153 -0
  187. package/src/runtime/dev/instruments.ts +403 -0
  188. package/src/runtime/dev/logger.ts +119 -0
  189. package/src/runtime/dev/performance-profiler.ts +367 -0
  190. package/src/runtime/dev/register-render-vitals.ts +276 -0
  191. package/src/runtime/dev/render-census.ts +354 -0
  192. package/src/runtime/dev/render-debug-adapter.ts +218 -0
  193. package/src/runtime/dev/render-memory.ts +226 -0
  194. package/src/runtime/dev/render-vitals.ts +338 -0
  195. package/src/runtime/dev/static-batch-advisor.ts +188 -0
  196. package/src/runtime/dev/webgl-frame-capture.ts +366 -0
  197. package/src/runtime/dev/webgl-gpu-timer.ts +53 -0
  198. package/src/runtime/dev-build.ts +47 -0
  199. package/src/runtime/game.ts +1636 -0
  200. package/src/runtime/gameplay-rng-trap.ts +135 -0
  201. package/src/runtime/host-context.ts +64 -0
  202. package/src/runtime/input-router.ts +169 -0
  203. package/src/runtime/mount-manifest.ts +480 -0
  204. package/src/runtime/pixi/authoring.ts +706 -0
  205. package/src/runtime/pixi/ingest.ts +116 -0
  206. package/src/runtime/pixi/physics-registry.ts +49 -0
  207. package/src/runtime/pixi/render-pass-bracket.ts +117 -0
  208. package/src/runtime/pixi/scene-capture.ts +179 -0
  209. package/src/runtime/pixi/system-adapters.ts +69 -0
  210. package/src/runtime/playtest.ts +22 -0
  211. package/src/runtime/presentation.ts +141 -0
  212. package/src/runtime/render-control.ts +642 -0
  213. package/src/runtime/render-seed.ts +77 -0
  214. package/src/runtime/run-ticks-settled.ts +73 -0
  215. package/src/runtime/setup/setup-audio.ts +72 -0
  216. package/src/services/audio-pose-guard.ts +2 -2
  217. package/src/services/game-audio.ts +152 -0
  218. package/src/services/game-network.ts +657 -0
  219. package/src/services/game-physics.ts +334 -0
  220. package/src/state-watch/StateWatchPanel.tsx +1 -1
  221. package/src/three/authoring/camera-runtime-inspector-section.tsx +3 -2
  222. package/src/three/authoring/constraint-inspector-section.tsx +6 -5
  223. package/src/three/authoring/model-asset-inspector-section.tsx +7 -6
  224. package/src/three/authoring/oid-source-persistence.ts +7 -7
  225. package/src/three/authoring/r3f-design-session.ts +58 -54
  226. package/src/three/authoring/r3f-source-authoring-adapter.ts +95 -91
  227. package/src/three/authoring/reflection-probe-inspector-section.tsx +3 -2
  228. package/src/three/authoring/three-authoring-adapter.ts +29 -29
  229. package/src/three/component-verbs/extract-menu.ts +7 -6
  230. package/src/three/component-verbs/fork-menu.ts +7 -6
  231. package/src/three/component-verbs/internals-menu.ts +2 -2
  232. package/src/three/story-documents/three-story-documents.tsx +13 -13
  233. package/src/three/three-board/ThreeBoardDocument.tsx +13 -12
  234. package/src/three/three-board/board-scene.ts +6 -6
  235. package/src/three/three-board/three-component-board.ts +2 -2
  236. package/src/services/game-audio-unlock.ts +0 -48
@@ -14,8 +14,9 @@
14
14
  * Degradation ladder rendered honestly (W3a contract, first system-adapter
15
15
  * consumer): no adapter → the register-an-adapter notice; an adapter missing
16
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).
17
+ * nothing is ever fabricated. The panel reads only the `NetworkingAdapter`
18
+ * interface: the game's own, or the editor's observer of its Colyseus rooms
19
+ * (`services/game-network.ts`).
19
20
  *
20
21
  * Mounted as the `network` workspace utility (drawer tab beside
21
22
  * Console/Profiler, `core-utilities.tsx`), available only while a
@@ -37,6 +38,8 @@ import type {
37
38
  ConnectionState,
38
39
  NetConditioning,
39
40
  NetMessageEvent,
41
+ NetServerInspection,
42
+ NetTypeTraffic,
40
43
  NetworkingAdapter,
41
44
  } from '@volter/editor-project/adapter';
42
45
  import { memo, useEffect, useReducer, useRef, useState } from 'react';
@@ -100,8 +103,43 @@ function formatLeaf(value: unknown): string {
100
103
  * are NOT mounted: a fresh snapshot every poll re-renders only the visible
101
104
  * rows, never a collapsed subtree.
102
105
  */
103
- function StateTreeNode({ name, value, depth }: { name: string; value: unknown; depth: number }) {
106
+ type StateEdit = (path: readonly (string | number)[], value: unknown) => Promise<void>;
107
+ type StateDelete = (path: readonly (string | number)[]) => Promise<void>;
108
+ type StateType = (path: readonly (string | number)[]) => string | null;
109
+
110
+ /** A field's replicated type beside its name, dim (Godot's Replication panel's column). */
111
+ function TypeTag({ type }: { type: string | null | undefined }) {
112
+ if (!type) return null;
113
+ return (
114
+ <span data-testid="net-tree-type" style={{ color: themeVars.content.dim, marginLeft: 6 }}>
115
+ {type}
116
+ </span>
117
+ );
118
+ }
119
+
120
+ function StateTreeNode({
121
+ name,
122
+ value,
123
+ depth,
124
+ path = [],
125
+ edit,
126
+ remove,
127
+ typeOf,
128
+ }: {
129
+ name: string;
130
+ value: unknown;
131
+ depth: number;
132
+ path?: readonly string[];
133
+ /** The server's state edit (Monitor's), when the adapter offers one: leaves become editable. */
134
+ edit?: StateEdit | undefined;
135
+ /** The server's state delete (Monitor's), when offered: each key gets a remove control. */
136
+ remove?: StateDelete | undefined;
137
+ /** The schema's declared type for a path, when the adapter reads one. */
138
+ typeOf?: StateType | undefined;
139
+ }) {
104
140
  const [expanded, setExpanded] = useState(depth === 0);
141
+ const [draft, setDraft] = useState<string | null>(null);
142
+ const [failed, setFailed] = useState<string | null>(null);
105
143
  const rowStyle: React.CSSProperties = {
106
144
  ...MONO,
107
145
  paddingLeft: 8 + depth * 14,
@@ -110,11 +148,71 @@ function StateTreeNode({ name, value, depth }: { name: string; value: unknown; d
110
148
  };
111
149
 
112
150
  if (!isBranch(value)) {
151
+ const editable = edit && (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean');
152
+ const commit = (next: unknown) => {
153
+ setDraft(null);
154
+ edit?.(path, next).then(
155
+ () => setFailed(null),
156
+ (caught: unknown) => setFailed(caught instanceof Error ? caught.message : String(caught)),
157
+ );
158
+ };
159
+ const parse = (text: string): unknown => {
160
+ if (typeof value === 'number') {
161
+ const number = Number(text);
162
+ return Number.isFinite(number) ? number : value;
163
+ }
164
+ return text;
165
+ };
113
166
  return (
114
- <div style={rowStyle} data-testid="net-tree-leaf">
167
+ <div style={rowStyle} data-testid="net-tree-leaf" data-path={path.join('.')}>
115
168
  <span style={{ color: themeVars.content.primary }}>{name}</span>
116
169
  <span style={{ color: themeVars.content.muted }}>: </span>
117
- <span style={{ color: themeVars.content.primary }}>{formatLeaf(value)}</span>
170
+ {draft !== null ? (
171
+ <TextInput
172
+ data-testid="net-tree-edit"
173
+ autoFocus
174
+ value={draft}
175
+ onChange={(event) => setDraft(event.target.value)}
176
+ onKeyDown={(event) => {
177
+ if (event.key === 'Enter') commit(parse(draft));
178
+ if (event.key === 'Escape') setDraft(null);
179
+ }}
180
+ onBlur={() => setDraft(null)}
181
+ style={{ width: 120 }}
182
+ />
183
+ ) : (
184
+ <span
185
+ data-testid={editable ? 'net-tree-value' : undefined}
186
+ title={editable ? (typeof value === 'boolean' ? 'Click to toggle on the server' : 'Click to edit on the server') : undefined}
187
+ style={{ color: themeVars.content.primary, cursor: editable ? 'text' : undefined }}
188
+ onClick={
189
+ editable
190
+ ? () => (typeof value === 'boolean' ? commit(!value) : setDraft(String(value)))
191
+ : undefined
192
+ }
193
+ >
194
+ {formatLeaf(value)}
195
+ </span>
196
+ )}
197
+ <TypeTag type={path.length > 0 ? typeOf?.(path) : null} />
198
+ {remove && path.length > 0 ? (
199
+ <button
200
+ type="button"
201
+ data-testid="net-tree-delete"
202
+ title="Delete this key on the server"
203
+ aria-label={`Delete ${path.join('.')} on the server`}
204
+ onClick={() =>
205
+ remove(path).then(
206
+ () => setFailed(null),
207
+ (caught: unknown) => setFailed(caught instanceof Error ? caught.message : String(caught)),
208
+ )
209
+ }
210
+ style={{ marginLeft: 8, border: 0, background: 'transparent', color: themeVars.content.muted, cursor: 'pointer' }}
211
+ >
212
+ ×
213
+ </button>
214
+ ) : null}
215
+ {failed ? <span style={{ color: themeVars.semantic.danger }}> {failed}</span> : null}
118
216
  </div>
119
217
  );
120
218
  }
@@ -136,10 +234,39 @@ function StateTreeNode({ name, value, depth }: { name: string; value: unknown; d
136
234
  {' '}
137
235
  {Array.isArray(value) ? `[${entries.length}]` : `{${entries.length}}`}
138
236
  </span>
237
+ <TypeTag type={path.length > 0 ? typeOf?.(path) : null} />
238
+ {failed ? <span style={{ color: themeVars.semantic.danger }}> {failed}</span> : null}
239
+ {remove && path.length > 0 ? (
240
+ <button
241
+ type="button"
242
+ data-testid="net-tree-delete"
243
+ title="Delete this key on the server"
244
+ aria-label={`Delete ${path.join('.')} on the server`}
245
+ onClick={(event) => {
246
+ event.stopPropagation();
247
+ remove(path).then(
248
+ () => setFailed(null),
249
+ (caught: unknown) => setFailed(caught instanceof Error ? caught.message : String(caught)),
250
+ );
251
+ }}
252
+ style={{ marginLeft: 8, border: 0, background: 'transparent', color: themeVars.content.muted, cursor: 'pointer' }}
253
+ >
254
+ ×
255
+ </button>
256
+ ) : null}
139
257
  </div>
140
258
  {expanded &&
141
259
  entries.map(([key, child]) => (
142
- <StateTreeNode key={key} name={key} value={child} depth={depth + 1} />
260
+ <StateTreeNode
261
+ key={key}
262
+ name={key}
263
+ value={child}
264
+ depth={depth + 1}
265
+ path={depth === 0 ? [key] : [...path, key]}
266
+ edit={edit}
267
+ remove={remove}
268
+ typeOf={typeOf}
269
+ />
143
270
  ))}
144
271
  </div>
145
272
  );
@@ -252,6 +379,7 @@ function ConditionerControls({ adapter }: { adapter: NetworkingAdapter }) {
252
379
  const [cond, setCond] = useState<NetConditioning>(
253
380
  () => adapter.getConditioning?.() ?? { latencyMs: 0, jitterMs: 0, packetLoss: 0 },
254
381
  );
382
+ const limits = adapter.getConditioningLimits?.() ?? {};
255
383
  const apply = (next: NetConditioning) => {
256
384
  setCond(next);
257
385
  adapter.setConditioning?.(next);
@@ -281,13 +409,23 @@ function ConditionerControls({ adapter }: { adapter: NetworkingAdapter }) {
281
409
  testId="net-cond-jitter"
282
410
  onChange={(v) => apply({ ...cond, jitterMs: Math.max(0, v) })}
283
411
  />
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
- />
412
+ {limits.packetLoss ? (
413
+ <span
414
+ data-testid="net-cond-loss-absent"
415
+ title={limits.packetLoss}
416
+ style={{ fontSize: fontSizeVar.sm, color: themeVars.content.muted }}
417
+ >
418
+ loss: not simulated
419
+ </span>
420
+ ) : (
421
+ <NumberInput
422
+ label="loss %"
423
+ value={cond.packetLoss * 100}
424
+ step={5}
425
+ testId="net-cond-loss"
426
+ onChange={(v) => apply({ ...cond, packetLoss: Math.min(1, Math.max(0, v / 100)) })}
427
+ />
428
+ )}
291
429
  </div>
292
430
  );
293
431
  }
@@ -445,6 +583,8 @@ export function NetworkInspectorPanel() {
445
583
  const adapterRef = useRef<NetworkingAdapter | null>(null);
446
584
  const [paused, setPaused] = useState(false);
447
585
  const [filter, setFilter] = useState('');
586
+ // The one message draft: sent as this client, or from the server to one client or all.
587
+ const [draft, setDraft] = useState<MessageDraft>({ type: '', payload: '' });
448
588
 
449
589
  useEffect(() => {
450
590
  const syncAdapter = () => {
@@ -550,6 +690,17 @@ export function NetworkInspectorPanel() {
550
690
  </AbsentNote>
551
691
  )}
552
692
 
693
+ {caps.server ? (
694
+ <ServerView
695
+ adapter={adapter}
696
+ ownSession={view.roomInfo?.sessionId ?? null}
697
+ ownRoom={view.roomInfo?.roomId ?? null}
698
+ draft={draft}
699
+ />
700
+ ) : null}
701
+ {caps.traffic ? <TrafficTable rows={adapter.getTrafficByType?.() ?? []} /> : null}
702
+ {caps.send ? <SendControls adapter={adapter} ping={caps.ping} draft={draft} setDraft={setDraft} /> : null}
703
+
553
704
  {/* Body: state tree | message log */}
554
705
  <div style={{ flex: 1, minHeight: 0, display: 'flex' }}>
555
706
  <div
@@ -564,7 +715,14 @@ export function NetworkInspectorPanel() {
564
715
  >
565
716
  {caps.stateTree ? (
566
717
  snapshot != null ? (
567
- <StateTreeNode name="state" value={snapshot} depth={0} />
718
+ <StateTreeNode
719
+ name="state"
720
+ value={snapshot}
721
+ depth={0}
722
+ edit={adapter.editServerState ? (path, next) => adapter.editServerState!(path, next) : undefined}
723
+ remove={adapter.deleteServerState ? (path) => adapter.deleteServerState!(path) : undefined}
724
+ typeOf={adapter.stateFieldType ? (path) => adapter.stateFieldType!(path) : undefined}
725
+ />
568
726
  ) : (
569
727
  <AbsentNote>Not connected — no replicated state to show.</AbsentNote>
570
728
  )
@@ -606,7 +764,9 @@ export function NetworkInspectorPanel() {
606
764
  variant="ghost"
607
765
  data-testid="net-log-clear"
608
766
  onClick={() => {
767
+ // Godot's Clear resets its tables too: the log and the traffic totals.
609
768
  logRef.current.clear();
769
+ adapter.clearTraffic?.();
610
770
  force();
611
771
  }}
612
772
  >
@@ -642,3 +802,278 @@ export function NetworkInspectorPanel() {
642
802
  </div>
643
803
  );
644
804
  }
805
+
806
+ /**
807
+ * Traffic per message type, both directions — Godot's network profiler reads a game's RPCs and
808
+ * its synchronizers as two tables of counts and sizes; a Colyseus room's are its messages and its
809
+ * state sync, which arrive here as rows of one table (`state`, `patch`, and each message type).
810
+ */
811
+ function TrafficTable({ rows }: { rows: readonly NetTypeTraffic[] }) {
812
+ const cell = { padding: `0 ${spaceVar[3]}`, textAlign: 'right' as const, whiteSpace: 'nowrap' as const };
813
+ const bytes = (value: number) => (value >= 1024 ? `${(value / 1024).toFixed(1)} KiB` : `${value} B`);
814
+ return (
815
+ <div
816
+ data-testid="net-traffic"
817
+ style={{ maxHeight: 132, overflow: 'auto', borderBottom: `1px solid ${themeVars.boundary.default}` }}
818
+ >
819
+ {rows.length === 0 ? (
820
+ <AbsentNote>No traffic yet.</AbsentNote>
821
+ ) : (
822
+ <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: fontSizeVar.sm, ...MONO }}>
823
+ <thead>
824
+ <tr style={{ color: themeVars.content.muted }}>
825
+ <th style={{ ...cell, textAlign: 'left' }}>Type</th>
826
+ <th style={cell}>In</th>
827
+ <th style={cell}>Bytes in</th>
828
+ <th style={cell}>Out</th>
829
+ <th style={cell}>Bytes out</th>
830
+ </tr>
831
+ </thead>
832
+ <tbody>
833
+ {rows.map((row) => (
834
+ <tr key={row.type} data-testid="net-traffic-row">
835
+ <td style={{ ...cell, textAlign: 'left' }}>{row.type}</td>
836
+ <td style={cell}>{row.countIn || '-'}</td>
837
+ <td style={cell}>{row.countIn ? bytes(row.bytesIn) : '-'}</td>
838
+ <td style={cell}>{row.countOut || '-'}</td>
839
+ <td style={cell}>{row.countOut ? bytes(row.bytesOut) : '-'}</td>
840
+ </tr>
841
+ ))}
842
+ </tbody>
843
+ </table>
844
+ )}
845
+ </div>
846
+ );
847
+ }
848
+
849
+ interface MessageDraft {
850
+ readonly type: string;
851
+ readonly payload: string;
852
+ }
853
+
854
+ /** The draft as a message: its type and its parsed JSON payload (none when empty). */
855
+ function draftMessage(draft: MessageDraft): { type: string; payload: unknown } {
856
+ if (draft.type.trim() === '') throw new Error('Name the message type first.');
857
+ return { type: draft.type, payload: draft.payload.trim() === '' ? undefined : (JSON.parse(draft.payload) as unknown) };
858
+ }
859
+
860
+ /**
861
+ * A message type and a JSON payload (Colyseus Monitor's Send dialog), sent into the room as this
862
+ * client so a server handler can be exercised without writing game code for it; the Server section
863
+ * sends the same draft from the server.
864
+ */
865
+ function SendControls({
866
+ adapter,
867
+ ping,
868
+ draft,
869
+ setDraft,
870
+ }: {
871
+ adapter: NetworkingAdapter;
872
+ ping: boolean;
873
+ draft: MessageDraft;
874
+ setDraft: (next: MessageDraft) => void;
875
+ }) {
876
+ const [rtt, setRtt] = useState<string | null>(null);
877
+ const measure = () => {
878
+ setRtt('…');
879
+ adapter.ping?.().then(
880
+ (ms) => setRtt(`${ms} ms`),
881
+ (caught: unknown) => setRtt(caught instanceof Error ? caught.message : String(caught)),
882
+ );
883
+ };
884
+ const [error, setError] = useState<string | null>(null);
885
+ const send = () => {
886
+ try {
887
+ const message = draftMessage(draft);
888
+ adapter.sendMessage?.(message.type, message.payload);
889
+ setError(null);
890
+ } catch (caught) {
891
+ setError(caught instanceof Error ? caught.message : String(caught));
892
+ }
893
+ };
894
+ return (
895
+ <div
896
+ data-testid="net-send"
897
+ style={{ display: 'flex', gap: spaceVar[2], alignItems: 'center', padding: spaceVar[2] }}
898
+ >
899
+ <TextInput
900
+ data-testid="net-send-type"
901
+ placeholder="Message type"
902
+ value={draft.type}
903
+ onChange={(event) => setDraft({ ...draft, type: event.target.value })}
904
+ style={{ width: 140 }}
905
+ />
906
+ <TextInput
907
+ data-testid="net-send-payload"
908
+ placeholder='Payload (JSON), e.g. {"x": 1}'
909
+ value={draft.payload}
910
+ onChange={(event) => setDraft({ ...draft, payload: event.target.value })}
911
+ style={{ flex: 1, minWidth: 80 }}
912
+ />
913
+ <Button type="button" variant="ghost" data-testid="net-send-button" disabled={draft.type.trim() === ''} onClick={send}>
914
+ Send as this client
915
+ </Button>
916
+ {ping ? (
917
+ <>
918
+ <Button type="button" variant="ghost" data-testid="net-ping" onClick={measure}>
919
+ Ping
920
+ </Button>
921
+ {rtt ? (
922
+ <span data-testid="net-rtt" style={{ fontSize: fontSizeVar.sm, ...MONO }}>
923
+ {rtt}
924
+ </span>
925
+ ) : null}
926
+ </>
927
+ ) : null}
928
+ {error ? (
929
+ <span data-testid="net-send-error" style={{ color: themeVars.semantic.danger, fontSize: fontSizeVar.sm }}>
930
+ {error}
931
+ </span>
932
+ ) : null}
933
+ </div>
934
+ );
935
+ }
936
+
937
+ /**
938
+ * The room server's side — Colyseus Monitor's room list and room view, read from Monitor's own API
939
+ * on the server: every room (Inspect one), the machine's CPU and memory, and the inspected room's
940
+ * clients. Its acts are Monitor's: Broadcast and Dispose for the room, Send and Disconnect for a
941
+ * client, the message being the Send row's draft.
942
+ */
943
+ function ServerView({
944
+ adapter,
945
+ ownSession,
946
+ ownRoom,
947
+ draft,
948
+ }: {
949
+ adapter: NetworkingAdapter;
950
+ ownSession: string | null;
951
+ ownRoom: string | null;
952
+ draft: MessageDraft;
953
+ }) {
954
+ const [inspection, setInspection] = useState<NetServerInspection | null | undefined>(undefined);
955
+ const [inspected, setInspected] = useState<string | undefined>(undefined);
956
+ const [error, setError] = useState<string | null>(null);
957
+ useEffect(() => {
958
+ let live = true;
959
+ const read = () => {
960
+ void adapter.inspectServer?.(inspected).then(
961
+ (next) => {
962
+ if (live) setInspection(next);
963
+ },
964
+ (caught: unknown) => {
965
+ if (live) setError(caught instanceof Error ? caught.message : String(caught));
966
+ },
967
+ );
968
+ };
969
+ read();
970
+ // Monitor refreshes its room view every 5 s; this reads the server every 2.
971
+ const id = setInterval(read, 2000);
972
+ return () => {
973
+ live = false;
974
+ clearInterval(id);
975
+ };
976
+ }, [adapter, inspected]);
977
+ const act = (run: () => Promise<void> | undefined) => {
978
+ try {
979
+ run()?.then(
980
+ () => setError(null),
981
+ (caught: unknown) => setError(caught instanceof Error ? caught.message : String(caught)),
982
+ );
983
+ } catch (caught) {
984
+ setError(caught instanceof Error ? caught.message : String(caught));
985
+ }
986
+ };
987
+ const seconds = (ms: number) => `${Math.round(ms / 1000)} s`;
988
+ const cell = { padding: `0 ${spaceVar[3]}`, whiteSpace: 'nowrap' as const };
989
+ if (inspection === undefined) return null;
990
+ if (inspection === null) {
991
+ return (
992
+ <AbsentNote testId="net-server-absent">
993
+ The room server serves no Monitor view (its `server` configuration sets VGAI_ROOM_MONITOR=1).
994
+ </AbsentNote>
995
+ );
996
+ }
997
+ const roomId = inspection.room?.roomId;
998
+ const actButton = (testId: string, label: string, run: () => Promise<void> | undefined) => (
999
+ <Button type="button" variant="ghost" data-testid={testId} onClick={() => act(run)}>
1000
+ {label}
1001
+ </Button>
1002
+ );
1003
+ return (
1004
+ <div
1005
+ data-testid="net-server"
1006
+ style={{ padding: `${spaceVar[2]} 0`, borderBottom: `1px solid ${themeVars.boundary.default}`, fontSize: fontSizeVar.sm }}
1007
+ >
1008
+ <div data-testid="net-server-summary" style={{ padding: `0 ${spaceVar[3]}`, color: themeVars.content.muted }}>
1009
+ Server · {inspection.rooms.length} room{inspection.rooms.length === 1 ? '' : 's'} · {inspection.connections}{' '}
1010
+ connection{inspection.connections === 1 ? '' : 's'}
1011
+ {inspection.cpuPercent !== null ? ` · CPU ${inspection.cpuPercent.toFixed(0)}%` : ''}
1012
+ {inspection.memory ? ` · memory ${Math.round(inspection.memory.usedMb)} / ${Math.round(inspection.memory.totalMb)} MB` : ''}
1013
+ {inspection.room ? ` · state ${inspection.room.stateBytes} B` : ''}
1014
+ </div>
1015
+ <table style={{ borderCollapse: 'collapse', ...MONO }}>
1016
+ <tbody>
1017
+ {inspection.rooms.map((room) => (
1018
+ <tr key={room.roomId} data-testid="net-server-room" data-inspected={room.roomId === roomId || undefined}>
1019
+ <td style={cell}>{room.name}</td>
1020
+ <td style={cell}>{room.roomId}</td>
1021
+ <td style={cell}>
1022
+ {room.clients}
1023
+ {room.maxClients !== null ? ` / ${room.maxClients}` : ''} clients
1024
+ </td>
1025
+ <td style={cell}>{room.locked ? 'locked' : 'open'}</td>
1026
+ <td style={cell}>{seconds(room.elapsedMs)}</td>
1027
+ <td style={cell}>
1028
+ {room.roomId === roomId ? (
1029
+ <>
1030
+ {actButton('net-server-broadcast', 'Broadcast', () => {
1031
+ const message = draftMessage(draft);
1032
+ return adapter.broadcast?.(message.type, message.payload, room.roomId);
1033
+ })}
1034
+ {actButton('net-server-dispose', 'Dispose', () => adapter.disposeRoom?.(room.roomId))}
1035
+ </>
1036
+ ) : (
1037
+ <Button type="button" variant="ghost" data-testid="net-server-inspect" onClick={() => setInspected(room.roomId)}>
1038
+ Inspect
1039
+ </Button>
1040
+ )}
1041
+ </td>
1042
+ </tr>
1043
+ ))}
1044
+ {inspection.room?.clients.map((client) => (
1045
+ <tr key={client.sessionId} data-testid="net-server-client">
1046
+ <td style={cell}>client</td>
1047
+ <td style={cell}>
1048
+ {client.sessionId}
1049
+ {client.sessionId === ownSession ? ' (this client)' : ''}
1050
+ </td>
1051
+ <td style={cell}>{seconds(client.elapsedMs)}</td>
1052
+ <td style={cell} colSpan={3}>
1053
+ {actButton('net-server-send', 'Send', () => {
1054
+ const message = draftMessage(draft);
1055
+ return adapter.sendToClient?.(client.sessionId, message.type, message.payload, roomId);
1056
+ })}
1057
+ {actButton('net-server-disconnect', 'Disconnect', () => adapter.disconnectClient?.(client.sessionId, roomId))}
1058
+ </td>
1059
+ </tr>
1060
+ ))}
1061
+ </tbody>
1062
+ </table>
1063
+ {inspection.room && inspection.room.roomId !== ownRoom && inspection.room.state !== undefined ? (
1064
+ // ANOTHER room's state, as Monitor's Inspect opens it: read from the server, and edited
1065
+ // and deleted in THAT room. The tree below the section stays this client's own room.
1066
+ <div data-testid="net-server-room-state" style={{ padding: `${spaceVar[2]} 0` }}>
1067
+ <StateTreeNode
1068
+ name={`room ${inspection.room.roomId}`}
1069
+ value={inspection.room.state}
1070
+ depth={0}
1071
+ edit={adapter.editServerState ? (path, next) => adapter.editServerState!(path, next, roomId) : undefined}
1072
+ remove={adapter.deleteServerState ? (path) => adapter.deleteServerState!(path, roomId) : undefined}
1073
+ />
1074
+ </div>
1075
+ ) : null}
1076
+ {error ? <AbsentNote>{error}</AbsentNote> : null}
1077
+ </div>
1078
+ );
1079
+ }
@@ -9,8 +9,8 @@
9
9
  * pause/filter, the sparkline sample history, and the polyline geometry —
10
10
  * lives here.
11
11
  *
12
- * Editor code never imports Colyseus (net-import-ban) — only the
13
- * `NetworkingAdapter` interface.
12
+ * The inspector reads only the `NetworkingAdapter` interface; decoding a
13
+ * game's Colyseus rooms is the observer's (`services/game-network.ts`).
14
14
  */
15
15
 
16
16
  import type {
@@ -82,6 +82,14 @@ export interface NetworkCapabilityMap {
82
82
  /** The adapter can SET the local identity (`setPlayerIdentity`) — the
83
83
  * inspector shows an EDITABLE name field. Implies `identity`. */
84
84
  identitySettable: boolean;
85
+ /** Traffic per message type (`getTrafficByType`). */
86
+ traffic: boolean;
87
+ /** Sending a message into the room as this client (`sendMessage`). */
88
+ send: boolean;
89
+ /** Measuring a round trip (`ping`). */
90
+ ping: boolean;
91
+ /** The room server's own view (`inspectServer`). */
92
+ server: boolean;
85
93
  }
86
94
 
87
95
  export function deriveNetworkCapabilities(
@@ -103,6 +111,10 @@ export function deriveNetworkCapabilities(
103
111
  stats: typeof adapter.getReplicationStats === 'function',
104
112
  identity: typeof adapter.getPlayerIdentity === 'function',
105
113
  identitySettable: typeof adapter.setPlayerIdentity === 'function',
114
+ traffic: typeof adapter.getTrafficByType === 'function',
115
+ send: typeof adapter.sendMessage === 'function',
116
+ ping: typeof adapter.ping === 'function',
117
+ server: typeof adapter.inspectServer === 'function',
106
118
  };
107
119
  }
108
120
 
@@ -1,4 +1,4 @@
1
- import type { LogEntry } from '@volter/editor-core/editor-api';
1
+ import type { LogEntry } from '@volter/editor-sdk/kit/editor-api';
2
2
  import type { TickStampedEvent } from '@volter/editor-project/adapter/system-adapter';
3
3
 
4
4
  /** Convert newly observed game-authored debug events into persistent JSONL entries. */