@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.
package/src/play-log.ts CHANGED
@@ -1,34 +1,33 @@
1
1
  /**
2
- * THE MODEL PLAY LOG: what a std game's run says happened.
2
+ * THE PLAY LOG: what the game's run says happened.
3
3
  *
4
4
  * Each player's page traces its run (std's trace, as the dev server's receiver writes it, `build/runs/<run>/<page>.jsonl`)
5
5
  * and the run reads each trace into this log, each entry tagged with its player's slot (`std-game.ts`, `source:
6
6
  * 'script'`). Play adds its own lifecycle entries (`source: 'play'`): `play-start`, `running`, `player-joined`,
7
7
  * `player-page-loaded`, `player-left`, `view`, `autoplay-on` and `autoplay-off` (each with `by`), `pause`, `resume`,
8
8
  * `step` and `speed` (each with the clock std answered), `capture` (its file), `script-error` (with its `phase`) and
9
- * `play-stop`. An agent reads them back with `cyclotron play-log` or `editor.contributedVerb('model-play-log')` in
9
+ * `play-stop`. An agent reads them back with `cyclotron play-log` or `editor.contributedVerb('play-log')` in
10
10
  * `eval`: the session's console feed keeps warnings and errors only.
11
11
  *
12
12
  * WHEN EACH HAPPENED: a trace entry carries its page's own time, its trace's step (std's trace format 3: `s`, sixty a
13
- * second of the page's steady clock) as `tick` and that step in seconds as `simT`; Play's own entries carry the run's
14
- * elapsed seconds since its `play-start` as `simT`, and the newest trace step heard as `tick`. So `--since` reads the
15
- * run's seconds, a page's from its own start.
13
+ * second of the page's game clock, its `performance.now` on a live page) as `tick` and that step in seconds as `simT`;
14
+ * Play's own entries carry the run's elapsed seconds since its `play-start` as `simT`, and the newest trace step heard
15
+ * as `tick`. So `--since` reads the run's seconds, a page's from its own start.
16
16
  *
17
- * ONE LOG PER MODEL DOCUMENT, each its document's latest run. A read names its document or takes the active Play's
18
- * (the latest started that still plays, else the latest started). Each Play replaces its document's log, and the log
19
- * outlives Stop so a finished run can be read. The run writes through the {@link ModelPlayRun} handle Play started:
20
- * once that run has ended its handle writes nothing, so a stale timer of an old run cannot reach the next run's log.
17
+ * ONE LOG, the latest run's. Each Play replaces it, and it outlives Stop so a finished run can be read. The run writes
18
+ * through the {@link PlayLogRun} handle Play started: once that run has ended its handle writes nothing, so a stale
19
+ * timer of an old run cannot reach the next run's log.
21
20
  *
22
- * BOUNDED AND PAGE-LIFETIME. The newest {@link MODEL_PLAY_LOG_CAPACITY} entries of a run are kept in a ring (older
23
- * ones are counted in `dropped`); one entry's facts are kept as JSON of at most {@link MODEL_PLAY_LOG_FACTS_CHARS}
21
+ * BOUNDED AND PAGE-LIFETIME. The newest {@link PLAY_LOG_CAPACITY} entries of a run are kept in a ring (older
22
+ * ones are counted in `dropped`); one entry's facts are kept as JSON of at most {@link PLAY_LOG_FACTS_CHARS}
24
23
  * characters. It is never stored: a reload of the page forgets it, as it forgets the run.
25
24
  *
26
- * THE GAME PANEL DRAWS IT LIVE: {@link subscribeModelPlayLog} hears every write, and {@link tailModelPlayLog} reads the
27
- * newest entries of one document without copying the ring.
25
+ * THE GAME PANEL DRAWS IT LIVE: {@link subscribePlayLog} hears every write, and {@link tailPlayLog} reads the newest
26
+ * entries without copying the ring.
28
27
  */
29
28
 
