Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 24 additions & 17 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,13 @@ When you add a new feature that needs to write data, route it through
`uuid`/`position`/`rotation`/`scale`/`visible`/`physics`/`_internal.three`
only — there is **no** `gameObject.game`; in the editor `game` is
typically `undefined`, so guard any `game.renderer.*` access.
- Lifecycle docs: `docs/behaviors/`.
- Lifecycle docs, catalog, and authoring guide: `docs/built-in-behaviors.md`.

## Lambdas (ECS)

`client/packages/editor-oss/src/lambdas/` — archetype-driven systems on
top of behaviors. Use when you need batched, dependency-scheduled work.
See `docs/lambdas/` for the architecture internals.
See `docs/lambdas.md` for the architecture internals.

## Scheduler, rendering, quality

Expand All @@ -135,8 +135,10 @@ lambda systems directly.

Two engines, one adapter surface:
`client/packages/editor-oss/src/physics/` plus the per-behavior glue in
`behaviors/erth/physics/`. Memory management for Ammo and shape-system
conventions are non-obvious — read `docs/physics/` before touching either.
`behaviors/stem/physics/`. Memory management for Ammo and shape-system
conventions are non-obvious — read the physics helper / `PhysicsSettings` /
`RigidBodyHandle` sections of `docs/gameobject-and-game-manager-api.md` and the
code itself before touching either.

## Editor UI

Expand Down Expand Up @@ -243,19 +245,24 @@ Pause and ask before:

| You're touching... | Read first |
|---|---|
| Behaviors / game logic | `behaviors/Behavior.ts`, then `docs/behaviors/` |
| Lambdas / ECS | `lambdas/`, then `docs/lambdas/` |
| Scheduler / frame loop / quality | `scheduler/`, `core/quality/`, `docs/engine-core/` |
| Physics | `physics/`, `behaviors/erth/physics/`, `docs/physics/` |
| Editor UI / import / camera | `editor/`, `controls/`, `serialization/`, `docs/editor/` |
| Runtime UI / HUD / UIKit | `behaviors/uikit/`, `behaviors/hud/`, `docs/ui/` |
| Multiplayer | `multiplayer/`, `multiplayer/worker/`, `docs/multiplayer/` |
| AI integration | `copilot/`, `agent/`, `server/server/controllers/tools/ai/`, `docs/ai/` |
| AI server | `server/main.go`, `server/server/server.go`, `docs/infrastructure/` |
| Persistence | `persistence/`, `docs/editor/` (asset-management.md) |
| Scene serialization | `object/`, `serialization/`, `docs/ARCHITECTURE.md` |
| Three.js conventions | `EngineRuntime.ts`, `render/`, `docs/engine-core/threejs-conventions.md` |
| Art budgets | `docs/art-specs/ART_SPECS.md` |
| Behaviors / game logic | `behaviors/Behavior.ts`, then `docs/built-in-behaviors.md` |
| Lambdas / ECS | `lambdas/`, then `docs/lambdas.md` |
| Import packs / reusable scripts | `editor/scripts/builtinPacks/`, then `docs/import-packs.md` |
| Runtime / engine API (`this.stem.*`) | `EngineRuntime.ts`, then `docs/runtime-api.md` |
| GameObject / GameManager API | `object/`, `behaviors/game/GameManager.ts`, then `docs/gameobject-and-game-manager-api.md` |
| Scheduler / frame loop / quality | `scheduler/`, `core/quality/`, then `docs/scheduler-and-editor-settings.md` |
| Physics | `physics/`, `behaviors/stem/physics/`, then `docs/gameobject-and-game-manager-api.md` (PhysicsSettings / RigidBodyHandle) |
| Editor UI / import / camera | `editor/`, `controls/`, `serialization/` (no dedicated doc) |
| Runtime UI / HUD / UIKit | `behaviors/uikit/`, `behaviors/hud/`, then `docs/uikit-api.md` |
| Multiplayer | `multiplayer/`, `multiplayer/worker/`, then `docs/multiplayer.md` |
| AI integration (client) | `copilot/`, `agent/`, `server/server/controllers/tools/ai/` |
| AI server (Go) | `server/cmd/ai-server/`, `server/server/server.go`, then `docs/architecture.md` |
| BYOK / provider keys | `docs/byok.md` |
| Persistence | `persistence/`, then `docs/server-side-storage.md` |
| Scene serialization | `object/`, `serialization/`, then `docs/architecture.md` |
| Three.js conventions | `EngineRuntime.ts`, `render/` (no dedicated doc) |
| Exporting a game | `scripts/`, then `docs/exporting-a-game.md` |
| Art budgets | `blog/docs/assets/10-art-specs.md` |

