Compare commits
3
Commits
cffa66514b
...
6a22396467
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a22396467 | ||
|
|
21e03128a8 | ||
|
|
e5b1964f3e |
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -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
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
@@ -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
|
||||||
|
}
|
||||||
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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
}
|
||||||
@@ -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) {
|
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 {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ type Config struct {
|
|||||||
Profile string
|
Profile string
|
||||||
Logger *log.Logger
|
Logger *log.Logger
|
||||||
Seed uint64
|
Seed uint64
|
||||||
|
NoProfile bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
@@ -138,6 +139,7 @@ func Open(p *project.Project, cfg Config, mods ...sdk.Module) (*App, error) {
|
|||||||
Debug: cfg.Debug,
|
Debug: cfg.Debug,
|
||||||
FailOnScriptErrors: true,
|
FailOnScriptErrors: true,
|
||||||
Profile: cfg.Profile,
|
Profile: cfg.Profile,
|
||||||
|
NoProfile: cfg.NoProfile,
|
||||||
}, sdk.AdaptAll(mods)...)
|
}, sdk.AdaptAll(mods)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package run
|
package run
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"aego/app/agent"
|
||||||
|
"aego/app/agent/mcp"
|
||||||
"bytes"
|
"bytes"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"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)
|
return toolTest(p, args[1:], stdout, stderr, mods)
|
||||||
case "docs":
|
case "docs":
|
||||||
return docstool.Tool(p, args[1:], stdout, stderr, sdk.AdaptAll(mods)...)
|
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])
|
fmt.Fprintf(stderr, "unknown tool %q\n", args[0])
|
||||||
return 2
|
return 2
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"aego/app/agent"
|
||||||
|
"aego/app/agent/mcp"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"aego/app/bootstrap"
|
"aego/app/bootstrap"
|
||||||
@@ -50,6 +53,8 @@ func dispatch(args []string) int {
|
|||||||
return cmdCheck(args[1:])
|
return cmdCheck(args[1:])
|
||||||
case "icons":
|
case "icons":
|
||||||
return cmdIcons(args[1:])
|
return cmdIcons(args[1:])
|
||||||
|
case "mcp":
|
||||||
|
return cmdMcp(args[1:])
|
||||||
case "docs":
|
case "docs":
|
||||||
return cmdDocs(args[1:])
|
return cmdDocs(args[1:])
|
||||||
case "test":
|
case "test":
|
||||||
@@ -60,6 +65,9 @@ func dispatch(args []string) int {
|
|||||||
usage()
|
usage()
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
if slices.Contains(agent.Groups(), args[0]) {
|
||||||
|
return cmdAgent(args)
|
||||||
|
}
|
||||||
fmt.Fprintf(os.Stderr, "unknown command %q\n", args[0])
|
fmt.Fprintf(os.Stderr, "unknown command %q\n", args[0])
|
||||||
usage()
|
usage()
|
||||||
return 2
|
return 2
|
||||||
@@ -83,6 +91,8 @@ func usage() {
|
|||||||
run *_test.aes script tests headless, or run the game twice and compare state hashes
|
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
|
dump script <pkg|Type> [--func name] disassemble a script package with source lines
|
||||||
docs export|check|build|serve documentation registry, site and playground
|
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)
|
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) {
|
func firstArg(args []string) (string, []string) {
|
||||||
if len(args) > 0 && len(args[0]) > 0 && args[0][0] != '-' {
|
if len(args) > 0 && len(args[0]) > 0 && args[0][0] != '-' {
|
||||||
return args[0], args[1:]
|
return args[0], args[1:]
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ func scriptTool(p *project.Project, args ...string) int {
|
|||||||
}
|
}
|
||||||
cmd := exec.Command(binary, append([]string{"--tool"}, args...)...)
|
cmd := exec.Command(binary, append([]string{"--tool"}, args...)...)
|
||||||
cmd.Dir = p.Root
|
cmd.Dir = p.Root
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
return exitCode(cmd.Run())
|
return exitCode(cmd.Run())
|
||||||
|
|||||||
@@ -160,26 +160,27 @@ type ScriptHost interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Editor struct {
|
type Editor struct {
|
||||||
Engine *engine.Engine
|
BeforeSwap func(*pipeline.Batch) func()
|
||||||
Renderer *render.Renderer
|
Engine *engine.Engine
|
||||||
Project *project.Project
|
Renderer *render.Renderer
|
||||||
Pipeline *pipeline.Pipeline
|
Project *project.Project
|
||||||
Loader *loader.Loader
|
Pipeline *pipeline.Pipeline
|
||||||
Types *scene.Types
|
Loader *loader.Loader
|
||||||
Docs *document.Manager
|
Types *scene.Types
|
||||||
Selection *selection.Service
|
Docs *document.Manager
|
||||||
Commands *command.Registry
|
Selection *selection.Service
|
||||||
UI *core.Context
|
Commands *command.Registry
|
||||||
DnD *dnd.State
|
UI *core.Context
|
||||||
Clipboard DataClipboard
|
DnD *dnd.State
|
||||||
Log *log.ChannelLogger
|
Clipboard DataClipboard
|
||||||
Settings SceneSettings
|
Log *log.ChannelLogger
|
||||||
Thumbs ThumbSource
|
Settings SceneSettings
|
||||||
Scripts ScriptHost
|
Thumbs ThumbSource
|
||||||
Code ScriptNav
|
Scripts ScriptHost
|
||||||
Shapes *shape.Registry
|
Code ScriptNav
|
||||||
Problems *problems.Service
|
Shapes *shape.Registry
|
||||||
Sections *module.Sections
|
Problems *problems.Service
|
||||||
|
Sections *module.Sections
|
||||||
|
|
||||||
ShowProblems func(filter string)
|
ShowProblems func(filter string)
|
||||||
RevealPath func(rel string)
|
RevealPath func(rel string)
|
||||||
@@ -428,6 +429,10 @@ func (e *Editor) ApplyBatch(b *pipeline.Batch) {
|
|||||||
if b == nil {
|
if b == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
var after func()
|
||||||
|
if e.BeforeSwap != nil {
|
||||||
|
after = e.BeforeSwap(b)
|
||||||
|
}
|
||||||
if e.Loader != nil && e.Pipeline != nil {
|
if e.Loader != nil && e.Pipeline != nil {
|
||||||
if len(b.Imported) > 0 {
|
if len(b.Imported) > 0 {
|
||||||
if err := e.Loader.Swap(b.Imported, e.Pipeline.Artifacts()); err != nil {
|
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)
|
e.Loader.Drop(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if after != nil {
|
||||||
|
after()
|
||||||
|
}
|
||||||
for id, err := range b.Failed {
|
for id, err := range b.Failed {
|
||||||
e.Notify(LevelWarn, "import failed for "+id.String()[:8]+": "+err.Error())
|
e.Notify(LevelWarn, "import failed for "+id.String()[:8]+": "+err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-13
@@ -84,19 +84,20 @@ func cloneFields(fs []aetext.Field) []aetext.Field {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Doc struct {
|
type Doc struct {
|
||||||
ID ids.AssetID
|
ID ids.AssetID
|
||||||
Path string
|
Path string
|
||||||
Type *schema.TypeDescriptor
|
Type *schema.TypeDescriptor
|
||||||
reg *schema.Registry
|
reg *schema.Registry
|
||||||
lib *data.Library
|
lib *data.Library
|
||||||
dd *mutate.DataDocument
|
dd *mutate.DataDocument
|
||||||
ptr unsafe.Pointer
|
ptr unsafe.Pointer
|
||||||
origin unsafe.Pointer
|
origin unsafe.Pointer
|
||||||
issues []string
|
issues []string
|
||||||
steps steps
|
steps steps
|
||||||
saved []byte
|
saved []byte
|
||||||
disk []byte
|
disk []byte
|
||||||
rev uint64
|
conflict []byte
|
||||||
|
rev uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDoc(env mutate.Env, lib *data.Library, id ids.AssetID, path string, raw []byte) (*Doc, error) {
|
func NewDoc(env mutate.Env, lib *data.Library, id ids.AssetID, path string, raw []byte) (*Doc, error) {
|
||||||
|
|||||||
@@ -130,7 +130,12 @@ func (s *Service) Doc(id ids.AssetID) (*Doc, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
d.Path, d.rev = rel, s.ctx.AssetRev
|
||||||
return d, d.load()
|
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) {
|
func (s *Service) Table(id ids.AssetID) (*Table, error) {
|
||||||
t := s.tables[id]
|
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
|
return t, nil
|
||||||
}
|
}
|
||||||
rel, raw, err := s.file(id)
|
rel, raw, err := s.file(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
t.Path, t.rev = rel, s.ctx.AssetRev
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
@@ -169,6 +177,14 @@ func (s *Service) Table(id ids.AssetID) (*Table, error) {
|
|||||||
return nt, nil
|
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) {
|
func (s *Service) RevertDoc(id ids.AssetID) {
|
||||||
delete(s.docs, id)
|
delete(s.docs, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,18 +22,19 @@ type Row struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Table struct {
|
type Table struct {
|
||||||
ID ids.AssetID
|
ID ids.AssetID
|
||||||
Path string
|
Path string
|
||||||
Type *schema.TypeDescriptor
|
Type *schema.TypeDescriptor
|
||||||
Rows []Row
|
Rows []Row
|
||||||
Issues []string
|
Issues []string
|
||||||
reg *schema.Registry
|
reg *schema.Registry
|
||||||
def unsafe.Pointer
|
def unsafe.Pointer
|
||||||
dd *mutate.DataDocument
|
dd *mutate.DataDocument
|
||||||
steps steps
|
steps steps
|
||||||
saved []byte
|
saved []byte
|
||||||
disk []byte
|
disk []byte
|
||||||
rev uint64
|
conflict []byte
|
||||||
|
rev uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTable(env mutate.Env, id ids.AssetID, path string, raw []byte) (*Table, error) {
|
func NewTable(env mutate.Env, id ids.AssetID, path string, raw []byte) (*Table, error) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
"aego/asset"
|
||||||
|
"aego/asset/pipeline"
|
||||||
"aego/core/errs"
|
"aego/core/errs"
|
||||||
"aego/core/ids"
|
"aego/core/ids"
|
||||||
"aego/editor/core/context"
|
"aego/editor/core/context"
|
||||||
@@ -99,6 +101,8 @@ func (s *Service) reimport(list []ids.AssetID) error {
|
|||||||
if len(list) == 0 {
|
if len(list) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
s.writing = true
|
||||||
|
defer func() { s.writing = false }()
|
||||||
batch, err := s.ctx.Pipeline.Reimport(list...)
|
batch, err := s.ctx.Pipeline.Reimport(list...)
|
||||||
s.ctx.ApplyBatch(batch)
|
s.ctx.ApplyBatch(batch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -291,3 +295,31 @@ func (s *Service) commit(label string, files []fileChange, src *source) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,9 +24,10 @@ import (
|
|||||||
const DefaultDir = "prefabs"
|
const DefaultDir = "prefabs"
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
ctx *context.Editor
|
ctx *context.Editor
|
||||||
infos map[infoKey]*Info
|
infos map[infoKey]*Info
|
||||||
fixed *sceneio.Library
|
writing bool
|
||||||
|
fixed *sceneio.Library
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx *context.Editor) *Service {
|
func New(ctx *context.Editor) *Service {
|
||||||
|
|||||||
@@ -214,6 +214,10 @@ func (a *App) Workspace() *workspace.Workspace {
|
|||||||
return a.ws
|
return a.ws
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) Data() *dataedit.Service {
|
||||||
|
return a.data
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) Play() *play.Controller {
|
func (a *App) Play() *play.Controller {
|
||||||
return a.play
|
return a.play
|
||||||
}
|
}
|
||||||
@@ -364,6 +368,7 @@ func (a *App) Init(e *engine.Engine) error {
|
|||||||
}
|
}
|
||||||
a.files = assetfiles.New(a.ctx)
|
a.files = assetfiles.New(a.ctx)
|
||||||
a.prefabs = prefabs.New(a.ctx)
|
a.prefabs = prefabs.New(a.ctx)
|
||||||
|
a.ctx.BeforeSwap = a.prefabs.External
|
||||||
a.data = dataedit.New(a.ctx)
|
a.data = dataedit.New(a.ctx)
|
||||||
a.data.OnLive = func() { a.play.Invalidate("a data asset was tuned during Play") }
|
a.data.OnLive = func() { a.play.Invalidate("a data asset was tuned during Play") }
|
||||||
a.ctx.Selection.Subscribe(a.data.Clear)
|
a.ctx.Selection.Subscribe(a.data.Clear)
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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 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
|
## aego docs
|
||||||
|
|
||||||
| Command | Meaning |
|
| Command | Meaning |
|
||||||
|
|||||||
@@ -22,11 +22,10 @@ func (d *Document) instantiate(op Op, res *Result) (step, error) {
|
|||||||
return step{}, err
|
return step{}, err
|
||||||
}
|
}
|
||||||
lib := d.library()
|
lib := d.library()
|
||||||
t, err := lib.Template(id)
|
if _, err := lib.Template(id); err != nil {
|
||||||
if err != nil {
|
|
||||||
return step{}, err
|
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 {
|
if err != nil {
|
||||||
return step{}, err
|
return step{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+15
@@ -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
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"notes": [
|
||||||
|
"project: 0 error(s), 0 warning(s)"
|
||||||
|
]
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"value": {
|
||||||
|
"assets": [],
|
||||||
|
"nodes": []
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+170
File diff suppressed because it is too large
Load Diff
+140
File diff suppressed because it is too large
Load Diff
+119
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -172,7 +172,7 @@ func groupRule(from, to group) string {
|
|||||||
|
|
||||||
// runtimeForbidden lists what the aego_runtime build must never reach.
|
// runtimeForbidden lists what the aego_runtime build must never reach.
|
||||||
var runtimeForbidden = with(scriptCompilerOnly, "asset/pipeline", "asset/importers/...", "project", "project/...", "editor/...",
|
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",
|
"app/projects", "app/run", "app/supervise", "app/assembly/editors", "app/assembly/playground",
|
||||||
"engine/docs", "cmd/aego", "cmd/aego-editor", "cmd/aego-playground", "cmd/sandbox",
|
"engine/docs", "cmd/aego", "cmd/aego-editor", "cmd/aego-playground", "cmd/sandbox",
|
||||||
"demos/...", "tests/...", "tools/...", "examples/...",
|
"demos/...", "tests/...", "tools/...", "examples/...",
|
||||||
|
|||||||
@@ -194,6 +194,8 @@ var fsAllowed = []string{
|
|||||||
"cmd/...",
|
"cmd/...",
|
||||||
"project",
|
"project",
|
||||||
"project/...",
|
"project/...",
|
||||||
|
"app/agent/...",
|
||||||
|
"tests/fixture",
|
||||||
"save", // slot files next to the player profile (D262)
|
"save", // slot files next to the player profile (D262)
|
||||||
"asset/pipeline",
|
"asset/pipeline",
|
||||||
"app/bootstrap",
|
"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/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"}},
|
{"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/..."}},
|
{"scene/mutate", []string{"editor/...", "ui/...", "platform/...", "engine", "engine/...", "app/..."}},
|
||||||
|
{"app/agent", []string{"editor/...", "ui/...", "platform/sdl"}},
|
||||||
{"runtime/...", []string{"editor/..."}},
|
{"runtime/...", []string{"editor/..."}},
|
||||||
{"runtime/...", []string{"asset/pipeline", "asset/importers/...", "project", "app/run", "app/edit", "app/export", "app/export/..."}},
|
{"runtime/...", []string{"asset/pipeline", "asset/importers/...", "project", "app/run", "app/edit", "app/export", "app/export/..."}},
|
||||||
{"sdk", []string{"editor/..."}},
|
{"sdk", []string{"editor/..."}},
|
||||||
|
|||||||
Reference in New Issue
Block a user