30
- /** One entry of a document's play log (`model-play-log`): read from a player's page's trace, or written by Play. */
31
- export interface ModelPlayLogEntry {
29
+ /** One entry of the play log (`play-log`): read from a player's page's trace, or written by Play. */
30
+ export interface PlayLogEntry {
32
31
  /** The entry's place in its run, from 0: a gap in `seq` is entries the bounded log dropped. */
33
32
  readonly seq: number;
34
33
  /** Wall-clock ms. */
@@ -44,19 +43,16 @@ export interface ModelPlayLogEntry {
44
43
  }
45
44
 
46
45
  /** Entries a run's ring keeps; the oldest beyond this are dropped and counted. */
47
- const MODEL_PLAY_LOG_CAPACITY = 5000;
46
+ const PLAY_LOG_CAPACITY = 5000;
48
47
  /** The longest JSON one entry's facts keep; longer facts are kept truncated, as a string. */
49
- const MODEL_PLAY_LOG_FACTS_CHARS = 2048;
48
+ const PLAY_LOG_FACTS_CHARS = 2048;
50
49
 
51
- export interface ModelPlayLogReading {
50
+ export interface PlayLogReading {
52
51
  /** The run is still playing; false after Stop, or when nothing has played. */
53
52
  readonly playing: boolean;
54
- /** The model document and what its run ran (the game's dev server command); null when nothing has played. */
55
- readonly documentId: string | null;
53
+ /** What the run ran (the game's dev server command); null when nothing has played. */
56
54
  readonly script: string | null;
57
55
  readonly startedAt: number | null;
58
- /** Every model document with a log, so a read can name another. */
59
- readonly documents: readonly string[];
60
56
  /** The run's clock now. */
61
57
  readonly simT: number;
62
58
  readonly tick: number;
@@ -65,12 +61,10 @@ export interface ModelPlayLogReading {
65
61
  readonly total: number;
66
62
  readonly dropped: number;
67
63
  /** The kept entries that match the read's filters, oldest first. */
68
- readonly entries: readonly ModelPlayLogEntry[];
64
+ readonly entries: readonly PlayLogEntry[];
69
65
  }
70
66
 
71
- export interface ModelPlayLogQuery {
72
- /** The model document whose log to read; the active Play's when omitted. */
73
- readonly documentId?: string;
67
+ export interface PlayLogQuery {
74
68
  /** Only entries at or after this game time (seconds). */
75
69
  readonly since?: number;
76
70
  /** Only entries of this kind. */
@@ -78,32 +72,31 @@ export interface ModelPlayLogQuery {
78
72
  }
79
73
 
80
74
  interface Run {
81
- readonly documentId: string;
82
75
  readonly script: string;
83
76
  readonly startedAt: number;
84
77
  playing: boolean;
85
78
  simT: number;
86
79
  tick: number;
87
- readonly ring: ModelPlayLogEntry[];
80
+ readonly ring: PlayLogEntry[];
88
81
  total: number;
89
82
  /** Every kind this run has written, for the panel's filter. */
90
83
  readonly kinds: Set<string>;
91
84
  }
92
85
 
93
86
  /** The run's door onto its own log: inert once the run has ended. */
94
- export interface ModelPlayRun {
87
+ export interface PlayLogRun {
95
88
  /** An entry, stamped with `at` (a trace line's page time) or else the run's elapsed seconds. */
96
- append(source: ModelPlayLogEntry['source'], kind: string, facts?: unknown, at?: { readonly simT: number; readonly tick: number }): void;
89
+ append(source: PlayLogEntry['source'], kind: string, facts?: unknown, at?: { readonly simT: number; readonly tick: number }): void;
97
90
  /** `play-stop`, then nothing more is written; the entries stay readable. */
98
91
  end(facts?: Record<string, unknown>): void;
99
92
  }
100
93
 
101
94
  // One registry per page, whichever copy of this module a contribution loaded it through: the
102
95
  // run writes it, the session verb reads it from the command module.
103
- const key = Symbol.for('volter.model-play-log');
104
- const page = globalThis as typeof globalThis & { [key]?: Map<string, Run> };
105
- const runs: Map<string, Run> = page[key] ??= new Map();
106
- const listenersKey = Symbol.for('volter.model-play-log.listeners');
96
+ const key = Symbol.for('volter.play-log');
97
+ const page = globalThis as typeof globalThis & { [key]?: { run: Run | null } };
98
+ const held: { run: Run | null } = page[key] ??= { run: null };
99
+ const listenersKey = Symbol.for('volter.play-log.listeners');
107
100
  const pageListeners = globalThis as typeof globalThis & { [listenersKey]?: Set<() => void> };
108
101
  const listeners: Set<() => void> = pageListeners[listenersKey] ??= new Set();
109
102
 
@@ -113,8 +106,8 @@ function publish(): void {
113
106
  }
114
107
  }
115
108
 
116
- /** Hear every write to any document's log (several may land in one frame; coalesce). */
117
- export function subscribeModelPlayLog(listener: () => void): () => void {
109
+ /** Hear every write to the log (several may land in one frame; coalesce). */
110
+ export function subscribePlayLog(listener: () => void): () => void {
118
111
  listeners.add(listener);
119
112
  return () => { listeners.delete(listener); };
120
113
  }
@@ -153,39 +146,36 @@ function keptFacts(facts: unknown): Record<string, unknown> | undefined {
153
146
  try { json = JSON.stringify(ordered); }
154
147
  catch (error) { return { unserializable: error instanceof Error ? error.message : String(error) }; }
155
148
  if (json === undefined) return undefined;
156
- if (json.length > MODEL_PLAY_LOG_FACTS_CHARS && ordered !== facts) json = JSON.stringify(shortened(ordered as Record<string, unknown>));
157
- if (json.length > MODEL_PLAY_LOG_FACTS_CHARS) return { truncated: json.slice(0, MODEL_PLAY_LOG_FACTS_CHARS) };
149
+ if (json.length > PLAY_LOG_FACTS_CHARS && ordered !== facts) json = JSON.stringify(shortened(ordered as Record<string, unknown>));
150
+ if (json.length > PLAY_LOG_FACTS_CHARS) return { truncated: json.slice(0, PLAY_LOG_FACTS_CHARS) };
158
151
  // A snapshot, so a script that goes on mutating what it logged does not rewrite history.
159
152
  const value: unknown = JSON.parse(json);
160
153
  return typeof value === 'object' && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : { value };
161
154
  }
162
155
 
163
- function append(run: Run, source: ModelPlayLogEntry['source'], kind: string, facts?: unknown, at?: { readonly simT: number; readonly tick: number }): void {
156
+ function append(run: Run, source: PlayLogEntry['source'], kind: string, facts?: unknown, at?: { readonly simT: number; readonly tick: number }): void {
164
157
  try {
165
158
  const kept = keptFacts(facts);
166
159
  const t = Date.now();
167
160
  const simT = at?.simT ?? (t - run.startedAt) / 1000;
168
161
  if (at !== undefined) run.tick = Math.max(run.tick, at.tick);
169
162
  run.simT = Math.max(run.simT, (t - run.startedAt) / 1000);
170
- const entry: ModelPlayLogEntry = {
163
+ const entry: PlayLogEntry = {
171
164
  seq: run.total, t, simT, tick: at?.tick ?? run.tick, kind: String(kind), source,
172
165
  ...(kept ? { facts: kept } : {}),
173
166
  };
174
- run.ring[run.total % MODEL_PLAY_LOG_CAPACITY] = entry;
167
+ run.ring[run.total % PLAY_LOG_CAPACITY] = entry;
175
168
  run.total += 1;
176
169
  run.kinds.add(entry.kind);
177
170
  } catch { /* A log that cannot be written is not the run's failure. */ }
178
171
  publish();
179
172
  }
180
173
 
181
- /** Play started from a fresh copy: the document's log replaced by an empty one at zero,
182
- * opened by `play-start`. */
183
- export function beginModelPlayLog(documentId: string, script: string): ModelPlayRun {
184
- const run: Run = { documentId, script, startedAt: Date.now(), playing: true, simT: 0, tick: 0, ring: [], total: 0, kinds: new Set() };
185
- // Re-inserted, so the map's order is the order Plays started.
186
- runs.delete(documentId);
187
- runs.set(documentId, run);
188
- append(run, 'play', 'play-start', { documentId, script });
174
+ /** A run started: the log replaced by an empty one at zero, opened by `play-start`. */
175
+ export function beginPlayLog(script: string): PlayLogRun {
176
+ const run: Run = { script, startedAt: Date.now(), playing: true, simT: 0, tick: 0, ring: [], total: 0, kinds: new Set() };
177
+ held.run = run;
178
+ append(run, 'play', 'play-start', { script });
189
179
  return {
190
180
  append(source, kind, facts, at) { if (run.playing) append(run, source, kind, facts, at); },
191
181
  end(facts) {
@@ -196,52 +186,46 @@ export function beginModelPlayLog(documentId: string, script: string): ModelPlay
196
186
  };
197
187
  }
198
188
 
199
- function activeRun(): Run | undefined {
200
- const started = [...runs.values()].reverse();
201
- return started.find((run) => run.playing) ?? started[0];
202
- }
203
-
204
- export function readModelPlayLog(query: ModelPlayLogQuery = {}): ModelPlayLogReading {
205
- const run = query.documentId === undefined ? activeRun() : runs.get(query.documentId);
206
- const documents = [...runs.keys()];
189
+ export function readPlayLog(query: PlayLogQuery = {}): PlayLogReading {
190
+ const { run } = held;
207
191
  if (!run) {
208
192
  return {
209
- playing: false, documentId: query.documentId ?? null, script: null, startedAt: null, documents,
210
- simT: 0, tick: 0, capacity: MODEL_PLAY_LOG_CAPACITY, total: 0, dropped: 0, entries: [],
193
+ playing: false, script: null, startedAt: null,
194
+ simT: 0, tick: 0, capacity: PLAY_LOG_CAPACITY, total: 0, dropped: 0, entries: [],
211
195
  };
212
196
  }
213
- const dropped = Math.max(0, run.total - MODEL_PLAY_LOG_CAPACITY);
214
- const entries: ModelPlayLogEntry[] = [];
197
+ const dropped = Math.max(0, run.total - PLAY_LOG_CAPACITY);
198
+ const entries: PlayLogEntry[] = [];
215
199
  for (let seq = dropped; seq < run.total; seq++) {
216
- const entry = run.ring[seq % MODEL_PLAY_LOG_CAPACITY]!;
200
+ const entry = run.ring[seq % PLAY_LOG_CAPACITY]!;
217
201
  if (query.since !== undefined && entry.simT < query.since) continue;
218
202
  if (query.kind !== undefined && entry.kind !== query.kind) continue;
219
203
  entries.push(entry);
220
204
  }
221
205
  return {
222
- playing: run.playing, documentId: run.documentId, script: run.script, startedAt: run.startedAt, documents,
206
+ playing: run.playing, script: run.script, startedAt: run.startedAt,
223
207
  simT: run.playing ? Math.max(run.simT, (Date.now() - run.startedAt) / 1000) : run.simT, tick: run.tick,
224
- capacity: MODEL_PLAY_LOG_CAPACITY, total: run.total, dropped, entries,
208
+ capacity: PLAY_LOG_CAPACITY, total: run.total, dropped, entries,
225
209
  };
226
210
  }
227
211
 
228
- export interface ModelPlayLogTail {
229
- /** Entries the document's latest run has written; 0 when it has no log. */
212
+ export interface PlayLogTail {
213
+ /** Entries the latest run has written; 0 when nothing has played. */
230
214
  readonly total: number;
231
215
  /** Every kind that run has written, in the order first seen. */
232
216
  readonly kinds: readonly string[];
233
217
  /** The newest kept entries (of the asked kind), at most `last`, oldest first. */
234
- readonly entries: readonly ModelPlayLogEntry[];
218
+ readonly entries: readonly PlayLogEntry[];
235
219
  }
236
220
 
237
- /** The newest `last` entries of one document's log, walked from the newest end. */
238
- export function tailModelPlayLog(documentId: string, last: number, kind?: string): ModelPlayLogTail {
239
- const run = runs.get(documentId);
221
+ /** The newest `last` entries of the log, walked from the newest end. */
222
+ export function tailPlayLog(last: number, kind?: string): PlayLogTail {
223
+ const { run } = held;
240
224
  if (!run) return { total: 0, kinds: [], entries: [] };
241
- const oldest = Math.max(0, run.total - MODEL_PLAY_LOG_CAPACITY);
242
- const entries: ModelPlayLogEntry[] = [];
225
+ const oldest = Math.max(0, run.total - PLAY_LOG_CAPACITY);
226
+ const entries: PlayLogEntry[] = [];
243
227
  for (let seq = run.total - 1; seq >= oldest && entries.length < last; seq--) {
244
- const entry = run.ring[seq % MODEL_PLAY_LOG_CAPACITY]!;
228
+ const entry = run.ring[seq % PLAY_LOG_CAPACITY]!;
245
229
  if (kind === undefined || entry.kind === kind) entries.push(entry);
246
230
  }
247
231
  return { total: run.total, kinds: [...run.kinds], entries: entries.reverse() };
@@ -10,8 +10,8 @@
10
10
  * Colyseus has no pause for a room.
11
11
  * - `inspect`: `{ control: 'inspect' }`, answered with the scene the page last drew and its labelled screen nodes,
12
12
  * `{ objects, screen, truncated? }` (at most 2000 objects and 500 nodes, `truncated` saying how many were left out).
13
- * - `act`: `{ control: 'act', actions, observe? }`, std-driver/2 `Action`s through the page's own input path, answered
14
- * with the observation (std-driver/2 `Observation`) where `observe` asks for one, else `{ ok: true }`. A refused
13
+ * - `act`: `{ control: 'act', actions, observe? }`, std-driver/3 `Action`s through the page's own input path, answered
14
+ * with the observation (std-driver/3 `Observation`) where `observe` asks for one, else `{ ok: true }`. A refused
15
15
  * action fails the whole control, the actions before it already taken.
16
16
  *
17
17
  * `clock`, `inspect` and `act` need a device of exactly one page (a tab or a frame, as each of Play's players is).
@@ -62,7 +62,7 @@ export function stdClock(device: string, asked: StdClockAction): Promise<StdCloc
62
62
  }
63
63
 
64
64
  /** One object of the scene a page last drew, as std's inspect reads it. */
65
- export interface StdInspectedObject {
65
+ interface StdInspectedObject {
66
66
  readonly name: string;
67
67
  /** three's own `type`. */
68
68
  readonly type: string;
@@ -83,8 +83,8 @@ export interface StdInspection {
83
83
  readonly truncated?: { readonly objects: number; readonly screen: number };
84
84
  }
85
85
 
86
- /** One labelled node of a page's screen, as std's inspect names it (std-driver/2's `NodeObserved`). */
87
- export interface StdScreenNode {
86
+ /** One labelled node of a page's screen, as std's inspect names it (std-driver/3's `NodeObserved`). */
87
+ interface StdScreenNode {
88
88
  readonly role?: string;
89
89
  readonly name?: string;
90
90
  readonly [key: string]: unknown;
@@ -94,13 +94,13 @@ export function stdInspect(device: string): Promise<StdInspection> {
94
94
  return control<StdInspection>(device, { control: 'inspect' });
95
95
  }
96
96
 
97
- /** One std-driver/2 action: a key pressed or let go, or the pointer at a point of the page (0 to 1, x right, y down)
97
+ /** One std-driver/3 action: a key pressed or let go, or the pointer at a point of the page (0 to 1, x right, y down)
98
98
  * with `button` pressed or let go. */
99
99
  export type StdAction =
100
100
  | { readonly key: { readonly key: string; readonly down: boolean } }
101
101
  | { readonly pointer: { readonly x: number; readonly y: number; readonly button?: number; readonly down?: boolean } };
102
102
 
103
- /** Actions taken through the page's own input path, as a player's own; the observation after them (std-driver/2's
103
+ /** Actions taken through the page's own input path, as a player's own; the observation after them (std-driver/3's
104
104
  * `Observation`) where asked, else `{ ok: true }`. */
105
105
  export function stdAct(device: string, actions: readonly StdAction[], observe = false): Promise<unknown> {
106
106
  return control(device, { control: 'act', actions, ...(observe ? { observe: true } : {}) });
package/src/std-export.ts CHANGED
@@ -49,14 +49,6 @@ function publish(next: StdExport): void {
49
49
  for (const listener of listeners) listener();
50
50
  }
51
51
 
52
- /** Whether the Game panel's Export menu is open: its Escape closes it alone, never the game's view as well
53
- * (`std-game.ts`'s view keys, which hear a key before the menu does). */
54
- let menuOpen = false;
55
- export const exportMenuOpen = (): boolean => menuOpen;
56
- export function setExportMenuOpen(open: boolean): void {
57
- menuOpen = open;
58
- }
59
-
60
52
  /** The latest export, running or ended; null before any. */
61
53
  export const stdExport = (): StdExport | null => current;
62
54
 
@@ -3,18 +3,24 @@
3
3
  * `std.json`). A std game carries no editor code: std's design has the editor add nothing to the game (design 8), so
4
4
  * the adapter a Cyclotron project would keep in `editor/volter.adapter.ts` is Play's. It is the one a models project
5
5
  * gets from `cyclotron create`, Blender's layout, keymap and properties inspector, with the game's Blender files as its
6
- * documents: every `.blend` under `levels/` and every prefab's under `prefabs/` (std's `prefabs/<name>/<name>.blend`).
7
- * A game with one level opens it.
6
+ * documents: every `.blend` under `levels/` and every prefab's under `prefabs/` (std's `prefabs/<name>/<name>.blend`),
7
+ * and each level (`levels/<Name>Level.tsx`) a document of its own (`stdLevelDocuments`), the main level the one the
8
+ * project opens on.
8
9
  */
9
10
  import { defineAdapter } from '@volter/project/adapter/adapter-module';
10
11
  import { ModelLayout } from '@volter/editor-blender/layouts';
11
12
  import { blenderKeymap } from '@volter/editor-blender/looks';
12
13
  // (the game's dev server serves this module, so the SDK's change channel it brings is that server's: it joins the
13
- // editor's page to the server's file events, which a std game's own modules never import, and the Scene view and
14
+ // editor's page to the server's file events, which a std game's own modules never import, and a level's document and
14
15
  // Play's remount hear each save's export through it)
15
16
  import '@volter/sdk/project-module-changes';
16
17
 
17
18
  export default defineAdapter({
18
19
  editor: { Layout: ModelLayout, keymap: blenderKeymap, inspector: 'properties' },
19
- documents: { find: [{ finder: 'modelsFromBlendFiles', include: ['levels/**/*.blend', 'prefabs/**/*.blend'], defaultIn: 'levels/' }] },
20
+ documents: {
21
+ find: [
22
+ { finder: 'modelsFromBlendFiles', include: ['levels/**/*.blend', 'prefabs/**/*.blend'] },
23
+ { finder: 'stdLevelDocuments', include: ['levels/**/*.tsx', 'levels/**/*.blend'] },
24
+ ],
25
+ },
20
26
  });