## Axis conventions

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ If all three print a version, you're ready for [Quick start](#quick-start).
## Documentation

- [Architecture overview](./docs/architecture.md) — how the editor, AI server, and multiplayer sidecar fit together.
- [Scheduler & editor settings](./docs/scheduler-and-editor-settings.md) — frame scheduler architecture, quality presets, performance controls, and profiling tools.
- [Built-in behaviors](./docs/built-in-behaviors.md) — the behavior model, the full catalog, and how to attach or author one.
- [Lambdas (ECS layer)](./docs/lambdas.md) — batched, dependency-scheduled systems over many objects, with examples.
- [Import packs](./docs/import-packs.md) — curated reusable script modules (`noise`, `prng`, `uikit-dual-mode`) and the `@import` workflow.
- [BYOK setup](./docs/byok.md) — connect your AI provider keys.
- [Multiplayer guide](./docs/multiplayer.md) — local sidecar and self-hosted deployment.
- [Exporting a game](./docs/exporting-a-game.md) — package a Player-only static site.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,13 @@ describe("ScriptCommandParser.parse", () => {
expect(result.params.filter).toBe("box");
});

it("parses list lights to a read-only scene query", () => {
// Lights are scene objects; `list lights` aliases to get_scene_objects
// so the copilot can enumerate them during inspection.
const result = ScriptCommandParser.parse("list lights");
expect(result.command).toBe("get_scene_objects");
});

