@vgai/live 0.5.2 → 0.5.4
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/dist/.tsbuildinfo +1 -0
- package/dist/editor.d.ts +78 -55
- package/dist/editor.js +129 -76
- package/dist/game-client/bridge-heartbeat.d.ts +49 -0
- package/dist/game-client/bridge-heartbeat.js +46 -0
- package/dist/game-client/bridge-transport.d.ts +75 -0
- package/dist/game-client/bridge-transport.js +19 -0
- package/dist/game-client/client.d.ts +293 -0
- package/dist/game-client/client.js +706 -0
- package/dist/game-client/errors.d.ts +57 -0
- package/dist/game-client/errors.js +76 -0
- package/dist/game-client/events-matcher.d.ts +41 -0
- package/dist/game-client/events-matcher.js +68 -0
- package/dist/game-client/failure-block.d.ts +93 -0
- package/dist/game-client/failure-block.js +97 -0
- package/dist/game-client/fast-forward.d.ts +125 -0
- package/dist/game-client/fast-forward.js +122 -0
- package/dist/game-client/hidden-recovery.d.ts +85 -0
- package/dist/game-client/hidden-recovery.js +105 -0
- package/dist/game-client/index.d.ts +40 -0
- package/dist/game-client/index.js +26 -0
- package/dist/game-client/perf-sampling.d.ts +56 -0
- package/dist/game-client/perf-sampling.js +85 -0
- package/dist/game-client/relay-transport.d.ts +100 -0
- package/dist/game-client/relay-transport.js +237 -0
- package/dist/game-client/screenshot-target.d.ts +60 -0
- package/dist/game-client/screenshot-target.js +68 -0
- package/dist/game-client/state-cap.d.ts +7 -0
- package/dist/game-client/state-cap.js +21 -0
- package/dist/game-client/types.d.ts +128 -0
- package/dist/game-client/types.js +15 -0
- package/dist/game-client/wait-for.d.ts +155 -0
- package/dist/game-client/wait-for.js +229 -0
- package/dist/game.d.ts +47 -18
- package/dist/game.js +59 -16
- package/dist/index.d.ts +46 -21
- package/dist/index.js +51 -20
- package/dist/session.d.ts +4 -4
- package/dist/session.js +7 -7
- package/dist/tools.d.ts +12 -3
- package/dist/tools.js +15 -6
- package/package.json +10 -5
- package/src/editor.ts +142 -96
- package/src/game-client/bridge-heartbeat.ts +61 -0
- package/src/game-client/bridge-transport.ts +73 -0
- package/src/game-client/client.ts +836 -0
- package/src/game-client/errors.ts +96 -0
- package/src/game-client/events-matcher.ts +106 -0
- package/src/game-client/failure-block.ts +199 -0
- package/src/game-client/fast-forward.ts +175 -0
- package/src/game-client/hidden-recovery.ts +149 -0
- package/src/game-client/index.ts +98 -0
- package/src/game-client/perf-sampling.ts +94 -0
- package/src/game-client/relay-transport.ts +311 -0
- package/src/game-client/screenshot-target.ts +91 -0
- package/src/game-client/state-cap.ts +29 -0
- package/src/game-client/types.ts +137 -0
- package/src/game-client/wait-for.ts +327 -0
- package/src/game.ts +96 -16
- package/src/index.ts +68 -31
- package/src/session.ts +8 -10
- package/src/tools.ts +19 -6
package/dist/editor.d.ts
CHANGED
|
@@ -1,42 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `LiveEditor` — the editor-control half of `@vgai/live`'s `{ editor, game,
|
|
3
|
-
* page }` (
|
|
3
|
+
* page }` (Wave 2). Methods are named after the
|
|
4
4
|
* ACTION, not the CLI flag spelling (e.g. `select('all')` rather than a
|
|
5
|
-
* separate `selectAll`, `showPanel('viewport-
|
|
6
|
-
* `vgai show viewport
|
|
5
|
+
* separate `selectAll`, `showPanel('viewport-play')` rather than
|
|
6
|
+
* `vgai show viewport play`'s two-token shape) — see each method's own doc
|
|
7
7
|
* comment for the exact CLI verb / `EditorClient` call it mirrors.
|
|
8
8
|
*
|
|
9
|
-
* Every method reuses `@vgai/editor-sdk`'s `EditorClient` — this module
|
|
10
|
-
*
|
|
11
|
-
* exception
|
|
12
|
-
* directly), not a live wire command — see its own doc comment.
|
|
9
|
+
* Every method reuses `@vgai/editor-sdk`'s `EditorClient` — this module never
|
|
10
|
+
* hand-rolls a `fetch` to `/__editor/command` itself. (WO-8 removed the one
|
|
11
|
+
* exception, `applyDiff`, which was FILE mode rather than a live wire command.)
|
|
13
12
|
*/
|
|
14
|
-
import type { ActiveDocumentCapture, AssetKind, AssetPreviewCapture, AssetPreviewOptions, EditorClient, EditorState, EditorView, PresentedEditorView, ShadingMode, ViewPreset, ViewportCapture } from '@vgai/editor-sdk';
|
|
13
|
+
import type { ActiveDocumentCapture, AssetKind, AssetPreviewCapture, AssetPreviewOptions, AssetPreviewSource, EditorClient, EditorState, EditorView, InspectedInspection, PresentedEditorView, ShadingMode, ViewPreset, ViewportCapture } from '@vgai/editor-sdk';
|
|
15
14
|
/** `vgai show <...>`'s four sub-verbs folded into one action name — see `showPanel`. */
|
|
16
|
-
export type PanelName = 'viewport-
|
|
17
|
-
/** Mirrors `@vgai/sdk`'s `project.scene.apply` operation input — the same shape `vgai apply-diff` builds from its `<scene> <patch>` file arguments. `patch` is a `SceneDiff` JSON object (validated by the operation's own Zod schema — a malformed patch throws, never silently no-ops). */
|
|
18
|
-
export interface ApplyDiffInput {
|
|
19
|
-
/** Project-relative path to the `.vscn.json` scene file to modify. */
|
|
20
|
-
scenePath: string;
|
|
21
|
-
/** A `SceneDiff`-shaped patch object. */
|
|
22
|
-
patch: unknown;
|
|
23
|
-
/** Validate + report only — never write to disk. Default `false`. */
|
|
24
|
-
dryRun?: boolean;
|
|
25
|
-
/** Refuse a stale write when the file's content hash no longer matches (optimistic concurrency) — omit to skip the check. */
|
|
26
|
-
baseHash?: string;
|
|
27
|
-
}
|
|
28
|
-
/** The `project.scene.apply` operation's result shape. */
|
|
29
|
-
export interface ApplyDiffResult {
|
|
30
|
-
dryRun: boolean;
|
|
31
|
-
written: boolean;
|
|
32
|
-
filesChanged: string[];
|
|
33
|
-
before: {
|
|
34
|
-
scene: unknown;
|
|
35
|
-
};
|
|
36
|
-
after: {
|
|
37
|
-
scene: unknown;
|
|
38
|
-
};
|
|
39
|
-
}
|
|
15
|
+
export type PanelName = 'viewport-edit' | 'viewport-play' | 'inspector' | 'console' | 'build';
|
|
40
16
|
/**
|
|
41
17
|
* Lightweight extension-based `AssetKind` guess for `openAsset`'s optional
|
|
42
18
|
* `kind` argument. Deliberately independent of (not shared with) the
|
|
@@ -46,18 +22,17 @@ export interface ApplyDiffResult {
|
|
|
46
22
|
*/
|
|
47
23
|
export declare function inferAssetKind(path: string): AssetKind;
|
|
48
24
|
export declare class LiveEditor {
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
private readonly projectRoot;
|
|
52
|
-
constructor(client: EditorClient,
|
|
53
|
-
/** Absolute project root this session is bound to — used only by `applyDiff` (FILE mode; every other method goes over the wire and never needs it). */
|
|
54
|
-
projectRoot: string);
|
|
25
|
+
#private;
|
|
26
|
+
constructor(client: EditorClient);
|
|
55
27
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
28
|
+
* The active authoring adapter's persistence destination — where a save would
|
|
29
|
+
* land (`status().savePath`).
|
|
30
|
+
*
|
|
31
|
+
* WO-8: this also took a `path` and OPENED that scene through
|
|
32
|
+
* `EditorClient.openScene` -> the `open-scene` relay verb. That verb now rejects
|
|
33
|
+
* (the `.vscn.json` format is deleted), so only the read remains.
|
|
59
34
|
*/
|
|
60
|
-
scene(
|
|
35
|
+
scene(): Promise<string | null>;
|
|
61
36
|
/**
|
|
62
37
|
* Make the connected human editor show the same subject/view as the agent.
|
|
63
38
|
* The returned URL is a compact, shareable projection — not a serialized
|
|
@@ -68,16 +43,6 @@ export declare class LiveEditor {
|
|
|
68
43
|
currentView(): Promise<EditorView>;
|
|
69
44
|
/** Capture the same center document the human is currently looking at. */
|
|
70
45
|
captureActiveDocument(size?: number): Promise<ActiveDocumentCapture>;
|
|
71
|
-
/**
|
|
72
|
-
* FILE mode — writes the scene file directly. NOT a live wire command:
|
|
73
|
-
* there is no live-editor wire op for this yet (`vgai apply-diff`'s own
|
|
74
|
-
* module doc names the identical gap: "LIVE MODE — NOT BUILT ... route the
|
|
75
|
-
* patch through a RUNNING editor"). Reuses the exact same engine
|
|
76
|
-
* machinery `vgai apply-diff` uses, through `@vgai/sdk`'s
|
|
77
|
-
* `project.scene.apply` operation (the operation-registry's own
|
|
78
|
-
* projection of that logic) — never hand-rolled here.
|
|
79
|
-
*/
|
|
80
|
-
applyDiff(input: ApplyDiffInput): Promise<ApplyDiffResult>;
|
|
81
46
|
play(opts?: {
|
|
82
47
|
seed?: number;
|
|
83
48
|
}): Promise<void>;
|
|
@@ -92,17 +57,75 @@ export declare class LiveEditor {
|
|
|
92
57
|
deselect(): Promise<void>;
|
|
93
58
|
/** No `id` -> focus the current selection (mirrors bare `vgai focus`); `id` given -> focus that entity. */
|
|
94
59
|
focus(id?: string): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Frame the edit viewport camera on one entity. Same framing as
|
|
62
|
+
* `focus(id)`, but an entity id the scene does not know THROWS, naming the
|
|
63
|
+
* id — where `focus` quietly does nothing. Reach for this whenever the next
|
|
64
|
+
* step reads the viewport (`screenshot()`, an
|
|
65
|
+
* `assetPreview(..., { stage: 'scene' })`): a framing that silently missed
|
|
66
|
+
* would otherwise be indistinguishable from one that worked.
|
|
67
|
+
*/
|
|
68
|
+
frame(entityId: string): Promise<void>;
|
|
95
69
|
view(preset: ViewPreset): Promise<void>;
|
|
96
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* Show several instances of the running game split-screen — the whole point
|
|
72
|
+
* of multiplayer authoring: `await editor.instances(2)` puts a second view
|
|
73
|
+
* beside the first, watchable by a human, each drivable by its own bot
|
|
74
|
+
* (`game.instance(id)` / `tools.run(name, params, { instance })`). Pass a
|
|
75
|
+
* TOTAL count (default "Player 1"/"Player 2"/… labels) or explicit names
|
|
76
|
+
* (`editor.instances(['Alice', 'Bob'])`, index 0 = primary). A count of `1`
|
|
77
|
+
* (or a single name) collapses back to a single view. Requires a live play
|
|
78
|
+
* session.
|
|
79
|
+
*/
|
|
80
|
+
instances(countOrNames: number | string[]): Promise<void>;
|
|
81
|
+
/** `vgai show <viewport <edit|play>|inspector|console|build>`'s four sub-verbs, folded into one action name. */
|
|
97
82
|
showPanel(name: PanelName): Promise<void>;
|
|
98
83
|
/** `kind` inferred from `path`'s extension when omitted (`inferAssetKind`) — pass it explicitly to override. */
|
|
99
84
|
openAsset(path: string, kind?: AssetKind): Promise<void>;
|
|
100
|
-
/**
|
|
101
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Captures the editor's native four-view preview. A bare string is a
|
|
87
|
+
* project-relative asset path (the common case); an explicit source object
|
|
88
|
+
* targets a path, a LIVE SCENE ENTITY (`assetPreview({ entityId }, …)`) —
|
|
89
|
+
* which is what makes `options.stage: 'scene'`, the entity photographed
|
|
90
|
+
* where it stands under the scene's own lighting, reachable from here — or
|
|
91
|
+
* RAW GLB BYTES (`assetPreview({ glbBase64 }, …)`), for a model that exists
|
|
92
|
+
* only in the calling Node process's memory and has never been written to
|
|
93
|
+
* disk. `stage` defaults to `'lab'`, the neutral Asset Lab staging this has
|
|
94
|
+
* always produced, and the bytes form is lab-only.
|
|
95
|
+
*/
|
|
96
|
+
assetPreview(source: string | AssetPreviewSource, options?: AssetPreviewOptions): Promise<AssetPreviewCapture>;
|
|
102
97
|
grid(on: boolean): Promise<void>;
|
|
103
98
|
helpers(on: boolean): Promise<void>;
|
|
104
99
|
stats(on: boolean): Promise<void>;
|
|
105
100
|
shading(mode: ShadingMode): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* READ the inspector, as data — the serialized inspection subject
|
|
103
|
+
* (`editor.inspect()`; design: `docs/ARCHITECTURE-CORE.md` §Editor chrome,
|
|
104
|
+
* "The Inspection Model"). This is the Figma-Inspect analog: whatever a
|
|
105
|
+
* human would see in the inspector right now — the subject's identity, its
|
|
106
|
+
* verbs, and every identified section in display order, with a `fields`
|
|
107
|
+
* section's CURRENT VALUES at their scriptable `path`s.
|
|
108
|
+
*
|
|
109
|
+
* Reach for it whenever the next step depends on what an object actually
|
|
110
|
+
* IS: `await editor.select(id)` then `await editor.inspect()` answers "what
|
|
111
|
+
* properties does this thing have, and what are they set to" in one call,
|
|
112
|
+
* against the same model the panel renders — no scene-graph reads, no
|
|
113
|
+
* guessing at property names.
|
|
114
|
+
*
|
|
115
|
+
* When the inspector is showing NOTHING — nothing selected on a surface
|
|
116
|
+
* with no empty-state subject of its own, which is most of them — the answer
|
|
117
|
+
* is `{none: true}`, so "the human sees no inspector" and "the read failed"
|
|
118
|
+
* are never the same value. A surface whose empty space IS a real thing (an
|
|
119
|
+
* open Asset Lab document) still answers with that subject, and never with
|
|
120
|
+
* another surface's.
|
|
121
|
+
*
|
|
122
|
+
* A `custom` section body is a named opaque: the editor renders it with
|
|
123
|
+
* React, so the wire reports its identity rather than pretending to describe
|
|
124
|
+
* its rendering — plus, when the section can say what it DISPLAYS, a `data`
|
|
125
|
+
* payload in its own vocabulary (`transform` carries
|
|
126
|
+
* `{position, rotation, scale}`, rotation in Euler XYZ degrees).
|
|
127
|
+
*/
|
|
128
|
+
inspect(): Promise<InspectedInspection>;
|
|
106
129
|
/** Mirrors `vgai status` — the full live editor state as JSON. */
|
|
107
130
|
status(): Promise<EditorState>;
|
|
108
131
|
/** A live viewport PNG (`EditorClient.captureViewport`) — no direct CLI verb exists; this is the closest wire read. */
|
package/dist/editor.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `LiveEditor` — the editor-control half of `@vgai/live`'s `{ editor, game,
|
|
3
|
-
* page }` (
|
|
3
|
+
* page }` (Wave 2). Methods are named after the
|
|
4
4
|
* ACTION, not the CLI flag spelling (e.g. `select('all')` rather than a
|
|
5
|
-
* separate `selectAll`, `showPanel('viewport-
|
|
6
|
-
* `vgai show viewport
|
|
5
|
+
* separate `selectAll`, `showPanel('viewport-play')` rather than
|
|
6
|
+
* `vgai show viewport play`'s two-token shape) — see each method's own doc
|
|
7
7
|
* comment for the exact CLI verb / `EditorClient` call it mirrors.
|
|
8
8
|
*
|
|
9
|
-
* Every method reuses `@vgai/editor-sdk`'s `EditorClient` — this module
|
|
10
|
-
*
|
|
11
|
-
* exception
|
|
12
|
-
* directly), not a live wire command — see its own doc comment.
|
|
9
|
+
* Every method reuses `@vgai/editor-sdk`'s `EditorClient` — this module never
|
|
10
|
+
* hand-rolls a `fetch` to `/__editor/command` itself. (WO-8 removed the one
|
|
11
|
+
* exception, `applyDiff`, which was FILE mode rather than a live wire command.)
|
|
13
12
|
*/
|
|
14
|
-
import { operations } from '@vgai/sdk';
|
|
15
13
|
const EXTENSION_KIND = {
|
|
16
14
|
'.glb': 'model',
|
|
17
15
|
'.gltf': 'model',
|
|
@@ -20,6 +18,7 @@ const EXTENSION_KIND = {
|
|
|
20
18
|
'.jpeg': 'image',
|
|
21
19
|
'.webp': 'image',
|
|
22
20
|
'.gif': 'image',
|
|
21
|
+
'.svg': 'image',
|
|
23
22
|
'.mp3': 'audio',
|
|
24
23
|
'.ogg': 'audio',
|
|
25
24
|
'.wav': 'audio',
|
|
@@ -33,8 +32,10 @@ const EXTENSION_KIND = {
|
|
|
33
32
|
* on. Callers with an unusual extension can always pass `kind` explicitly.
|
|
34
33
|
*/
|
|
35
34
|
export function inferAssetKind(path) {
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
// WO-8: `.vscn.json` mapped to 'scene' here. That format is deleted, and
|
|
36
|
+
// `.prefab.json` is likewise retired (PRs #578/#581/#589) but the `'prefab'`
|
|
37
|
+
// AssetKind itself still exists in `@vgai/editor-sdk`, so the mapping is left
|
|
38
|
+
// for that retirement's own follow-up rather than half-removed here.
|
|
38
39
|
if (path.endsWith('.prefab.json'))
|
|
39
40
|
return 'prefab';
|
|
40
41
|
if (path.endsWith('.mat.json'))
|
|
@@ -44,25 +45,24 @@ export function inferAssetKind(path) {
|
|
|
44
45
|
return EXTENSION_KIND[ext] ?? 'json';
|
|
45
46
|
}
|
|
46
47
|
export class LiveEditor {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
this
|
|
48
|
+
/** `#`-private, not `private`: `vgai eval --list` enumerates this object's
|
|
49
|
+
* real runtime members, and TypeScript's erased `private` would leave the
|
|
50
|
+
* raw `EditorClient` advertised beside them. See `./game-client/`'s
|
|
51
|
+
* `client.ts` (GameClient's field block) for the full reasoning. */
|
|
52
|
+
#client;
|
|
53
|
+
constructor(client) {
|
|
54
|
+
this.#client = client;
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
57
|
+
* The active authoring adapter's persistence destination — where a save would
|
|
58
|
+
* land (`status().savePath`).
|
|
59
|
+
*
|
|
60
|
+
* WO-8: this also took a `path` and OPENED that scene through
|
|
61
|
+
* `EditorClient.openScene` -> the `open-scene` relay verb. That verb now rejects
|
|
62
|
+
* (the `.vscn.json` format is deleted), so only the read remains.
|
|
59
63
|
*/
|
|
60
|
-
async scene(
|
|
61
|
-
|
|
62
|
-
await this.client.openScene(path);
|
|
63
|
-
return path;
|
|
64
|
-
}
|
|
65
|
-
const state = await this.client.getState();
|
|
64
|
+
async scene() {
|
|
65
|
+
const state = await this.#client.getState();
|
|
66
66
|
return state.savePath;
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
@@ -71,121 +71,174 @@ export class LiveEditor {
|
|
|
71
71
|
* workspace or document payload.
|
|
72
72
|
*/
|
|
73
73
|
async present(view) {
|
|
74
|
-
return this
|
|
74
|
+
return this.#client.present(view);
|
|
75
75
|
}
|
|
76
76
|
/** The human editor's actual active document, selection, camera and utility. */
|
|
77
77
|
async currentView() {
|
|
78
|
-
return this
|
|
78
|
+
return this.#client.currentView();
|
|
79
79
|
}
|
|
80
80
|
/** Capture the same center document the human is currently looking at. */
|
|
81
81
|
async captureActiveDocument(size) {
|
|
82
|
-
return this
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* `project.scene.apply` operation (the operation-registry's own
|
|
91
|
-
* projection of that logic) — never hand-rolled here.
|
|
82
|
+
return this.#client.captureActiveDocument(size);
|
|
83
|
+
}
|
|
84
|
+
/*
|
|
85
|
+
* `applyDiff` lived here. It dispatched `project.scene.apply`, one of the
|
|
86
|
+
* twelve `.vscn` document tools deleted with the format, so there is nothing
|
|
87
|
+
* left for it to call. `ApplyDiffInput` / `ApplyDiffResult` went with it, and
|
|
88
|
+
* so did `projectRoot` — this was the ONE method that used it (every other
|
|
89
|
+
* method goes over the wire).
|
|
92
90
|
*/
|
|
93
|
-
async applyDiff(input) {
|
|
94
|
-
const outcome = await operations.dispatch('project.scene.apply', input, {
|
|
95
|
-
projectRoot: this.projectRoot,
|
|
96
|
-
});
|
|
97
|
-
if (!outcome.ok) {
|
|
98
|
-
throw new Error(`@vgai/live: applyDiff failed [${outcome.error.code}]: ${outcome.error.message}`);
|
|
99
|
-
}
|
|
100
|
-
return outcome.data;
|
|
101
|
-
}
|
|
102
91
|
async play(opts) {
|
|
103
|
-
await this
|
|
92
|
+
await this.#client.play(opts);
|
|
104
93
|
}
|
|
105
94
|
async stop() {
|
|
106
|
-
await this
|
|
95
|
+
await this.#client.stop();
|
|
107
96
|
}
|
|
108
97
|
async pause() {
|
|
109
|
-
await this
|
|
98
|
+
await this.#client.pause();
|
|
110
99
|
}
|
|
111
100
|
async resume() {
|
|
112
|
-
await this
|
|
101
|
+
await this.#client.resume();
|
|
113
102
|
}
|
|
114
103
|
/** Advance `n` frames (default 1) — `EditorClient.step()` sent `n` times, mirroring `vgai step` run repeatedly. */
|
|
115
104
|
async step(n = 1) {
|
|
116
105
|
for (let i = 0; i < n; i++) {
|
|
117
|
-
await this
|
|
106
|
+
await this.#client.step();
|
|
118
107
|
}
|
|
119
108
|
}
|
|
120
109
|
/** `'all'` -> `EditorClient.selectAll()` (mirrors `vgai select --all`); otherwise `EditorClient.select(id)` (mirrors `vgai select <entityId>`). */
|
|
121
110
|
async select(id) {
|
|
122
111
|
if (id === 'all') {
|
|
123
|
-
await this
|
|
112
|
+
await this.#client.selectAll();
|
|
124
113
|
return;
|
|
125
114
|
}
|
|
126
|
-
await this
|
|
115
|
+
await this.#client.select(id);
|
|
127
116
|
}
|
|
128
117
|
/** Mirrors `vgai deselect`. */
|
|
129
118
|
async deselect() {
|
|
130
|
-
await this
|
|
119
|
+
await this.#client.select(null);
|
|
131
120
|
}
|
|
132
121
|
/** No `id` -> focus the current selection (mirrors bare `vgai focus`); `id` given -> focus that entity. */
|
|
133
122
|
async focus(id) {
|
|
134
123
|
if (id !== undefined) {
|
|
135
|
-
await this
|
|
124
|
+
await this.#client.focusEntity(id);
|
|
136
125
|
return;
|
|
137
126
|
}
|
|
138
|
-
await this
|
|
127
|
+
await this.#client.focusSelection();
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Frame the edit viewport camera on one entity. Same framing as
|
|
131
|
+
* `focus(id)`, but an entity id the scene does not know THROWS, naming the
|
|
132
|
+
* id — where `focus` quietly does nothing. Reach for this whenever the next
|
|
133
|
+
* step reads the viewport (`screenshot()`, an
|
|
134
|
+
* `assetPreview(..., { stage: 'scene' })`): a framing that silently missed
|
|
135
|
+
* would otherwise be indistinguishable from one that worked.
|
|
136
|
+
*/
|
|
137
|
+
async frame(entityId) {
|
|
138
|
+
await this.#client.frameEntity(entityId);
|
|
139
139
|
}
|
|
140
140
|
async view(preset) {
|
|
141
|
-
await this
|
|
141
|
+
await this.#client.viewPreset(preset);
|
|
142
142
|
}
|
|
143
|
-
/**
|
|
143
|
+
/**
|
|
144
|
+
* Show several instances of the running game split-screen — the whole point
|
|
145
|
+
* of multiplayer authoring: `await editor.instances(2)` puts a second view
|
|
146
|
+
* beside the first, watchable by a human, each drivable by its own bot
|
|
147
|
+
* (`game.instance(id)` / `tools.run(name, params, { instance })`). Pass a
|
|
148
|
+
* TOTAL count (default "Player 1"/"Player 2"/… labels) or explicit names
|
|
149
|
+
* (`editor.instances(['Alice', 'Bob'])`, index 0 = primary). A count of `1`
|
|
150
|
+
* (or a single name) collapses back to a single view. Requires a live play
|
|
151
|
+
* session.
|
|
152
|
+
*/
|
|
153
|
+
async instances(countOrNames) {
|
|
154
|
+
await this.#client.setInstanceCount(countOrNames);
|
|
155
|
+
}
|
|
156
|
+
/** `vgai show <viewport <edit|play>|inspector|console|build>`'s four sub-verbs, folded into one action name. */
|
|
144
157
|
async showPanel(name) {
|
|
145
158
|
switch (name) {
|
|
146
|
-
case 'viewport-
|
|
147
|
-
await this
|
|
159
|
+
case 'viewport-edit':
|
|
160
|
+
await this.#client.showViewport('edit');
|
|
148
161
|
return;
|
|
149
|
-
case 'viewport-
|
|
150
|
-
await this
|
|
162
|
+
case 'viewport-play':
|
|
163
|
+
await this.#client.showViewport('play');
|
|
151
164
|
return;
|
|
152
165
|
case 'inspector':
|
|
153
|
-
await this
|
|
166
|
+
await this.#client.showInspector();
|
|
154
167
|
return;
|
|
155
168
|
case 'console':
|
|
156
|
-
await this
|
|
169
|
+
await this.#client.toggleConsole();
|
|
157
170
|
return;
|
|
158
171
|
case 'build':
|
|
159
|
-
await this
|
|
172
|
+
await this.#client.showBuild();
|
|
160
173
|
return;
|
|
161
174
|
}
|
|
162
175
|
}
|
|
163
176
|
/** `kind` inferred from `path`'s extension when omitted (`inferAssetKind`) — pass it explicitly to override. */
|
|
164
177
|
async openAsset(path, kind) {
|
|
165
|
-
await this
|
|
178
|
+
await this.#client.openAsset(path, kind ?? inferAssetKind(path));
|
|
166
179
|
}
|
|
167
|
-
/**
|
|
168
|
-
|
|
169
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Captures the editor's native four-view preview. A bare string is a
|
|
182
|
+
* project-relative asset path (the common case); an explicit source object
|
|
183
|
+
* targets a path, a LIVE SCENE ENTITY (`assetPreview({ entityId }, …)`) —
|
|
184
|
+
* which is what makes `options.stage: 'scene'`, the entity photographed
|
|
185
|
+
* where it stands under the scene's own lighting, reachable from here — or
|
|
186
|
+
* RAW GLB BYTES (`assetPreview({ glbBase64 }, …)`), for a model that exists
|
|
187
|
+
* only in the calling Node process's memory and has never been written to
|
|
188
|
+
* disk. `stage` defaults to `'lab'`, the neutral Asset Lab staging this has
|
|
189
|
+
* always produced, and the bytes form is lab-only.
|
|
190
|
+
*/
|
|
191
|
+
async assetPreview(source, options) {
|
|
192
|
+
return this.#client.captureAssetPreview(typeof source === 'string' ? { assetPath: source } : source, options);
|
|
170
193
|
}
|
|
171
194
|
async grid(on) {
|
|
172
|
-
await this
|
|
195
|
+
await this.#client.setGrid(on);
|
|
173
196
|
}
|
|
174
197
|
async helpers(on) {
|
|
175
|
-
await this
|
|
198
|
+
await this.#client.setHelpers(on);
|
|
176
199
|
}
|
|
177
200
|
async stats(on) {
|
|
178
|
-
await this
|
|
201
|
+
await this.#client.setStats(on);
|
|
179
202
|
}
|
|
180
203
|
async shading(mode) {
|
|
181
|
-
await this
|
|
204
|
+
await this.#client.setShadingMode(mode);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* READ the inspector, as data — the serialized inspection subject
|
|
208
|
+
* (`editor.inspect()`; design: `docs/ARCHITECTURE-CORE.md` §Editor chrome,
|
|
209
|
+
* "The Inspection Model"). This is the Figma-Inspect analog: whatever a
|
|
210
|
+
* human would see in the inspector right now — the subject's identity, its
|
|
211
|
+
* verbs, and every identified section in display order, with a `fields`
|
|
212
|
+
* section's CURRENT VALUES at their scriptable `path`s.
|
|
213
|
+
*
|
|
214
|
+
* Reach for it whenever the next step depends on what an object actually
|
|
215
|
+
* IS: `await editor.select(id)` then `await editor.inspect()` answers "what
|
|
216
|
+
* properties does this thing have, and what are they set to" in one call,
|
|
217
|
+
* against the same model the panel renders — no scene-graph reads, no
|
|
218
|
+
* guessing at property names.
|
|
219
|
+
*
|
|
220
|
+
* When the inspector is showing NOTHING — nothing selected on a surface
|
|
221
|
+
* with no empty-state subject of its own, which is most of them — the answer
|
|
222
|
+
* is `{none: true}`, so "the human sees no inspector" and "the read failed"
|
|
223
|
+
* are never the same value. A surface whose empty space IS a real thing (an
|
|
224
|
+
* open Asset Lab document) still answers with that subject, and never with
|
|
225
|
+
* another surface's.
|
|
226
|
+
*
|
|
227
|
+
* A `custom` section body is a named opaque: the editor renders it with
|
|
228
|
+
* React, so the wire reports its identity rather than pretending to describe
|
|
229
|
+
* its rendering — plus, when the section can say what it DISPLAYS, a `data`
|
|
230
|
+
* payload in its own vocabulary (`transform` carries
|
|
231
|
+
* `{position, rotation, scale}`, rotation in Euler XYZ degrees).
|
|
232
|
+
*/
|
|
233
|
+
async inspect() {
|
|
234
|
+
return this.#client.inspect();
|
|
182
235
|
}
|
|
183
236
|
/** Mirrors `vgai status` — the full live editor state as JSON. */
|
|
184
237
|
async status() {
|
|
185
|
-
return this
|
|
238
|
+
return this.#client.getState();
|
|
186
239
|
}
|
|
187
240
|
/** A live viewport PNG (`EditorClient.captureViewport`) — no direct CLI verb exists; this is the closest wire read. */
|
|
188
241
|
async screenshot(size) {
|
|
189
|
-
return this
|
|
242
|
+
return this.#client.captureViewport(size);
|
|
190
243
|
}
|
|
191
244
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-3 dogfood friction #2 — a legitimately-progressing script built from
|
|
3
|
+
* many short `game.input.hold()` calls looked WEDGED to anything watching its
|
|
4
|
+
* stdout for liveness, even though it was making real progress. Root cause: `GameInput.hold` (client.ts) collapsed
|
|
5
|
+
* what used to be a `waitSimTime` POLL loop into a single bridge call per
|
|
6
|
+
* hold (see its own doc comment — deliberately, to cut transport round
|
|
7
|
+
* trips), so a spec built from many holds and little else can go silent on
|
|
8
|
+
* stdout between them for minutes at a time.
|
|
9
|
+
*
|
|
10
|
+
* `wait-for.ts`'s `maybeHeartbeat` already solves the analogous problem for
|
|
11
|
+
* the `waitFor`/`waitSimTime` POLL loop, but it is deliberately SIM-TICK
|
|
12
|
+
* aware (a frozen sim clock must stay heartbeat-silent so that loop's own
|
|
13
|
+
* ~30s stall guard, `WAIT_FOR_STALL_POLL_LIMIT`, can still diagnose a
|
|
14
|
+
* genuinely-stuck game rather than a heartbeat papering over it). This is
|
|
15
|
+
* the companion for `GameClient`'s generic bridge dispatch
|
|
16
|
+
* (`callBridge`/`callBridgeAsync`), which has no poll loop and no sim tick
|
|
17
|
+
* to check at all: "wire activity" here just means a bridge call was
|
|
18
|
+
* actually DISPATCHED — sequential awaits mean a new dispatch can only
|
|
19
|
+
* happen once the previous one resolved, so a genuinely wedged page (bridge
|
|
20
|
+
* calls that never resolve at all) still goes — and stays — silent. The
|
|
21
|
+
* throttle exists only to bound
|
|
22
|
+
* stdout volume during a burst of fast calls, not to filter out "fake"
|
|
23
|
+
* activity.
|
|
24
|
+
*/
|
|
25
|
+
export declare const BRIDGE_HEARTBEAT_INTERVAL_MS = 30000;
|
|
26
|
+
/** Mutable-by-replacement bookkeeping a caller threads through successive
|
|
27
|
+
* `maybeBridgeHeartbeat` calls — mirrors `wait-for.ts`'s `HeartbeatState`
|
|
28
|
+
* shape/style, minus the tick field (no sim-progress condition here). */
|
|
29
|
+
export interface BridgeHeartbeatState {
|
|
30
|
+
lastEmitWallMs: number;
|
|
31
|
+
}
|
|
32
|
+
/** The liveness line this prints to stdout — greppable, and prefixed
|
|
33
|
+
* distinctly from `wait-for.ts`'s `vgai-heartbeat` so the two liveness
|
|
34
|
+
* sources are distinguishable in a run's log. */
|
|
35
|
+
export declare function formatBridgeHeartbeatLine(testTitle: string, method: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Pure decision, called once per bridge dispatch: should a heartbeat print
|
|
38
|
+
* now, and what's the updated bookkeeping? Returns the SAME `state` object
|
|
39
|
+
* (referentially) when nothing should emit, so a caller can cheaply no-op.
|
|
40
|
+
*/
|
|
41
|
+
export declare function maybeBridgeHeartbeat(opts: {
|
|
42
|
+
nowMs: number;
|
|
43
|
+
method: string;
|
|
44
|
+
testTitle: string;
|
|
45
|
+
state: BridgeHeartbeatState;
|
|
46
|
+
}): {
|
|
47
|
+
line: string | null;
|
|
48
|
+
state: BridgeHeartbeatState;
|
|
49
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-3 dogfood friction #2 — a legitimately-progressing script built from
|
|
3
|
+
* many short `game.input.hold()` calls looked WEDGED to anything watching its
|
|
4
|
+
* stdout for liveness, even though it was making real progress. Root cause: `GameInput.hold` (client.ts) collapsed
|
|
5
|
+
* what used to be a `waitSimTime` POLL loop into a single bridge call per
|
|
6
|
+
* hold (see its own doc comment — deliberately, to cut transport round
|
|
7
|
+
* trips), so a spec built from many holds and little else can go silent on
|
|
8
|
+
* stdout between them for minutes at a time.
|
|
9
|
+
*
|
|
10
|
+
* `wait-for.ts`'s `maybeHeartbeat` already solves the analogous problem for
|
|
11
|
+
* the `waitFor`/`waitSimTime` POLL loop, but it is deliberately SIM-TICK
|
|
12
|
+
* aware (a frozen sim clock must stay heartbeat-silent so that loop's own
|
|
13
|
+
* ~30s stall guard, `WAIT_FOR_STALL_POLL_LIMIT`, can still diagnose a
|
|
14
|
+
* genuinely-stuck game rather than a heartbeat papering over it). This is
|
|
15
|
+
* the companion for `GameClient`'s generic bridge dispatch
|
|
16
|
+
* (`callBridge`/`callBridgeAsync`), which has no poll loop and no sim tick
|
|
17
|
+
* to check at all: "wire activity" here just means a bridge call was
|
|
18
|
+
* actually DISPATCHED — sequential awaits mean a new dispatch can only
|
|
19
|
+
* happen once the previous one resolved, so a genuinely wedged page (bridge
|
|
20
|
+
* calls that never resolve at all) still goes — and stays — silent. The
|
|
21
|
+
* throttle exists only to bound
|
|
22
|
+
* stdout volume during a burst of fast calls, not to filter out "fake"
|
|
23
|
+
* activity.
|
|
24
|
+
*/
|
|
25
|
+
export const BRIDGE_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
26
|
+
/** The liveness line this prints to stdout — greppable, and prefixed
|
|
27
|
+
* distinctly from `wait-for.ts`'s `vgai-heartbeat` so the two liveness
|
|
28
|
+
* sources are distinguishable in a run's log. */
|
|
29
|
+
export function formatBridgeHeartbeatLine(testTitle, method) {
|
|
30
|
+
return `vgai-bridge-heartbeat ${testTitle} method=${method}`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Pure decision, called once per bridge dispatch: should a heartbeat print
|
|
34
|
+
* now, and what's the updated bookkeeping? Returns the SAME `state` object
|
|
35
|
+
* (referentially) when nothing should emit, so a caller can cheaply no-op.
|
|
36
|
+
*/
|
|
37
|
+
export function maybeBridgeHeartbeat(opts) {
|
|
38
|
+
const wallElapsed = opts.nowMs - opts.state.lastEmitWallMs;
|
|
39
|
+
if (wallElapsed >= BRIDGE_HEARTBEAT_INTERVAL_MS) {
|
|
40
|
+
return {
|
|
41
|
+
line: formatBridgeHeartbeatLine(opts.testTitle, opts.method),
|
|
42
|
+
state: { lastEmitWallMs: opts.nowMs },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return { line: null, state: opts.state };
|
|
46
|
+
}
|