21 Commits
Author SHA1 Message Date
sanspie 208875264f Stage 9.5c3: docs runtime polish
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 12m43s
ci / checks (push) Successful in 16m57s
2026-10-20 02:33:48 +03:00
sanspie 8c21400bb1 Stage 9.5c2: docs session, Try it, sandbox (D293–D294) 2026-10-20 01:31:40 +03:00
sanspie e83521a34a Stage 9.5c1: web export (D295) 2026-10-20 00:59:39 +03:00
sanspie 3f5584d062 Stage 9.5b3b: component, settings and CLI reference (D292) 2026-10-20 00:11:43 +03:00
sanspie 6ce72aa1ec Stage 9.5b3a: docs IA and site shell (D292) 2026-10-19 23:27:11 +03:00
sanspie 9ee2b0b05c Stage 9.5b2: playground with Game View (D291) 2026-10-19 23:10:55 +03:00
sanspie 99dacd7a75 Stage 9.5b1: web platform and WebGL2 backend (D289–D290) 2026-10-19 22:47:08 +03:00
sanspie 5447383f2b gitignore: aego-runtime binary and .syso resources 2026-10-19 20:39:03 +03:00
sanspie 6745d027fb Stage 9.5a: export v1 (D286–D288) 2026-10-19 20:27:58 +03:00
sanspie e7dd01371e script/abi: Parse accepts the untyped Handle<> the encoder writes 2026-10-19 20:27:40 +03:00
sanspie 68802195b6 Stage 9.0: editor workflow 2026-10-19 20:27:40 +03:00
sanspie 7cba364aed Stage 9.4d4: aego test --determinism 2026-10-19 02:18:58 +03:00
sanspie 35e9849cae Stage 9.4d3: keyframes 2026-10-19 02:15:10 +03:00
sanspie f21d455d68 Stage 9.4d3a: CloneInto, spawn pool 2026-10-19 01:51:39 +03:00
sanspie d1a16d8208 Stage 9.4d2: rewind 2026-10-19 01:47:39 +03:00
sanspie 2a1826f876 Stage 9.4d1: save games 2026-10-19 00:37:50 +03:00
sanspie 3af2d5d977 Stage 9.4c: data assets, validators 2026-10-18 22:58:02 +03:00
sanspie b61a1656ad Stage 9.4b: signals, tags, spawn 2026-10-18 22:58:02 +03:00
sanspie 0fbf22a090 Stage 9.4a: curves, tween, timers
core/curve with presets, keys, wraps; KindCurve in schema, scenes, prefabs and the inspector curve editor.
modules/tween: tweens and timers in per-world pools, game and unscaled domains, script field targets via contracts/scriptfields.
Host handle types (abi.Handle, bind.Handle, manifest v5), yield on awaitable handles via contracts/scriptwait.
Script modules aego/curve, aego/tween, aego/timer; platformer enemy uses a cooldown and a fade-out tween.
Docs: tween.md, curves.md, language reference; golden for the curve editor.
2026-10-18 21:32:39 +03:00
sanspie 5e763aa73c Stage 9.2–9.3: input and settings 2026-10-18 21:32:39 +03:00
sanspie 6093ca9d8b Stage 9.1: time and debug 2026-10-17 23:14:21 +03:00
116 changed files with 1678 additions and 11199 deletions
+1 -91
View File
File diff suppressed because one or more lines are too long
-1
View File
@@ -98,7 +98,6 @@ vet: bootstrap
lint: bootstrap
go run honnef.co/go/tools/cmd/staticcheck@$(STATICCHECK_VERSION) $(LINT_PKGS)
go run honnef.co/go/tools/cmd/staticcheck@$(STATICCHECK_VERSION) -tags aego_editor ./editor/... ./app/... ./tests/...
# All graph variants, the semantic pass and the negative rule tests.
depcheck:
-146
View File
File diff suppressed because it is too large Load Diff
-85
View File
@@ -1,85 +0,0 @@
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
@@ -1,66 +0,0 @@
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
+6 -2
View File
@@ -116,8 +116,12 @@ func TestPlayLeavesEditSceneByteIdentical(t *testing.T) {
t.Fatal("Spin is not registered")
}
speed, _ := td.Field("speed")
add := commands.NewMutation("Add "+td.FullName, commands.EnvFor(es),
commands.AddComp(es, spinner, td, []commands.FieldValue{{Field: speed.ID, Value: schema.Float32(3)}}))
add := &commands.AddComponent{
Node: spinner,
Type: td.ID,
Name: td.FullName,
Values: []commands.FieldValue{{Field: speed.ID, Value: schema.Float32(3)}},
}
if err := d.History.Exec(add); err != nil {
t.Fatal(err)
}
+1 -1
View File
@@ -42,7 +42,7 @@ func TestColliderFitsSprite(t *testing.T) {
}
for _, name := range []string{"Crate", "Bare"} {
id := es.ID(es.Find(es.Root(), name))
if err := d.History.Exec(commands.NewMutation("Add "+td.FullName, commands.WithOnAdd(commands.EnvFor(es), ctx.OnAdd), commands.AddComp(es, id, td, nil))); err != nil {
if err := d.History.Exec(&commands.AddComponent{Node: id, Type: td.ID, Name: td.FullName, OnAdd: ctx.OnAdd(td)}); err != nil {
t.Fatal(err)
}
}
+6 -2
View File
@@ -51,8 +51,12 @@ func openSpinner(t *testing.T) *spinFixture {
}
fd, _ := td.Field("speed")
f.typ, f.speed = td.ID, fd.ID
add := commands.NewMutation("Add "+td.FullName, commands.EnvFor(es),
commands.AddComp(es, f.spinner, td, []commands.FieldValue{{Field: fd.ID, Value: schema.Float32(3)}}))
add := &commands.AddComponent{
Node: f.spinner,
Type: td.ID,
Name: td.FullName,
Values: []commands.FieldValue{{Field: fd.ID, Value: schema.Float32(3)}},
}
if err := d.History.Exec(add); err != nil {
t.Fatal(err)
}
-65
View File
@@ -1,65 +0,0 @@
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(), NoProfile: true}, mods...)
a, err := Open(p, Config{Headless: true, FrameTime: o.FrameTime, Seed: o.Seed, ScenePath: o.ScenePath, Logger: log.Nop()}, mods...)
if err != nil {
return nil, nil, nil, err
}
-2
View File
@@ -43,7 +43,6 @@ type Config struct {
Profile string
Logger *log.Logger
Seed uint64
NoProfile bool
}
type App struct {
@@ -139,7 +138,6 @@ 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,13 +1,10 @@
package run
import (
"aego/app/agent"
"aego/app/agent/mcp"
"bytes"
"flag"
"fmt"
"io"
"os"
"sort"
"strings"
@@ -42,10 +39,6 @@ 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,14 +1,11 @@
package main
import (
"aego/app/agent"
"aego/app/agent/mcp"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"aego/app/bootstrap"
@@ -53,8 +50,6 @@ 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":
@@ -65,9 +60,6 @@ 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
@@ -91,8 +83,6 @@ 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
`)
}
@@ -107,40 +97,6 @@ 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,7 +23,6 @@ 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())
+11 -6
View File
@@ -3,6 +3,7 @@ package assetops
import (
"os"
"path"
"regexp"
"sort"
"strconv"
"strings"
@@ -11,8 +12,6 @@ import (
"aego/core/ids"
"aego/editor/core/context"
"aego/editor/core/document"
"aego/project/refactor"
"aego/project/rename"
)
const (
@@ -20,6 +19,8 @@ const (
opsLimit = 64
)
var packageDecl = regexp.MustCompile(`(?m)^[ \t]*package[ \t]+([A-Za-z_][A-Za-z0-9_]*)`)
type FileOp struct {
Dir bool
ID ids.AssetID
@@ -179,8 +180,8 @@ func setPackage(ctx *context.Editor, rel, pkg string) error {
return err
}
c := d.Code
if a, b, ok := refactor.PackageSpan(c.Bytes()); ok {
c.ReplaceRanges([][2]int{{a, b}}, [][]byte{[]byte(pkg)})
if m := packageDecl.FindSubmatchIndex(c.Bytes()); m != nil {
c.ReplaceRanges([][2]int{{m[2], m[3]}}, [][]byte{[]byte(pkg)})
} else {
c.ReplaceRanges([][2]int{{0, 0}}, [][]byte{[]byte("package " + pkg + "\n\n")})
}
@@ -206,7 +207,7 @@ func Stem(p string) string {
}
func ValidName(s string) bool {
return rename.ValidName(s)
return s != "" && s != "." && s != ".." && !strings.HasPrefix(s, ".") && !strings.ContainsAny(s, "/\\")
}
func ValidIdent(s string) bool {
@@ -269,7 +270,11 @@ func ScriptsRel(dir string) (string, bool) {
}
func PackageOf(ctx *context.Editor, dir string) string {
return refactor.PackageOf(ctx.Project, dir)
if rel, ok := ScriptsRel(dir); ok && rel != "" {
return path.Base(rel)
}
m := moduleOf(ctx)
return m[strings.LastIndexByte(m, '/')+1:]
}
func FileStem(name string) string {
+4 -2
View File
@@ -9,7 +9,6 @@ import (
"aego/core/ids"
"aego/editor/core/context"
"aego/editor/core/history/commands"
"aego/project/refactor"
"aego/scene/scriptcomp"
"aego/schema"
)
@@ -21,7 +20,10 @@ func shortName(full string) string {
}
func moduleOf(ctx *context.Editor) string {
return refactor.Module(ctx.Project)
if ctx.Project != nil && ctx.Project.Module != "" {
return ctx.Project.Module
}
return "game"
}
func ScriptPrefix(ctx *context.Editor, rel string) string {
+18 -22
View File
@@ -10,7 +10,6 @@ import (
"aego/editor/core/history/commands"
"aego/format/aetext"
"aego/scene"
"aego/scene/mutate"
"aego/scene/sceneio"
)
@@ -238,24 +237,12 @@ func Paste(ctx *context.Editor) error {
if err != nil {
return err
}
frags, err := commands.Fragments(data)
if err != nil {
return err
}
parent, index := pasteTarget(ctx)
ops := make([]mutate.Op, len(frags))
for i, f := range frags {
at := -1
if index >= 0 {
at = index + i
}
ops[i] = commands.Insert(parent, at, f)
}
cmd := commands.NewMutation("Create Paste", commands.EnvFor(d.Scene()), ops...)
cmd := &commands.Create{Parent: parent, Index: index, Fragment: data, Name: "Paste"}
if err := d.History.Exec(cmd); err != nil {
return err
}
ctx.Select(cmd.Created()...)
ctx.Select(append([]ids.NodeID(nil), cmd.Roots()...)...)
return nil
}
@@ -280,13 +267,17 @@ func Duplicate(ctx *context.Editor) error {
tx.Abort()
return err
}
c := commands.NewMutation("Create "+s.Name(h), commands.EnvFor(s),
commands.Insert(s.ID(s.Parent(h)), commands.ChildIndex(s, h)+1, string(data)))
c := &commands.Create{
Parent: s.ID(s.Parent(h)),
Index: commands.ChildIndex(s, h) + 1,
Fragment: data,
Name: s.Name(h),
}
if err := tx.Exec(c); err != nil {
tx.Abort()
return err
}
created = append(created, c.Created()...)
created = append(created, c.Roots()...)
}
tx.Commit()
ctx.Select(created...)
@@ -303,13 +294,18 @@ func Delete(ctx *context.Editor) error {
if len(tops) == 0 {
return nil
}
ops := make([]mutate.Op, len(tops))
targets := make([]ids.NodeID, len(tops))
for i, h := range tops {
ops[i] = mutate.RemoveNode(commands.Ref(s.ID(h)))
targets[i] = s.ID(h)
}
if err := d.History.Exec(commands.NewMutation("Delete", commands.EnvFor(s), ops...)); err != nil {
return err
tx := d.History.Begin("Delete")
for _, id := range targets {
if err := tx.Exec(&commands.Delete{Node: id}); err != nil {
tx.Abort()
return err
}
}
tx.Commit()
ctx.Select()
return nil
}
+17 -5
View File
@@ -113,7 +113,8 @@ func (e Env) encodeComponent(td *schema.TypeDescriptor, values []commands.FieldV
sc := e.scratch()
h := sc.Create(sc.Root(), "component")
sc.Flush()
if err := putComponent(sc, h, td, values); err != nil {
add := &commands.AddComponent{Node: sc.ID(h), Type: td.ID, Name: td.FullName, Values: values}
if err := add.Do(target{sc}); err != nil {
return nil, err
}
return sceneio.SaveSubtree(sc, h, true), nil
@@ -121,14 +122,18 @@ func (e Env) encodeComponent(td *schema.TypeDescriptor, values []commands.FieldV
func (e Env) decodeComponent(data []byte) (*schema.TypeDescriptor, []commands.FieldValue, error) {
sc := e.scratch()
root, err := loadScratch(sc, data)
if err != nil {
c := &commands.Create{Index: -1, Fragment: data}
if err := c.Do(target{sc}); err != nil {
return nil, nil, err
}
roots := c.Roots()
if len(roots) == 0 {
return nil, nil, errs.New(errs.EditorCommand, "clipboard component is empty")
}
reg := sc.Registry()
unknown, hasUnknown := reg.ByName("engine.Unknown")
var td *schema.TypeDescriptor
sc.ComponentsOf(sc.Handle(root), func(t schema.TypeID) {
sc.ComponentsOf(sc.Handle(roots[0]), func(t schema.TypeID) {
if td != nil || (hasUnknown && t == unknown.ID) {
return
}
@@ -137,7 +142,7 @@ func (e Env) decodeComponent(data []byte) (*schema.TypeDescriptor, []commands.Fi
if td == nil {
return nil, nil, errs.New(errs.EditorCommand, "clipboard component type is not loaded in this editor")
}
values, err := componentValues(sc, root, td)
values, err := componentValues(sc, roots[0], td)
if err != nil {
return nil, nil, err
}
@@ -184,6 +189,13 @@ func (e Env) ReadComponent() (*schema.TypeDescriptor, []commands.FieldValue, boo
return td, values, true
}
func (e Env) onAdd(td *schema.TypeDescriptor) func(*scene.Scene, scene.NodeHandle) {
if e.OnAdd == nil {
return nil
}
return e.OnAdd(td)
}
func (e Env) PasteComponent(d *document.SceneDocument, nodes []ids.NodeID) error {
td, values, ok := e.ReadComponent()
if !ok {
-28
View File
@@ -1,28 +0,0 @@
package clipboard
import (
"aego/core/errs"
"aego/core/ids"
"aego/editor/core/history/commands"
"aego/scene"
"aego/scene/sceneio"
"aego/schema"
)
func loadScratch(sc *scene.Scene, data []byte) (ids.NodeID, error) {
root := sc.Root()
created, _, err := sceneio.LoadInto(sc, root, data, "fragment", sceneio.LoadOptions{KeepIDs: true})
if err != nil {
return ids.NodeID{}, err
}
for _, h := range created {
if sc.Parent(h) == root {
return sc.ID(h), nil
}
}
return ids.NodeID{}, errs.New(errs.EditorCommand, "the fragment holds no node")
}
func putComponent(sc *scene.Scene, h scene.NodeHandle, td *schema.TypeDescriptor, list []commands.FieldValue) error {
return commands.NewMutation("", commands.EnvFor(sc), commands.AddRaw(sc, sc.ID(h), td, list)).Do(target{sc})
}
+12 -8
View File
@@ -22,7 +22,6 @@ type compData struct {
func (e Env) pasteInto(tx *history.Transaction, d *document.SceneDocument, nodes []ids.NodeID, td *schema.TypeDescriptor, values []commands.FieldValue) error {
s := d.Scene()
env := commands.WithOnAdd(commands.EnvFor(s), e.OnAdd)
for _, n := range nodes {
h := s.Handle(n)
if h.IsZero() || !s.IsAlive(h) {
@@ -32,7 +31,7 @@ func (e Env) pasteInto(tx *history.Transaction, d *document.SceneDocument, nodes
if !s.Accepts(h, td.ID) {
continue
}
if err := tx.Exec(commands.NewMutation("Add "+td.FullName, env, commands.AddComp(s, n, td, values))); err != nil {
if err := tx.Exec(&commands.AddComponent{Node: n, Type: td.ID, Name: td.FullName, Values: values, OnAdd: e.onAdd(td)}); err != nil {
return err
}
continue
@@ -95,7 +94,8 @@ func (e Env) CopyComponents(s *scene.Scene, node ids.NodeID, types []schema.Type
if err != nil {
return err
}
if err := putComponent(sc, h, td, values); err != nil {
add := &commands.AddComponent{Node: sc.ID(h), Type: td.ID, Name: td.FullName, Values: values}
if err := add.Do(target{sc}); err != nil {
return err
}
}
@@ -118,10 +118,11 @@ func (e Env) readComponents() ([]compData, bool) {
data = []byte(strings.TrimPrefix(t, componentsText))
}
sc := e.scratch()
root, err := loadScratch(sc, data)
if err != nil {
cr := &commands.Create{Index: -1, Fragment: data}
if err := cr.Do(target{sc}); err != nil || len(cr.Roots()) == 0 {
return nil, false
}
root := cr.Roots()[0]
reg := sc.Registry()
unknown, hasUnknown := reg.ByName("engine.Unknown")
var out []compData
@@ -185,11 +186,14 @@ func (e Env) EncodeScript(s *scene.Scene, node ids.NodeID, name string) ([]byte,
func (e Env) decodeScript(data []byte) (string, schema.PropertyBag, error) {
sc := e.scratch()
root, err := loadScratch(sc, data)
if err != nil {
cr := &commands.Create{Index: -1, Fragment: data}
if err := cr.Do(target{sc}); err != nil {
return "", schema.PropertyBag{}, err
}
scs, ok := scriptcomp.ScriptsOf(sc, sc.Handle(root))
if len(cr.Roots()) == 0 {
return "", schema.PropertyBag{}, errs.New(errs.EditorCommand, "the preset is empty")
}
scs, ok := scriptcomp.ScriptsOf(sc, sc.Handle(cr.Roots()[0]))
if !ok || len(scs.Attachments) == 0 {
return "", schema.PropertyBag{}, errs.New(errs.EditorCommand, "the preset holds no script")
}
+20 -28
View File
@@ -160,27 +160,26 @@ type ScriptHost interface {
}
type Editor struct {
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
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)
@@ -429,10 +428,6 @@ 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 {
@@ -443,9 +438,6 @@ 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())
}
File diff suppressed because it is too large Load Diff
+13 -34
View File
@@ -15,7 +15,6 @@ import (
"aego/data"
"aego/editor/core/context"
"aego/editor/core/document"
"aego/scene/mutate"
"aego/schema"
)
@@ -109,18 +108,6 @@ func (s *Service) file(id ids.AssetID) (string, []byte, error) {
return rel, raw, nil
}
func (s *Service) env() mutate.Env {
env := mutate.Env{Types: s.ctx.Types}
if pl := s.ctx.Pipeline; pl != nil {
env.Lookup = pl.Lookup
env.Assets = func(id ids.AssetID) bool {
_, ok := pl.PathOf(id)
return ok || asset.IsBuiltin(id)
}
}
return env
}
func (s *Service) Doc(id ids.AssetID) (*Doc, error) {
d := s.docs[id]
if d != nil && d.rev == s.ctx.AssetRev {
@@ -130,12 +117,7 @@ func (s *Service) Doc(id ids.AssetID) (*Doc, error) {
if err != nil {
return nil, err
}
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) {
if d != nil && (d.Dirty() || bytes.Equal(raw, d.disk)) {
d.Path, d.rev = rel, s.ctx.AssetRev
return d, d.load()
}
@@ -143,7 +125,11 @@ func (s *Service) Doc(id ids.AssetID) (*Doc, error) {
if err != nil {
return nil, err
}
nd, err := NewDoc(s.env(), lib, id, rel, raw)
parsed, err := data.ParseDoc(raw, rel)
if err != nil {
return nil, err
}
nd, err := NewDoc(s.reg(), lib, id, rel, parsed)
if err != nil {
return nil, err
}
@@ -154,21 +140,22 @@ 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 {
if t != nil && (t.rev == s.ctx.AssetRev || t.Dirty()) {
return t, nil
}
rel, raw, err := s.file(id)
if err != nil {
return nil, err
}
if t != nil && (t.Dirty() || bytes.Equal(raw, t.disk)) {
if t.Dirty() {
s.conflict(rel, raw, t.disk, &t.conflict)
}
if t != nil && bytes.Equal(raw, t.disk) {
t.Path, t.rev = rel, s.ctx.AssetRev
return t, nil
}
nt, err := NewTable(s.env(), id, rel, raw)
parsed, err := data.ParseTable(raw, rel)
if err != nil {
return nil, err
}
nt, err := NewTable(s.reg(), id, rel, parsed)
if err != nil {
return nil, err
}
@@ -177,14 +164,6 @@ 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)
}
File diff suppressed because it is too large Load Diff
+23 -2
View File
@@ -20,7 +20,6 @@ import (
"aego/editor/core/selection"
"aego/engine"
"aego/engine/module"
"aego/project"
"aego/scene"
"aego/scene/sceneio"
)
@@ -442,7 +441,29 @@ func (m *Manager) UniquePath(rel string) string {
}
func WriteAtomic(abs string, data []byte) error {
return project.WriteAtomic(abs, data)
dir := filepath.Dir(abs)
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
tmp, err := os.CreateTemp(dir, ".aego-*.tmp")
if err != nil {
return err
}
name := tmp.Name()
if _, err := tmp.Write(data); err != nil {
tmp.Close()
os.Remove(name)
return err
}
if err := tmp.Close(); err != nil {
os.Remove(name)
return err
}
if err := os.Rename(name, abs); err != nil {
os.Remove(name)
return err
}
return nil
}
func encode(d *SceneDocument) []byte {
+2 -13
View File
@@ -1,7 +1,6 @@
package gizmo
import (
"aego/core/errs"
"aego/editor/core/history/commands"
"aego/editor/core/props"
"aego/schema"
@@ -15,25 +14,15 @@ func SetProperty(e *EditCtx, t schema.TypeID, fd *schema.FieldDescriptor, v sche
if h.IsZero() {
return
}
td, ok := e.Scene.Registry().ByID(t)
if !ok {
e.Err = errs.New(errs.SchemaUnknownType, "component type is not registered")
return
}
nv, err := props.Normalize(fd, v)
if err != nil {
e.Err = err
return
}
before, err := e.Scene.GetProperty(h, t, fd.ID)
if err != nil {
e.Err = err
return
}
if props.Equal(before, nv) {
if props.Equal(before, v) {
return
}
if err := e.Tx.Exec(commands.SetField(e.Scene, e.Node, td, fd, nv, props.Label(fd))); err != nil {
if err := e.Tx.Exec(&commands.SetProperty{Node: e.Node, Type: t, Field: fd.ID, Before: before, After: v, Name: props.Label(fd)}); err != nil {
e.Err = err
}
}
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
File diff suppressed because it is too large Load Diff
+51
View File
@@ -3,10 +3,61 @@ package commands
import (
"aego/core/errs"
"aego/core/ids"
"aego/core/log"
"aego/editor/core/history"
"aego/scene"
"aego/scene/sceneio"
)
type Replace struct {
Node ids.NodeID
Before []byte
After []byte
Name string
Post func(s *scene.Scene)
parent ids.NodeID
index int
ready bool
}
func (c *Replace) Label() string {
if c.Name != "" {
return c.Name
}
return "Replace"
}
func (c *Replace) Do(t history.Target) error {
s := t.Scene()
if err := c.swap(s, c.After); err != nil {
return err
}
if c.Post != nil {
c.Post(s)
}
return nil
}
func (c *Replace) Undo(t history.Target) error {
return c.swap(t.Scene(), c.Before)
}
func (c *Replace) Merge(history.Command) bool {
return false
}
func (c *Replace) swap(s *scene.Scene, data []byte) error {
if !c.ready {
h := s.Handle(c.Node)
if h.IsZero() || !s.IsAlive(h) {
return errs.New(errs.SceneDeadNode, "node is not in the scene", log.F("node", c.Node.String()))
}
c.parent, c.index, c.ready = s.ID(s.Parent(h)), ChildIndex(s, h), true
}
_, err := ReplaceNode(s, c.Node, c.parent, c.index, data, nil)
return err
}
func ReplaceNode(s *scene.Scene, node, parent ids.NodeID, index int, data []byte, lib *sceneio.Library) (scene.NodeHandle, error) {
if h := s.Handle(node); !h.IsZero() && s.IsAlive(h) {
s.Destroy(h)
+4 -4
View File
@@ -18,7 +18,7 @@ func TestReparent3DPreservesWorld(t *testing.T) {
s.Flush()
before := s.World4(n)
h := history.New(target{s})
if err := h.Exec(reparent(s, n, p)); err != nil {
if err := h.Exec(&Reparent{Node: s.ID(n), NewParent: s.ID(p), NewIndex: -1, KeepWorld: true}); err != nil {
t.Fatal(err)
}
if s.Parent(n) != p {
@@ -39,7 +39,7 @@ func TestReparent3DUnderTwoDFails(t *testing.T) {
n := s.Create3D(s.Root(), "N")
s.Flush()
h := history.New(target{s})
err := h.Exec(reparent(s, n, flat))
err := h.Exec(&Reparent{Node: s.ID(n), NewParent: s.ID(flat), NewIndex: -1, KeepWorld: true})
if !errs.Has(err, errs.SceneSpaceMismatch) {
t.Fatalf("err = %v", err)
}
@@ -57,7 +57,7 @@ func TestReparent2DIntoThreeDKeepsWorld(t *testing.T) {
s.Flush()
before := s.World4(n)
h := history.New(target{s})
if err := h.Exec(reparent(s, n, p)); err != nil {
if err := h.Exec(&Reparent{Node: s.ID(n), NewParent: s.ID(p), NewIndex: -1, KeepWorld: true}); err != nil {
t.Fatal(err)
}
if !s.Under3D(n) || !s.World4(n).NearlyEqual(before, 1e-4) {
@@ -97,7 +97,7 @@ func TestDeleteUndoKeepsSpace(t *testing.T) {
s.Flush()
id := s.ID(n)
h := history.New(target{s})
if err := h.Exec(del(s, id)); err != nil {
if err := h.Exec(&Delete{Node: id}); err != nil {
t.Fatal(err)
}
h.Undo()
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -480,7 +480,6 @@ func (c *Controller) CopyToEdit(nodes []ids.NodeID, t schema.TypeID) error {
if !ok {
return errs.New(errs.SchemaUnknownType, "component type is not registered")
}
env := commands.EnvFor(es)
tx := real.History.Begin("Copy " + td.FullName + " from Play")
for _, n := range nodes {
if _, has := es.GetByType(es.Handle(n), t); !has {
@@ -493,7 +492,7 @@ func (c *Controller) CopyToEdit(nodes []ids.NodeID, t schema.TypeID) error {
}
values = append(values, commands.FieldValue{Field: td.Fields[i].ID, Value: v})
}
if err := tx.Exec(commands.NewMutation("Add "+td.FullName, env, commands.AddComp(es, n, td, values))); err != nil {
if err := tx.Exec(&commands.AddComponent{Node: n, Type: t, Name: td.FullName, Values: values}); err != nil {
tx.Abort()
return err
}
+2 -12
View File
@@ -3,7 +3,6 @@ package playdiff
import (
"reflect"
"strconv"
"strings"
"aego/core/curve"
"aego/core/ids"
@@ -13,7 +12,6 @@ import (
"aego/editor/core/history/commands"
"aego/editor/core/props"
"aego/scene"
"aego/scene/mutate"
"aego/scene/scriptcomp"
"aego/schema"
)
@@ -96,16 +94,8 @@ func Touched(h *history.History) map[Key]bool {
}
h.Walk(func(c history.Command) {
switch c := c.(type) {
case *commands.Mutation:
for _, ch := range c.Changes() {
switch {
case ch.Op != mutate.KindSet && ch.Op != mutate.KindReset:
case ch.Type != 0:
out[Key{Node: ch.NodeID, Type: ch.Type, Field: ch.Field}] = true
case strings.HasPrefix(ch.Path, "transform."):
out[Key{Node: ch.NodeID}] = true
}
}
case *commands.SetProperty:
out[Key{Node: c.Node, Type: c.Type, Field: c.Field}] = true
case *commands.SetTransform:
out[Key{Node: c.Node}] = true
case *commands.SetTransform3D:
+1 -12
View File
@@ -45,7 +45,7 @@ func pair(t *testing.T) (*document.SceneDocument, *document.SceneDocument, ids.N
}
s.Flush()
d := m.New(rel, s)
if err := d.History.Exec(commands.NewMutation("Add "+td.FullName, commands.EnvFor(s), commands.AddComp(s, id, td, nil))); err != nil {
if err := d.History.Exec(&commands.AddComponent{Node: id, Type: td.ID, Name: td.FullName}); err != nil {
t.Fatal(err)
}
docs[i] = d
@@ -108,14 +108,3 @@ func TestApplyIsOneStep(t *testing.T) {
t.Fatal("undo must restore the edit scene")
}
}
func TestTouchedSeesInspectorEdits(t *testing.T) {
_, play, id, td := pair(t)
fd, _ := td.Field("tint")
if err := play.Props.Set(nil, []ids.NodeID{id}, td.ID, fd.ID, schema.Color(mathx.Color{G: 1, A: 1})); err != nil {
t.Fatal(err)
}
if !playdiff.Touched(play.History)[playdiff.Key{Node: id, Type: td.ID, Field: fd.ID}] {
t.Fatal("a field set through the Inspector is not touched")
}
}
@@ -1,4 +1,4 @@
package refactor
package prefabs
import (
"slices"
@@ -309,7 +309,3 @@ func (b *baker) Nearest(id ids.AssetID, key prefab.Key) (ids.AssetID, bool) {
}
return d.owner, true
}
func Nearest(lib *sceneio.Library, id ids.AssetID, key prefab.Key) (ids.AssetID, bool) {
return newBaker(lib).Nearest(id, key)
}
File diff suppressed because it is too large Load Diff
+19 -44
View File
@@ -3,16 +3,14 @@ package prefabs
import (
"os"
"slices"
"strings"
"aego/asset"
"aego/asset/pipeline"
"aego/core/errs"
"aego/core/ids"
"aego/editor/core/context"
"aego/editor/core/document"
"aego/editor/core/history"
"aego/editor/core/history/commands"
"aego/project/refactor"
"aego/scene"
"aego/scene/prefab"
"aego/scene/sceneio"
@@ -82,18 +80,25 @@ func readFile(abs string) ([]byte, error) {
return data, nil
}
func (s *Service) files(plan *refactor.ApplyPlan) ([]fileChange, error) {
if s.ctx.Pipeline == nil {
return nil, errs.New(errs.EditorCommand, "the asset pipeline is not available")
}
edits, err := plan.Edits(s.ctx.Types.Schema(), s.ctx.Pipeline)
if err != nil {
return nil, err
}
out := make([]fileChange, len(edits))
for i, e := range edits {
out[i] = fileChange{id: e.ID, rel: e.Path, before: e.Before, after: e.After}
func (s *Service) files(edits map[ids.AssetID]*prefab.Node) ([]fileChange, error) {
reg := s.ctx.Types.Schema()
var out []fileChange
for id, m := range edits {
rel, ok := s.pathOf(id)
if !ok {
return nil, errs.New(errs.EditorDocument, "prefab "+id.Short()+" is not in the project")
}
before, err := readFile(s.ctx.Pipeline.Abs(rel))
if err != nil {
return nil, err
}
after := sceneio.EncodePrefab(reg, m)
if string(after) == string(before) {
continue
}
out = append(out, fileChange{id: id, rel: rel, before: before, after: after})
}
slices.SortFunc(out, func(a, b fileChange) int { return strings.Compare(a.rel, b.rel) })
return out, nil
}
@@ -101,8 +106,6 @@ 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 {
@@ -295,31 +298,3 @@ 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)
}
}
+1 -2
View File
@@ -3,7 +3,6 @@ package prefabs
import (
"aego/core/ids"
"aego/editor/core/document"
"aego/project/refactor"
"aego/scene"
"aego/scene/prefab"
"aego/scene/sceneio"
@@ -260,5 +259,5 @@ func (s *Service) nearestOf(d *document.SceneDocument, node ids.NodeID) (ids.Ass
if err != nil {
return ids.AssetID{}, false
}
return refactor.Nearest(s.lib(), pi.Prefab, key)
return newBaker(s.lib()).Nearest(pi.Prefab, key)
}

Some files were not shown because too many files have changed in this diff Show More