@volter/play 0.5.211 → 0.5.213

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.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A STD GAME'S LEVELS AS ITS DOCUMENTS: each level component, `levels/<Name>Level.tsx`, is a document of its own
3
+ * (`std-level`, drawn by `std-level.document.tsx` as the game draws it), named as its component is less `Level`, as a level is the
4
+ * document an engine opens. Its source is its record: its own code, or the `levels/<name>.blend` std's export writes
5
+ * it from, as a model's `.blend` is the source of the component the game places. The project opens on its main level.
6
+ */
7
+ import { z } from 'zod';
8
+ import { CODE_LEVEL, sceneLevelOf } from '../src/std-level-files';
9
+
10
+ export const finder = {
11
+ name: 'stdLevelDocuments',
12
+ // (paths only: whether a component is a Blender level's is read from the files beside it)
13
+ reads: 'files',
14
+ schema: z
15
+ .object({
16
+ finder: z.literal('stdLevelDocuments'),
17
+ include: z
18
+ .array(z.string().min(1))
19
+ .min(1)
20
+ .describe("Project-relative globs of the levels' files (`levels/**/*.tsx`, and `levels/**/*.blend` to know which components a Blender level writes)"),
21
+ })
22
+ .strict(),
23
+ run(_selection: { finder: 'stdLevelDocuments'; include: readonly string[] }, input: { files?: readonly string[] }) {
24
+ const files = input.files ?? [];
25
+ const entries = files
26
+ .filter((path) => CODE_LEVEL.test(path))
27
+ .map((path) => ({
28
+ id: `level:${path}`,
29
+ // (named as an engine names its levels: `MainLevel` is Main)
30
+ label: CODE_LEVEL.exec(path)?.[1]?.replace(/Level$/, '') ?? path,
31
+ kind: 'std-level',
32
+ region: null,
33
+ authorable: false,
34
+ reach: { kind: 'root-mount' as const },
35
+ // (the level's source: a Blender level's `.blend`, else its own code)
36
+ source: { path: sceneLevelOf(path, files) ?? path },
37
+ }));
38
+ // (the project opens on its level, as an engine opens its main level: the game's Main, else the level last worked
39
+ // on, the files coming newest first)
40
+ const opened = entries.find((entry) => entry.label === 'Main') ?? entries[0];
41
+ return { entries, notes: [] as string[], ...(opened === undefined ? {} : { default: opened.id }) };
42
+ },
43
+ } as const;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * PLAY'S PAGE HALF (`contributes.volter.page`, `docs/adr/plugin-apis.md`): Play, the project's. It registers its Game
3
+ * panel through editor-blender's API (`registerPlay`, whose bottom area shows it), which play declares in
4
+ * `extensionDependencies`, and keeps the project's run (`../src/game-view.tsx`): Play runs the project's volter-ai/std
5
+ * game whatever document is in front, its own dev pages each a player's (`../src/std-game.ts`), in the Game view.
6
+ */
7
+ import type { BlenderPageApi, PlayTool } from '@volter/editor-blender/api';
8
+ import { registerVerbCommands, registerViewCommands } from '@volter/sdk/commands';
9
+ import type { PluginContext } from '@volter/sdk/plugins';
10
+ import { onProjectChange } from '@volter/sdk/active-project';
11
+ import { editorHost } from '@volter/sdk/host';
12
+ import type { ThreePageApi } from '@volter/editor-threejs/api';
13
+ import { setGameFailure, subscribeGame } from '../src/game-run';
14
+ import { keepEditSave, savedEdit } from '../src/game-controls';
15
+ import { type GameRunner, keepGameRuns } from '../src/game-view';
16
+ import { beginPlayLog } from '../src/play-log';
17
+ import { isStdGame, runStdGame, STD_DEV, stdGameNow, subscribeStdGame } from '../src/std-game';
18
+ import { keepEditorBlender } from '../src/editor-blender';
19
+ import { keepEditorThreejs } from '../src/editor-threejs';
20
+ import { startStdLevels, stdLevelsSettled } from '../src/std-level';
21
+ import { GamePanel, PLAY_VIEW_VERBS } from './game-panel';
22
+ import { playVerbs } from './play.verbs';
23
+
24
+ const NOT_STD = 'This project is not a std game: it has no std.json, or no @volter-ai/std-dev among its packages (`cyclotron add-play` makes it one).';
25
+
26
+ const EDITOR_BLENDER = 'volter.@volter/editor-blender';
27
+ const EDITOR_THREEJS = 'volter.@volter/editor-threejs';
28
+
29
+ /** The game run in the Game view's stage, once the edit Play saved first has saved (and exported). */
30
+ const runGame: GameRunner = (stage) => {
31
+ const failed = (title: string, failure: string) => {
32
+ const run = beginPlayLog(STD_DEV);
33
+ run.append('play', 'script-error', { phase: 'stage', message: `${title}: ${failure}` });
34
+ run.end({ reason: 'stage-failed' });
35
+ setGameFailure(`${title}: ${failure}`);
36
+ stage.report(title, failure);
37
+ };
38
+ const saved = savedEdit().then((failure) => {
39
+ if (failure !== null) editorHost().console.warn(`Play's save failed, so the game plays its last export: ${failure}`, 'play');
40
+ });
41
+ // (asked now: a project add-play has just made a std game plays without a reload)
42
+ let stopped = false, stop: (() => void) | null = null;
43
+ void stdGameNow().then((std) => {
44
+ if (stopped) return;
45
+ if (std) stop = runStdGame(stage, failed, saved);
46
+ else failed('The game could not be started', NOT_STD);
47
+ });
48
+ return () => { stopped = true; stop?.(); };
49
+ };
50
+
51
+ export function activate(context: PluginContext) {
52
+ // Declared in `extensionDependencies`, so it is active before this runs.
53
+ const blender = context.extensions.getExtension<BlenderPageApi>(EDITOR_BLENDER)?.exports;
54
+ if (!blender) throw new Error(`${EDITOR_BLENDER} answered no API.`);
55
+ // (the edit Play saves first: the `.blend` Blender holds, whichever document is in front, and std's export of it)
56
+ keepEditSave(() => {
57
+ const held = blender.modelDocument.held();
58
+ return (held === null ? Promise.resolve() : blender.modelDocument.flush(held))
59
+ .then(() => stdLevelsSettled())
60
+ .then(() => null, (error: unknown) => error instanceof Error ? error.message : String(error));
61
+ });
62
+ const tool: PlayTool = {
63
+ Panel: GamePanel,
64
+ hasGame: () => isStdGame() ?? null,
65
+ subscribe(listener) {
66
+ const stopGame = subscribeGame(listener), stopProject = onProjectChange(listener), stopStd = subscribeStdGame(listener);
67
+ return () => { stopGame(); stopProject(); stopStd(); };
68
+ },
69
+ };
70
+ keepEditorBlender(blender);
71
+ // (a std level in code is drawn in editor-threejs's viewport, `std-level.document.tsx`; declared in `extensionDependencies`)
72
+ const three = context.extensions.getExtension<ThreePageApi>(EDITOR_THREEJS)?.exports;
73
+ if (!three) throw new Error(`${EDITOR_THREEJS} answered no API.`);
74
+ keepEditorThreejs(three);
75
+ const unregister = blender.registerPlay(tool);
76
+ // (the project's run: Play opens the Game view and runs the game there, whatever document is in front)
77
+ const stopGameRuns = keepGameRuns(runGame);
78
+ // A std game's levels exported on save, and its prefabs a linked asset library (`std-level.ts`).
79
+ const stopStdLevels = startStdLevels(blender);
80
+ // The session verbs and the Game panel's verbs, as the Code-OSS commands package.json declares.
81
+ registerVerbCommands(context, playVerbs);
82
+ registerViewCommands(context, 'play', PLAY_VIEW_VERBS);
83
+ context.subscriptions.push({
84
+ dispose() {
85
+ stopGameRuns();
86
+ unregister();
87
+ stopStdLevels();
88
+ keepEditSave(null);
89
+ keepEditorBlender(null);
90
+ keepEditorThreejs(null);
91
+ },
92
+ });
93
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * THE PLAY VERBS of the session wire (`@volter/sdk/commands`): each the Code-OSS command `volter.<verb>`, registered by
3
+ * play's page half (`play.page.tsx`), with its budget on its `contributes.commands` entry.
4
+ *
5
+ * `play-log`, the read behind `cyclotron play-log` and `editor.contributedVerb('play-log')` in `eval`, answers the game's
6
+ * play log (`../src/play-log.ts`) — the current run's, or the last one's after Stop — filtered by `since` (game seconds,
7
+ * inclusive) and `kind`. A read only: no play gate, since an empty log is the honest answer when nothing has played.
8
+ *
9
+ * `play`, the agent's tool for Play (declared in this package's `contributes.languageModelTools`), runs the Game panel's
10
+ * own view verbs, the ones `volter.play.<action>` runs (`game-panel.tsx`), so an agent drives the run the person sees
11
+ * and answers with the game's state. Its `play` and `restart` answer as `cyclotron play play` does: once a player's page
12
+ * has joined the run, or with why the game didn't start (`started`), never at "playing" alone.
13
+ */
14
+ import type { VerbHandler } from '@volter/sdk/commands';
15
+ import { executeCommand } from '@volter/sdk/editor-commands';
16
+ import { readPlayLog } from '../src/play-log';
17
+ import { gameClock, gamePlaying, subscribeGame, subscribeGameClock } from '../src/game-run';
18
+
19
+ const ACTIONS = ['state', 'play', 'stop', 'pause', 'resume', 'restart'] as const;
20
+
21
+ /** The actions that start a run, which answer once it runs. */
22
+ const STARTS: ReadonlySet<string> = new Set(['play', 'restart']);
23
+
24
+ /**
25
+ * THE RUN STARTED, OR WHY NOT: resolved once a player's page has joined it (`GameClock.running`), rejected with the
26
+ * run's failure or with Play switched off first, as the CLI's `play` reads the run. No deadline of its own: the run
27
+ * starts on drawn frames, which a hidden tab doesn't draw, so a healthy start can take as long as the tab stays hidden;
28
+ * a relayed call (`eval`, the limited view) ends at `volter.play`'s budget, its `timeoutMs`.
29
+ */
30
+ function started(): Promise<void> {
31
+ return new Promise((resolve, reject) => {
32
+ const stops: (() => void)[] = [];
33
+ const settle = (): void => {
34
+ const clock = gameClock();
35
+ const failure = clock.failure !== null && clock.failure !== '' ? clock.failure : null;
36
+ if (failure === null && gamePlaying() && !clock.running) return;
37
+ for (const stop of stops) stop();
38
+ if (failure !== null) reject(new Error(`Play did not start: ${failure}. The play log has the entry (play-log, kind script-error).`));
39
+ else if (!gamePlaying()) reject(new Error('Play switched itself off before a game started.'));
40
+ else resolve();
41
+ };
42
+ stops.push(subscribeGame(settle), subscribeGameClock(settle));
43
+ settle();
44
+ });
45
+ }
46
+
47
+ export const playVerbs: Readonly<Record<string, VerbHandler>> = {
48
+ play: async (command) => {
49
+ const action = command['action'];
50
+ if (typeof action !== 'string' || !(ACTIONS as readonly string[]).includes(action))
51
+ return { ok: false, error: `play's action is one of ${ACTIONS.join(', ')}; it was given ${JSON.stringify(action)}.` };
52
+ const answer = await executeCommand(`volter.play.${action}`, {});
53
+ if (!STARTS.has(action)) return { ok: true, data: { result: answer ?? null } };
54
+ try {
55
+ await started();
56
+ } catch (error) {
57
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
58
+ }
59
+ return { ok: true, data: { result: (await executeCommand('volter.play.state', {})) ?? null } };
60
+ },
61
+ 'play-log': (command) => {
62
+ const since = command['since'];
63
+ const kind = command['kind'];
64
+ if (since !== undefined && (typeof since !== 'number' || !Number.isFinite(since)))
65
+ return { ok: false, error: `play-log's since is game seconds, a number; it was given ${JSON.stringify(since)}.` };
66
+ if (kind !== undefined && typeof kind !== 'string')
67
+ return { ok: false, error: `play-log's kind is an entry kind, a string; it was given ${JSON.stringify(kind)}.` };
68
+ const reading = readPlayLog({
69
+ ...(since === undefined ? {} : { since }),
70
+ ...(kind === undefined ? {} : { kind }),
71
+ });
72
+ return { ok: true, data: { ...reading } };
73
+ },
74
+ };