@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.
@@ -83,7 +83,7 @@ var blenderHalfOf = (root) => join(stdDev(root), "blender", "blend-to-scene.py")
83
83
  var isLevelGlb = (path) => (/^levels\/.+\.glb$/.test(path) || /^prefabs\/([^/]+)\/\1\.glb$/.test(path)) && !path.includes("\\") && path.split("/").every((part) => part !== "" && part !== "." && part !== "..");
84
84
  function runStdLevel(root, glb) {
85
85
  return new Promise((resolve2) => {
86
- const run = spawn(process.execPath, [join(stdDev(root), "bin", "std.js"), "level", glb, "--description", glb.replace(/\.glb$/, ".level.json")], { cwd: root, windowsHide: true });
86
+ const run = spawn(process.execPath, [join(stdDev(root), "bin", "std.js"), "level-scene", glb], { cwd: root, windowsHide: true });
87
87
  let output = "";
88
88
  run.stdout.on("data", (chunk) => {
89
89
  output += chunk.toString();
@@ -257,28 +257,35 @@ async function runningServer(root) {
257
257
  const record = JSON.parse(text);
258
258
  return alive(record.pid) ? record : null;
259
259
  }
260
- var ERRORS_KEPT = 500;
260
+ var OUTPUT_KEPT = 500;
261
261
  var LINE_MOST = 4e3;
262
262
  var owned = /* @__PURE__ */ new Map();
263
- function heardError(server, chunk) {
264
- const text = chunk === null ? server.decoder.end() : server.decoder.write(chunk);
265
- const lines = stripVTControlCharacters(server.partial + text).split(/\r?\n/);
266
- server.partial = chunk === null ? "" : (lines.pop() ?? "").slice(-LINE_MOST);
267
- server.errors.push(...lines.filter((line, at) => chunk !== null || at < lines.length - 1 || line !== ""));
268
- const over = server.errors.length - ERRORS_KEPT;
263
+ function heardOutput(stream, chunk) {
264
+ const text = chunk === null ? stream.decoder.end() : stream.decoder.write(chunk);
265
+ const lines = stripVTControlCharacters(stream.partial + text).split(/\r?\n/);
266
+ stream.partial = chunk === null ? "" : (lines.pop() ?? "").slice(-LINE_MOST);
267
+ stream.lines.push(...lines.filter((line, at) => chunk !== null || at < lines.length - 1 || line !== ""));
268
+ const over = stream.lines.length - OUTPUT_KEPT;
269
269
  if (over > 0) {
270
- server.errors.splice(0, over);
271
- server.dropped += over;
270
+ stream.lines.splice(0, over);
271
+ stream.dropped += over;
272
272
  }
273
273
  }
274
- function errorsFrom(root, server, from) {
274
+ var lineOf = (asked) => asked === null ? null : Number(asked) || 0;
275
+ function streamFrom(stream, from) {
276
+ const next = stream.dropped + stream.lines.length;
277
+ return { lines: from === null ? [] : stream.lines.slice(Math.max(0, from - stream.dropped)), next };
278
+ }
279
+ function outputFrom(root, server, from) {
275
280
  const mine = owned.get(root);
276
281
  const pid = mine?.child.pid ?? null;
277
- if (!mine || pid === null) return { lines: [], next: 0, server: null };
278
- const next = mine.dropped + mine.errors.length;
279
- if (from === null) return { lines: [], next, server: pid };
280
- const start = server === pid ? from : 0;
281
- return { lines: mine.errors.slice(Math.max(0, start - mine.dropped)), next, server: pid };
282
+ if (!mine || pid === null) return { server: null, stdout: { lines: [], next: 0 }, stderr: { lines: [], next: 0 } };
283
+ const start = (line) => line === null ? null : server === pid ? line : 0;
284
+ return {
285
+ server: pid,
286
+ stdout: streamFrom(mine.streams.stdout, start(from.stdout)),
287
+ stderr: streamFrom(mine.streams.stderr, start(from.stderr))
288
+ };
282
289
  }
283
290
  function endOwned() {
284
291
  for (const { child } of owned.values()) if (child.pid !== void 0 && child.exitCode === null) signalProcessTree(child.pid, "SIGTERM");
@@ -291,15 +298,18 @@ async function startedServer(root) {
291
298
  if (!mine || mine.child.exitCode !== null || mine.failed !== null) {
292
299
  const npm = npmProcess(["run", "dev", "--", "--no-open"], root);
293
300
  const child = spawn(npm.program, npm.args, { cwd: root, detached: process.platform !== "win32", windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
294
- mine = { child, said: "", failed: null, errors: [], dropped: 0, partial: "", decoder: new StringDecoder("utf8") };
301
+ const stream = () => ({ lines: [], dropped: 0, partial: "", decoder: new StringDecoder("utf8") });
302
+ mine = { child, said: "", failed: null, streams: { stdout: stream(), stderr: stream() } };
295
303
  const kept = mine;
296
304
  const hear = (chunk) => {
297
305
  kept.said = (kept.said + chunk.toString("utf8")).slice(-4e3);
298
306
  };
299
307
  child.stdout?.on("data", hear);
300
308
  child.stderr?.on("data", hear);
301
- child.stderr?.on("data", (chunk) => heardError(kept, chunk));
302
- child.stderr?.on("end", () => heardError(kept, null));
309
+ for (const name of ["stdout", "stderr"]) {
310
+ child[name]?.on("data", (chunk) => heardOutput(kept.streams[name], chunk));
311
+ child[name]?.on("end", () => heardOutput(kept.streams[name], null));
312
+ }
303
313
  child.on("error", (error) => {
304
314
  kept.failed = error.message;
305
315
  });
@@ -360,8 +370,8 @@ async function traceFrom(root, page, from) {
360
370
  var PLAY_MS = 15 * 6e4;
361
371
  async function playGoal(root, body, response) {
362
372
  const asked = body;
363
- if (typeof asked?.goal !== "string" || asked.goal === "" || asked.bots !== void 0 && !(Number.isInteger(asked.bots) && Number(asked.bots) >= 1))
364
- return answer(response, 400, { error: `A play is { goal, bots? }: a goal of the game's bot, and a whole number of bots; it was given ${JSON.stringify(body)}.` });
373
+ if (typeof asked?.goal !== "string" || asked.goal === "" || asked.bots !== void 0 && !(Number.isInteger(asked.bots) && Number(asked.bots) >= 1) || asked.device !== void 0 && (typeof asked.device !== "string" || asked.device === ""))
374
+ return answer(response, 400, { error: `A play is { goal, bots?, device? }: a goal of the game's bot, a whole number of bots, and one of std's devices by its id; it was given ${JSON.stringify(body)}.` });
365
375
  const running = await runningServer(root);
366
376
  if (!running) return answer(response, 409, { error: "The game's dev server is not running: press Play." });
367
377
  response.statusCode = 200;
@@ -382,7 +392,7 @@ async function playGoal(root, body, response) {
382
392
  if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) socket.close();
383
393
  };
384
394
  const limit = setTimeout(() => end(false, `Play ended std's play of ${asked.goal}: it ran ${PLAY_MS / 6e4} minutes.`), PLAY_MS);
385
- socket.addEventListener("open", () => socket.send(JSON.stringify({ goal: asked.goal, bots: asked.bots ?? 1, device: "headless" })));
395
+ socket.addEventListener("open", () => socket.send(JSON.stringify({ goal: asked.goal, bots: asked.bots ?? 1, device: asked.device ?? "headless" })));
386
396
  socket.addEventListener("message", (event) => {
387
397
  const message = JSON.parse(String(event.data));
388
398
  if (message.end !== void 0) end(message.end.ok === true);
@@ -407,7 +417,9 @@ function answer(response, status, value) {
407
417
  async function askStd(root, path, init) {
408
418
  const running = await runningServer(root);
409
419
  if (!running) return { status: 409, body: { error: "The game's dev server is not running: press Play." } };
410
- const response = await fetch(`${running.url}${path}?token=${running.token}`, init);
420
+ const at = new URL(path, running.url);
421
+ at.searchParams.set("token", running.token);
422
+ const response = await fetch(at, init);
411
423
  return { status: response.status, body: await response.json() };
412
424
  }
413
425
  function stdDevPlugin(services) {
@@ -449,14 +461,14 @@ function stdDevPlugin(services) {
449
461
  return;
450
462
  }
451
463
  const control = /^\/devices\/([^/]+)\/control$/.exec(url.pathname)?.[1];
452
- const asked = url.pathname === "/" && request.method === "GET" ? Promise.resolve({ status: 200, body: stdGame(root) }) : url.pathname === "/start" && request.method === "POST" ? startedServer(root).then(({ url: at }) => ({ status: 200, body: { url: at } })) : url.pathname === "/devices" && request.method === "GET" ? askStd(root, "/__std/devices") : control !== void 0 && request.method === "POST" ? askStd(
464
+ const asked = url.pathname === "/" && request.method === "GET" ? Promise.resolve({ status: 200, body: stdGame(root) }) : url.pathname === "/start" && request.method === "POST" ? startedServer(root).then(({ url: at }) => ({ status: 200, body: { url: at } })) : url.pathname === "/devices" && request.method === "GET" ? askStd(root, "/__std/devices") : url.pathname === "/scene" && request.method === "GET" ? askStd(root, `/__std/scene${url.search}`) : control !== void 0 && request.method === "POST" ? askStd(
453
465
  root,
454
466
  `/__std/devices/${encodeURIComponent(decodeURIComponent(control))}/control`,
455
467
  { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(bodyOf(request)) }
456
- ) : url.pathname === "/level" && (request.method === "GET" || request.method === "POST") ? levelAnswer(root, request.method, bodyOf(request)) : url.pathname === "/goals" && request.method === "GET" ? botGoals(root) : url.pathname === "/pages" && request.method === "GET" ? pageSlots(root).then((body) => ({ status: 200, body })) : url.pathname === "/trace" && request.method === "GET" && PAGE_ID.test(url.searchParams.get("page") ?? "") ? traceFrom(root, url.searchParams.get("page"), Number(url.searchParams.get("from") ?? 0)).then((body) => ({ status: 200, body })) : url.pathname === "/errors" && request.method === "GET" ? Promise.resolve({ status: 200, body: errorsFrom(
468
+ ) : url.pathname === "/level" && (request.method === "GET" || request.method === "POST") ? levelAnswer(root, request.method, bodyOf(request)) : url.pathname === "/goals" && request.method === "GET" ? botGoals(root) : url.pathname === "/pages" && request.method === "GET" ? pageSlots(root).then((body) => ({ status: 200, body })) : url.pathname === "/trace" && request.method === "GET" && PAGE_ID.test(url.searchParams.get("page") ?? "") ? traceFrom(root, url.searchParams.get("page"), Number(url.searchParams.get("from") ?? 0)).then((body) => ({ status: 200, body })) : url.pathname === "/output" && request.method === "GET" ? Promise.resolve({ status: 200, body: outputFrom(
457
469
  root,
458
470
  url.searchParams.has("server") ? Number(url.searchParams.get("server")) : null,
459
- url.searchParams.has("from") ? Number(url.searchParams.get("from")) || 0 : null
471
+ { stdout: lineOf(url.searchParams.get("stdout")), stderr: lineOf(url.searchParams.get("stderr")) }
460
472
  ) }) : Promise.resolve({ status: 404, body: { error: `${STD_ROUTE}${url.pathname} is not a route of Play's` } });
461
473
  asked.then(
462
474
  ({ status, body }) => answer(response, status, body),
@@ -468,7 +480,7 @@ function stdDevPlugin(services) {
468
480
  }
469
481
 
470
482
  // serving/std-scene.ts
471
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
483
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2 } from "node:fs";
472
484
  import { dirname as dirname3, join as join3, relative, sep } from "node:path";
473
485
  import { fileURLToPath } from "node:url";
474
486
 
@@ -511,6 +523,8 @@ function levelFiles(blend) {
511
523
  grading: `${dir}/${base}.grading.tsx`
512
524
  };
513
525
  }
526
+ var BLEND_LEVEL = /^levels\/.+\.blend$/;
527
+ var CODE_LEVEL = /^levels\/(?:[^/]+\/)*([A-Z][A-Za-z0-9]*Level)\.tsx$/;
514
528
 
515
529
  // serving/std-scene.ts
516
530
  var PREFIX = "/@volter-std-scene/";
@@ -518,11 +532,11 @@ var VIRTUAL = "\0volter-std-scene:";
518
532
  var RUNTIME = "volter-std-scene-runtime";
519
533
  var RUNTIME_ID = "\0volter-std-scene-runtime";
520
534
  var RUNTIME_FILE = join3(dirname3(fileURLToPath(import.meta.url)), "..", "serving", "std-scene-runtime.js");
521
- var LEVEL = /^levels\/.+\.blend$/;
522
535
  var MARK = "volter-std-scene";
523
536
  var RUNTIME_PACKAGES = ["@react-three/fiber", "@react-three/rapier"];
524
537
  var THROUGH = /* @__PURE__ */ new Map([["postprocessing", "@volter-ai/std/level/client"]]);
525
538
  var installedFolder = (root, name) => installedPackageDir(root, name.split("/").slice(0, name.startsWith("@") ? 2 : 1).join("/"));
539
+ var refused = (why) => `throw new Error(${JSON.stringify(why)});`;
526
540
  var fileUrl = (file, version) => `${fsFileUrl(file)}?${MARK}=${version}`;
527
541
  function sceneModule(root, blend, read, version) {
528
542
  const files = levelFiles(blend);
@@ -559,6 +573,31 @@ function sceneModule(root, blend, read, version) {
559
573
  ""
560
574
  ].join("\n");
561
575
  }
576
+ function codeSceneModule(root, file, read) {
577
+ const component = CODE_LEVEL.exec(file)[1];
578
+ read.add(normalizePath(join3(root, file)));
579
+ if (!existsSync3(join3(root, file))) throw new Error(`${file} is not in the game: a level in code is levels/<Name>Level.tsx.`);
580
+ return [
581
+ `import { mountScene as mountLevel } from ${JSON.stringify(RUNTIME)};`,
582
+ `import { ${component} } from ${JSON.stringify(`/${file}`)};`,
583
+ `export const component = ${JSON.stringify(file)};`,
584
+ "export const canvas = { shadows: 'percentage' };",
585
+ "export const gradingExtra = false;",
586
+ `const level = { Level: ${component}, Environment: null, canvas: { shadows: 'percentage' } };`,
587
+ "export const mountScene = (stage) => mountLevel(level, stage);",
588
+ "export const viewTransform = async () => null;",
589
+ ""
590
+ ].join("\n");
591
+ }
592
+ function levelModules(root) {
593
+ let found;
594
+ try {
595
+ found = readdirSync(join3(root, "levels"), { recursive: true, encoding: "utf8" });
596
+ } catch {
597
+ return [];
598
+ }
599
+ return found.filter((file) => /\.tsx?$/.test(file) && !file.split(sep).some((segment) => segment.startsWith(".") || segment === "node_modules")).map((file) => `levels/${file.split(sep).join("/")}`).sort();
600
+ }
562
601
  function gameFile(root, file) {
563
602
  if (file === null) return false;
564
603
  const at = relative(root, file);
@@ -587,14 +626,16 @@ function stdScenePlugin(services) {
587
626
  return {
588
627
  name: "volter-play-std-scene",
589
628
  enforce: "pre",
590
- // (a std game's Scene view and its levels' modules prebundled as the session starts: found on the view's first open,
629
+ // (a std game's level documents and its levels' modules prebundled as the session starts: found on the view's first open,
591
630
  // Vite would optimize them again then, and the editor's page, which does not reload for it, would hold two copies of
592
- // React. The levels' files are crawled, and the runtime's own packages are included where the game has them)
631
+ // React. The levels' files are crawled, and the runtime's own packages are included where the game has them. The
632
+ // files are listed, never a pattern: one entry that is a pattern makes Vite glob the session's whole list, which
633
+ // skips every `node_modules`, and so every installed package's crawl entry, `globEntries` in its dependency scan)
593
634
  config() {
594
635
  const root = stdGame2();
595
636
  if (root === null) return void 0;
596
637
  const include = RUNTIME_PACKAGES.filter((name) => installedFolder(root, name) !== null);
597
- return { optimizeDeps: { entries: ["levels/**/*.{ts,tsx}"], include } };
638
+ return { optimizeDeps: { entries: levelModules(root), include } };
598
639
  },
599
640
  async resolveId(source, importer) {
600
641
  const ours = importer?.startsWith(VIRTUAL) === true || importer === RUNTIME_ID;
@@ -608,26 +649,27 @@ function stdScenePlugin(services) {
608
649
  }
609
650
  if (!source.startsWith(PREFIX)) return null;
610
651
  const [path = "", query = ""] = source.slice(PREFIX.length).split("?");
611
- const blend = decodeURIComponent(path);
612
- if (!LEVEL.test(blend)) throw new Error(`${blend} is not a std level's Blender file (levels/<name>.blend).`);
613
- return `${VIRTUAL}${blend}?${query}`;
652
+ const level = decodeURIComponent(path);
653
+ if (!BLEND_LEVEL.test(level) && !CODE_LEVEL.test(level))
654
+ throw new Error(`${level} is not a std level: a level's Blender file (levels/<name>.blend) or a level in code (levels/<Name>Level.tsx).`);
655
+ return `${VIRTUAL}${level}?${query}`;
614
656
  },
615
657
  load(id) {
616
658
  if (id === RUNTIME_ID) return readFileSync2(RUNTIME_FILE, "utf8");
617
659
  if (id.startsWith(VIRTUAL)) {
618
660
  const root2 = stdGame2();
619
- if (!root2) throw new Error("The open project is not a std game (no std.json), so it has no level to draw.");
620
- const blend = id.slice(VIRTUAL.length).replace(/\?.*$/, "");
661
+ if (!root2) return refused("The open project is not a std game (no std.json), so it has no level to draw.");
662
+ const level = id.slice(VIRTUAL.length).replace(/\?.*$/, "");
621
663
  const read = /* @__PURE__ */ new Set();
622
- scenes.set(blend, { id, read });
623
- return sceneModule(root2, blend, read, version);
664
+ scenes.set(level, { id, read });
665
+ return CODE_LEVEL.test(level) ? codeSceneModule(root2, level, read) : sceneModule(root2, level, read, version);
624
666
  }
625
667
  const [file = id, query] = id.split("?");
626
668
  const root = query === "url" ? stdGame2() : null;
627
669
  if (!root || !gameFile(root, file)) return null;
628
670
  return `export default ${JSON.stringify(fileUrl(file, version))};`;
629
671
  },
630
- // (a Scene view depends on its module's whole import closure among the game's files, the modules whose import failed
672
+ // (a level's document depends on its module's whole import closure among the game's files, the modules whose import failed
631
673
  // to resolve within it and the files its source was written from. Any file of those written, made or removed bumps
632
674
  // the content version, invalidates the closure, so each of its modules and URLs is served anew, and is announced on
633
675
  // the session's change channel naming the levels it reached)
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@volter/play",
3
3
  "publisher": "volter",
4
- "version": "0.5.212",
4
+ "version": "0.5.214",
5
5
  "engines": {
6
6
  "vscode": "^1.104.0"
7
7
  },
8
8
  "author": "Volter AI, Inc.",
9
9
  "license": "Apache-2.0",
10
10
  "type": "module",
11
- "description": "Play for a volter-ai/std game in Cyclotron: the game's own dev pages as std devices in the level's stage, the Game panel, a level's export on save and its Scene view.",
11
+ "description": "Play for a volter-ai/std game in Cyclotron: the game's own dev pages as std devices in the level's stage, the Game panel, a level's export on save and a level in code's document.",
12
12
  "repository": {
13
13
  "type": "git",
14
14
  "url": "git+https://github.com/volter-ai/editor.git",
@@ -34,7 +34,8 @@
34
34
  "contributes": {
35
35
  "volter": {
36
36
  "activationEvents": [
37
- "onCustomEditor:volter.blender.model"
37
+ "onCustomEditor:volter.blender.model",
38
+ "onDocument:std-level"
38
39
  ],
39
40
  "serving": [
40
41
  "./dist-node/serving.mjs"
@@ -44,7 +45,8 @@
44
45
  ],
45
46
  "editors": [
46
47
  {
47
- "document": "./contributions/std-scene.document.tsx"
48
+ "document": "./contributions/std-level.document.tsx",
49
+ "kind": "std-level"
48
50
  }
49
51
  ],
50
52
  "projectAdapters": [
@@ -52,11 +54,17 @@
52
54
  "when": "std.json",
53
55
  "module": "./src/std-game-adapter.ts"
54
56
  }
57
+ ],
58
+ "finders": [
59
+ {
60
+ "name": "stdLevelDocuments",
61
+ "module": "./contributions/levels.finder.ts"
62
+ }
55
63
  ]
56
64
  },
57
65
  "languageModelTools": [
58
66
  {
59
- "name": "model-play-log",
67
+ "name": "play-log",
60
68
  "tags": [
61
69
  "volter"
62
70
  ],
@@ -65,10 +73,6 @@
65
73
  "inputSchema": {
66
74
  "type": "object",
67
75
  "properties": {
68
- "documentId": {
69
- "type": "string",
70
- "description": "The model document; the active Play's when omitted."
71
- },
72
76
  "since": {
73
77
  "type": "number",
74
78
  "description": "Keep entries at or after this many game seconds."
@@ -83,12 +87,12 @@
83
87
  }
84
88
  },
85
89
  {
86
- "name": "model-play",
90
+ "name": "play",
87
91
  "tags": [
88
92
  "volter"
89
93
  ],
90
94
  "displayName": "Play control",
91
- "modelDescription": "Control Play for the open level's std game: state, play, stop, pause, resume or restart. Answers the game's state.",
95
+ "modelDescription": "Control Play for the project's std game: state, play, stop, pause, resume or restart. Answers the game's state: play and restart answer once a player's page has joined the game (clock.running), or fail with why it did not start.",
92
96
  "inputSchema": {
93
97
  "type": "object",
94
98
  "properties": {
@@ -113,91 +117,89 @@
113
117
  ],
114
118
  "commands": [
115
119
  {
116
- "command": "volter.model-play",
117
- "title": "model-play",
118
- "category": "Play",
119
- "volter": {
120
- "derivedRefresh": "none"
121
- }
122
- },
123
- {
124
- "command": "volter.model-play-log",
125
- "title": "model-play-log",
120
+ "command": "volter.play",
121
+ "title": "play",
126
122
  "category": "Play",
127
123
  "volter": {
128
- "derivedRefresh": "none"
124
+ "derivedRefresh": "none",
125
+ "timeoutMs": 120000
129
126
  }
130
127
  },
131
128
  {
132
- "command": "volter.std-scene",
133
- "title": "Open Scene View",
129
+ "command": "volter.play-log",
130
+ "title": "play-log",
134
131
  "category": "Play",
135
132
  "volter": {
136
133
  "derivedRefresh": "none"
137
134
  }
138
135
  },
139
136
  {
140
- "command": "volter.model-play.state",
137
+ "command": "volter.play.state",
141
138
  "title": "state",
142
139
  "category": "Game"
143
140
  },
144
141
  {
145
- "command": "volter.model-play.mode",
142
+ "command": "volter.play.mode",
146
143
  "title": "mode",
147
144
  "category": "Game"
148
145
  },
149
146
  {
150
- "command": "volter.model-play.game-mode",
147
+ "command": "volter.play.game-mode",
151
148
  "title": "Switch to Game Mode",
152
149
  "category": "Game"
153
150
  },
154
151
  {
155
- "command": "volter.model-play.animation-mode",
152
+ "command": "volter.play.animation-mode",
156
153
  "title": "Switch to Animation Mode",
157
154
  "category": "Game"
158
155
  },
159
156
  {
160
- "command": "volter.model-play.play",
157
+ "command": "volter.play.play",
161
158
  "title": "Play",
162
159
  "category": "Game"
163
160
  },
164
161
  {
165
- "command": "volter.model-play.stop",
162
+ "command": "volter.play.stop",
166
163
  "title": "Stop",
167
164
  "category": "Game"
168
165
  },
169
166
  {
170
- "command": "volter.model-play.pause",
167
+ "command": "volter.play.pause",
171
168
  "title": "Pause",
172
169
  "category": "Game"
173
170
  },
174
171
  {
175
- "command": "volter.model-play.resume",
172
+ "command": "volter.play.resume",
176
173
  "title": "Resume",
177
174
  "category": "Game"
178
175
  },
179
176
  {
180
- "command": "volter.model-play.step",
177
+ "command": "volter.play.step",
181
178
  "title": "Step One Tick",
182
179
  "category": "Game"
183
180
  },
184
181
  {
185
- "command": "volter.model-play.speed",
182
+ "command": "volter.play.speed",
186
183
  "title": "speed",
187
184
  "category": "Game"
188
185
  },
189
186
  {
190
- "command": "volter.model-play.autoplay",
187
+ "command": "volter.play.players",
188
+ "title": "players",
189
+ "category": "Game"
190
+ },
191
+ {
192
+ "command": "volter.play.autoplay",
191
193
  "title": "autoplay",
192
194
  "category": "Game"
193
195
  },
194
196
  {
195
- "command": "volter.model-play.restart",
197
+ "command": "volter.play.restart",
196
198
  "title": "Restart",
197
199
  "category": "Game"
198
200
  },
199
201
  {
200
- "command": "volter.model-play.export",
202
+ "command": "volter.play.export",
201
203
  "title": "export",
202
204
  "category": "Game"
203
205
  }
@@ -205,50 +207,70 @@
205
207
  "menus": {
206
208
  "commandPalette": [
207
209
  {
208
- "command": "volter.model-play",
210
+ "command": "volter.play",
209
211
  "when": "false"
210
212
  },
211
213
  {
212
- "command": "volter.model-play-log",
214
+ "command": "volter.play-log",
213
215
  "when": "false"
214
216
  },
215
217
  {
216
- "command": "volter.model-play.state",
218
+ "command": "volter.play.state",
217
219
  "when": "false"
218
220
  },
219
221
  {
220
- "command": "volter.model-play.mode",
222
+ "command": "volter.play.mode",
221
223
  "when": "false"
222
224
  },
223
225
  {
224
- "command": "volter.model-play.speed",
226
+ "command": "volter.play.speed",
225
227
  "when": "false"
226
228
  },
227
229
  {
228
- "command": "volter.model-play.autoplay",
230
+ "command": "volter.play.players",
229
231
  "when": "false"
230
232
  },
231
233
  {
232
- "command": "volter.model-play.export",
234
+ "command": "volter.play.autoplay",
235
+ "when": "false"
236
+ },
237
+ {
238
+ "command": "volter.play.export",
233
239
  "when": "false"
234
240
  }
235
241
  ]
242
+ },
243
+ "configuration": {
244
+ "title": "Play",
245
+ "properties": {
246
+ "volter.play.players": {
247
+ "type": "integer",
248
+ "default": 1,
249
+ "minimum": 1,
250
+ "maximum": 8,
251
+ "scope": "window",
252
+ "description": "How many players a std game's Play starts: yours, and a bot playing the Game panel's goal for each other. One where unset. The Game panel's Players [-][+] and `play players <n>` write it to the project's settings (.vscode/settings.json), and a running game's players join or leave whenever it changes."
253
+ }
254
+ }
236
255
  }
237
256
  },
257
+ "dependencies": {
258
+ "zod": "^4.3.6"
259
+ },
238
260
  "peerDependencies": {
239
261
  "react": "^19.0.0",
240
262
  "react-dom": "^19.0.0",
241
263
  "three": ">=0.186.0 <0.187.0",
242
- "@volter/sdk": "0.5.212",
243
- "@volter/project": "0.5.212",
264
+ "@volter/sdk": "0.5.214",
265
+ "@volter/project": "0.5.214",
244
266
  "vite": "^6.4.3 || ^7.0.0 || ^8.0.0",
245
- "@volter/live": "0.5.212",
246
- "@volter/editor-blender": "0.1.151",
247
- "@volter/editor-threejs": "0.5.212",
267
+ "@volter/live": "0.5.214",
268
+ "@volter/editor-blender": "0.1.153",
269
+ "@volter/editor-threejs": "0.5.214",
248
270
  "@react-three/fiber": "^9.8.2",
249
271
  "@react-three/rapier": "^2.2.0",
250
272
  "postprocessing": "^6.39.5",
251
- "@volter-ai/std": "^0.8.0"
273
+ "@volter-ai/std": "^0.8.0-0"
252
274
  },
253
275
  "scripts": {
254
276
  "typecheck": "tsc -p tsconfig.json",
@@ -259,10 +281,10 @@
259
281
  "access": "public"
260
282
  },
261
283
  "devDependencies": {
262
- "@volter/editor-blender": "0.1.151",
263
- "@volter/editor-threejs": "0.5.212",
264
- "@volter/live": "0.5.212",
265
- "@volter/sdk": "0.5.212",
284
+ "@volter/editor-blender": "0.1.153",
285
+ "@volter/editor-threejs": "0.5.214",
286
+ "@volter/live": "0.5.214",
287
+ "@volter/sdk": "0.5.214",
266
288
  "esbuild": "0.25.12",
267
289
  "typescript": "5.9.3"
268
290
  },
package/serving/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `@volter/play`'s server half (`package.json#contributes.volter.serving`): a std game's dev server as Play reaches it,
3
3
  * and its level export's Node half (`std-dev.ts`); a std level's scene as a module of the game's own graph, which the
4
- * Scene view draws (`std-scene.ts`).
4
+ * level's document draws (`std-scene.ts`).
5
5
  */
6
6
  import type { ProjectServingServices } from '@volter/sdk/session/project-serving';
7
7
  import { stdDevPlugin } from './std-dev';