@volter/play 0.5.212 → 0.5.214

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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * A STD LEVEL'S GENERATED FILES, named from its `.blend` as std's level export names them (std-dev's level-names.ts
3
- * `levelNames`, beside `writeGrading`): the files the Scene view's module is written from (`../serving/std-scene.ts`).
3
+ * `levelNames`, beside `writeGrading`): the files a level's module is written from (`../serving/std-scene.ts`).
4
4
  */
5
5
 
6
6
  /** std's PascalCase and camelCase of a level's base name (std-dev's level-names.ts `pascal` and `camel`). */
@@ -31,3 +31,20 @@ export function levelFiles(blend: string): {
31
31
  grading: `${dir}/${base}.grading.tsx`,
32
32
  };
33
33
  }
34
+
35
+ /** A level's Blender file (`levels/<name>.blend`), whose export writes its component. */
36
+ export const BLEND_LEVEL = /^levels\/.+\.blend$/;
37
+
38
+ /** A level in code: `levels/<Name>Level.tsx`, its component the file's export of that name (std's starter's own level),
39
+ * where no level's Blender file in that folder writes it. */
40
+ export const CODE_LEVEL = /^levels\/(?:[^/]+\/)*([A-Z][A-Za-z0-9]*Level)\.tsx$/;
41
+
42
+ /** The level a level's document draws for a file of the game's (from its folder, among `files`, the game's file list): a
43
+ * level's Blender file as itself; a level's component as the Blender file that writes it where one does, else as a
44
+ * level in code; anything else none. */
45
+ export function sceneLevelOf(file: string, files: readonly string[]): string | null {
46
+ if (BLEND_LEVEL.test(file)) return file;
47
+ if (!CODE_LEVEL.test(file)) return null;
48
+ const writer = files.find((other) => BLEND_LEVEL.test(other) && levelFiles(other).component === file);
49
+ return writer ?? file;
50
+ }
package/src/std-level.ts CHANGED
@@ -8,9 +8,9 @@
8
8
  * Blender half, std-dev's `blend-to-scene.py`, is the save's own script (editor-blender's `modelDocument.setSaveScript`):
9
9
  * it runs in the save's turn, on the very scene that save wrote, before anything else reaches that Blender, and the
10
10
  * save's answer carries the files it wrote (`levels/<name>.glb` and what goes beside it). Then, one save after another,
11
- * those files are carried into the project and the Node half, `std level <glb> --description <json>`, runs in the
12
- * game's folder (`../serving/std-dev.ts`, its `/level` route). std's dev server takes the level from there by its own
13
- * hot reload (std-dev's `level-hot.ts`). A save is a person's Save, the workbench's autosave a second after an edit (a
11
+ * those files are carried into the project and the Node half, `std level-scene <glb>`, runs in the game's folder
12
+ * (`../serving/std-dev.ts`, its `/level` route). std's dev server takes the level from there, its rooms reloaded by
13
+ * Colyseus's own dev mode. A save is a person's Save, the workbench's autosave a second after an edit (a
14
14
  * viewport drag writes Blender once, at its release), an export's flush or the save that retires a document.
15
15
  * - THE GAME'S `prefabs/` IS A BLENDER ASSET LIBRARY WHOSE ASSETS ARE LINKED, not appended ("Prefabs": Blender's
16
16
  * link-or-append is the library's import method, which std cannot set).
@@ -64,7 +64,7 @@ async function finishExport(saved: ModelDocumentSaved): Promise<void> {
64
64
  const node = (await answer.json()) as { code?: number | null; output?: string; error?: string };
65
65
  if (!answer.ok) throw new Error(node.error ?? `${ROUTE} answered HTTP ${answer.status}`);
66
66
  if (node.output?.trim()) ledger.log(node.output.trimEnd(), SOURCE);
67
- if (node.code !== 0) ledger.error(`std's export of ${blend} failed in Node (std level ${glb}): its exit was ${String(node.code)}`, SOURCE);
67
+ if (node.code !== 0) ledger.error(`std's export of ${blend} failed in Node (std level-scene ${glb}): its exit was ${String(node.code)}`, SOURCE);
68
68
  }
69
69
 
70
70
  /** Start following the project's saves; the answer stops it and withdraws the save script and the prefabs library. */
@@ -1,12 +1,12 @@
1
1
  /**
2
- * A STD GAME'S VIEW BAR, at the top of the play stage's overlay over the viewport (`std-game.ts`): Edit, each Player N
3
- * labelled "you" or "autopilot", Grid, and Players [-][+], each one a client joining or leaving. A press on it gives the
4
- * keyboard back to the editor, so the view keys work again while a player's page held it. And the grid's tiles, each
2
+ * A STD GAME'S VIEW BAR, at the top of the Game view (`game-view.tsx`, `std-game.ts`): Scene (the current level,
3
+ * live), each Player N labelled "you" or "autopilot", and Grid (how many play is the Game panel's Players, beside Play). A press on it gives
4
+ * the keyboard back to the editor, so the view keys work again while a player's page held it. And the grid's tiles, each
5
5
  * a button that opens its view. Drawn with the play area's own widget (`PlayAreaButton`).
6
6
  */
7
7
  import { useSyncExternalStore } from 'react';
8
8
  import { editorBlender } from './editor-blender';
9
- import { addStdPlayer, removeStdPlayer, setStdView, stdRun, stdRunVersion, stdTile, subscribeStdRun, type StdPlayer, type StdRun } from './std-game';
9
+ import { setStdView, stdRun, stdRunVersion, stdTile, subscribeStdRun, type StdPlayer, type StdRun } from './std-game';
10
10
 
11
11
  function useRun(): StdRun | null {
12
12
  useSyncExternalStore(subscribeStdRun, stdRunVersion, stdRunVersion);
@@ -32,8 +32,8 @@ export function StdViewBar() {
32
32
  padding: '2px 6px', pointerEvents: 'auto', background: TIMELINE_THEME.header, color: TIMELINE_CHROME.text,
33
33
  border: `1px solid ${TIMELINE_CHROME.rule}`, borderRadius: TIMELINE_CHROME.widgetRadius, font: '11px Inter, system-ui, sans-serif' }}>
34
34
  <div style={{ display: 'flex' }} role="group" aria-label="View">
35
- <PlayAreaButton testId="std-view-edit" label="Edit the level while the game runs (Escape)" pressed={view === 'edit'} wide onClick={() => setStdView('edit')}>
36
- Edit
35
+ <PlayAreaButton testId="std-view-scene" label="Scene: the current level, live" pressed={view === 'scene'} wide onClick={() => setStdView('scene')}>
36
+ Scene
37
37
  </PlayAreaButton>
38
38
  {players.map((player, index) => (
39
39
  <PlayAreaButton key={player.slot} testId={`std-view-player-${player.number}`} wide pressed={view === index}
@@ -45,16 +45,6 @@ export function StdViewBar() {
45
45
  Grid
46
46
  </PlayAreaButton>
47
47
  </div>
48
- <div style={{ display: 'flex', alignItems: 'center' }} role="group" aria-label="Players">
49
- <span style={{ padding: '0 4px' }}>Players</span>
50
- <PlayAreaButton testId="std-players-remove" label="One player fewer: the last one's client leaves" disabled={players.length <= 1} onClick={removeStdPlayer}>
51
- −
52
- </PlayAreaButton>
53
- <span data-testid="std-players-count" style={{ minWidth: 14, textAlign: 'center' }}>{players.length}</span>
54
- <PlayAreaButton testId="std-players-add" label="One more player: a client joins" disabled={run.url === null} onClick={addStdPlayer}>
55
- +
56
- </PlayAreaButton>
57
- </div>
58
48
  </div>
59
49
  );
60
50
  }
@@ -64,7 +54,7 @@ export function StdGridTiles() {
64
54
  if (!run || run.view !== 'grid') return null;
65
55
  const { timelineGeometry: { TIMELINE_CHROME } } = editorBlender();
66
56
  const count = run.players.length + 1;
67
- const tiles = [{ label: 'Edit', open: 'edit' as const }, ...run.players.map((player, index) => ({ label: `Player ${player.number} · ${driver(run, player, index)}`, open: index }))];
57
+ const tiles = [{ label: 'Scene', open: 'scene' as const }, ...run.players.map((player, index) => ({ label: `Player ${player.number} · ${driver(run, player, index)}`, open: index }))];
68
58
  return tiles.map(({ label, open }, tile) => {
69
59
  const box = stdTile(tile, count);
70
60
  return <button key={label} type="button" data-testid={`std-tile-${tile}`} title={`Open ${label}`} onClick={() => setStdView(open)}
@@ -1,44 +0,0 @@
1
- /**
2
- * THE MODEL PLAY VERBS of the session wire (`@volter/sdk/commands`): each the Code-OSS command `volter.<verb>`, registered
3
- * by play's page half (`play.page.tsx`), with its budget on its `contributes.commands` entry.
4
- *
5
- * `model-play-log`, the read behind `cyclotron play-log` and `editor.contributedVerb('model-play-log')` in `eval`, answers one model
6
- * document's play log (`../src/play-log.ts`) — `documentId`'s, or the active Play's when omitted; the current run's, or
7
- * the last one's after Stop — filtered by `since` (game seconds, inclusive) and `kind`. A read only: no play gate,
8
- * since an empty log is the honest answer when nothing has played.
9
- *
10
- * `model-play`, the agent's tool for Play (declared in this package's `contributes.languageModelTools`), runs the Game
11
- * panel's own view verbs, the ones `volter.model-play.<action>` runs (`game-panel.tsx`), so an agent drives the run the
12
- * person sees and answers with the game's state.
13
- */
14
- import type { VerbHandler } from '@volter/sdk/commands';
15
- import { executeCommand } from '@volter/sdk/editor-commands';
16
- import { readModelPlayLog } from '../src/play-log';
17
-
18
- const ACTIONS = ['state', 'play', 'stop', 'pause', 'resume', 'restart'] as const;
19
-
20
- export const modelPlayVerbs: Readonly<Record<string, VerbHandler>> = {
21
- 'model-play': async (command) => {
22
- const action = command['action'];
23
- if (typeof action !== 'string' || !(ACTIONS as readonly string[]).includes(action))
24
- return { ok: false, error: `model-play's action is one of ${ACTIONS.join(', ')}; it was given ${JSON.stringify(action)}.` };
25
- return { ok: true, data: { result: (await executeCommand(`volter.model-play.${action}`, {})) ?? null } };
26
- },
27
- 'model-play-log': (command) => {
28
- const since = command['since'];
29
- const kind = command['kind'];
30
- const documentId = command['documentId'];
31
- if (documentId !== undefined && typeof documentId !== 'string')
32
- return { ok: false, error: `model-play-log's documentId is a model document id, a string; it was given ${JSON.stringify(documentId)}.` };
33
- if (since !== undefined && (typeof since !== 'number' || !Number.isFinite(since)))
34
- return { ok: false, error: `model-play-log's since is game seconds, a number; it was given ${JSON.stringify(since)}.` };
35
- if (kind !== undefined && typeof kind !== 'string')
36
- return { ok: false, error: `model-play-log's kind is an entry kind, a string; it was given ${JSON.stringify(kind)}.` };
37
- const reading = readModelPlayLog({
38
- ...(documentId === undefined ? {} : { documentId }),
39
- ...(since === undefined ? {} : { since }),
40
- ...(kind === undefined ? {} : { kind }),
41
- });
42
- return { ok: true, data: { ...reading } };
43
- },
44
- };
package/src/model-play.ts DELETED
@@ -1,149 +0,0 @@
1
- /**
2
- * WHETHER A MODEL DOCUMENT IS PLAYING (Play in the Game panel, `contributions/game-panel.tsx`; drawn by
3
- * `blender-runtime.document.tsx`). Playing, the document's stage shows the std game's players' pages (`std-game.ts`);
4
- * the model, its selection and its history stand as they were, and Stop returns to them. Kept for the page's life and
5
- * never stored: a reload opens the model, not a game.
6
- *
7
- * AND ITS CLOCK, AS STD LAST ANSWERED IT. Pause, step and speed are std's `clock` control on each player's page
8
- * (`std-controls.ts`); each answer is the page's clock (`{ paused, speed, time, frame }`), kept here per document
9
- * beside whether a game runs and why not, and announced through the clock's own subscription. The speed is the
10
- * person's: kept across Plays, and said to each player's page as it joins.
11
- *
12
- * AND AUTOPLAY ARMED WHILE STOPPED: the next start drives the viewed player's bot as its page joins, with the goal it
13
- * was armed with (`std-game.ts`). Stop drops the arm.
14
- */
15
- const listeners = new Set<() => void>();
16
- const clockListeners = new Set<() => void>();
17
- const playing = new Set<string>();
18
-
19
- /** The speeds the transport offers, slowest first: std's clock takes 0.25 to 4. */
20
- export const MODEL_PLAY_SPEEDS: readonly number[] = [0.25, 0.5, 1, 2, 4];
21
-
22
- export interface ModelPlayClock {
23
- /** The viewed player's page's game seconds and frames, as std last answered them. */
24
- readonly time: number;
25
- readonly tick: number;
26
- readonly paused: boolean;
27
- readonly speed: number;
28
- /** The run is playing but no game runs: its dev server could not start, or no player's page joined. */
29
- readonly failure: string | null;
30
- /** A player's page has joined this run (`DocumentPlayClock.running`). */
31
- readonly running: boolean;
32
- }
33
- const STILL: ModelPlayClock = { time: 0, tick: 0, paused: false, speed: 1, failure: null, running: false };
34
- /** Replaced, never mutated, so a snapshot read by `useSyncExternalStore` changes identity exactly when it changes
35
- * value. */
36
- const clocks = new Map<string, ModelPlayClock>();
37
- /** Bumped by Restart; the document keys its detached copy on it (`DocumentPlayTransport`). */
38
- const generations = new Map<string, number>();
39
- /** Autoplay armed while stopped, with the goal it drives (null: the game's own). */
40
- const arms = new Map<string, { readonly goal: string | null }>();
41
-
42
- function publish(): void {
43
- for (const listener of [...listeners]) listener();
44
- }
45
- export function publishModelPlayClock(): void {
46
- for (const listener of [...clockListeners]) listener();
47
- }
48
- function setClock(documentId: string, next: Partial<ModelPlayClock>): void {
49
- clocks.set(documentId, { ...modelPlayClock(documentId), ...next });
50
- publishModelPlayClock();
51
- }
52
-
53
- export function modelPlaying(documentId: string): boolean {
54
- return playing.has(documentId);
55
- }
56
-
57
- export function setModelPlaying(documentId: string, value: boolean): void {
58
- if (playing.has(documentId) === value) {
59
- // Stop with nothing playing is how a closing document lets go of its play: an arm made while stopped goes with it.
60
- if (!value && arms.delete(documentId)) publishModelPlayClock();
61
- return;
62
- }
63
- if (value) {
64
- playing.add(documentId);
65
- // A new run's clock starts at zero and running; the speed is the person's and stays.
66
- setClock(documentId, { time: 0, tick: 0, paused: false, failure: null, running: false });
67
- } else {
68
- playing.delete(documentId);
69
- arms.delete(documentId);
70
- // The clock keeps the stopped run's time and frames, so the panel still says how far it got.
71
- setClock(documentId, { paused: false, failure: null, running: false });
72
- }
73
- publish();
74
- }
75
-
76
- /** The run's report that no game runs although the run plays (`null` once a player's page joins, which is also the
77
- * moment the run is `running`). */
78
- export function setModelPlayFailure(documentId: string, failure: string | null): void {
79
- const clock = modelPlayClock(documentId);
80
- const running = failure === null;
81
- if (!playing.has(documentId) || (clock.failure === failure && clock.running === running)) return;
82
- setClock(documentId, { failure, running });
83
- }
84
-
85
- /** A page's clock as std answered it (`std-controls.ts`'s `StdClock`), kept as the run's. */
86
- export function setModelPlayClock(documentId: string, clock: { readonly paused: boolean; readonly speed: number; readonly time: number; readonly frame: number }): void {
87
- const now = modelPlayClock(documentId);
88
- if (now.paused === clock.paused && now.speed === clock.speed && now.time === clock.time && now.tick === clock.frame) return;
89
- setClock(documentId, { paused: clock.paused, speed: clock.speed, time: clock.time, tick: clock.frame });
90
- }
91
-
92
- /** The person's speed, kept for the page's life and said to each player's page as it joins. */
93
- export function setModelPlaySpeed(documentId: string, speed: number): void {
94
- if (!MODEL_PLAY_SPEEDS.includes(speed))
95
- throw new Error(`Play speed ${speed} is not one the transport offers: ${MODEL_PLAY_SPEEDS.join(', ')}.`);
96
- if (modelPlayClock(documentId).speed !== speed) setClock(documentId, { speed });
97
- }
98
-
99
- export function subscribeModelPlay(listener: () => void): () => void {
100
- listeners.add(listener);
101
- return () => { listeners.delete(listener); };
102
- }
103
-
104
- export function modelPlayClock(documentId: string): ModelPlayClock {
105
- return clocks.get(documentId) ?? STILL;
106
- }
107
-
108
- export function subscribeModelPlayClock(listener: () => void): () => void {
109
- clockListeners.add(listener);
110
- return () => { clockListeners.delete(listener); };
111
- }
112
-
113
- export function modelPlayGeneration(documentId: string): number {
114
- return generations.get(documentId) ?? 0;
115
- }
116
-
117
- /**
118
- * BEGIN THE RUN AGAIN. Playing stays true throughout (nothing returns to the model) and the generation moves, which is
119
- * what makes the document detach a new copy and run Play's runner again: every player's page loaded afresh. A document
120
- * that is not playing simply starts.
121
- */
122
- export function restartModelPlay(documentId: string): void {
123
- if (!playing.has(documentId)) { setModelPlaying(documentId, true); return; }
124
- generations.set(documentId, modelPlayGeneration(documentId) + 1);
125
- setClock(documentId, { time: 0, tick: 0, paused: false, failure: null, running: false });
126
- publish();
127
- }
128
-
129
- /** Autoplay armed for the next start, and the goal it drives; null when it is not armed. */
130
- export function modelPlayArm(documentId: string): { readonly goal: string | null } | null {
131
- return arms.get(documentId) ?? null;
132
- }
133
-
134
- /** Arm (or disarm) autoplay for the next start, while stopped: the Game panel's Autoplay button before Play. Disarming
135
- * always succeeds. */
136
- export function armModelPlayAutoplay(documentId: string, armed: boolean, goal: string | null = null): void {
137
- if (armed && playing.has(documentId))
138
- throw new Error(`${documentId} is already playing; switch autoplay on instead of arming it.`);
139
- if (armed) arms.set(documentId, { goal });
140
- else if (!arms.delete(documentId)) return;
141
- publishModelPlayClock();
142
- }
143
-
144
- /** The arm taken by the start it was made for. */
145
- export function takeModelPlayArm(documentId: string): { readonly goal: string | null } | null {
146
- const arm = arms.get(documentId) ?? null;
147
- if (arm !== null) { arms.delete(documentId); publishModelPlayClock(); }
148
- return arm;
149
- }
@@ -1,59 +0,0 @@
1
- /**
2
- * THE LEVEL A STD GAME'S SCENE VIEW DRAWS (`../contributions/std-scene.document.tsx`): the std level whose Blender
3
- * document was active last (`levels/<name>.blend`), or the one `volter.std-scene` named as it opened the view. Another
4
- * project forgets it.
5
- */
6
- import type { EditorCommandMessage, EditorCommandResult } from '@volter/sdk/commands';
7
- import { onProjectChange } from '@volter/sdk/active-project';
8
- import { editorHost } from '@volter/sdk/host';
9
- import { isStdGame, subscribeStdGame } from './std-game';
10
-
11
- /** The Scene view's document contribution. */
12
- export const STD_SCENE_DOCUMENT = 'std-scene.document';
13
- const LEVEL = /^levels\/.+\.blend$/;
14
- const LEVEL_DOCUMENT = /^document:model:(levels\/.+\.blend)$/;
15
-
16
- let level: string | null = null;
17
- let version = 0;
18
- const listeners = new Set<() => void>();
19
- const publish = (next: string | null): void => {
20
- if (next === level) return;
21
- level = next;
22
- version += 1;
23
- for (const listener of [...listeners]) listener();
24
- };
25
-
26
- export const sceneLevel = (): string | null => level;
27
- export const sceneLevelVersion = (): number => version;
28
- export function subscribeSceneLevel(listener: () => void): () => void {
29
- listeners.add(listener);
30
- return () => { listeners.delete(listener); };
31
- }
32
-
33
- /** Follow the active document: a std level's Blender document makes its level the Scene view's. */
34
- export function followSceneLevel(): () => void {
35
- const look = (): void => {
36
- const found = LEVEL_DOCUMENT.exec(editorHost().documents.activeId() ?? '')?.[1];
37
- if (found !== undefined && isStdGame() === true) publish(found);
38
- };
39
- const stopDocuments = editorHost().documents.subscribe(look);
40
- // (whether the project is a std game is answered later than its first document opens)
41
- const stopStd = subscribeStdGame(look);
42
- const stopProject = onProjectChange(() => publish(null));
43
- look();
44
- return () => { stopDocuments(); stopStd(); stopProject(); };
45
- }
46
-
47
- /** `volter.std-scene { level? }`: open the Scene view on `level` (a `levels/<name>.blend`), or on the level it draws. */
48
- export async function openStdScene(command: EditorCommandMessage): Promise<EditorCommandResult> {
49
- if (isStdGame() !== true) return { ok: false, error: 'The open project is not a std game (no std.json), so it has no Scene view.' };
50
- const asked = command['level'];
51
- if (asked !== undefined && (typeof asked !== 'string' || !LEVEL.test(asked))) {
52
- return { ok: false, error: `std-scene opens a level's Blender file, levels/<name>.blend; it was given ${JSON.stringify(asked)}.` };
53
- }
54
- const at = asked ?? level;
55
- if (at === null) return { ok: false, error: "No std level is open: open a level's .blend (levels/<name>.blend), or name one as { level }." };
56
- publish(at);
57
- if (!(await editorHost().workspace.open({ kind: 'tool', id: STD_SCENE_DOCUMENT }))) return { ok: false, error: `${STD_SCENE_DOCUMENT} is not registered.` };
58
- return { ok: true, data: { level: at } };
59
- }