it("parses get with target", () => {
const result = ScriptCommandParser.parse("get MyBox");
expect(result.command).toBe("get_object");
Expand Down
3 changes: 3 additions & 0 deletions client/packages/editor-oss/src/agent/script-tool/aliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ export const ALIAS_MAP: Record<string, AliasMapping> = {

// Scene queries
"list objects": {command: "get_scene_objects"},
// Lights are scene objects; enumerate them via the scene tree, then read
// one with `get light <Target>` (-> get_light_settings).
"list lights": {command: "get_scene_objects"},
"list assets": {command: "list_scene_assets"},
"list imports": {command: "list_scene_assets", staticParams: {type: "imports"}},
"list files": {command: "list_scene_assets", staticParams: {type: "files"}},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import {describe, expect, it, vi} from "vitest";

import {deriveCheckPlan, deriveCheckProbes, runScriptCheck} from "./checkScript";
import {deriveCheckPlan, deriveCheckProbes, isReadOnlyCommand, runScriptCheck} from "./checkScript";
import {ScriptExecutor} from "./ScriptExecutor";

describe("isReadOnlyCommand", () => {
it("classifies get_/list_/search_ prefixes and player/select as read-only", () => {
for (const cmd of ["get_scene_objects", "get_light_settings", "list_behaviors", "list_lambdas", "search_external_assets", "player", "select"]) {
expect(isReadOnlyCommand(cmd), cmd).toBe(true);
}
});

it("classifies mutating commands as not read-only", () => {
for (const cmd of ["modify_object", "create_primitive", "set_light_properties", "delete_object", "attach_behavior", "import", "save"]) {
expect(isReadOnlyCommand(cmd), cmd).toBe(false);
}
});
});

describe("checkScript probe derivation", () => {
it("derives an object getter probe for primitive creation", () => {
const lines = ScriptExecutor.parseScript('add box name="Crate" position=1,2,3 color=#ff0000');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ function deriveProbeForCommand(
return probes;
}

function isReadOnlyCommand(command: string): boolean {
export function isReadOnlyCommand(command: string): boolean {
return READ_ONLY_PREFIXES.some(prefix => command.startsWith(prefix)) || command === "player" || command === "select";
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,22 @@ describe("playgroundStemscriptPlan", () => {
expect(() => validateInspectionStemscript("behavior attach Player behaviorId=character")).toThrow(/not allowed in inspection/);
});

it("allows `list lights` and any engine read-only command in inspection", () => {
// Regression: `list lights` previously aborted the whole request because
// it was absent from a narrow hardcoded allowlist. It now resolves to a
// real read-only command (get_scene_objects) and is permitted.
const result = validateInspectionStemscript(
["list lights", "get light DirectionalLight", "get render settings"].join("\n"),
);
expect(result.executableCommands).toBe(3);
expect(result.script).toContain("list lights");

// Read-only-prefixed commands the playground globally disallows (external
// search, library, project tasks) stay rejected even in inspection.
expect(() => validateInspectionStemscript("search_external_assets")).toThrow(/not allowed in inspection/);
expect(() => validateInspectionStemscript("list_project_tasks")).toThrow(/not allowed in inspection/);
});

it("rejects file and external-asset commands in playground mode", () => {
expect(() => validateGeneratedStemscript("import model Tree filepath=models/tree.glb")).toThrow(/not allowed/);
expect(() => validateGeneratedStemscript("generate model prompt=\"make a spaceship\"")).toThrow(/not allowed/);
Expand Down
32 changes: 8 additions & 24 deletions client/packages/editor-oss/src/copilot/playgroundStemscriptPlan.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {isReadOnlyCommand} from "../agent/script-tool/checkScript";
import {ScriptExecutor} from "../agent/script-tool/ScriptExecutor";

export interface PlaygroundStemscriptPlan {
Expand Down Expand Up @@ -32,29 +33,12 @@ const DISALLOWED_COMMANDS = new Set([
"add_model_to_scene",
"set_external_texture",
]);
const READ_ONLY_COMMANDS = new Set([
"get_scene_objects",
"get_object",
"get_object_settings",
"get_material_settings",
"get_behavior_settings",
"get_selected_object",
"get_player",
"list_scene_assets",
"get_scene_asset",
"list_behaviors",
"get_behavior",
"list_lambdas",
"get_lambda",
"get_physics_settings",
"get_light_settings",
"get_vfx",
"list_prefabs",
"get_prefab",
"get_camera_settings",
"get_editor_settings",
"get_scene_setting",
]);
// Inspection allows any command the engine classifies as read-only
// (get_/list_/search_ + player/select via isReadOnlyCommand), so the copilot
// can inspect the full scene and every asset type — except commands the
// playground globally disallows (external search, library, project tasks).
const isAllowedInspectionCommand = (command: string): boolean =>
isReadOnlyCommand(command) && !DISALLOWED_COMMANDS.has(command);

const stripCodeFence = (value: string): string => {
const trimmed = value.trim();
Expand Down Expand Up @@ -159,7 +143,7 @@ export function validateGeneratedStemscript(script: string): ValidatedStemscript
}

export function validateInspectionStemscript(script: string): ValidatedStemscript {
return validateStemscript(script, command => !READ_ONLY_COMMANDS.has(command), "inspection");
return validateStemscript(script, command => !isAllowedInspectionCommand(command), "inspection");
}

function validateStemscript(
Expand Down
47 changes: 42 additions & 5 deletions client/packages/site/src/components/MarkdownPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,58 @@ import type {DocEntry} from "../content/docs-nav";
// Build-time raw imports keep docs in the static bundle (no fetch at runtime).
// Vite's `?raw` query returns the file as a string.
import architectureMd from "../../../../../docs/architecture.md?raw";
import builtInBehaviorsMd from "../../../../../docs/built-in-behaviors.md?raw";
import byokMd from "../../../../../docs/byok.md?raw";
import exportingMd from "../../../../../docs/exporting-a-game.md?raw";
import gameObjectAndGameManagerApiMd from "../../../../../docs/gameobject-and-game-manager-api.md?raw";
import importPacksMd from "../../../../../docs/import-packs.md?raw";
import lambdasMd from "../../../../../docs/lambdas.md?raw";
import multiplayerMd from "../../../../../docs/multiplayer.md?raw";
import runtimeApiMd from "../../../../../docs/runtime-api.md?raw";
import schedulerAndEditorSettingsMd from "../../../../../docs/scheduler-and-editor-settings.md?raw";
import serverSideStorageMd from "../../../../../docs/server-side-storage.md?raw";
import uikitApiMd from "../../../../../docs/uikit-api.md?raw";
import readmeMd from "../../../../../README.md?raw";
import contributingMd from "../../../../../CONTRIBUTING.md?raw";
import defaultSceneSettingsImg from "../../../../../docs/assets/default-scene-settings.png";
import directionalLightSettingsImg from "../../../../../docs/assets/directional-light-settings.png";
import editorProjectTabMapImg from "../../../../../docs/assets/editor-project-tab-map.png";
import projectSettingsOverviewImg from "../../../../../docs/assets/project-settings-overview.png";
import schedulerBehaviorPerformanceImg from "../../../../../docs/assets/scheduler-behavior-performance.png";
import schedulerControlsImg from "../../../../../docs/assets/scheduler-controls.png";
import schedulerLambdaExplorerImg from "../../../../../docs/assets/scheduler-lambda-explorer.png";
import schedulerQualityPresetsImg from "../../../../../docs/assets/scheduler-quality-presets.png";
import schedulerSettingsOverviewImg from "../../../../../docs/assets/scheduler-settings-overview.png";

const SOURCES: Record<string, string> = {
"repo-docs:architecture.md": architectureMd,
"repo-docs:built-in-behaviors.md": builtInBehaviorsMd,
"repo-docs:byok.md": byokMd,
"repo-docs:exporting-a-game.md": exportingMd,
"repo-docs:gameobject-and-game-manager-api.md": gameObjectAndGameManagerApiMd,
"repo-docs:import-packs.md": importPacksMd,
"repo-docs:lambdas.md": lambdasMd,
"repo-docs:multiplayer.md": multiplayerMd,
"repo-docs:runtime-api.md": runtimeApiMd,
"repo-docs:scheduler-and-editor-settings.md": schedulerAndEditorSettingsMd,
"repo-docs:server-side-storage.md": serverSideStorageMd,
"repo-docs:uikit-api.md": uikitApiMd,
"repo-root:README.md": readmeMd,
"repo-root:CONTRIBUTING.md": contributingMd,
};

const IMAGE_SOURCES: Record<string, string> = {
"docs/assets/default-scene-settings.png": defaultSceneSettingsImg,
"docs/assets/directional-light-settings.png": directionalLightSettingsImg,
"docs/assets/editor-project-tab-map.png": editorProjectTabMapImg,
"docs/assets/project-settings-overview.png": projectSettingsOverviewImg,
"docs/assets/scheduler-behavior-performance.png": schedulerBehaviorPerformanceImg,
"docs/assets/scheduler-controls.png": schedulerControlsImg,
"docs/assets/scheduler-lambda-explorer.png": schedulerLambdaExplorerImg,
"docs/assets/scheduler-quality-presets.png": schedulerQualityPresetsImg,
"docs/assets/scheduler-settings-overview.png": schedulerSettingsOverviewImg,
};

interface Props {
entry: DocEntry;
}
Expand Down Expand Up @@ -70,10 +99,18 @@ function rewriteRepoLinks(md: string, entry: DocEntry): string {
// Anchor non-curated relative links to the GitHub source so users don't
// get 404s when a doc links to a file we haven't surfaced on the site.
const basePath = entry.source === "repo-root" ? "" : "docs/";
return md.replace(/\]\((?!https?:|#|\/)([^)]+)\)/g, (_match, rel: string) => {
if (rel.startsWith("docs/")) {
return `](${GITHUB_URL}/blob/main/${rel})`;
}
return `](${GITHUB_URL}/blob/main/${basePath}${rel})`;
const rawGithubBase = GITHUB_URL.replace("https://github.com/", "https://raw.githubusercontent.com/");
return md.replace(/(!?)\[([^\]]*)\]\((?!https?:|#|\/)([^)]+)\)/g, (_match, bang: string, label: string, rel: string) => {
const repoPath = normalizeRepoPath(rel.startsWith("docs/") ? rel : `${basePath}${rel}`);
const target = bang
? (IMAGE_SOURCES[repoPath] ?? `${rawGithubBase}/main/${repoPath}`)
: `${GITHUB_URL}/blob/main/${repoPath}`;
return `${bang}[${label}](${target})`;
});
}

function normalizeRepoPath(path: string): string {
return path
.replace(/^\.\//, "")
.replace(/\/\.\//g, "/");
}
9 changes: 9 additions & 0 deletions client/packages/site/src/content/docs-nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,19 @@ export const DOC_SECTIONS: DocSection[] = [
label: "Engine",
entries: [
{slug: "architecture", title: "Architecture", source: "repo-docs", file: "architecture.md"},
{slug: "scheduler-and-editor-settings", title: "Scheduler & editor settings", source: "repo-docs", file: "scheduler-and-editor-settings.md"},
{slug: "exporting-a-game", title: "Exporting a game", source: "repo-docs", file: "exporting-a-game.md"},
{slug: "server-side-storage", title: "Server-side storage & version control", source: "repo-docs", file: "server-side-storage.md"},
],
},
{
label: "Scripting",
entries: [
{slug: "built-in-behaviors", title: "Built-in behaviors", source: "repo-docs", file: "built-in-behaviors.md"},
{slug: "lambdas", title: "Lambdas (ECS)", source: "repo-docs", file: "lambdas.md"},
{slug: "import-packs", title: "Import packs", source: "repo-docs", file: "import-packs.md"},
],
},
{
label: "APIs",
entries: [
Expand Down
Binary file added docs/assets/default-scene-settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/directional-light-settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/editor-project-tab-map.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/project-settings-overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/scheduler-behavior-performance.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/scheduler-controls.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/scheduler-lambda-explorer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/scheduler-quality-presets.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/scheduler-settings-overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading