3 Commits
Author SHA1 Message Date
sanspie 6a22396467 Stage 9 complete
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 22m2s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 23m5s
2026-10-20 14:06:22 +03:00
sanspie 21e03128a8 Stage 9.6d2: MCP server and external edits in the editor (D297, D300) 2026-10-20 13:56:45 +03:00
sanspie e5b1964f3e Stage 9.6d1: agent layer and CLI (D299) 2026-10-20 13:48:01 +03:00
42 changed files with 4741 additions and 56 deletions
+83
View File
File diff suppressed because one or more lines are too long
+146
View File
File diff suppressed because it is too large Load Diff
+85
View File
@@ -0,0 +1,85 @@
package agent
import (
"bytes"
"encoding/json"
"testing"
)
func mustFind(t *testing.T, name string) *Command {
t.Helper()
c, ok := Find(name)
if !ok {
t.Fatalf("no command %s", name)
}
return c
}
func TestParseArgs(t *testing.T) {
set := mustFind(t, "scene.set")
a, f, err := parseArgs(set, []string{"s.aescene", "--json", "Player", "--dry-run", "Body2D.mass", "-3"})
if err != nil || !f.json || a.Str("value") != "-3" || a.Str("node") != "Player" || !a.Bool("dry-run") {
t.Fatalf("flags after positionals: %v %+v %v", a, f, err)
}
add := mustFind(t, "scene.add-node")
for _, toks := range [][]string{{"s", "N", "--index=1"}, {"s", "N", "--index", "1"}} {
a, _, err := parseArgs(add, toks)
if n, _ := a.Int("index", -1); err != nil || n != 1 {
t.Fatalf("%v: %v %v", toks, a, err)
}
}
a, _, err = parseArgs(set, []string{"s", "--", "Player", "Body2D.mass", "--odd"})
if err != nil || a.Str("value") != "--odd" {
t.Fatalf("after --: %v %v", a, err)
}
if _, _, err := parseArgs(set, []string{"s", "--nope"}); err == nil {
t.Fatal("an unknown flag was accepted")
}
if _, _, err := parseArgs(set, []string{"s", "a", "b", "c", "d"}); err == nil {
t.Fatal("an extra positional was accepted")
}
var out, errw bytes.Buffer
if code := Main(nil, []string{"scene", "set", "s", "--nope"}, &out, &errw, Options{}); code != 2 {
t.Fatalf("unknown flag exit %d", code)
}
if code := Main(nil, []string{"scene", "set", "a", "b", "c", "d", "e"}, &out, &errw, Options{}); code != 2 {
t.Fatalf("extra positional exit %d", code)
}
if code := Main(nil, []string{"scene", "help"}, &out, &errw, Options{}); code != 0 {
t.Fatalf("help exit %d", code)
}
}
func TestSchemasAndToolNames(t *testing.T) {
seen := map[string]bool{}
for _, c := range Commands() {
if seen[c.Tool()] {
t.Fatalf("tool name %s is used twice", c.Tool())
}
seen[c.Tool()] = true
b, err := json.Marshal(c.Schema())
if err != nil {
t.Fatal(err)
}
var sch struct {
Type string `json:"type"`
Properties map[string]map[string]any `json:"properties"`
Required []string `json:"required"`
}
if err := json.Unmarshal(b, &sch); err != nil || sch.Type != "object" {
t.Fatalf("%s: %s", c.Name, b)
}
var want []string
for _, x := range c.AllArgs() {
if x.Required {
want = append(want, x.Name)
}
if _, ok := sch.Properties[x.Name]; !ok {
t.Fatalf("%s: no property %s", c.Name, x.Name)
}
}
if len(want) != len(sch.Required) {
t.Fatalf("%s: required %v, want %v", c.Name, sch.Required, want)
}
}
}
File diff suppressed because it is too large Load Diff
+194
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+180
View File
File diff suppressed because it is too large Load Diff
+66
View File
@@ -0,0 +1,66 @@
package agent
import (
"strconv"
"aego/asset"
"aego/project/refs"
"aego/scene/mutate"
)
func projectCommands() []*Command {
return []*Command{
{Name: "project.validate", Doc: "missing assets and nodes, import errors, prefabs and scenes that do not load", Run: (*Session).projectValidate},
}
}
func (s *Session) projectValidate(Args) (*Output, error) {
out := &Output{}
add := func(err bool, node, msg string) {
out.Problems = append(out.Problems, mutate.Problem{Error: err, Node: node, Message: msg})
}
for _, m := range refs.MissingAssets(s.Project, s.Pipeline) {
add(true, "", "missing asset "+m.Label())
}
for _, m := range refs.MissingNodes(s.Pipeline) {
add(true, m.Scene+" "+m.Path, m.Owner+"."+m.Field+" references a node that is not in the scene")
}
if s.scriptErr != nil {
add(true, "", "script types: "+s.scriptErr.Error())
}
for _, e := range s.libErrs {
add(true, "", e.Error())
}
for _, x := range s.Pipeline.Assets() {
if x.Error != "" {
add(true, x.Path, x.Error)
continue
}
if x.Kind != asset.ArtScene && x.Kind != asset.ArtPrefab {
continue
}
src, err := s.read(x.Path)
if err != nil {
add(true, x.Path, err.Error())
continue
}
doc, err := mutate.Load(src, x.Path, s.env())
if err != nil {
add(true, x.Path, err.Error())
continue
}
for _, p := range doc.Problems {
add(p.Error, x.Path+" "+p.Node, p.Message)
}
}
errs, warns := 0, 0
for _, p := range out.Problems {
if p.Error {
errs++
} else {
warns++
}
}
out.Notes = []string{"project: " + strconv.Itoa(errs) + " error(s), " + strconv.Itoa(warns) + " warning(s)"}
return out, nil
}
+237
View File
File diff suppressed because it is too large Load Diff
+393
View File
File diff suppressed because it is too large Load Diff
+166
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+65
View File
@@ -0,0 +1,65 @@
package run
import (
"bytes"
"encoding/hex"
"strconv"
"strings"
"aego/app/agent"
"aego/core/log"
"aego/core/rewind"
"aego/project"
"aego/sdk"
)
const maxRunLog = 200
func AgentRunner(mods ...sdk.Module) agent.Runner {
return func(p *project.Project, r agent.RunRequest) (agent.RunReport, error) {
var buf bytes.Buffer
lg := log.New(log.NewTextSink(&buf))
lg.SetLevel(log.Warn)
frames, seed := r.Frames, r.Seed
if frames <= 0 {
frames = 120
}
if seed == 0 {
seed = 1
}
a, err := Open(p, Config{Headless: true, FrameTime: defaultDetStep, Seed: seed, ScenePath: r.Scene, Logger: lg, NoProfile: true}, mods...)
if err != nil {
return agent.RunReport{Log: logLines(buf.String())}, err
}
rep := agent.RunReport{}
driver := a.Driver()
for rep.Frames < frames {
if !a.Engine.Step(driver) {
rep.Error = "the game stopped at frame " + strconv.Itoa(rep.Frames+1)
if err := a.Engine.Err(); err != nil {
rep.Error += ": " + err.Error()
}
break
}
rep.Frames++
}
sum, _, _ := WorldHash(a.Engine.MainWorld(), rewind.NewHasher())
rep.Hash = hex.EncodeToString(sum[:])
a.Close()
rep.Log = logLines(buf.String())
return rep, nil
}
}
func logLines(s string) []string {
var out []string
for _, l := range strings.Split(strings.TrimSpace(s), "\n") {
if l = strings.TrimSpace(l); l != "" {
out = append(out, l)
}
}
if len(out) > maxRunLog {
out = append(out[:maxRunLog], "…")
}
return out
}
+1 -1
View File
@@ -78,7 +78,7 @@ func WorldHash(w *engine.World, h *rewind.Hasher) (sum [32]byte, covered, skippe
}
func determinismPass(p *project.Project, o DeterminismOptions, mods []sdk.Module) ([]checkpoint, []string, []string, error) {
a, err := Open(p, Config{Headless: true, FrameTime: o.FrameTime, Seed: o.Seed, ScenePath: o.ScenePath, Logger: log.Nop()}, mods...)
a, err := Open(p, Config{Headless: true, FrameTime: o.FrameTime, Seed: o.Seed, ScenePath: o.ScenePath, Logger: log.Nop(), NoProfile: true}, mods...)
if err != nil {
return nil, nil, nil, err
}
+2
View File
@@ -43,6 +43,7 @@ type Config struct {
Profile string
Logger *log.Logger
Seed uint64
NoProfile bool
}
type App struct {
@@ -138,6 +139,7 @@ func Open(p *project.Project, cfg Config, mods ...sdk.Module) (*App, error) {
Debug: cfg.Debug,
FailOnScriptErrors: true,
Profile: cfg.Profile,
NoProfile: cfg.NoProfile,
}, sdk.AdaptAll(mods)...)
if err != nil {
return nil, err
+7
View File
@@ -1,10 +1,13 @@
package run
import (
"aego/app/agent"
"aego/app/agent/mcp"
"bytes"
"flag"
"fmt"
"io"
"os"
"sort"
"strings"
@@ -39,6 +42,10 @@ func Tool(p *project.Project, args []string, stdout, stderr io.Writer, mods ...s
return toolTest(p, args[1:], stdout, stderr, mods)
case "docs":
return docstool.Tool(p, args[1:], stdout, stderr, sdk.AdaptAll(mods)...)
case "agent":
return agent.Main(p, args[1:], stdout, stderr, agent.Options{Mods: mods, Run: AgentRunner(mods...)})
case "mcp":
return mcp.Main(p, args[1:], os.Stdin, stdout, stderr, agent.Options{Mods: mods, Run: AgentRunner(mods...)})
}
fmt.Fprintf(stderr, "unknown tool %q\n", args[0])
return 2
File diff suppressed because it is too large Load Diff
+44
View File
@@ -1,11 +1,14 @@
package main
import (
"aego/app/agent"
"aego/app/agent/mcp"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"aego/app/bootstrap"
@@ -50,6 +53,8 @@ func dispatch(args []string) int {
return cmdCheck(args[1:])
case "icons":
return cmdIcons(args[1:])
case "mcp":
return cmdMcp(args[1:])
case "docs":
return cmdDocs(args[1:])
case "test":
@@ -60,6 +65,9 @@ func dispatch(args []string) int {
usage()
return 0
}
if slices.Contains(agent.Groups(), args[0]) {
return cmdAgent(args)
}
fmt.Fprintf(os.Stderr, "unknown command %q\n", args[0])
usage()
return 2
@@ -83,6 +91,8 @@ func usage() {
run *_test.aes script tests headless, or run the game twice and compare state hashes
dump script <pkg|Type> [--func name] disassemble a script package with source lines
docs export|check|build|serve documentation registry, site and playground
mcp [--project dir] [--watch] serve the project to an AI agent over MCP (stdio)
scene|prefab|data|refs|asset|schema|project <verb> ... edit project files without the editor, see aego scene help
`)
}
@@ -97,6 +107,40 @@ func openProject(dir string) (*project.Project, error) {
return project.Load(root)
}
func projectFlag(args []string) string {
for i, a := range args {
switch {
case a == "--project" && i+1 < len(args):
return args[i+1]
case strings.HasPrefix(a, "--project="):
return strings.TrimPrefix(a, "--project=")
}
}
return ""
}
func cmdAgent(args []string) int {
p, err := openProject(projectFlag(args))
if err != nil {
return fail(err)
}
if bootstrap.NeedsNative(p) {
return scriptTool(p, append([]string{"agent"}, args...)...)
}
return agent.Main(p, args, os.Stdout, os.Stderr, agent.Options{Run: run.AgentRunner()})
}
func cmdMcp(args []string) int {
p, err := openProject(projectFlag(args))
if err != nil {
return fail(err)
}
if bootstrap.NeedsNative(p) {
return scriptTool(p, append([]string{"mcp"}, args...)...)
}
return mcp.Main(p, args, os.Stdin, os.Stdout, os.Stderr, agent.Options{Run: run.AgentRunner()})
}
func firstArg(args []string) (string, []string) {
if len(args) > 0 && len(args[0]) > 0 && args[0][0] != '-' {
return args[0], args[1:]
+1
View File
@@ -23,6 +23,7 @@ func scriptTool(p *project.Project, args ...string) int {
}
cmd := exec.Command(binary, append([]string{"--tool"}, args...)...)
cmd.Dir = p.Root
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return exitCode(cmd.Run())
+28 -20
View File
@@ -160,26 +160,27 @@ type ScriptHost interface {
}
type Editor struct {
Engine *engine.Engine
Renderer *render.Renderer
Project *project.Project
Pipeline *pipeline.Pipeline
Loader *loader.Loader
Types *scene.Types
Docs *document.Manager
Selection *selection.Service
Commands *command.Registry
UI *core.Context
DnD *dnd.State
Clipboard DataClipboard
Log *log.ChannelLogger
Settings SceneSettings
Thumbs ThumbSource
Scripts ScriptHost
Code ScriptNav
Shapes *shape.Registry
Problems *problems.Service
Sections *module.Sections
BeforeSwap func(*pipeline.Batch) func()
Engine *engine.Engine
Renderer *render.Renderer
Project *project.Project
Pipeline *pipeline.Pipeline
Loader *loader.Loader
Types *scene.Types
Docs *document.Manager
Selection *selection.Service
Commands *command.Registry
UI *core.Context
DnD *dnd.State
Clipboard DataClipboard
Log *log.ChannelLogger
Settings SceneSettings
Thumbs ThumbSource
Scripts ScriptHost
Code ScriptNav
Shapes *shape.Registry
Problems *problems.Service
Sections *module.Sections
ShowProblems func(filter string)
RevealPath func(rel string)
@@ -428,6 +429,10 @@ func (e *Editor) ApplyBatch(b *pipeline.Batch) {
if b == nil {
return
}
var after func()
if e.BeforeSwap != nil {
after = e.BeforeSwap(b)
}
if e.Loader != nil && e.Pipeline != nil {
if len(b.Imported) > 0 {
if err := e.Loader.Swap(b.Imported, e.Pipeline.Artifacts()); err != nil {
@@ -438,6 +443,9 @@ func (e *Editor) ApplyBatch(b *pipeline.Batch) {
e.Loader.Drop(id)
}
}
if after != nil {
after()
}
for id, err := range b.Failed {
e.Notify(LevelWarn, "import failed for "+id.String()[:8]+": "+err.Error())
}
+14 -13
View File
@@ -84,19 +84,20 @@ func cloneFields(fs []aetext.Field) []aetext.Field {
}
type Doc struct {
ID ids.AssetID
Path string
Type *schema.TypeDescriptor
reg *schema.Registry
lib *data.Library
dd *mutate.DataDocument
ptr unsafe.Pointer
origin unsafe.Pointer
issues []string
steps steps
saved []byte
disk []byte
rev uint64
ID ids.AssetID
Path string
Type *schema.TypeDescriptor
reg *schema.Registry
lib *data.Library
dd *mutate.DataDocument
ptr unsafe.Pointer
origin unsafe.Pointer
issues []string
steps steps
saved []byte
disk []byte
conflict []byte
rev uint64
}
func NewDoc(env mutate.Env, lib *data.Library, id ids.AssetID, path string, raw []byte) (*Doc, error) {
+19 -3
View File
@@ -130,7 +130,12 @@ func (s *Service) Doc(id ids.AssetID) (*Doc, error) {
if err != nil {
return nil, err
}
if d != nil && (d.Dirty() || bytes.Equal(raw, d.disk)) {
if d != nil && d.Dirty() {
s.conflict(rel, raw, d.disk, &d.conflict)
d.Path, d.rev = rel, s.ctx.AssetRev
return d, d.load()
}
if d != nil && bytes.Equal(raw, d.disk) {
d.Path, d.rev = rel, s.ctx.AssetRev
return d, d.load()
}
@@ -149,14 +154,17 @@ func (s *Service) Doc(id ids.AssetID) (*Doc, error) {
func (s *Service) Table(id ids.AssetID) (*Table, error) {
t := s.tables[id]
if t != nil && (t.rev == s.ctx.AssetRev || t.Dirty()) {
if t != nil && t.rev == s.ctx.AssetRev {
return t, nil
}
rel, raw, err := s.file(id)
if err != nil {
return nil, err
}
if t != nil && bytes.Equal(raw, t.disk) {
if t != nil && (t.Dirty() || bytes.Equal(raw, t.disk)) {
if t.Dirty() {
s.conflict(rel, raw, t.disk, &t.conflict)
}
t.Path, t.rev = rel, s.ctx.AssetRev
return t, nil
}
@@ -169,6 +177,14 @@ func (s *Service) Table(id ids.AssetID) (*Table, error) {
return nt, nil
}
func (s *Service) conflict(rel string, raw, disk []byte, seen *[]byte) {
if bytes.Equal(raw, disk) || bytes.Equal(raw, *seen) {
return
}
*seen = raw
s.ctx.Notify(context.LevelWarn, rel+" changed on disk while it has unsaved changes: Save keeps yours, Revert loads the disk version")
}
func (s *Service) RevertDoc(id ids.AssetID) {
delete(s.docs, id)
}
+13 -12
View File
@@ -22,18 +22,19 @@ type Row struct {
}
type Table struct {
ID ids.AssetID
Path string
Type *schema.TypeDescriptor
Rows []Row
Issues []string
reg *schema.Registry
def unsafe.Pointer
dd *mutate.DataDocument
steps steps
saved []byte
disk []byte
rev uint64
ID ids.AssetID
Path string
Type *schema.TypeDescriptor
Rows []Row
Issues []string
reg *schema.Registry
def unsafe.Pointer
dd *mutate.DataDocument
steps steps
saved []byte
disk []byte
conflict []byte
rev uint64
}
func NewTable(env mutate.Env, id ids.AssetID, path string, raw []byte) (*Table, error) {
+32
View File
@@ -4,6 +4,8 @@ import (
"os"
"slices"
"aego/asset"
"aego/asset/pipeline"
"aego/core/errs"
"aego/core/ids"
"aego/editor/core/context"
@@ -99,6 +101,8 @@ func (s *Service) reimport(list []ids.AssetID) error {
if len(list) == 0 {
return nil
}
s.writing = true
defer func() { s.writing = false }()
batch, err := s.ctx.Pipeline.Reimport(list...)
s.ctx.ApplyBatch(batch)
if err != nil {
@@ -291,3 +295,31 @@ func (s *Service) commit(label string, files []fileChange, src *source) error {
}
return nil
}
func (s *Service) External(b *pipeline.Batch) func() {
if s.writing || s.ctx.Docs.InPlay() {
return nil
}
changed := map[ids.AssetID]bool{}
for _, e := range b.Imported {
if e.Kind == asset.ArtPrefab {
changed[e.ID] = true
}
}
if len(changed) == 0 {
return nil
}
plan := s.affected(s.lib(), changed, true)
if len(plan) == 0 {
return nil
}
return func() {
lib := s.lib()
for _, t := range plan {
if _, err := commands.ReplaceNode(t.doc.Scene(), t.node, t.parent, t.index, t.block, lib); err != nil {
s.ctx.Notify(context.LevelError, "cannot refresh the prefab instances in "+t.doc.Path+": "+err.Error())
}
}
clear(s.infos)
}
}
+4 -3
View File
@@ -24,9 +24,10 @@ import (
const DefaultDir = "prefabs"
type Service struct {
ctx *context.Editor
infos map[infoKey]*Info
fixed *sceneio.Library
ctx *context.Editor
infos map[infoKey]*Info
writing bool
fixed *sceneio.Library
}
func New(ctx *context.Editor) *Service {
+5
View File
@@ -214,6 +214,10 @@ func (a *App) Workspace() *workspace.Workspace {
return a.ws
}
func (a *App) Data() *dataedit.Service {
return a.data
}
func (a *App) Play() *play.Controller {
return a.play
}
@@ -364,6 +368,7 @@ func (a *App) Init(e *engine.Engine) error {
}
a.files = assetfiles.New(a.ctx)
a.prefabs = prefabs.New(a.ctx)
a.ctx.BeforeSwap = a.prefabs.External
a.data = dataedit.New(a.ctx)
a.data.OnLive = func() { a.play.Invalidate("a data asset was tuned during Play") }
a.ctx.Selection.Subscribe(a.data.Clear)
+48
View File
@@ -0,0 +1,48 @@
---
id: agents
title: Working with AI agents
group: tools
order: 3
---
# Working with AI agents
`aego mcp` serves a project to an AI agent over the Model Context Protocol. The agent edits scenes, prefabs and data assets through the same operations the editor uses, reads the documentation and the type schema, and runs scenes headless to check its work. Every tool is also a command of `aego`, so anything an agent does can be repeated by hand or in CI.
## Connecting
Claude Code, from the project directory:
claude mcp add aego -- aego mcp --project . --watch
or a `.mcp.json` next to `project.aego`:
{"mcpServers": {"aego": {"command": "aego", "args": ["mcp", "--watch"]}}}
Other clients take the same command. The server speaks MCP over stdio; logs go to stderr. A project with native code is served by its own binary, so its components are known.
## Tools
Each tool has the name of its command with underscores (`scene set` is `scene_set`) and the same arguments; `aego scene help` lists them, and the CLI reference has the full table. `docs_search` and `docs_read` read the documentation. Write tools take `dry-run`, which reports the changes and writes nothing.
## Resources
| URI | Content |
|---|---|
| `aego://project/manifest` | `project.aego` |
| `aego://project/problems` | what `project_validate` reports |
| `aego://docs/index`, `aego://docs/<id>` | documentation |
| `aego://schema/index`, `aego://schema/<type>` | types and their fields |
| `aego://file/<path>` | a scene, prefab, data asset or table as text |
With `--watch` the server polls the project and sends `resources/updated` for subscribed resources when files change, including changes made in the editor.
## A typical session
1. `scene_tree` and `schema_show` to see what is there and what a component takes.
2. Changes with `scene_set`, `scene_add_node`, `scene_add_comp`, `prefab_instantiate`, or several at once with `scene_apply`; `dry-run` first when unsure.
3. `scene_validate`, then `scene_run` with a number of frames: the log has the warnings and errors, and the state hash shows whether two runs behave the same.
## With the editor open
The editor and the agent can work on the same project. A scene, prefab or data asset changed on disk reloads in the editor when it has no unsaved changes; with unsaved changes the editor asks whether to reload or keep its version, and data assets warn and keep yours until you save or revert. Prefab instances in open scenes follow a changed prefab file. While the editor is open, tools that create assets refuse: create them in the editor, or close it.
+59
View File
@@ -100,6 +100,65 @@ Reimports every asset; `--force` ignores the artifact cache.
`aego dump script <package|Type> [--func name]` disassembles a compiled script package with its source lines.
## aego mcp
Serves the project to an AI agent over MCP on stdio; see [Working with AI agents](ref:agents).
| Flag | Meaning |
|---|---|
| `--watch` | send resource updates when project files change |
| `--interval d` | how often `--watch` polls, 500ms by default |
| `--project dir` | project directory |
## Editing from the command line
These commands read and change scenes, prefabs, data assets and references without the editor, through the same operations the editor uses, and every change can be undone by applying its inverse. Paths are relative to the project root; a node is a path from the root (`Player/Sprite`) or `#<id>`; a value is written in scene syntax. Every command takes `--json` (a stable machine-readable report), `--quiet`, `--strict` (warnings fail too) and `--project dir`; commands that write take `--dry-run`, which reports the changes and touches nothing. The exit status is 0 on success, 1 on an error and 2 when the result has error-level problems. While the editor has the project open, commands that create assets refuse to run; edits of existing files are picked up by the editor.
<!-- agent:begin -->
| Command | Meaning |
|---|---|
| `aego asset delete <asset> [--force] [--dry-run]` | delete an asset; refuses while it is referenced unless --force |
| `aego asset list [--kind v]` | assets with their ids and kinds |
| `aego asset move <asset> <to> [--dry-run]` | move an asset with its .meta; the id and every reference stay |
| `aego asset rename <asset> <name> [--dry-run]` | rename an asset in its folder |
| `aego data add-row <file> <id> [--dry-run]` | add a row with defaults |
| `aego data base <file> [<asset>] [--dry-run]` | set or clear the base asset of a data asset |
| `aego data export <file> <csv> [--dry-run]` | write the rows of a table to a csv file |
| `aego data get <file> [--row v]` | fields set in a data asset or a table row; omitted fields use the base or the type default |
| `aego data import <file> <csv> [--dry-run]` | replace the rows of a table with a csv file |
| `aego data remove-row <file> <id> [--dry-run]` | remove a row |
| `aego data rename-row <file> <id> <name> [--dry-run]` | change a row id |
| `aego data reset <file> <path> [--row v] [--dry-run]` | remove a field so it falls back to the base or the default |
| `aego data rows <file>` | row ids of a table |
| `aego data set <file> <path> <value> [--row v] [--dry-run]` | set a field |
| `aego prefab apply <file> <node> [--level v] [--owner v] [--field v] [--nearest] [--dry-run]` | write instance overrides into the prefab file |
| `aego prefab create <file> <node> <out> [--dry-run]` | turn a node into a prefab file and link the node to it |
| `aego prefab instantiate <file> <prefab> [--parent v] [--name v] [--index v] [--dry-run]` | add an instance of a prefab |
| `aego prefab overrides <file> <node>` | list the overrides of the instance a node belongs to |
| `aego prefab revert <file> <node> [--level v] [--owner v] [--field v] [--dry-run]` | drop instance overrides |
| `aego prefab unpack <file> <node> [--deep] [--dry-run]` | turn an instance into plain nodes |
| `aego project validate` | missing assets and nodes, import errors, prefabs and scenes that do not load |
| `aego refs find <asset>` | files and fields that reference an asset |
| `aego refs fix <from> [<to>] [--dry-run]` | point every reference to one asset at another (none clears them); originals go to .aego/recovery |
| `aego refs missing` | references to assets and nodes that do not exist |
| `aego scene add-comp <file> <node> <comp> [--dry-run]` | add a component with its defaults |
| `aego scene add-node <file> <name> [--parent v] [--space v] [--index v] [--dry-run]` | add an empty node |
| `aego scene apply <file> [--tx v] [--tx-file v] [--dry-run]` | apply a transaction (tx version=1 { op … }) in one step |
| `aego scene get <file> <node> [<path>]` | a node with its transform and every saved field, or one field |
| `aego scene move <file> <node> <parent> [--index v] [--keep-world] [--dry-run]` | move a node under another parent |
| `aego scene new <file> [--dry-run]` | create an empty scene |
| `aego scene remove-comp <file> <node> <comp> [--dry-run]` | remove a component |
| `aego scene remove-node <file> <node> [--dry-run]` | remove a node with its children |
| `aego scene rename <file> <node> <name> [--dry-run]` | rename a node |
| `aego scene reset <file> <node> <path> [--dry-run]` | reset a field to its default, a transform part to identity |
| `aego scene run [<file>] [--frames v] [--seed v]` | run a scene headless and report the log and the state hash |
| `aego scene set <file> <node> <path> <value> [--dry-run]` | set a field |
| `aego scene tree <file>` | node tree with IDs, components, scripts and prefab links |
| `aego scene validate <file>` | load problems and references to nodes the scene does not have |
| `aego schema list [--kind v]` | component and data types |
| `aego schema show <type>` | fields of a type: kind, range, unit, default, flags and docs |
<!-- agent:end -->
## aego docs
| Command | Meaning |
+2 -3
View File
@@ -22,11 +22,10 @@ func (d *Document) instantiate(op Op, res *Result) (step, error) {
return step{}, err
}
lib := d.library()
t, err := lib.Template(id)
if err != nil {
if _, err := lib.Template(id); err != nil {
return step{}, err
}
h, err := sceneio.Instantiate(s, p, t)
h, err := d.load(p, sceneio.EncodeInstance(d.reg, ids.NewNodeID(), &prefab.Instance{Prefab: id}), "prefab")
if err != nil {
return step{}, err
}
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
{
"ok": true,
"value": [
{
"id": "7a1e0c5b3d9f4a2e8c6b1d0f9e8a7c02",
"path": "prefabs/enemy.aeprefab",
"kind": "prefab"
},
{
"id": "7a1e0c5b3d9f4a2e8c6b1d0f9e8a7c03",
"path": "prefabs/fast_enemy.aeprefab",
"kind": "prefab"
}
]
}
+6
View File
@@ -0,0 +1,6 @@
{
"ok": true,
"notes": [
"project: 0 error(s), 0 warning(s)"
]
}
+7
View File
@@ -0,0 +1,7 @@
{
"ok": true,
"value": {
"assets": [],
"nodes": []
}
}
+67
View File
@@ -0,0 +1,67 @@
{
"ok": true,
"value": {
"id": "#c0000000000000000000000000000003",
"name": "Player",
"path": "Root/Player",
"enabled": true,
"transform": {
"position": "[0, 2.95]",
"rotation": "0",
"scale": "[1, 1]",
"space": "2d"
},
"components": [
{
"type": "engine.SpriteRenderer",
"fields": {
"blend": "premultiplied",
"filter": "nearest",
"flipX": "false",
"flipY": "false",
"layer": "0",
"order": "2",
"origin": "[0.5, 0.5]",
"size": "[1, 1]",
"sprite": "asset(\"ae900000000000000000000000000010\")",
"tint": "#4aa3ffff"
}
},
{
"type": "game.Health",
"fields": {
"current": "5",
"hits": "0",
"max": "5"
}
},
{
"type": "physics.Body2D",
"fields": {
"allowSleep": "false",
"angularDamping": "0",
"bullet": "false",
"fixedRotation": "true",
"gravityScale": "1",
"interpolate": "true",
"layer": "2",
"linearDamping": "0",
"mask": "-1",
"mass": "0",
"massMode": "auto",
"type": "dynamic"
}
},
{
"type": "save.Persist",
"fields": {
"noEnabled": "false",
"noTransform": "false"
}
}
],
"scripts": [
"mygame.player.PlayerController"
]
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+87
View File
@@ -0,0 +1,87 @@
package fixture
import (
"io/fs"
"os"
"path/filepath"
"strings"
"testing"
"aego/core/mathx"
"aego/sdk"
)
type health struct {
Max float32 `aego:"max,min=1"`
Current float32 `aego:"current,min=0"`
Hits int64 `aego:"hits"`
}
type enemyStats struct {
Speed float32 `aego:"speed,min=0,max=10"`
Cooldown float32 `aego:"cooldown,min=0,max=5"`
Damage int64 `aego:"damage,min=0"`
Tint mathx.Color `aego:"tint"`
}
type Platformer struct{}
func (Platformer) Info() sdk.ModuleInfo {
return sdk.ModuleInfo{ID: "mygame", Version: "0.1.0"}
}
func (Platformer) Register(r *sdk.Registry) error {
if err := sdk.Component[health](r, sdk.Name("game.Health"), sdk.Persist("current", "hits")).Err(); err != nil {
return err
}
if err := sdk.DataType[enemyStats](r, sdk.Name("mygame.EnemyStats")).Err(); err != nil {
return err
}
m := r.ScriptModule("mygame/combat")
sdk.Proc2(m, "Damage", func(c *sdk.ScriptCtx, target sdk.NodeHandle, amount int64) {
if h, ok := sdk.Get[health](c.Scene, target); ok {
h.Current -= float32(amount)
}
})
sdk.Func1(m, "HealthOf", func(c *sdk.ScriptCtx, target sdk.NodeHandle) float32 {
if h, ok := sdk.Get[health](c.Scene, target); ok {
return h.Current
}
return 0
})
return nil
}
func Copy(t testing.TB, name string) string {
t.Helper()
src := filepath.Join("..", "..", "examples", name)
dst := t.TempDir()
err := filepath.WalkDir(src, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
rel, _ := filepath.Rel(src, p)
if d.IsDir() {
switch rel {
case ".aego", "native", "exports":
return filepath.SkipDir
}
return os.MkdirAll(filepath.Join(dst, rel), 0o755)
}
if rel == "go.mod" || rel == "go.sum" {
return nil
}
data, err := os.ReadFile(p)
if err != nil {
return err
}
if rel == "project.aego" {
data = []byte(strings.Replace(string(data), "native=true", "native=false", 1))
}
return os.WriteFile(filepath.Join(dst, rel), data, 0o644)
})
if err != nil {
t.Fatal(err)
}
return dst
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -172,7 +172,7 @@ func groupRule(from, to group) string {
// runtimeForbidden lists what the aego_runtime build must never reach.
var runtimeForbidden = with(scriptCompilerOnly, "asset/pipeline", "asset/importers/...", "project", "project/...", "editor/...",
"app", "app/bootstrap", "app/docstool", "app/edit", "app/export", "app/export/...", "app/playground",
"app", "app/agent/...", "app/bootstrap", "app/docstool", "app/edit", "app/export", "app/export/...", "app/playground",
"app/projects", "app/run", "app/supervise", "app/assembly/editors", "app/assembly/playground",
"engine/docs", "cmd/aego", "cmd/aego-editor", "cmd/aego-playground", "cmd/sandbox",
"demos/...", "tests/...", "tools/...", "examples/...",
+3
View File
@@ -194,6 +194,8 @@ var fsAllowed = []string{
"cmd/...",
"project",
"project/...",
"app/agent/...",
"tests/fixture",
"save", // slot files next to the player profile (D262)
"asset/pipeline",
"app/bootstrap",
@@ -244,6 +246,7 @@ var denied = []struct {
{"app/playground", []string{"platform/sdl", "gpu/gl33", "gpu/gl33/...", "asset/pipeline", "asset/importers/...", "modules/render3d", "modules/render3d/...", "editor/..."}},
{"app/docsview", []string{"platform/sdl", "gpu/gl33", "gpu/gl33/...", "editor/shell", "editor/shell/...", "asset/importers/...", "script/compiler", "script/workspace"}},
{"scene/mutate", []string{"editor/...", "ui/...", "platform/...", "engine", "engine/...", "app/..."}},
{"app/agent", []string{"editor/...", "ui/...", "platform/sdl"}},
{"runtime/...", []string{"editor/..."}},
{"runtime/...", []string{"asset/pipeline", "asset/importers/...", "project", "app/run", "app/edit", "app/export", "app/export/..."}},
{"sdk", []string{"editor/..."}},