@volter/editor-game 0.5.67 → 0.5.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +7 -7
- package/contributions/autoplay.service.ts +1 -1
- package/contributions/build-player.document.tsx +95 -0
- package/contributions/build-profiles.document.tsx +1 -1
- package/contributions/build-progress.status.tsx +2 -2
- package/contributions/build.header.tsx +1 -1
- package/contributions/connection.status.tsx +1 -1
- package/contributions/coverage.service.ts +2 -2
- package/contributions/game-eval.command.ts +6 -6
- package/contributions/gameplay.command.ts +2 -2
- package/contributions/generations.status.tsx +1 -1
- package/contributions/ingest.service.ts +2 -2
- package/contributions/play.command.ts +6 -6
- package/contributions/react/react-inspector.service.ts +1 -1
- package/package.json +9 -8
- package/serving/index.ts +1 -1
- package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
- package/src/asset-budget/asset-budget-model.ts +14 -14
- package/src/asset-budget/optimize-apply.ts +8 -8
- package/src/bridge/call.ts +3 -3
- package/src/bridge/dispatch.ts +12 -12
- package/src/bridge/screenshot.ts +6 -6
- package/src/build/BuildProfilesPanel.tsx +19 -8
- package/src/build/build-session.ts +35 -0
- package/src/canvas/canvas-board/CanvasBoardDocument.tsx +78 -5
- package/src/canvas/canvas-board/canvas-component-board.ts +1 -1
- package/src/canvas/canvas-design-mount.ts +1 -1
- package/src/coverage/live-project-verbs.ts +3 -3
- package/src/coverage/native-system-coverage.ts +3 -3
- package/src/coverage/root-coverage.ts +2 -2
- package/src/coverage/session-coverage.ts +2 -2
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +2 -2
- package/src/game-document/DevicePresetPicker.tsx +6 -6
- package/src/game-document/GameCaptureFrameButton.tsx +1 -1
- package/src/game-document/GameDocument.tsx +2 -2
- package/src/game-document/GamePanel.tsx +9 -9
- package/src/game-document/InstanceInspectorPicker.tsx +1 -1
- package/src/game-document/device-preview.ts +4 -4
- package/src/game-document/physics-debug.ts +2 -2
- package/src/generation/GenerationActivity.tsx +22 -22
- package/src/generation/GenerationGallery.css +79 -79
- package/src/generation/generation-documents.tsx +25 -25
- package/src/generation/generation-presentation.ts +1 -1
- package/src/host/adapter-reach.ts +1 -1
- package/src/host/adapter-runtime-bindings.ts +5 -5
- package/src/host/after-paint.ts +3 -3
- package/src/host/api/configurations.ts +1 -1
- package/src/host/authoring/canvas-runtime-recognition.ts +2 -2
- package/src/host/authoring/contract-hierarchy-authoring.ts +6 -6
- package/src/host/authoring/contract-scenes-stories.ts +5 -5
- package/src/host/authoring/ingest-data-writer.ts +1 -1
- package/src/host/authoring/ingest-source-persistence.ts +2 -2
- package/src/host/authoring/mount-isolated-pixi-screen.ts +1 -1
- package/src/host/authoring/pixi-authoring-adapter.ts +206 -28
- package/src/host/authoring/pixi-creatable-kinds.ts +80 -0
- package/src/host/authoring/pixi-live-write-target.ts +6 -5
- package/src/host/authoring/pixi-source-write-target.ts +118 -20
- package/src/host/authoring/pixi-structure-history.ts +3 -3
- package/src/host/binding-resolver.ts +6 -6
- package/src/host/browser-transpile.ts +10 -10
- package/src/host/canvas-preview-frames.ts +1 -1
- package/src/host/components/CameraAuthoringOverlay.tsx +14 -14
- package/src/host/components/HeaderTelemetry.tsx +11 -11
- package/src/host/components/PixiIsolationSceneContent.tsx +3 -3
- package/src/host/components/ResolutionPicker.tsx +4 -4
- package/src/host/components/scene-document.tsx +3 -3
- package/src/host/components/world-root-stage-binding.tsx +1 -1
- package/src/host/components/world-root-stage.ts +1 -1
- package/src/host/coverage/authoring-read-probe.ts +1 -1
- package/src/host/coverage/capability-coverage.ts +22 -22
- package/src/host/coverage/game-contract-seam-evidence.ts +8 -8
- package/src/host/coverage/project-verb-coverage.ts +1 -1
- package/src/host/coverage/system-adapter-coverage.ts +19 -6
- package/src/host/game-location-guard.ts +3 -3
- package/src/host/game-module-access.ts +6 -6
- package/src/host/game-realm-page.ts +3 -3
- package/src/host/game-realm-reclaim.ts +1 -1
- package/src/host/gameplay-recording.ts +5 -5
- package/src/host/gated-globals.ts +13 -13
- package/src/host/ingest/registry.ts +14 -14
- package/src/host/play-control-hook.ts +1 -1
- package/src/host/playwright-shim.ts +2 -2
- package/src/host/projection/pixi.ts +2 -2
- package/src/host/react-mount-runtime.ts +1 -1
- package/src/host/roots/canvas-root.tsx +13 -1
- package/src/host/roots/r3f-root.tsx +1 -1
- package/src/host/scene-view-drawability.ts +1 -1
- package/src/host/sdk/tools.ts +1 -1
- package/src/host/served-bundle-runtime-modules.ts +2 -0
- package/src/host/staged-projects.ts +1 -1
- package/src/host/stories/story-pixi-preview.ts +1 -1
- package/src/host/stories/story-three-preview.ts +1 -1
- package/src/host/three-ingest-runtime.ts +1 -1
- package/src/ingest/active-ingest.ts +1 -1
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
- package/src/ingest/authoring/ingest-root-adapter.ts +11 -11
- package/src/ingest/capture-wait-report.ts +4 -4
- package/src/ingest/discovery-public-ingest.ts +4 -4
- package/src/ingest/dom-stub-mark.ts +1 -1
- package/src/ingest/entry-load.ts +1 -1
- package/src/ingest/game-contract-realm.ts +3 -3
- package/src/ingest/game-pointer-lock.ts +1 -1
- package/src/ingest/ingest-canvas-scene-document.tsx +3 -3
- package/src/ingest/ingest-evidence-hook.ts +6 -6
- package/src/ingest/ingest-frame-snapshot.ts +2 -2
- package/src/ingest/ingest-play-commands.ts +1 -1
- package/src/ingest/ingest-play-control.ts +11 -11
- package/src/ingest/ingest-render-debug.ts +5 -5
- package/src/ingest/ingest-siblings.ts +3 -3
- package/src/ingest/module-mode.ts +2 -2
- package/src/ingest/mount-canvas-ingest-root.ts +10 -10
- package/src/ingest/mount-coverage.ts +3 -3
- package/src/ingest/mount-dom-ingest-root.ts +7 -7
- package/src/ingest/mount-ingest-root.ts +10 -10
- package/src/ingest/mount-three-ingest-root.ts +5 -5
- package/src/ingest/resolve-three.ts +2 -2
- package/src/ingest/served-bundle.ts +6 -6
- package/src/ingest/served-html-boot.ts +8 -8
- package/src/ingest/surface-dom.ts +9 -9
- package/src/ingest/surface-three.ts +1 -1
- package/src/ingest/types.ts +1 -1
- package/src/ingest/unmount-ingest-root.ts +3 -3
- package/src/network/NetworkInspectorPanel.tsx +521 -54
- package/src/network/network-inspector-model.ts +6 -0
- package/src/play/play-boot-stall.ts +1 -1
- package/src/play/play-mode.ts +31 -30
- package/src/play/play-recording.ts +12 -12
- package/src/play-bar/PlayBar.tsx +8 -8
- package/src/play-bar/PlayerCountPicker.tsx +3 -3
- package/src/profiler/FrameDebuggerPanel.tsx +2 -2
- package/src/profiler/PerformancePanel.tsx +4 -4
- package/src/react/design-time-react-mount.ts +5 -5
- package/src/react/dom-authoring-adapter.ts +3 -3
- package/src/react/react-inspector-section.tsx +2 -2
- package/src/react/react-world-authoring-adapter.ts +20 -20
- package/src/react/story-documents/story-documents.tsx +4 -4
- package/src/react/ui-component-board.ts +1 -1
- package/src/runtime/adapter/audio-meter.ts +1 -1
- package/src/runtime/adapter/ingest/contract-debug-adapter.ts +4 -4
- package/src/runtime/adapter/ingest/contract-system-adapters.ts +9 -9
- package/src/runtime/adapter/ingest/merge-debug-adapters.ts +2 -2
- package/src/runtime/adapter/ingest/observation-debug-adapter.ts +1 -1
- package/src/runtime/adapter/native-debug-module.ts +5 -5
- package/src/runtime/audio/pose-guard.ts +1 -1
- package/src/runtime/core/game-scoped-slot.ts +1 -1
- package/src/runtime/create-runtime.ts +15 -15
- package/src/runtime/debug-bridge.ts +25 -25
- package/src/runtime/debug-registry.ts +17 -17
- package/src/runtime/dev/instruments.ts +1 -1
- package/src/runtime/dev/performance-profiler.ts +2 -2
- package/src/runtime/dev/register-render-vitals.ts +2 -2
- package/src/runtime/dev/static-batch-advisor.ts +3 -3
- package/src/runtime/dev-build.ts +3 -3
- package/src/runtime/game.ts +5 -5
- package/src/runtime/input-router.ts +24 -11
- package/src/runtime/mount-manifest.ts +9 -9
- package/src/runtime/pixi/authoring.ts +1 -1
- package/src/runtime/pixi/ingest.ts +1 -1
- package/src/runtime/presentation.ts +1 -1
- package/src/runtime/render-control.ts +21 -21
- package/src/runtime/render-seed.ts +3 -3
- package/src/runtime/run-ticks-settled.ts +1 -1
- package/src/services/game-audio.ts +3 -3
- package/src/services/game-network.ts +125 -15
- package/src/three/authoring/model-asset-inspector-section.css +15 -15
- package/src/three/authoring/model-asset-inspector-section.tsx +7 -7
- package/src/three/authoring/r3f-design-session.ts +10 -10
- package/src/three/authoring/r3f-source-authoring-adapter.ts +5 -5
- package/src/three/authoring/three-authoring-adapter.ts +5 -4
- package/src/three/authoring/typed-three-inspector.ts +1 -1
- package/src/three/story-documents/three-story-documents.tsx +3 -3
- package/src/three/three-board/board-scene.ts +9 -9
- package/src/three/three-board/three-component-board.ts +1 -1
|
@@ -25,8 +25,11 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { editorHost } from '@volter/editor-sdk/host';
|
|
28
|
+
import { NETWORK_AUTOSTART_SECTION } from '../services/game-network';
|
|
28
29
|
import {
|
|
29
30
|
Button,
|
|
31
|
+
Checkbox,
|
|
32
|
+
TextArea,
|
|
30
33
|
DisclosureIcon,
|
|
31
34
|
fontSizeVar,
|
|
32
35
|
NumberInput,
|
|
@@ -37,6 +40,7 @@ import {
|
|
|
37
40
|
import type {
|
|
38
41
|
ConnectionState,
|
|
39
42
|
NetConditioning,
|
|
43
|
+
NetEntityTraffic,
|
|
40
44
|
NetMessageEvent,
|
|
41
45
|
NetServerInspection,
|
|
42
46
|
NetTypeTraffic,
|
|
@@ -574,6 +578,9 @@ function NetworkHeader({
|
|
|
574
578
|
);
|
|
575
579
|
}
|
|
576
580
|
|
|
581
|
+
const AUTOSTART_SECTION = NETWORK_AUTOSTART_SECTION;
|
|
582
|
+
const DRAFT_SECTION = 'networkMessageDraft';
|
|
583
|
+
|
|
577
584
|
// --- The panel ------------------------------------------------------------
|
|
578
585
|
|
|
579
586
|
export function NetworkInspectorPanel() {
|
|
@@ -583,8 +590,21 @@ export function NetworkInspectorPanel() {
|
|
|
583
590
|
const adapterRef = useRef<NetworkingAdapter | null>(null);
|
|
584
591
|
const [paused, setPaused] = useState(false);
|
|
585
592
|
const [filter, setFilter] = useState('');
|
|
593
|
+
// Godot's Autostart: whether a game that starts running starts recording (kept per checkout).
|
|
594
|
+
const [autostart, setAutostart] = useState(
|
|
595
|
+
// Off unless chosen, as Godot's is: a run records once Start is pressed.
|
|
596
|
+
() => editorHost().projectLocalState.read<boolean>(AUTOSTART_SECTION) ?? false,
|
|
597
|
+
);
|
|
586
598
|
// The one message draft: sent as this client, or from the server to one client or all.
|
|
587
|
-
|
|
599
|
+
// Kept per checkout, as Monitor keeps its Send draft in its browser storage.
|
|
600
|
+
const [draft, setDraftState] = useState<MessageDraft>(
|
|
601
|
+
// Monitor's own first draft: `message_type` and an empty object.
|
|
602
|
+
() => editorHost().projectLocalState.read<MessageDraft>(DRAFT_SECTION) ?? { type: 'message_type', payload: '{}' },
|
|
603
|
+
);
|
|
604
|
+
const setDraft = (next: MessageDraft): void => {
|
|
605
|
+
editorHost().projectLocalState.write(DRAFT_SECTION, next);
|
|
606
|
+
setDraftState(next);
|
|
607
|
+
};
|
|
588
608
|
|
|
589
609
|
useEffect(() => {
|
|
590
610
|
const syncAdapter = () => {
|
|
@@ -660,7 +680,7 @@ export function NetworkInspectorPanel() {
|
|
|
660
680
|
data-testid="network-inspector"
|
|
661
681
|
// §2.31 P2 amendment: text-dense output region reads over a local
|
|
662
682
|
// frosted layer (inert for non-frost themes).
|
|
663
|
-
className="
|
|
683
|
+
className="volter-content-frost"
|
|
664
684
|
style={{
|
|
665
685
|
flex: 1,
|
|
666
686
|
minHeight: 0,
|
|
@@ -696,9 +716,68 @@ export function NetworkInspectorPanel() {
|
|
|
696
716
|
ownSession={view.roomInfo?.sessionId ?? null}
|
|
697
717
|
ownRoom={view.roomInfo?.roomId ?? null}
|
|
698
718
|
draft={draft}
|
|
719
|
+
setDraft={setDraft}
|
|
699
720
|
/>
|
|
700
721
|
) : null}
|
|
722
|
+
{caps.recording || caps.traffic ? (
|
|
723
|
+
// Godot's profiler bar, above the tables it governs: Start/Stop, Clear, Autostart.
|
|
724
|
+
<div
|
|
725
|
+
data-testid="net-profiler-bar"
|
|
726
|
+
style={{
|
|
727
|
+
display: 'flex',
|
|
728
|
+
gap: spaceVar[3],
|
|
729
|
+
alignItems: 'center',
|
|
730
|
+
padding: spaceVar[2],
|
|
731
|
+
borderBottom: `1px solid ${themeVars.boundary.default}`,
|
|
732
|
+
}}
|
|
733
|
+
>
|
|
734
|
+
{caps.recording ? (
|
|
735
|
+
<>
|
|
736
|
+
<Button
|
|
737
|
+
type="button"
|
|
738
|
+
variant="ghost"
|
|
739
|
+
data-testid="net-recording"
|
|
740
|
+
aria-pressed={adapter.isRecording?.() === true}
|
|
741
|
+
onClick={() => {
|
|
742
|
+
adapter.setRecording?.(!adapter.isRecording?.());
|
|
743
|
+
force();
|
|
744
|
+
}}
|
|
745
|
+
>
|
|
746
|
+
{adapter.isRecording?.() ? 'Stop' : 'Start'}
|
|
747
|
+
</Button>
|
|
748
|
+
</>
|
|
749
|
+
) : null}
|
|
750
|
+
<Button
|
|
751
|
+
type="button"
|
|
752
|
+
variant="ghost"
|
|
753
|
+
data-testid="net-log-clear"
|
|
754
|
+
onClick={() => {
|
|
755
|
+
// Godot's Clear resets its tables; the log clears with them.
|
|
756
|
+
logRef.current.clear();
|
|
757
|
+
adapter.clearTraffic?.();
|
|
758
|
+
force();
|
|
759
|
+
}}
|
|
760
|
+
>
|
|
761
|
+
Clear
|
|
762
|
+
</Button>
|
|
763
|
+
{caps.recording ? (
|
|
764
|
+
<label style={{ display: 'flex', gap: spaceVar[1], alignItems: 'center', whiteSpace: 'nowrap' }}>
|
|
765
|
+
<Checkbox
|
|
766
|
+
data-testid="net-autostart"
|
|
767
|
+
checked={autostart}
|
|
768
|
+
onChange={(event) => {
|
|
769
|
+
const on = event.currentTarget.checked;
|
|
770
|
+
editorHost().projectLocalState.write(AUTOSTART_SECTION, on);
|
|
771
|
+
setAutostart(on);
|
|
772
|
+
}}
|
|
773
|
+
/>
|
|
774
|
+
Autostart
|
|
775
|
+
</label>
|
|
776
|
+
) : null}
|
|
777
|
+
</div>
|
|
778
|
+
) : null}
|
|
701
779
|
{caps.traffic ? <TrafficTable rows={adapter.getTrafficByType?.() ?? []} /> : null}
|
|
780
|
+
{caps.entityTraffic ? <EntityTrafficTable rows={adapter.getTrafficByEntity?.() ?? []} /> : null}
|
|
702
781
|
{caps.send ? <SendControls adapter={adapter} ping={caps.ping} draft={draft} setDraft={setDraft} /> : null}
|
|
703
782
|
|
|
704
783
|
{/* Body: state tree | message log */}
|
|
@@ -747,31 +826,20 @@ export function NetworkInspectorPanel() {
|
|
|
747
826
|
padding: spaceVar[2],
|
|
748
827
|
}}
|
|
749
828
|
>
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
variant="ghost"
|
|
765
|
-
data-testid="net-log-clear"
|
|
766
|
-
onClick={() => {
|
|
767
|
-
// Godot's Clear resets its tables too: the log and the traffic totals.
|
|
768
|
-
logRef.current.clear();
|
|
769
|
-
adapter.clearTraffic?.();
|
|
770
|
-
force();
|
|
771
|
-
}}
|
|
772
|
-
>
|
|
773
|
-
Clear
|
|
774
|
-
</Button>
|
|
829
|
+
{caps.recording ? null : (
|
|
830
|
+
<Button
|
|
831
|
+
type="button"
|
|
832
|
+
variant="ghost"
|
|
833
|
+
data-testid="net-log-pause"
|
|
834
|
+
onClick={() => {
|
|
835
|
+
const next = !paused;
|
|
836
|
+
logRef.current.paused = next;
|
|
837
|
+
setPaused(next);
|
|
838
|
+
}}
|
|
839
|
+
>
|
|
840
|
+
{paused ? 'Resume' : 'Pause'}
|
|
841
|
+
</Button>
|
|
842
|
+
)}
|
|
775
843
|
<TextInput
|
|
776
844
|
data-testid="net-log-filter"
|
|
777
845
|
placeholder="Filter by type"
|
|
@@ -808,6 +876,277 @@ export function NetworkInspectorPanel() {
|
|
|
808
876
|
* its synchronizers as two tables of counts and sizes; a Colyseus room's are its messages and its
|
|
809
877
|
* state sync, which arrive here as rows of one table (`state`, `patch`, and each message type).
|
|
810
878
|
*/
|
|
879
|
+
/** Godot's synchronizer table, for a Colyseus room: each replicated entity's incoming syncs (the
|
|
880
|
+
* patches that changed it), the field changes they carried, and the size range of those patches. */
|
|
881
|
+
function EntityTrafficTable({ rows }: { rows: readonly NetEntityTraffic[] }) {
|
|
882
|
+
const cell = { padding: `0 ${spaceVar[3]}`, textAlign: 'right' as const, whiteSpace: 'nowrap' as const };
|
|
883
|
+
return (
|
|
884
|
+
<div
|
|
885
|
+
data-testid="net-entity-traffic"
|
|
886
|
+
style={{ maxHeight: 132, overflow: 'auto', borderBottom: `1px solid ${themeVars.boundary.default}` }}
|
|
887
|
+
>
|
|
888
|
+
{rows.length === 0 ? (
|
|
889
|
+
<AbsentNote>No state patches yet.</AbsentNote>
|
|
890
|
+
) : (
|
|
891
|
+
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: fontSizeVar.sm, ...MONO }}>
|
|
892
|
+
<thead>
|
|
893
|
+
<tr style={{ color: themeVars.content.muted }}>
|
|
894
|
+
<th style={{ ...cell, textAlign: 'left' }}>Entity</th>
|
|
895
|
+
<th
|
|
896
|
+
style={cell}
|
|
897
|
+
title="Syncs in - out, as Godot pairs them: the state patches that changed it, and none out, because a Colyseus client sends no state"
|
|
898
|
+
>
|
|
899
|
+
Count
|
|
900
|
+
</th>
|
|
901
|
+
<th style={cell} title="Field changes those patches carried">Changes</th>
|
|
902
|
+
<th
|
|
903
|
+
style={cell}
|
|
904
|
+
title="Size in - out: the size range of the patches that carried it (they carried other entities too), and none out"
|
|
905
|
+
>
|
|
906
|
+
Size
|
|
907
|
+
</th>
|
|
908
|
+
</tr>
|
|
909
|
+
</thead>
|
|
910
|
+
<tbody>
|
|
911
|
+
{rows.map((row) => (
|
|
912
|
+
<tr key={row.path} data-testid="net-entity-traffic-row">
|
|
913
|
+
<td style={{ ...cell, textAlign: 'left' }}>{row.path}</td>
|
|
914
|
+
<td style={cell}>{`${row.syncs} - 0`}</td>
|
|
915
|
+
<td style={cell}>{row.changes}</td>
|
|
916
|
+
<td style={cell}>
|
|
917
|
+
{`${row.patchBytesMin === row.patchBytesMax ? `${row.patchBytesMin} B` : `${row.patchBytesMin}…${row.patchBytesMax} B`} - 0 B`}
|
|
918
|
+
</td>
|
|
919
|
+
</tr>
|
|
920
|
+
))}
|
|
921
|
+
</tbody>
|
|
922
|
+
</table>
|
|
923
|
+
)}
|
|
924
|
+
</div>
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
|
|
929
|
+
|
|
930
|
+
/** A leaf's text as typed: a JSON literal when it reads as one (5, true, null, {}), else a string. */
|
|
931
|
+
function readLiteral(text: string): Json {
|
|
932
|
+
try {
|
|
933
|
+
return JSON.parse(text) as Json;
|
|
934
|
+
} catch {
|
|
935
|
+
return text;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Colyseus Monitor's payload editor: the message's JSON as a tree whose values are edited in
|
|
941
|
+
* place, keys and items added and removed; the Text view is the same payload as raw JSON.
|
|
942
|
+
*/
|
|
943
|
+
function PayloadEditor({ text, onChange }: { text: string; onChange: (text: string) => void }) {
|
|
944
|
+
const [view, setView] = useState<'tree' | 'text'>('tree');
|
|
945
|
+
let parsed: Json | undefined;
|
|
946
|
+
try {
|
|
947
|
+
parsed = text.trim() === '' ? {} : (JSON.parse(text) as Json);
|
|
948
|
+
} catch {
|
|
949
|
+
parsed = undefined;
|
|
950
|
+
}
|
|
951
|
+
const write = (next: Json) => onChange(JSON.stringify(next));
|
|
952
|
+
return (
|
|
953
|
+
<div data-testid="net-payload-editor" style={{ display: 'flex', flexDirection: 'column', gap: spaceVar[1] }}>
|
|
954
|
+
<div style={{ display: 'flex', gap: spaceVar[1] }}>
|
|
955
|
+
{(['tree', 'text'] as const).map((key) => (
|
|
956
|
+
<Button
|
|
957
|
+
key={key}
|
|
958
|
+
type="button"
|
|
959
|
+
variant="ghost"
|
|
960
|
+
aria-pressed={view === key}
|
|
961
|
+
data-testid={`net-payload-view-${key}`}
|
|
962
|
+
onClick={() => setView(key)}
|
|
963
|
+
>
|
|
964
|
+
{key === 'tree' ? 'Tree' : 'Text'}
|
|
965
|
+
</Button>
|
|
966
|
+
))}
|
|
967
|
+
</div>
|
|
968
|
+
{view === 'text' || parsed === undefined ? (
|
|
969
|
+
<>
|
|
970
|
+
<TextArea
|
|
971
|
+
data-testid="net-send-dialog-payload"
|
|
972
|
+
rows={6}
|
|
973
|
+
value={text}
|
|
974
|
+
onChange={(event) => onChange(event.target.value)}
|
|
975
|
+
style={MONO}
|
|
976
|
+
/>
|
|
977
|
+
{parsed === undefined ? (
|
|
978
|
+
<span style={{ color: themeVars.content.muted }}>Not JSON yet, so the tree cannot show it.</span>
|
|
979
|
+
) : null}
|
|
980
|
+
</>
|
|
981
|
+
) : (
|
|
982
|
+
<div style={{ ...MONO, fontSize: fontSizeVar.sm }}>
|
|
983
|
+
<PayloadNode value={parsed} onChange={write} depth={0} />
|
|
984
|
+
</div>
|
|
985
|
+
)}
|
|
986
|
+
</div>
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function PayloadNode({ value, onChange, depth }: { value: Json; onChange: (next: Json) => void; depth: number }) {
|
|
991
|
+
const [newKey, setNewKey] = useState('');
|
|
992
|
+
const [newValue, setNewValue] = useState('');
|
|
993
|
+
const pad = { paddingLeft: depth * 12 };
|
|
994
|
+
if (value !== null && typeof value === 'object') {
|
|
995
|
+
const isArray = Array.isArray(value);
|
|
996
|
+
const entries: [string, Json][] = isArray ? value.map((item, index) => [String(index), item]) : Object.entries(value);
|
|
997
|
+
const set = (key: string, next: Json) => {
|
|
998
|
+
if (isArray) onChange(value.map((item, index) => (String(index) === key ? next : item)));
|
|
999
|
+
else onChange({ ...value, [key]: next });
|
|
1000
|
+
};
|
|
1001
|
+
const remove = (key: string) => {
|
|
1002
|
+
if (isArray) onChange(value.filter((_, index) => String(index) !== key));
|
|
1003
|
+
else onChange(Object.fromEntries(Object.entries(value).filter(([entry]) => entry !== key)));
|
|
1004
|
+
};
|
|
1005
|
+
return (
|
|
1006
|
+
<div data-testid="net-payload-branch">
|
|
1007
|
+
{entries.map(([key, child]) => (
|
|
1008
|
+
<div key={key} style={pad}>
|
|
1009
|
+
<span style={{ color: themeVars.content.muted }}>{key}: </span>
|
|
1010
|
+
{child !== null && typeof child === 'object' ? (
|
|
1011
|
+
<span style={{ color: themeVars.content.muted }}>{Array.isArray(child) ? '[' : '{'}</span>
|
|
1012
|
+
) : null}
|
|
1013
|
+
<button
|
|
1014
|
+
type="button"
|
|
1015
|
+
data-testid="net-payload-remove"
|
|
1016
|
+
title={`Remove ${key}`}
|
|
1017
|
+
onClick={() => remove(key)}
|
|
1018
|
+
style={{ background: 'none', border: 0, color: themeVars.content.muted, cursor: 'pointer' }}
|
|
1019
|
+
>
|
|
1020
|
+
×
|
|
1021
|
+
</button>
|
|
1022
|
+
{child !== null && typeof child === 'object' ? (
|
|
1023
|
+
<PayloadNode value={child} onChange={(next) => set(key, next)} depth={depth + 1} />
|
|
1024
|
+
) : (
|
|
1025
|
+
<PayloadLeaf value={child} onChange={(next) => set(key, next)} />
|
|
1026
|
+
)}
|
|
1027
|
+
</div>
|
|
1028
|
+
))}
|
|
1029
|
+
<div style={{ ...pad, display: 'flex', gap: spaceVar[1] }}>
|
|
1030
|
+
{isArray ? null : (
|
|
1031
|
+
<TextInput
|
|
1032
|
+
data-testid="net-payload-new-key"
|
|
1033
|
+
placeholder="key"
|
|
1034
|
+
value={newKey}
|
|
1035
|
+
onChange={(event) => setNewKey(event.target.value)}
|
|
1036
|
+
style={{ width: 90 }}
|
|
1037
|
+
/>
|
|
1038
|
+
)}
|
|
1039
|
+
<TextInput
|
|
1040
|
+
data-testid="net-payload-new-value"
|
|
1041
|
+
placeholder="value"
|
|
1042
|
+
value={newValue}
|
|
1043
|
+
onChange={(event) => setNewValue(event.target.value)}
|
|
1044
|
+
style={{ width: 110 }}
|
|
1045
|
+
/>
|
|
1046
|
+
<Button
|
|
1047
|
+
type="button"
|
|
1048
|
+
variant="ghost"
|
|
1049
|
+
data-testid="net-payload-add"
|
|
1050
|
+
disabled={!isArray && newKey.trim() === ''}
|
|
1051
|
+
onClick={() => {
|
|
1052
|
+
const next = readLiteral(newValue);
|
|
1053
|
+
if (isArray) onChange([...value, next]);
|
|
1054
|
+
else onChange({ ...value, [newKey.trim()]: next });
|
|
1055
|
+
setNewKey('');
|
|
1056
|
+
setNewValue('');
|
|
1057
|
+
}}
|
|
1058
|
+
>
|
|
1059
|
+
Add
|
|
1060
|
+
</Button>
|
|
1061
|
+
</div>
|
|
1062
|
+
</div>
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
return <PayloadLeaf value={value} onChange={onChange} />;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
type LeafType = 'string' | 'number' | 'boolean' | 'null';
|
|
1069
|
+
|
|
1070
|
+
/** A leaf keeps its type, as Monitor's editor does, until its type picker changes it. */
|
|
1071
|
+
function PayloadLeaf({ value, onChange }: { value: Json; onChange: (next: Json) => void }) {
|
|
1072
|
+
const type: LeafType =
|
|
1073
|
+
value === null ? 'null' : typeof value === 'number' ? 'number' : typeof value === 'boolean' ? 'boolean' : 'string';
|
|
1074
|
+
const shown = type === 'string' ? String(value) : JSON.stringify(value);
|
|
1075
|
+
const [text, setText] = useState(shown);
|
|
1076
|
+
useEffect(() => setText(shown), [shown]);
|
|
1077
|
+
const as = (to: LeafType, raw: string): Json => {
|
|
1078
|
+
if (to === 'null') return null;
|
|
1079
|
+
if (to === 'boolean') return raw.trim() === 'true';
|
|
1080
|
+
if (to === 'number') {
|
|
1081
|
+
const n = Number(raw);
|
|
1082
|
+
return Number.isFinite(n) ? n : 0;
|
|
1083
|
+
}
|
|
1084
|
+
return raw;
|
|
1085
|
+
};
|
|
1086
|
+
return (
|
|
1087
|
+
<span style={{ display: 'inline-flex', gap: 4 }}>
|
|
1088
|
+
{type === 'boolean' ? (
|
|
1089
|
+
<input
|
|
1090
|
+
type="checkbox"
|
|
1091
|
+
data-testid="net-payload-leaf-boolean"
|
|
1092
|
+
checked={value === true}
|
|
1093
|
+
onChange={(event) => onChange(event.currentTarget.checked)}
|
|
1094
|
+
/>
|
|
1095
|
+
) : type === 'null' ? (
|
|
1096
|
+
<span style={{ color: themeVars.content.muted }}>null</span>
|
|
1097
|
+
) : (
|
|
1098
|
+
<TextInput
|
|
1099
|
+
data-testid="net-payload-leaf"
|
|
1100
|
+
value={text}
|
|
1101
|
+
onChange={(event) => setText(event.target.value)}
|
|
1102
|
+
onBlur={() => onChange(as(type, text))}
|
|
1103
|
+
onKeyDown={(event) => {
|
|
1104
|
+
if (event.key === 'Enter') onChange(as(type, text));
|
|
1105
|
+
}}
|
|
1106
|
+
style={{ width: 120 }}
|
|
1107
|
+
/>
|
|
1108
|
+
)}
|
|
1109
|
+
<select
|
|
1110
|
+
data-testid="net-payload-leaf-type"
|
|
1111
|
+
aria-label="Value type"
|
|
1112
|
+
value={type}
|
|
1113
|
+
onChange={(event) => onChange(as(event.currentTarget.value as LeafType, text))}
|
|
1114
|
+
>
|
|
1115
|
+
{(['string', 'number', 'boolean', 'null'] as const).map((option) => (
|
|
1116
|
+
<option key={option} value={option}>
|
|
1117
|
+
{option}
|
|
1118
|
+
</option>
|
|
1119
|
+
))}
|
|
1120
|
+
</select>
|
|
1121
|
+
</span>
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
type RoomSortKey = 'name' | 'roomId' | 'clients' | 'locked' | 'elapsedMs';
|
|
1126
|
+
const ROOM_COLUMNS: readonly (readonly [RoomSortKey, string])[] = [
|
|
1127
|
+
['name', 'Name'],
|
|
1128
|
+
['roomId', 'Room'],
|
|
1129
|
+
['clients', 'Clients'],
|
|
1130
|
+
['locked', 'Locked'],
|
|
1131
|
+
['elapsedMs', 'Elapsed'],
|
|
1132
|
+
];
|
|
1133
|
+
|
|
1134
|
+
function sortRooms<Room extends Record<RoomSortKey, unknown>>(
|
|
1135
|
+
rooms: readonly Room[],
|
|
1136
|
+
sort: { key: RoomSortKey; descending: boolean },
|
|
1137
|
+
): Room[] {
|
|
1138
|
+
const value = (room: Room): number | string => {
|
|
1139
|
+
const field = room[sort.key];
|
|
1140
|
+
return typeof field === 'number' ? field : typeof field === 'boolean' ? Number(field) : String(field);
|
|
1141
|
+
};
|
|
1142
|
+
return [...rooms].sort((a, b) => {
|
|
1143
|
+
const left = value(a);
|
|
1144
|
+
const right = value(b);
|
|
1145
|
+
const order = left < right ? -1 : left > right ? 1 : 0;
|
|
1146
|
+
return sort.descending ? -order : order;
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
|
|
811
1150
|
function TrafficTable({ rows }: { rows: readonly NetTypeTraffic[] }) {
|
|
812
1151
|
const cell = { padding: `0 ${spaceVar[3]}`, textAlign: 'right' as const, whiteSpace: 'nowrap' as const };
|
|
813
1152
|
const bytes = (value: number) => (value >= 1024 ? `${(value / 1024).toFixed(1)} KiB` : `${value} B`);
|
|
@@ -945,14 +1284,23 @@ function ServerView({
|
|
|
945
1284
|
ownSession,
|
|
946
1285
|
ownRoom,
|
|
947
1286
|
draft,
|
|
1287
|
+
setDraft,
|
|
948
1288
|
}: {
|
|
949
1289
|
adapter: NetworkingAdapter;
|
|
950
1290
|
ownSession: string | null;
|
|
951
1291
|
ownRoom: string | null;
|
|
952
1292
|
draft: MessageDraft;
|
|
1293
|
+
setDraft: (draft: MessageDraft) => void;
|
|
953
1294
|
}) {
|
|
1295
|
+
// Monitor's Send dialog: opened from a client's Send or from Broadcast, titled by its target,
|
|
1296
|
+
// editing the one message draft (which Monitor keeps too, in its browser storage).
|
|
1297
|
+
const [sendTarget, setSendTarget] = useState<{ sessionId: string | null } | null>(null);
|
|
1298
|
+
const [sendError, setSendError] = useState<string | null>(null);
|
|
1299
|
+
const [roomTab, setRoomTab] = useState<'clients' | 'state'>('clients');
|
|
954
1300
|
const [inspection, setInspection] = useState<NetServerInspection | null | undefined>(undefined);
|
|
955
1301
|
const [inspected, setInspected] = useState<string | undefined>(undefined);
|
|
1302
|
+
// Monitor's room grid sorts by any column.
|
|
1303
|
+
const [sort, setSort] = useState<{ key: RoomSortKey; descending: boolean }>({ key: 'elapsedMs', descending: true });
|
|
956
1304
|
const [error, setError] = useState<string | null>(null);
|
|
957
1305
|
useEffect(() => {
|
|
958
1306
|
let live = true;
|
|
@@ -990,7 +1338,7 @@ function ServerView({
|
|
|
990
1338
|
if (inspection === null) {
|
|
991
1339
|
return (
|
|
992
1340
|
<AbsentNote testId="net-server-absent">
|
|
993
|
-
The room server serves no Monitor view (its `server` configuration sets
|
|
1341
|
+
The room server serves no Monitor view (its `server` configuration sets VOLTER_ROOM_MONITOR=1).
|
|
994
1342
|
</AbsentNote>
|
|
995
1343
|
);
|
|
996
1344
|
}
|
|
@@ -1013,8 +1361,25 @@ function ServerView({
|
|
|
1013
1361
|
{inspection.room ? ` · state ${inspection.room.stateBytes} B` : ''}
|
|
1014
1362
|
</div>
|
|
1015
1363
|
<table style={{ borderCollapse: 'collapse', ...MONO }}>
|
|
1364
|
+
<thead>
|
|
1365
|
+
<tr style={{ color: themeVars.content.muted }}>
|
|
1366
|
+
{ROOM_COLUMNS.map(([key, label]) => (
|
|
1367
|
+
<th
|
|
1368
|
+
key={key}
|
|
1369
|
+
data-testid={`net-server-sort-${key}`}
|
|
1370
|
+
aria-sort={sort.key === key ? (sort.descending ? 'descending' : 'ascending') : 'none'}
|
|
1371
|
+
style={{ ...cell, textAlign: 'left', cursor: 'pointer', fontWeight: 'normal' }}
|
|
1372
|
+
onClick={() => setSort({ key, descending: sort.key === key ? !sort.descending : false })}
|
|
1373
|
+
>
|
|
1374
|
+
{label}
|
|
1375
|
+
{sort.key === key ? (sort.descending ? ' ▾' : ' ▴') : ''}
|
|
1376
|
+
</th>
|
|
1377
|
+
))}
|
|
1378
|
+
<th style={cell} />
|
|
1379
|
+
</tr>
|
|
1380
|
+
</thead>
|
|
1016
1381
|
<tbody>
|
|
1017
|
-
{inspection.rooms.map((room) => (
|
|
1382
|
+
{sortRooms(inspection.rooms, sort).map((room) => (
|
|
1018
1383
|
<tr key={room.roomId} data-testid="net-server-room" data-inspected={room.roomId === roomId || undefined}>
|
|
1019
1384
|
<td style={cell}>{room.name}</td>
|
|
1020
1385
|
<td style={cell}>{room.roomId}</td>
|
|
@@ -1026,22 +1391,115 @@ function ServerView({
|
|
|
1026
1391
|
<td style={cell}>{seconds(room.elapsedMs)}</td>
|
|
1027
1392
|
<td style={cell}>
|
|
1028
1393
|
{room.roomId === roomId ? (
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1394
|
+
<Button
|
|
1395
|
+
type="button"
|
|
1396
|
+
variant="ghost"
|
|
1397
|
+
data-testid="net-server-broadcast"
|
|
1398
|
+
onClick={() => {
|
|
1399
|
+
setSendError(null);
|
|
1400
|
+
setSendTarget({ sessionId: null });
|
|
1401
|
+
}}
|
|
1402
|
+
>
|
|
1403
|
+
Broadcast
|
|
1404
|
+
</Button>
|
|
1036
1405
|
) : (
|
|
1037
1406
|
<Button type="button" variant="ghost" data-testid="net-server-inspect" onClick={() => setInspected(room.roomId)}>
|
|
1038
1407
|
Inspect
|
|
1039
1408
|
</Button>
|
|
1040
1409
|
)}
|
|
1410
|
+
{/* Monitor offers Dispose on every room of its list. */}
|
|
1411
|
+
{actButton('net-server-dispose', 'Dispose', () => adapter.disposeRoom?.(room.roomId))}
|
|
1041
1412
|
</td>
|
|
1042
1413
|
</tr>
|
|
1043
1414
|
))}
|
|
1044
|
-
|
|
1415
|
+
</tbody>
|
|
1416
|
+
</table>
|
|
1417
|
+
{sendTarget ? (
|
|
1418
|
+
// Monitor's dialog, drawn in the panel rather than over the editor: during Play a field
|
|
1419
|
+
// over the editor would hand the game every key typed into it.
|
|
1420
|
+
<div
|
|
1421
|
+
data-testid="net-send-dialog"
|
|
1422
|
+
role="group"
|
|
1423
|
+
aria-labelledby="net-send-dialog-title"
|
|
1424
|
+
style={{ margin: spaceVar[2], padding: spaceVar[3], border: `1px solid ${themeVars.boundary.default}`, borderRadius: 4 }}
|
|
1425
|
+
>
|
|
1426
|
+
<div id="net-send-dialog-title" style={{ color: themeVars.content.primary, marginBottom: spaceVar[2] }}>
|
|
1427
|
+
{sendTarget.sessionId
|
|
1428
|
+
? `Send message to client (${sendTarget.sessionId})`
|
|
1429
|
+
: 'Broadcast message to all clients'}
|
|
1430
|
+
</div>
|
|
1431
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: spaceVar[2] }}>
|
|
1432
|
+
<label style={{ display: 'flex', flexDirection: 'column', gap: spaceVar[1] }}>
|
|
1433
|
+
<span style={{ color: themeVars.content.muted }}>Message type</span>
|
|
1434
|
+
<TextInput
|
|
1435
|
+
data-testid="net-send-dialog-type"
|
|
1436
|
+
value={draft.type}
|
|
1437
|
+
onChange={(event) => setDraft({ ...draft, type: event.target.value })}
|
|
1438
|
+
/>
|
|
1439
|
+
</label>
|
|
1440
|
+
<label style={{ display: 'flex', flexDirection: 'column', gap: spaceVar[1] }}>
|
|
1441
|
+
<span style={{ color: themeVars.content.muted }}>Message payload (JSON)</span>
|
|
1442
|
+
<PayloadEditor text={draft.payload} onChange={(payload) => setDraft({ ...draft, payload })} />
|
|
1443
|
+
</label>
|
|
1444
|
+
{sendError ? <span data-testid="net-send-dialog-error">{sendError}</span> : null}
|
|
1445
|
+
</div>
|
|
1446
|
+
<div style={{ display: 'flex', gap: spaceVar[2], justifyContent: 'flex-end', marginTop: spaceVar[2] }}>
|
|
1447
|
+
<Button type="button" variant="ghost" onClick={() => setSendTarget(null)}>
|
|
1448
|
+
Cancel
|
|
1449
|
+
</Button>
|
|
1450
|
+
<Button
|
|
1451
|
+
type="button"
|
|
1452
|
+
data-testid="net-send-dialog-send"
|
|
1453
|
+
onClick={() => {
|
|
1454
|
+
const target = sendTarget;
|
|
1455
|
+
void (async () => {
|
|
1456
|
+
try {
|
|
1457
|
+
const message = draftMessage(draft);
|
|
1458
|
+
await (target.sessionId
|
|
1459
|
+
? adapter.sendToClient?.(target.sessionId, message.type, message.payload, roomId)
|
|
1460
|
+
: adapter.broadcast?.(message.type, message.payload, roomId));
|
|
1461
|
+
setSendError(null);
|
|
1462
|
+
setSendTarget(null);
|
|
1463
|
+
} catch (caught) {
|
|
1464
|
+
setSendError(caught instanceof Error ? caught.message : String(caught));
|
|
1465
|
+
}
|
|
1466
|
+
})();
|
|
1467
|
+
}}
|
|
1468
|
+
>
|
|
1469
|
+
Send
|
|
1470
|
+
</Button>
|
|
1471
|
+
</div>
|
|
1472
|
+
</div>
|
|
1473
|
+
) : null}
|
|
1474
|
+
{inspection.room ? (
|
|
1475
|
+
// Monitor's room view: a Clients tab and a State tab. The State tab is the SERVER's state
|
|
1476
|
+
// of the inspected room, edited and deleted there; the tree below this section stays this
|
|
1477
|
+
// client's replicated copy of its own room.
|
|
1478
|
+
<div data-testid="net-server-room-view" style={{ padding: `${spaceVar[2]} 0` }}>
|
|
1479
|
+
<div role="tablist" style={{ display: 'flex', gap: spaceVar[2], padding: `0 ${spaceVar[3]}` }}>
|
|
1480
|
+
{(
|
|
1481
|
+
[
|
|
1482
|
+
['clients', `Clients (${inspection.room.clients.length})`],
|
|
1483
|
+
['state', 'State'],
|
|
1484
|
+
] as const
|
|
1485
|
+
).map(([key, label]) => (
|
|
1486
|
+
<Button
|
|
1487
|
+
key={key}
|
|
1488
|
+
type="button"
|
|
1489
|
+
variant="ghost"
|
|
1490
|
+
role="tab"
|
|
1491
|
+
aria-selected={roomTab === key}
|
|
1492
|
+
data-testid={`net-server-tab-${key}`}
|
|
1493
|
+
onClick={() => setRoomTab(key)}
|
|
1494
|
+
>
|
|
1495
|
+
{label}
|
|
1496
|
+
</Button>
|
|
1497
|
+
))}
|
|
1498
|
+
</div>
|
|
1499
|
+
{roomTab === 'clients' ? (
|
|
1500
|
+
<table style={{ borderCollapse: 'collapse', ...MONO }}>
|
|
1501
|
+
<tbody>
|
|
1502
|
+
{inspection.room.clients.map((client) => (
|
|
1045
1503
|
<tr key={client.sessionId} data-testid="net-server-client">
|
|
1046
1504
|
<td style={cell}>client</td>
|
|
1047
1505
|
<td style={cell}>
|
|
@@ -1050,27 +1508,36 @@ function ServerView({
|
|
|
1050
1508
|
</td>
|
|
1051
1509
|
<td style={cell}>{seconds(client.elapsedMs)}</td>
|
|
1052
1510
|
<td style={cell} colSpan={3}>
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1511
|
+
<Button
|
|
1512
|
+
type="button"
|
|
1513
|
+
variant="ghost"
|
|
1514
|
+
data-testid="net-server-send"
|
|
1515
|
+
onClick={() => {
|
|
1516
|
+
setSendError(null);
|
|
1517
|
+
setSendTarget({ sessionId: client.sessionId });
|
|
1518
|
+
}}
|
|
1519
|
+
>
|
|
1520
|
+
Send
|
|
1521
|
+
</Button>
|
|
1057
1522
|
{actButton('net-server-disconnect', 'Disconnect', () => adapter.disconnectClient?.(client.sessionId, roomId))}
|
|
1058
1523
|
</td>
|
|
1059
1524
|
</tr>
|
|
1060
1525
|
))}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1526
|
+
</tbody>
|
|
1527
|
+
</table>
|
|
1528
|
+
) : inspection.room.state !== undefined ? (
|
|
1529
|
+
<div data-testid="net-server-room-state">
|
|
1530
|
+
<StateTreeNode
|
|
1531
|
+
name={`room ${inspection.room.roomId}${inspection.room.roomId === ownRoom ? ' (this client’s room)' : ''}`}
|
|
1532
|
+
value={inspection.room.state}
|
|
1533
|
+
depth={0}
|
|
1534
|
+
edit={adapter.editServerState ? (path, next) => adapter.editServerState!(path, next, roomId) : undefined}
|
|
1535
|
+
remove={adapter.deleteServerState ? (path) => adapter.deleteServerState!(path, roomId) : undefined}
|
|
1536
|
+
/>
|
|
1537
|
+
</div>
|
|
1538
|
+
) : (
|
|
1539
|
+
<AbsentNote>The server sent no state for this room.</AbsentNote>
|
|
1540
|
+
)}
|
|
1074
1541
|
</div>
|
|
1075
1542
|
) : null}
|
|
1076
1543
|
{error ? <AbsentNote>{error}</AbsentNote> : null}
|