7 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
sanspie cffa66514b Stage 9.6c2: prefab apply and data editing without the editor (D298)
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 21m2s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 22m8s
2026-10-20 13:05:10 +03:00
sanspie 2744cff481 Stage 9.6c1: project refs and refactorings without the editor (D298)
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 23m31s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 21m57s
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
2026-10-20 12:12:18 +03:00
sanspie 91052a54db make lint: staticcheck the editor build
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 / checks (push) Successful in 16m26s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m48s
2026-10-20 12:06:56 +03:00
sanspie 7f4d87ef2c Stage 9: gameplay toolkit, export and web, agent tooling 2026-10-20 12:06:56 +03:00
1078 changed files with 80211 additions and 3894 deletions
+11 -1
View File
@@ -94,4 +94,14 @@ jobs:
- name: example project
run: |
/tmp/aego check examples/native-rotator
/tmp/aego run examples/native-rotator --headless --frames 60
/tmp/aego run examples/native-rotator --headless --frames 60
- name: export packages
run: go test -count=1 ./runtime/... ./vfs/exportfs ./app/export/...
- name: export and run headless
run: go test -count=1 -run 'TestExport' -v ./tests/integration
- name: editor app bundle
if: runner.os == 'macOS'
run: |
make editor-app
plutil -lint ".cache/Aego Editor.app/Contents/Info.plist"
codesign --verify --deep ".cache/Aego Editor.app"
+6
View File
@@ -11,4 +11,10 @@ tools/genicons/genicons
/aego
/aego-editor
/aego-playground
/aego-runtime
*.syso
*.test
/tests/golden/testdata/actual-*
tools/websmoke/node_modules/
tools/websmoke/golden/actual-*.png
+272 -40
View File
File diff suppressed because one or more lines are too long
+91 -7
View File
File diff suppressed because it is too large Load Diff
+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
-2
View File
@@ -8,7 +8,6 @@ import (
"aego/core/log"
"aego/engine/base"
"aego/engine/module"
"aego/platform/input"
"aego/scene"
"aego/script/bind"
)
@@ -16,7 +15,6 @@ import (
type Options struct {
Seed uint64
Testing bool
Input *input.AxisConfig
}
type Entry struct {
+54 -3
View File
@@ -4,10 +4,20 @@ import (
"aego/app/assembly"
"aego/engine/base"
"aego/engine/module"
"aego/modules/accessibility"
"aego/modules/audio"
"aego/modules/dataasset"
"aego/modules/debug"
"aego/modules/display"
inputmod "aego/modules/input"
"aego/modules/physics"
"aego/modules/render2d"
"aego/modules/render3d"
"aego/modules/savegame"
"aego/modules/scripting"
settingsmod "aego/modules/settings"
"aego/modules/spawn"
"aego/modules/tween"
"aego/script/stdlib"
)
@@ -15,6 +25,10 @@ func core() assembly.Entry {
return assembly.Entry{ID: base.CoreID, New: func(assembly.Options) module.Module { return base.Core{} }}
}
func inputEntry() assembly.Entry {
return assembly.Entry{ID: inputmod.ID, New: func(assembly.Options) module.Module { return inputmod.New() }}
}
func render2dEntry() assembly.Entry {
return assembly.Entry{ID: render2d.ID, New: func(assembly.Options) module.Module { return render2d.New() }}
}
@@ -25,7 +39,7 @@ func render3dEntry() assembly.Entry {
func scriptEntry() assembly.Entry {
return assembly.Entry{ID: scripting.ID, New: func(o assembly.Options) module.Module {
return scripting.New(stdlib.Options{Seed: o.Seed, Testing: o.Testing, Input: o.Input})
return scripting.New(stdlib.Options{Seed: o.Seed, Testing: o.Testing})
}}
}
@@ -33,6 +47,43 @@ func physicsEntry() assembly.Entry {
return assembly.Entry{ID: physics.ID, New: func(assembly.Options) module.Module { return physics.New() }}
}
func Default() assembly.Catalog {
return assembly.Catalog{core(), render2dEntry(), render3dEntry(), scriptEntry(), physicsEntry()}
func debugEntry() assembly.Entry {
return assembly.Entry{ID: debug.ID, New: func(assembly.Options) module.Module { return debug.New() }}
}
func displayEntry() assembly.Entry {
return assembly.Entry{ID: display.ID, New: func(assembly.Options) module.Module { return display.New() }}
}
func audioEntry() assembly.Entry {
return assembly.Entry{ID: audio.ID, New: func(assembly.Options) module.Module { return audio.New() }}
}
func accessibilityEntry() assembly.Entry {
return assembly.Entry{ID: accessibility.ID, New: func(assembly.Options) module.Module { return accessibility.New() }}
}
func settingsEntry() assembly.Entry {
return assembly.Entry{ID: settingsmod.ID, Requires: []string{inputmod.ID}, New: func(assembly.Options) module.Module { return settingsmod.New() }}
}
func tweenEntry() assembly.Entry {
return assembly.Entry{ID: tween.ID, Requires: []string{scripting.ID}, New: func(assembly.Options) module.Module { return tween.New() }}
}
func dataEntry() assembly.Entry {
return assembly.Entry{ID: dataasset.ID, New: func(assembly.Options) module.Module { return dataasset.New() }}
}
func spawnEntry() assembly.Entry {
return assembly.Entry{ID: spawn.ID, New: func(assembly.Options) module.Module { return spawn.New() }}
}
func savegameEntry() assembly.Entry {
return assembly.Entry{ID: savegame.ID, Requires: []string{scripting.ID, spawn.ID}, New: func(assembly.Options) module.Module { return savegame.New() }}
}
func Default() assembly.Catalog {
return assembly.Catalog{core(), dataEntry(), inputEntry(), render2dEntry(), render3dEntry(), scriptEntry(), physicsEntry(), debugEntry(),
displayEntry(), audioEntry(), accessibilityEntry(), settingsEntry(), tweenEntry(), spawnEntry(), savegameEntry()}
}
+1 -1
View File
@@ -3,5 +3,5 @@ package builtin
import "aego/app/assembly"
func Headless() assembly.Catalog {
return assembly.Catalog{core(), scriptEntry(), physicsEntry()}
return assembly.Catalog{core(), dataEntry(), inputEntry(), scriptEntry(), physicsEntry(), debugEntry(), displayEntry(), audioEntry(), accessibilityEntry(), settingsEntry(), tweenEntry(), spawnEntry(), savegameEntry()}
}
+7
View File
@@ -0,0 +1,7 @@
package builtin
import "aego/app/assembly"
func Runtime() assembly.Catalog {
return Default()
}
+5 -1
View File
@@ -24,6 +24,10 @@ func Manifest(user ...module.Module) (*manifest.Manifest, error) {
}
func AuditIcons(user ...module.Module) (*module.IconReport, error) {
return AuditIconsWith(nil, user...)
}
func AuditIconsWith(extra []module.IconUse, user ...module.Module) (*module.IconReport, error) {
types, err := assembly.NewTypes()
if err != nil {
return nil, err
@@ -32,7 +36,7 @@ func AuditIcons(user ...module.Module) (*module.IconReport, error) {
if err != nil {
return nil, err
}
return module.Audit(types, bind.NewManifest(), append(mods, user...)...)
return module.AuditUses(extra, types, bind.NewManifest(), append(mods, user...)...)
}
func IconSources() []string {
+7 -1
View File
@@ -2,6 +2,10 @@ package editors
import (
"aego/app/assembly"
"aego/modules/debug"
debugeditor "aego/modules/debug/editor"
inputmod "aego/modules/input"
inputeditor "aego/modules/input/editor"
"aego/modules/physics"
physicseditor "aego/modules/physics/editor"
"aego/modules/render2d"
@@ -12,7 +16,9 @@ import (
func With(c assembly.Catalog) assembly.Catalog {
return c.
With(inputmod.ID, inputeditor.Hook).
With(render2d.ID, render2deditor.Hook).
With(render3d.ID, render3deditor.Hook).
With(physics.ID, physicseditor.Hook)
With(physics.ID, physicseditor.Hook).
With(debug.ID, debugeditor.Hook)
}
+11
View File
@@ -44,3 +44,14 @@ func (h hooked) Shutdown() {
sd.Shutdown()
}
}
func (h hooked) Reconfigure(ctx *module.AttachContext) error {
if rc, ok := h.inner.(module.Reconfigurer); ok {
return rc.Reconfigure(ctx)
}
return nil
}
func (h hooked) Unwrap() module.Module {
return h.inner
}
+18 -1
View File
@@ -4,17 +4,34 @@ import (
"aego/app/assembly"
"aego/engine/base"
"aego/engine/module"
"aego/modules/debug"
inputmod "aego/modules/input"
"aego/modules/physics"
"aego/modules/render2d"
"aego/modules/scripting"
"aego/script/stdlib"
"aego/script/vm"
)
func Catalog() assembly.Catalog {
return assembly.Catalog{
{ID: base.CoreID, New: func(assembly.Options) module.Module { return base.Core{} }},
{ID: inputmod.ID, New: func(assembly.Options) module.Module { return inputmod.New() }},
{ID: render2d.ID, New: func(assembly.Options) module.Module { return render2d.New() }},
{ID: scripting.ID, New: func(o assembly.Options) module.Module {
return scripting.New(stdlib.Options{Seed: o.Seed, Testing: o.Testing, Input: o.Input})
return scripting.New(stdlib.Options{Seed: o.Seed, Testing: o.Testing})
}},
{ID: physics.ID, New: func(assembly.Options) module.Module { return physics.New() }},
{ID: debug.ID, New: func(assembly.Options) module.Module { return debug.New() }},
}
}
const (
SandboxBudget = 2_000_000
SandboxHeap = 64 << 20
SandboxNodes = 10_000
)
func SandboxVM() vm.Options {
return vm.Options{Budget: SandboxBudget, MaxHeap: SandboxHeap}
}
+48 -2
View File
File diff suppressed because it is too large Load Diff
+140
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
package crash
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestBundleHidesHome(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
root := filepath.Join(home, "game")
dir, err := Write(Bundle{Root: root, Kind: "panic", Where: "panel inspector", Panic: "boom in " + root, Stack: []byte(root + "/x.go:1")})
if err != nil {
t.Fatal(err)
}
for _, f := range []string{"info.aetext", "go.stack"} {
data, err := os.ReadFile(filepath.Join(dir, f))
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(data), home) || !strings.Contains(string(data), "~/game") {
t.Fatalf("%s is not sanitized: %s", f, data)
}
}
}
+114 -26
View File
File diff suppressed because it is too large Load Diff
+70
View File
@@ -0,0 +1,70 @@
package docstool
import (
"fmt"
"io"
"strings"
"aego/app/playground"
"aego/engine/docs"
)
const tryFrames = 60
func CheckTry(reg *docs.Registry, w io.Writer) int {
pg, err := playground.New()
if err != nil {
fmt.Fprintln(w, "try: cannot start the docs runtime:", err)
return 1
}
pg.SetGame(playground.GameConfig{Headless: true, FrameTime: 1.0 / 60})
defer func() {
if g := pg.Game(); g != nil {
g.Close()
}
}()
sandbox := map[string]bool{}
for _, e := range pg.TryScenes() {
sandbox[e.ID] = true
}
bad := 0
for _, e := range reg.All() {
if e.Try == "" {
continue
}
var msgs []string
add := func(msg string) {
for _, m := range msgs {
if m == msg || strings.Contains(m, msg) || strings.Contains(msg, m) {
return
}
}
msgs = append(msgs, msg)
}
switch res := pg.PlayScene(e.ID, []byte(e.Try)); {
case !sandbox[e.ID]:
add("belongs to a module the docs runtime does not include")
case !res.Started:
add("does not load: " + res.Error)
default:
if err := pg.Game().Step(tryFrames); err != nil {
add("stops the engine: " + err.Error())
}
}
poll := pg.Poll()
for _, l := range poll.Log {
if l.Level != "INFO" && l.Level != "DEBUG" {
add(l.Text)
}
}
for _, f := range poll.Faults {
add(f.Code + " " + f.Message)
}
for _, m := range msgs {
fmt.Fprintf(w, "%s: error: try scene %s\n", e.ID, m)
}
bad += len(msgs)
}
pg.Stop()
return bad
}
+398
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+73
View File
@@ -0,0 +1,73 @@
//go:build aego_editor
package docsview
import (
"encoding/json"
"strconv"
"aego/editor/core/docsession"
)
type request struct {
Scene string `json:"scene"`
Select string `json:"select"`
Component string `json:"component"`
Field string `json:"field"`
Value float64 `json:"value"`
}
type Reply struct {
Text string `json:"text,omitempty"`
Error string `json:"error,omitempty"`
State *docsession.State `json:"state,omitempty"`
Log []Line `json:"log,omitempty"`
}
func (a *App) Call(method string, payload []byte) Reply {
var req request
if len(payload) > 0 {
if err := json.Unmarshal(payload, &req); err != nil {
return Reply{Error: err.Error()}
}
}
s := a.session
var err error
switch method {
case "load":
err = s.Load([]byte(req.Scene), req.Select)
case "reset":
err = s.Reset()
case "text":
case "play":
s.TogglePause()
case "undo":
err = s.Undo()
case "redo":
err = s.Redo()
case "set":
err = s.Set(req.Component, req.Field, req.Value)
case "state":
st := s.State()
return Reply{State: &st}
case "log":
out := Reply{Log: a.lines}
a.lines = nil
return out
default:
return Reply{Error: "unknown method " + strconv.Quote(method)}
}
if err != nil {
return Reply{Error: err.Error()}
}
st := s.State()
return Reply{Text: string(s.Text()), State: &st}
}
func (a *App) Reply(method, payload string) string {
out, err := json.Marshal(a.Call(method, []byte(payload)))
if err != nil {
return `{"error":"cannot encode the reply"}`
}
return string(out)
}
+70
View File
@@ -0,0 +1,70 @@
//go:build aego_editor
package docsview
import (
playmods "aego/app/assembly/playground"
"aego/core/log"
"aego/editor/core/context"
"aego/engine"
"aego/engine/module"
"aego/scene"
"aego/script/bind"
"aego/script/bytecode"
scriptruntime "aego/script/runtime"
)
type scriptHost struct {
reg *bind.Registry
types *scene.Types
man *module.Manifest
log *log.Logger
mods []*bytecode.Module
}
func (h *scriptHost) Errors() int {
return 0
}
func (h *scriptHost) Attach(w *engine.World, s *scene.Scene) (func(), error) {
rt := scriptruntime.New(scriptruntime.Options{Bindings: h.reg, Schema: h.types.Schema(), Log: h.log, VM: playmods.SandboxVM()})
if err := rt.Link(h.mods); err != nil {
rt.Close()
return nil, err
}
w.SetService(scriptruntime.ServiceID, rt)
if err := rt.Instantiate(s); err != nil {
w.RemoveService(scriptruntime.ServiceID)
rt.Close()
return nil, err
}
detach, err := h.man.Attach(&module.AttachContext{World: w, Scene: s, Log: h.log, Editor: true, Play: true})
if err != nil {
w.RemoveService(scriptruntime.ServiceID)
rt.Close()
return nil, err
}
return func() {
detach()
w.RemoveService(scriptruntime.ServiceID)
rt.Close()
}, nil
}
func (h *scriptHost) Problems() ([]context.ScriptProblem, uint64) {
return nil, 0
}
func (h *scriptHost) Stats() (context.ScriptStats, bool) {
return context.ScriptStats{}, false
}
func (h *scriptHost) Status() (uint64, bool) {
return 0, false
}
func (h *scriptHost) Watch(*engine.World, scene.NodeHandle, string, string, int) (string, float64, bool, bool) {
return "", 0, false, false
}
var _ context.ScriptHost = (*scriptHost)(nil)
+97
View File
@@ -0,0 +1,97 @@
package edit
import (
"strconv"
"strings"
"aego/app/crash"
"aego/core/ids"
"aego/core/log"
"aego/editor/core/context"
"aego/scene"
"aego/scene/sceneio"
"aego/script/runtime"
"aego/script/vm"
)
func scriptStack(f *vm.Fault) string {
var b strings.Builder
for _, fr := range f.Stack {
b.WriteString(fr.File + ":" + strconv.Itoa(fr.Line) + " " + fr.Function + "\n")
}
return b.String()
}
func names(s *scene.Scene) func(ids.NodeID) string {
return func(id ids.NodeID) string {
if h := s.Handle(id); !h.IsZero() && s.IsAlive(h) {
return s.Path(h)
}
return id.String()[:8]
}
}
func (s *Session) moduleIDs() string {
var out []string
for _, m := range s.Modules.Modules {
out = append(out, m.ID)
}
return strings.Join(out, ",")
}
func (s *Session) crash(kind, where string, rec any, stack []byte, script string) {
defer func() { recover() }()
b := crash.Bundle{
Root: s.Project.Root, Kind: kind, Where: where, Panic: rec, Stack: stack, Script: script,
Trace: s.Engine.Trace(), Log: s.ring,
Info: [][2]string{{"project", s.Project.Name}, {"gpu", s.Engine.Caps().Renderer}, {"modules", s.moduleIDs()}},
}
var ctx *context.Editor
if s.App != nil {
ctx = s.App.Context()
}
if ctx != nil {
ctx.Docs.WriteRecovery()
if pc := s.App.Play(); pc != nil && pc.World() != nil {
b.Events = pc.Recorder()
if sc := pc.Scene(); sc != nil {
b.Names = names(sc)
if s.crashScene {
b.Scene = sceneio.Save(sc)
}
}
}
}
dir, err := crash.Write(b)
if err != nil {
s.log.Error("cannot write the crash bundle", log.F("err", err.Error()))
return
}
s.log.Error("crash bundle written", log.F("dir", dir), log.F("where", where))
if ctx != nil {
ctx.Notify(context.LevelError, "Crash report saved to "+dir)
}
}
func (s *Session) scriptPanic(boundary string, v any, stack []byte) {
s.crash("panic", boundary, v, stack, "")
}
func (s *Session) fault(rep runtime.FaultReport) {
if rep.Fault != nil && rep.Fault.Code == vm.CodeNativePanic {
s.crash("fault", rep.Type, rep.Fault.Message, []byte(rep.Fault.Detail), scriptStack(rep.Fault))
}
}
func (s *Session) spike() {
if s.spikeMs <= 0 {
return
}
ms := s.Engine.Stats().FrameTime * 1000
now := s.Engine.Platform().Now()
if ms < s.spikeMs || now-s.lastSpike < 10 {
return
}
s.lastSpike = now
s.crash("spike", strconv.FormatFloat(ms, 'f', 1, 64)+" ms frame", nil, nil, "")
}
+32
View File
File diff suppressed because it is too large Load Diff
+2 -6
View File
@@ -116,12 +116,8 @@ func TestPlayLeavesEditSceneByteIdentical(t *testing.T) {
t.Fatal("Spin is not registered")
}
speed, _ := td.Field("speed")
add := &commands.AddComponent{
Node: spinner,
Type: td.ID,
Name: td.FullName,
Values: []commands.FieldValue{{Field: speed.ID, Value: schema.Float32(3)}},
}
add := commands.NewMutation("Add "+td.FullName, commands.EnvFor(es),
commands.AddComp(es, spinner, td, []commands.FieldValue{{Field: speed.ID, Value: schema.Float32(3)}}))
if err := d.History.Exec(add); err != nil {
t.Fatal(err)
}
+89
View File
@@ -0,0 +1,89 @@
package edit
import (
"os/exec"
"runtime"
"aego/app/bootstrap"
"aego/app/export"
"aego/app/projects"
"aego/asset/pipeline"
"aego/editor/core/services"
"aego/engine/problems"
"aego/project"
)
type exporter struct {
p *project.Project
pl *pipeline.Pipeline
engine string
}
func newExporter(p *project.Project, pl *pipeline.Pipeline) services.Exporter {
if pl == nil {
return nil
}
root, err := projects.EngineRoot("")
if err != nil {
return nil
}
return &exporter{p: p, pl: pl, engine: root}
}
func (x *exporter) Targets() []string {
return export.Targets()
}
func (x *exporter) Out(target, profile string) string {
t, err := export.ParseTarget(target)
if err != nil {
return ""
}
prof, err := export.ParseProfile(profile)
if err != nil {
return ""
}
return export.DefaultOut(x.p, t, prof)
}
func (x *exporter) Prepare() any {
return export.Take(x.pl)
}
func (x *exporter) Check(prep any, profile string) []problems.Diagnostic {
snap, ok := prep.(*export.Snapshot)
if !ok {
return nil
}
prof, err := export.ParseProfile(profile)
if err != nil {
return nil
}
return export.Check(x.p, snap, prof, bootstrap.NeedsNative(x.p))
}
func (x *exporter) Export(prep any, req services.ExportRequest, progress func(float32, string)) (services.ExportReport, error) {
snap, _ := prep.(*export.Snapshot)
rep, err := export.Export(export.Options{
Project: x.p, Engine: x.engine, Target: req.Target, Profile: req.Profile, Out: req.Out,
Snapshot: snap, Progress: progress,
})
var out services.ExportReport
if rep != nil {
out = services.ExportReport{Out: rep.Out, Lines: rep.Lines(), Issues: rep.Diagnostics, Blocked: rep.Blocked}
}
return out, err
}
func (x *exporter) Reveal(path string) error {
var cmd *exec.Cmd
switch runtime.GOOS {
case "darwin":
cmd = exec.Command("open", path)
case "windows":
cmd = exec.Command("explorer", path)
default:
cmd = exec.Command("xdg-open", path)
}
return cmd.Start()
}
+59 -4
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
package edit
import (
"os"
"runtime"
"testing"
)
func TestMain(m *testing.M) {
dir, err := os.MkdirTemp("", "aego-config-")
if err != nil {
panic(err)
}
switch runtime.GOOS {
case "darwin":
os.Setenv("HOME", dir)
case "windows":
os.Setenv("AppData", dir)
default:
os.Setenv("XDG_CONFIG_HOME", dir)
}
code := m.Run()
os.RemoveAll(dir)
os.Exit(code)
}
+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.AddComponent{Node: id, Type: td.ID, Name: td.FullName, OnAdd: ctx.OnAdd(td)}); err != nil {
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 {
t.Fatal(err)
}
}
+3 -3
View File
@@ -170,7 +170,7 @@ func TestProfilerSectionsFollowPlay(t *testing.T) {
}
prof := s.App.Profiler()
drawPanel(s, prof)
expectSections(t, prof.Sections(), "Frame")
expectSections(t, prof.Sections(), "Frame", "Memory")
pc := s.App.Play()
if err := pc.Start(d); err != nil {
t.Fatal(err)
@@ -179,7 +179,7 @@ func TestProfilerSectionsFollowPlay(t *testing.T) {
s.Step()
}
drawPanel(s, prof)
expectSections(t, prof.Sections(), "Frame", "Game", "Scripts", "Physics")
expectSections(t, prof.Sections(), "Frame", "Memory", "Game", "Scripts", "Physics")
if _, err := ctx.Docs.Open("scenes/other.aescene"); err != nil {
t.Fatal(err)
}
@@ -189,5 +189,5 @@ func TestProfilerSectionsFollowPlay(t *testing.T) {
pc.Stop()
s.Step()
drawPanel(s, prof)
expectSections(t, prof.Sections(), "Frame")
expectSections(t, prof.Sections(), "Frame", "Memory")
}
+2 -6
View File
@@ -51,12 +51,8 @@ func openSpinner(t *testing.T) *spinFixture {
}
fd, _ := td.Field("speed")
f.typ, f.speed = td.ID, fd.ID
add := &commands.AddComponent{
Node: f.spinner,
Type: td.ID,
Name: td.FullName,
Values: []commands.FieldValue{{Field: fd.ID, Value: schema.Float32(3)}},
}
add := commands.NewMutation("Add "+td.FullName, commands.EnvFor(es),
commands.AddComp(es, f.spinner, td, []commands.FieldValue{{Field: fd.ID, Value: schema.Float32(3)}}))
if err := d.History.Exec(add); err != nil {
t.Fatal(err)
}
+11 -1
View File
@@ -36,6 +36,8 @@ type scriptHost struct {
probRev uint64
probs []context.ScriptProblem
stats context.ScriptStats
fault func(runtime.FaultReport)
panicked func(boundary string, v any, stack []byte)
}
func (h *scriptHost) channel() *log.ChannelLogger {
@@ -158,7 +160,7 @@ func (h *scriptHost) Attach(w *engine.World, s *scene.Scene) (func(), error) {
if err := h.sync(); err != nil {
return nil, err
}
rt := runtime.New(runtime.Options{Bindings: h.bindings, Schema: h.schema, Log: h.log})
rt := runtime.New(runtime.Options{Bindings: h.bindings, Schema: h.schema, Log: h.log, OnFault: h.fault, OnPanic: h.panicked})
if err := rt.Link(h.prog.Ordered()); err != nil {
return nil, err
}
@@ -193,3 +195,11 @@ func (h *scriptHost) Attach(w *engine.World, s *scene.Scene) (func(), error) {
}
}, nil
}
func (h *scriptHost) Watch(w *engine.World, node scene.NodeHandle, script, field string, lane int) (string, float64, bool, bool) {
rt, ok := engine.ServiceOf[*runtime.Runtime](w, runtime.ServiceID)
if !ok || rt == nil {
return "", 0, false, false
}
return rt.WatchField(node, script, field, lane)
}
+208
View File
File diff suppressed because it is too large Load Diff
+198
View File
File diff suppressed because it is too large Load Diff
+196
View File
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
+25
View File
@@ -0,0 +1,25 @@
package macos
import (
"encoding/binary"
"image"
"strings"
"testing"
)
func TestICNSHeader(t *testing.T) {
data, err := ICNS(image.NewNRGBA(image.Rect(0, 0, 64, 64)))
if err != nil {
t.Fatal(err)
}
if string(data[:4]) != "icns" || int(binary.BigEndian.Uint32(data[4:])) != len(data) {
t.Fatal("bad icns header")
}
}
func TestPlistEscapesAndBundleID(t *testing.T) {
p := string(Plist(Info{Name: "A & B", Executable: "ab", ID: BundleID("My_Game/x"), Version: "1.0"}, "icon"))
if !strings.Contains(p, "A &amp; B") || !strings.Contains(p, "dev.aego.my.game.x") {
t.Fatal(p)
}
}
+67
View File
@@ -0,0 +1,67 @@
package export
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"aego/app/export/icon"
"aego/app/export/macos"
"aego/core/errs"
)
const desktopEntry = "[Desktop Entry]\nType=Application\nName=%s\nExec=sh -c 'cd \"$(dirname \"%%k\")\" && exec ./%s'\nTerminal=false\nCategories=Game;\n"
func (j *job) pack() error {
switch j.t.OS {
case "linux":
return j.packLinux()
case "darwin":
return j.packDarwin()
case "js":
return j.packWeb()
}
return nil
}
func (j *job) packLinux() error {
if j.icon != nil {
data, err := icon.PNG(icon.Resize(j.icon, 256, 256))
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(j.out, "icon.png"), data, 0o644); err != nil {
return errs.Wrap(err, errs.VFSIO, "cannot write the icon")
}
}
entry := fmt.Sprintf(desktopEntry, j.p.Name, j.l.exe)
if err := os.WriteFile(filepath.Join(j.out, j.l.exe+".desktop"), []byte(entry), 0o755); err != nil {
return errs.Wrap(err, errs.VFSIO, "cannot write the .desktop file")
}
return nil
}
func (j *job) packDarwin() error {
info := macos.Info{
Name: j.p.Name, Executable: j.l.exe, ID: macos.BundleID(j.p.Module),
Version: GameVersion(j.p), MinOS: "11.0", Category: "public.app-category.games",
}
if err := macos.Fill(j.l.app, info, j.icon); err != nil {
return err
}
if runtime.GOOS != "darwin" {
return nil
}
path, err := exec.LookPath("codesign")
if err != nil {
return nil
}
out, err := exec.Command(path, "--force", "--deep", "--sign", "-", j.l.app).CombinedOutput()
if err != nil {
return errs.New(errs.BuildFailed, "ad-hoc signing failed: "+strings.TrimSpace(string(out)))
}
return nil
}

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