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
178 changed files with 14577 additions and 1831 deletions
+100 -6
View File
File diff suppressed because one or more lines are too long
+31 -3
View File
@@ -20,6 +20,14 @@ EXPORT_HOME = $(CURDIR)/.cache/export-home
CHROMIUM ?= $(shell command -v chromium || command -v chromium-browser || command -v google-chrome)
WEB_SMOKE = cd tools/websmoke && npm ci --no-audit --no-fund && CHROMIUM=$(CHROMIUM) node smoke.mjs --site=$(CURDIR)/site
WASM_BUDGET = 8388608
WEB_DEMO_PROJECT ?= examples/platformer
WEB_DEMO = cd tools/websmoke && CHROMIUM=$(CHROMIUM) node demo.mjs --site=$(CURDIR)/site
WEB_DEMO_BUDGET = @size=$$(stat -c %s site/demo/game.wasm.gz); echo "demo game.wasm.gz: $$size bytes"; \
if [ $$size -gt $(WASM_BUDGET) ]; then echo "the web demo is over the 8 MB gzip budget"; exit 1; fi
TRY_BUDGET = 10485760
WEB_TRY = cd tools/websmoke && CHROMIUM=$(CHROMIUM) node try.mjs --site=$(CURDIR)/site
TRY_SIZE_CHECK = gzip -9 -k -f site/try/aego-docs.wasm; size=$$(stat -c %s site/try/aego-docs.wasm.gz); echo "aego-docs.wasm.gz: $$size bytes"; \
if [ $$size -gt $(TRY_BUDGET) ]; then echo "the docs runtime is over the 10 MB gzip budget, see make try-size"; exit 1; fi
# Module targets, appended by modules/*/module.mk.
MODULE_CI :=
MODULE_BENCH_ALLOCS :=
@@ -30,7 +38,7 @@ BENCH_PHYSICS_PKGS :=
.DEFAULT_GOAL := bootstrap
-include modules/*/module.mk
.PHONY: bootstrap build test test-nonative test-editor test-runtime test-gl vet lint depcheck smoke run genfont genicons icons icons-doc res genpreviews golden golden-update site-headless bench bench-baseline bench-scripts bench-build bench-compare bench-profile bench-scripts-run bench-allocs playground playground-size site site-check cli example example-platformer check-all ci clean fuzz runtime export-smoke editor-app shaders test-wasm web-smoke web-smoke-update
.PHONY: bootstrap build test test-nonative test-editor test-runtime test-gl vet lint depcheck smoke run genfont genicons icons icons-doc res genpreviews golden golden-update site-headless bench bench-baseline bench-scripts bench-build bench-compare bench-profile bench-scripts-run bench-allocs playground playground-size site site-check cli example example-platformer check-all ci clean fuzz runtime export-smoke editor-app shaders test-wasm web-smoke web-smoke-update try-size
XVFB := $(shell command -v xvfb-run 2>/dev/null)
GL_OVERRIDE = MESA_GL_VERSION_OVERRIDE=3.3 MESA_GLSL_VERSION_OVERRIDE=330
@@ -70,6 +78,10 @@ test-runtime: bootstrap
# Engine on gpu/null under Node, plus the js packages compile (D289).
test-wasm:
GOOS=js GOARCH=wasm go build ./cmd/aego-playground ./platform/web ./gpu/webgl2 ./engine
GOOS=js GOARCH=wasm go build -tags aego_runtime -o /dev/null ./cmd/aego-runtime
@if GOOS=js GOARCH=wasm go list -tags aego_runtime -deps ./cmd/aego-runtime | grep -E '^aego/(platform/sdl|gpu/gl33|asset/pipeline|asset/importers|editor)'; then echo "the web runtime pulls a native, pipeline or editor package"; exit 1; fi
GOOS=js GOARCH=wasm go build -tags aego_editor -o /dev/null ./cmd/aego-try
@if GOOS=js GOARCH=wasm go list -tags aego_editor -deps ./cmd/aego-try | grep -E '^aego/(platform/sdl|gpu/gl33|editor/shell|editor/panels/|asset/importers/script)' | grep -vE '^aego/editor/panels/(inspector|gameview)$$'; then echo "the docs runtime pulls a native, shell or extra panel package"; exit 1; fi
GOOS=js GOARCH=wasm go vet ./platform/web ./gpu/webgl2
GOOS=js GOARCH=wasm go test -exec=$(WASM_EXEC) $(WASM_TEST_PKGS)
@@ -86,6 +98,7 @@ 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:
@@ -190,10 +203,13 @@ bench-profile:
go tool pprof -top -nodecount 40 -diff_base $(BENCH_DIR)/base.prof $(BENCH_DIR)/head.prof
site: cli
.cache/bin/aego docs build --out site --repo $(DOCS_REPO)
.cache/bin/aego docs build --out site --repo $(DOCS_REPO) --demo
.cache/bin/aego export $(WEB_DEMO_PROJECT) --target web --release --out site/demo
gzip -9 -k -f site/play/aego.wasm
@size=$$(stat -c %s site/play/aego.wasm.gz); echo "aego.wasm.gz: $$size bytes"; \
if [ $$size -gt $(WASM_BUDGET) ]; then echo "playground is over the 8 MB gzip budget, see make playground-size"; exit 1; fi
$(WEB_DEMO_BUDGET)
@$(TRY_SIZE_CHECK)
playground:
@if GOOS=js GOARCH=wasm go list -deps ./cmd/aego-playground | grep -E '^aego/(platform/sdl|gpu/gl33|asset/pipeline|asset/importers|modules/render3d)'; then echo "playground graph pulls a native or pipeline package"; exit 1; fi
@@ -201,10 +217,13 @@ playground:
site-headless:
CGO_ENABLED=0 go build -tags aego_nonative -o .cache/bin/aego-docs ./cmd/aego
.cache/bin/aego-docs docs build --out site --engine $(CURDIR) --repo $(DOCS_REPO)
.cache/bin/aego-docs docs build --out site --engine $(CURDIR) --repo $(DOCS_REPO) --demo
.cache/bin/aego-docs export $(WEB_DEMO_PROJECT) --target web --release --engine $(CURDIR) --out site/demo
gzip -9 -k -f site/play/aego.wasm
@size=$$(stat -c %s site/play/aego.wasm.gz); echo "aego.wasm.gz: $$size bytes"; \
if [ $$size -gt $(WASM_BUDGET) ]; then echo "playground is over the 8 MB gzip budget, see make playground-size"; exit 1; fi
$(WEB_DEMO_BUDGET)
@$(TRY_SIZE_CHECK)
site-check: cli
.cache/bin/aego docs check
@@ -217,11 +236,20 @@ playground-size:
go run ./tools/wasmsize -top 40 .cache/aego-playground.wasm
go run ./tools/wasmsize -top 15 -depth 2 .cache/aego-playground.wasm
try-size:
mkdir -p .cache
GOOS=js GOARCH=wasm go build -tags aego_editor -trimpath -o .cache/aego-try.wasm ./cmd/aego-try
go run ./tools/wasmsize -top 40 .cache/aego-try.wasm
go run ./tools/wasmsize -top 15 -depth 2 .cache/aego-try.wasm
web-smoke: site
$(WEB_SMOKE) --report=$(CURDIR)/.cache/web-bench.json
$(WEB_DEMO)
$(WEB_TRY)
web-smoke-update: site
$(WEB_SMOKE) --update
$(WEB_DEMO) --update
cli: bootstrap
go build -o .cache/bin/aego ./cmd/aego
+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
+11
View File
@@ -10,6 +10,7 @@ import (
"aego/modules/render2d"
"aego/modules/scripting"
"aego/script/stdlib"
"aego/script/vm"
)
func Catalog() assembly.Catalog {
@@ -24,3 +25,13 @@ func Catalog() assembly.Catalog {
{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}
}
+79 -24
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)
+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)
}
+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)
}
}
+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)
}
+17 -2
View File
@@ -67,6 +67,17 @@ func (j *job) toolchain() error {
if _, err := exec.LookPath("go"); err != nil {
return errs.New(errs.BuildFailed, "export needs the Go toolchain on this machine; the exported game does not, see docs/export.md")
}
if j.t.Web() {
root, err := goRoot()
if err != nil {
return err
}
if _, err := wasmExecPath(root); err != nil {
return err
}
j.goroot = root
return nil
}
cc := j.cc()
if !j.t.Host() && cc == "" {
return errs.New(errs.BuildFailed, "exporting to "+j.t.String()+" from "+runtime.GOOS+"/"+runtime.GOARCH+
@@ -164,8 +175,12 @@ func (j *job) build() error {
args = append(args, pkg)
cmd := exec.Command("go", args...)
cmd.Dir = root
cmd.Env = append(os.Environ(), "GOOS="+j.t.OS, "GOARCH="+j.t.Arch, "CGO_ENABLED=1")
if cc := j.cc(); cc != "" {
cgo := "CGO_ENABLED=1"
if j.t.Web() {
cgo = "CGO_ENABLED=0"
}
cmd.Env = append(os.Environ(), "GOOS="+j.t.OS, "GOARCH="+j.t.Arch, cgo)
if cc := j.cc(); cc != "" && !j.t.Web() {
cmd.Env = append(cmd.Env, "CC="+cc)
}
if j.t.OS == "darwin" {
+18 -9
View File
@@ -70,6 +70,7 @@ func (j *job) data() error {
var total int64
count := 0
var failed error
j.files = j.files[:0]
j.snap.idx.Each(func(e *asset.Entry) {
if failed != nil || e.Missing || e.Error != "" || exportSkip(e.Kind) {
return
@@ -98,6 +99,7 @@ func (j *job) data() error {
}
written[name] = true
names = append(names, name)
j.files = append(j.files, webFile{Path: bundle.ArtifactsDir + "/" + name, Size: int64(len(raw))})
s.Bytes += int64(len(raw))
total += int64(len(raw))
}
@@ -120,7 +122,9 @@ func (j *job) data() error {
if err := os.WriteFile(filepath.Join(j.l.data, bundle.ManifestFile), raw, 0o644); err != nil {
return errs.Wrap(err, errs.VFSIO, "cannot write the data manifest")
}
j.manifestSize = int64(len(raw))
slices.Sort(names)
slices.SortFunc(j.files, func(a, b webFile) int { return strings.Compare(a.Path, b.Path) })
h := sha256.New()
h.Write([]byte(strings.Join(names, "\n")))
h.Write(raw)
@@ -147,7 +151,7 @@ func (j *job) manifest() error {
p := j.p
x := &bundle.Export{
Engine: engine.Version, Game: p.Name, GameVersion: GameVersion(p), Module: p.Module,
Target: j.t.String(), Profile: j.profile, Binary: j.rep.Binary, BinarySize: j.rep.BinarySize,
Target: j.t.String(), Profile: j.profile, Binary: j.rep.Binary, BinarySize: j.rep.BinarySize, BinaryGzip: j.rep.BinaryGzip,
StartScene: p.MainScene, Modules: j.rep.Modules, Libs: j.libs(), Data: j.rep.Data,
DataSize: j.rep.DataSize, Artifacts: j.rep.Artifacts, Hash: j.rep.Hash,
BuildMillis: time.Since(j.start).Milliseconds(), Time: time.Now().UTC().Format(time.RFC3339),
@@ -169,19 +173,24 @@ func (j *job) sdlLicense() string {
return ""
}
func (j *job) libs() []bundle.Lib {
lib := bundle.Lib{Name: "SDL3", Link: "static"}
src := j.sdlLicense()
func (j *job) license(lib bundle.Lib, src, name string) bundle.Lib {
if src == "" {
return []bundle.Lib{lib}
return lib
}
data, err := os.ReadFile(src)
if err != nil {
return []bundle.Lib{lib}
return lib
}
dst := filepath.Join(j.out, "licenses", "SDL3.txt")
dst := filepath.Join(j.out, "licenses", name)
if os.MkdirAll(filepath.Dir(dst), 0o755) == nil && os.WriteFile(dst, data, 0o644) == nil {
lib.License = "licenses/SDL3.txt"
lib.License = "licenses/" + name
}
return []bundle.Lib{lib}
return lib
}
func (j *job) libs() []bundle.Lib {
if j.t.Web() {
return []bundle.Lib{j.license(bundle.Lib{Name: "Go wasm_exec.js", Link: "script", File: webExec}, filepath.Join(j.goroot, "LICENSE"), "Go.txt")}
}
return []bundle.Lib{j.license(bundle.Lib{Name: "SDL3", Link: "static"}, j.sdlLicense(), "SDL3.txt")}
}
+36 -15
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -21,6 +21,8 @@ func (j *job) pack() error {
return j.packLinux()
case "darwin":
return j.packDarwin()
case "js":
return j.packWeb()
}
return nil
}
+6 -1
View File
@@ -24,6 +24,7 @@ type Report struct {
Out string `json:"out"`
Binary string `json:"binary,omitempty"`
BinarySize int64 `json:"binarySize"`
BinaryGzip int64 `json:"binaryGzip,omitempty"`
Data []bundle.Size `json:"data"`
DataSize int64 `json:"dataSize"`
Artifacts int `json:"artifacts"`
@@ -70,7 +71,11 @@ func Bytes(n int64) string {
func (r *Report) Lines() [][2]string {
out := [][2]string{{"Target", r.Target}, {"Profile", r.Profile}, {"Output", r.Out}}
if r.Binary != "" {
out = append(out, [2]string{"Binary", r.Binary + " (" + Bytes(r.BinarySize) + ")"})
s := r.Binary + " (" + Bytes(r.BinarySize)
if r.BinaryGzip > 0 {
s += ", gzip " + Bytes(r.BinaryGzip)
}
out = append(out, [2]string{"Binary", s + ")"})
}
if r.Artifacts > 0 {
out = append(out, [2]string{"Data", Bytes(r.DataSize) + " in " + strconv.Itoa(r.Artifacts) + " artifacts"})
+208
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
<!doctype html>
<html lang="en" data-build="{{.Build}}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}}</title>
{{if .Icon}}<link rel="icon" href="favicon.png">{{else}}<link rel="icon" href="data:,">{{end}}
<style>
html, body { margin: 0; height: 100%; background: #0d0e10; color: #e4e3df; overflow: hidden; font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
canvas { display: block; width: 100%; height: 100%; outline: none; touch-action: none; }
.loader { position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; background: #0d0e10; transition: opacity 0.25s; }
.loader.done { opacity: 0; pointer-events: none; }
.loader .title { font-size: 20px; font-weight: 600; }
.progress { width: min(360px, 80vw); height: 6px; border-radius: 3px; background: #2d3036; overflow: hidden; }
#bar { width: 0; height: 100%; background: #6e9bf5; transition: width 0.1s; }
#bar.indeterminate { width: 30%; animation: slide 1s ease-in-out infinite alternate; }
@keyframes slide { from { margin-left: 0; } to { margin-left: 70%; } }
#status { color: #9a9890; font-size: 13px; }
.loader.failed #bar { background: #f0776c; }
#error { max-width: min(640px, 90vw); max-height: 40vh; overflow: auto; margin: 0; padding: 10px 12px; white-space: pre-wrap; color: #f0776c; background: #1d1f23; border-radius: 6px; font: 12px/1.5 ui-monospace, Menlo, Consolas, monospace; }
#fullscreen { position: fixed; top: 10px; right: 10px; width: 36px; height: 36px; border: 0; border-radius: 6px; background: rgba(0, 0, 0, 0.45); color: #fff; font-size: 18px; cursor: pointer; opacity: 0.5; }
#fullscreen:hover { opacity: 1; }
</style>
</head>
<body>
<canvas id="game" data-aego tabindex="0" aria-label="{{.Title}}"></canvas>
<div id="loader" class="loader" role="status" aria-live="polite">
<div class="title">{{.Title}}</div>
<div class="progress"><div id="bar"></div></div>
<div id="status">Loading…</div>
<pre id="error" hidden></pre>
</div>
<button id="fullscreen" type="button" title="Fullscreen" aria-label="Fullscreen" hidden>⛶</button>
<script src="wasm_exec.js?v={{.Build}}"></script>
<script src="loader.js?v={{.Build}}"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+85
View File
@@ -0,0 +1,85 @@
package export
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"aego/project"
"aego/runtime/bundle"
)
var webTarget = Target{OS: "js", Arch: "wasm"}
func TestParseWeb(t *testing.T) {
for _, s := range []string{"web", "WEB", "js/wasm", "wasm"} {
tg, err := ParseTarget(s)
if err != nil || !tg.Web() || tg.String() != "web" {
t.Fatalf("%s: %+v %v", s, tg, err)
}
}
}
func TestWebLayout(t *testing.T) {
p := &project.Project{Name: "My Game", Root: t.TempDir()}
l := layoutFor(webTarget, "/out", p)
if l.binary() != filepath.Join("/out", WebWasm) || l.data != filepath.Join("/out", bundle.DataDir) {
t.Fatalf("layout %+v", l)
}
if !strings.HasSuffix(DefaultOut(p, webTarget, bundle.Release), "web-"+bundle.Release) {
t.Fatal(DefaultOut(p, webTarget, bundle.Release))
}
}
func TestPackWeb(t *testing.T) {
root, err := goRoot()
if err != nil {
t.Skip(err)
}
out := t.TempDir()
wasm := bytes.Repeat([]byte("wasm"), 4096)
j := &job{
p: &project.Project{Name: `<Game & "Co">`, Module: "game"}, t: webTarget, out: out, goroot: root,
rep: &Report{Hash: strings.Repeat("ab", 32), BinarySize: int64(len(wasm))},
files: []webFile{{Path: "artifacts/a.tex", Size: 3}}, manifestSize: 8,
}
j.l = layoutFor(webTarget, out, j.p)
if err := os.MkdirAll(j.l.data, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(j.l.binary(), wasm, 0o644); err != nil {
t.Fatal(err)
}
if err := j.packWeb(); err != nil {
t.Fatal(err)
}
page, _ := os.ReadFile(filepath.Join(out, webIndex))
if !strings.Contains(string(page), "&lt;Game &amp;") || strings.Contains(string(page), "<Game") {
t.Fatal("the title is not escaped")
}
if !strings.Contains(string(page), `data-build="abababababab"`) {
t.Fatal("the build key is missing")
}
raw, err := os.ReadFile(filepath.Join(j.l.data, WebFiles))
if err != nil {
t.Fatal(err)
}
var list webList
if err := json.Unmarshal(raw, &list); err != nil {
t.Fatal(err)
}
if list.Wasm != int64(len(wasm)) || list.Manifest.Size != 8 || len(list.Files) != 1 || list.Files[0].Path != "artifacts/a.tex" {
t.Fatalf("files.json %+v", list)
}
for _, f := range []string{webExec, webLoader, WebWasm + ".gz"} {
if _, err := os.Stat(filepath.Join(out, f)); err != nil {
t.Errorf("%s is missing", f)
}
}
if j.rep.BinaryGzip <= 0 || j.rep.BinaryGzip >= j.rep.BinarySize {
t.Fatalf("wasm gzip size %d of %d", j.rep.BinaryGzip, j.rep.BinarySize)
}
}
+14 -2
View File
@@ -1,8 +1,11 @@
package playground
import (
"fmt"
"strings"
playmods "aego/app/assembly/playground"
"aego/asset"
"aego/asset/loader"
"aego/core/log"
@@ -99,6 +102,7 @@ type Game struct {
bench bench
running bool
looping bool
overflow bool
}
type driver struct {
@@ -195,7 +199,7 @@ func (g *Game) Engine() *engine.Engine {
func (g *Game) Play(name string, src []byte, mods []*bytecode.Module) error {
g.Stop()
rt := scriptruntime.New(scriptruntime.Options{Bindings: g.reg, Schema: g.types.Schema(), Log: g.log})
rt := scriptruntime.New(scriptruntime.Options{Bindings: g.reg, Schema: g.types.Schema(), Log: g.log, VM: playmods.SandboxVM()})
if err := rt.Link(mods); err != nil {
rt.Close()
return err
@@ -225,7 +229,7 @@ func (g *Game) Play(name string, src []byte, mods []*bytecode.Module) error {
return err
}
g.detach = d
g.example, g.running, g.faults = name, true, 0
g.example, g.running, g.faults, g.overflow = name, true, 0, false
g.start()
return nil
}
@@ -272,6 +276,10 @@ func (g *Game) update(dt float64) {
g.frameMs += (ft - g.frameMs) * statsAlpha
}
g.bench.t += dt
if g.scene != nil && !g.overflow && g.scene.Stats().Alive > playmods.SandboxNodes {
g.overflow = true
g.clog.Error(fmt.Sprintf("the scene grew past %d nodes, the game was stopped", playmods.SandboxNodes))
}
}
func hue(i, n int) mathx.Color {
@@ -351,6 +359,10 @@ func (g *Game) newFaults() []Fault {
}
func (g *Game) Poll() Poll {
if g.overflow {
g.overflow = false
g.Stop()
}
p := Poll{Log: g.lines, Faults: g.newFaults()}
g.lines = nil
s := &p.Stats
+11
View File
@@ -109,3 +109,14 @@ func (p *Playground) Poll() Poll {
func (p *Playground) LoseContext(lose bool) bool {
return p.game != nil && p.game.LoseContext(lose)
}
func (p *Playground) PlayScene(name string, src []byte) PlayResult {
g, err := p.host()
if err != nil {
return PlayResult{Error: err.Error()}
}
if err := g.Play(name, src, nil); err != nil {
return PlayResult{Error: err.Error()}
}
return PlayResult{Started: true}
}
+11 -3
View File
@@ -31,7 +31,6 @@ import (
"aego/script/lexer"
"aego/script/runtime"
"aego/script/source"
"aego/script/vm"
"aego/script/workspace"
)
@@ -43,7 +42,6 @@ const (
ScriptsDir = "scripts"
DefaultFrames = 120
MaxFrames = 3600
Budget = 2_000_000
)
type Diagnostic struct {
@@ -284,7 +282,7 @@ func (p *Playground) Run(files map[string]string, frames int) Result {
res.Log = append(res.Log, record(&frame, r))
}))
lg.SetLevel(log.Debug)
rt := runtime.New(runtime.Options{Bindings: p.reg, Schema: p.types.Schema(), Log: lg, VM: vm.Options{Budget: Budget}})
rt := runtime.New(runtime.Options{Bindings: p.reg, Schema: p.types.Schema(), Log: lg, VM: playmods.SandboxVM()})
defer rt.Close()
if err := rt.Link(mods); err != nil {
res.Diagnostics = append(res.Diagnostics, failed(err)...)
@@ -495,3 +493,13 @@ func Format(res Result) []string {
}
return append(out, strconv.Itoa(res.Frames)+" frames")
}
func (p *Playground) TryScenes() []docs.Entry {
var out []docs.Entry
for _, e := range p.docs.All() {
if e.Try != "" {
out = append(out, e)
}
}
return out
}
+86
View File
@@ -0,0 +1,86 @@
package playground
import (
"strings"
"testing"
"aego/script/vm"
)
const loopKick = `package boxes
script Kick {
func Update(dt float) {
n := 0
for n >= 0 {
n++
}
}
}
`
const heapKick = `package boxes
script Kick {
func Update(dt float) {
s := "ab"
for s != "" {
s = s + s
}
}
}
`
const saveScript = `package demo
import "aego/save"
script S {
func Ready() {
save.Save("slot")
}
}
`
func faultOf(t *testing.T, p *Playground, code string) Fault {
t.Helper()
res := p.Play(ScenePrefix+"boxes", map[string]string{"main.aes": code})
if !res.Started {
t.Fatalf("did not start: %s %+v", res.Error, res.Diagnostics)
}
if err := p.Game().Step(3); err != nil {
t.Fatal(err)
}
for _, f := range p.Poll().Faults {
if f.Code == vm.CodeBudget {
return f
}
}
t.Fatal("the script was not stopped by the sandbox")
return Fault{}
}
func TestSandboxInstructionBudget(t *testing.T) {
p := newHeadless(t)
faultOf(t, p, loopKick)
if !p.Poll().Stats.Running {
t.Fatal("the game stopped with the script")
}
}
func TestSandboxHeapLimit(t *testing.T) {
p := newHeadless(t)
if f := faultOf(t, p, heapKick); !strings.Contains(f.Message, "heap") {
t.Fatalf("fault %+v", f)
}
}
func TestSandboxHasNoSaves(t *testing.T) {
p := newHeadless(t)
for _, d := range p.Compile(map[string]string{"main.aes": saveScript}) {
if d.Severity == "error" {
return
}
}
t.Fatal("aego/save compiled in the sandbox")
}
+65
View File
@@ -0,0 +1,65 @@
package run
import (
"bytes"
"encoding/hex"
"strconv"
"strings"
"aego/app/agent"
"aego/core/log"
"aego/core/rewind"
"aego/project"
"aego/sdk"
)
const maxRunLog = 200
func AgentRunner(mods ...sdk.Module) agent.Runner {
return func(p *project.Project, r agent.RunRequest) (agent.RunReport, error) {
var buf bytes.Buffer
lg := log.New(log.NewTextSink(&buf))
lg.SetLevel(log.Warn)
frames, seed := r.Frames, r.Seed
if frames <= 0 {
frames = 120
}
if seed == 0 {
seed = 1
}
a, err := Open(p, Config{Headless: true, FrameTime: defaultDetStep, Seed: seed, ScenePath: r.Scene, Logger: lg, NoProfile: true}, mods...)
if err != nil {
return agent.RunReport{Log: logLines(buf.String())}, err
}
rep := agent.RunReport{}
driver := a.Driver()
for rep.Frames < frames {
if !a.Engine.Step(driver) {
rep.Error = "the game stopped at frame " + strconv.Itoa(rep.Frames+1)
if err := a.Engine.Err(); err != nil {
rep.Error += ": " + err.Error()
}
break
}
rep.Frames++
}
sum, _, _ := WorldHash(a.Engine.MainWorld(), rewind.NewHasher())
rep.Hash = hex.EncodeToString(sum[:])
a.Close()
rep.Log = logLines(buf.String())
return rep, nil
}
}
func logLines(s string) []string {
var out []string
for _, l := range strings.Split(strings.TrimSpace(s), "\n") {
if l = strings.TrimSpace(l); l != "" {
out = append(out, l)
}
}
if len(out) > maxRunLog {
out = append(out[:maxRunLog], "…")
}
return out
}
+1 -1
View File
@@ -78,7 +78,7 @@ func WorldHash(w *engine.World, h *rewind.Hasher) (sum [32]byte, covered, skippe
}
func determinismPass(p *project.Project, o DeterminismOptions, mods []sdk.Module) ([]checkpoint, []string, []string, error) {
a, err := Open(p, Config{Headless: true, FrameTime: o.FrameTime, Seed: o.Seed, ScenePath: o.ScenePath, Logger: log.Nop()}, mods...)
a, err := Open(p, Config{Headless: true, FrameTime: o.FrameTime, Seed: o.Seed, ScenePath: o.ScenePath, Logger: log.Nop(), NoProfile: true}, mods...)
if err != nil {
return nil, nil, nil, err
}
+2
View File
@@ -43,6 +43,7 @@ type Config struct {
Profile string
Logger *log.Logger
Seed uint64
NoProfile bool
}
type App struct {
@@ -138,6 +139,7 @@ func Open(p *project.Project, cfg Config, mods ...sdk.Module) (*App, error) {
Debug: cfg.Debug,
FailOnScriptErrors: true,
Profile: cfg.Profile,
NoProfile: cfg.NoProfile,
}, sdk.AdaptAll(mods)...)
if err != nil {
return nil, err
+7
View File
@@ -1,10 +1,13 @@
package run
import (
"aego/app/agent"
"aego/app/agent/mcp"
"bytes"
"flag"
"fmt"
"io"
"os"
"sort"
"strings"
@@ -39,6 +42,10 @@ func Tool(p *project.Project, args []string, stdout, stderr io.Writer, mods ...s
return toolTest(p, args[1:], stdout, stderr, mods)
case "docs":
return docstool.Tool(p, args[1:], stdout, stderr, sdk.AdaptAll(mods)...)
case "agent":
return agent.Main(p, args[1:], stdout, stderr, agent.Options{Mods: mods, Run: AgentRunner(mods...)})
case "mcp":
return mcp.Main(p, args[1:], os.Stdin, stdout, stderr, agent.Options{Mods: mods, Run: AgentRunner(mods...)})
}
fmt.Fprintf(stderr, "unknown tool %q\n", args[0])
return 2
+40
View File
@@ -0,0 +1,40 @@
//go:build js && aego_editor
package main
import (
"syscall/js"
"aego/app/docsview"
)
func main() {
g := js.Global()
api := g.Get("Object").New()
app, err := docsview.New(docsview.Options{})
if err != nil {
api.Set("error", err.Error())
} else {
app.OnChange = func(text string) {
if f := g.Get("aegoDocsChanged"); f.Type() == js.TypeFunction {
f.Invoke(text)
}
}
api.Set("call", js.FuncOf(func(_ js.Value, args []js.Value) any {
method, payload := "", ""
if len(args) > 0 {
method = args[0].String()
}
if len(args) > 1 {
payload = args[1].String()
}
return app.Reply(method, payload)
}))
go app.Run()
}
g.Set("aegoDocs", api)
if ready := g.Get("aegoDocsReady"); ready.Type() == js.TypeFunction {
ready.Invoke()
}
select {}
}
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -15,7 +15,7 @@ const docsUsage = `usage: aego docs <command> [flags]
export [--out file] write the documentation registry as JSON
check [--strict] broken links, empty summaries, undocumented bindings
build [--out site] generate the static site with the playground
build [--out site] [--demo] generate the static site with the playground
serve [--addr host:port] build, serve on localhost and rebuild on changes
flags: --project dir, --engine dir, --playground=false`
@@ -64,6 +64,7 @@ func cmdDocs(args []string) int {
play := fs.Bool("playground", true, "build the playground and Run buttons")
engineDir := fs.String("engine", "", "path to the aego source tree")
repo := fs.String("repo", "", "source repository linked from the site header")
demo := fs.Bool("demo", false, "link the web demo at demo/ from the landing page")
dir := fs.String("project", "", "project directory")
if err := fs.Parse(args[1:]); err != nil {
return 2
@@ -73,6 +74,7 @@ func cmdDocs(args []string) int {
return fail(err)
}
env.Repo = *repo
env.Demo = *demo
site := func(def string) string {
if *out != "" {
return *out
+1 -1
View File
@@ -13,7 +13,7 @@ import (
const exportUsage = `usage: aego export [dir] [flags]
--target host|linux|windows|darwin[/amd64|/arm64] default host
--target host|linux|windows|darwin[/amd64|/arm64]|web default host
--development | --release default development
--out dir output directory, default <project>/exports/<os>-<arch>-<profile>
--cc compiler C compiler for a cross target, or CC_<OS>_<ARCH>
+44
View File
@@ -1,11 +1,14 @@
package main
import (
"aego/app/agent"
"aego/app/agent/mcp"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"aego/app/bootstrap"
@@ -50,6 +53,8 @@ func dispatch(args []string) int {
return cmdCheck(args[1:])
case "icons":
return cmdIcons(args[1:])
case "mcp":
return cmdMcp(args[1:])
case "docs":
return cmdDocs(args[1:])
case "test":
@@ -60,6 +65,9 @@ func dispatch(args []string) int {
usage()
return 0
}
if slices.Contains(agent.Groups(), args[0]) {
return cmdAgent(args)
}
fmt.Fprintf(os.Stderr, "unknown command %q\n", args[0])
usage()
return 2
@@ -83,6 +91,8 @@ func usage() {
run *_test.aes script tests headless, or run the game twice and compare state hashes
dump script <pkg|Type> [--func name] disassemble a script package with source lines
docs export|check|build|serve documentation registry, site and playground
mcp [--project dir] [--watch] serve the project to an AI agent over MCP (stdio)
scene|prefab|data|refs|asset|schema|project <verb> ... edit project files without the editor, see aego scene help
`)
}
@@ -97,6 +107,40 @@ func openProject(dir string) (*project.Project, error) {
return project.Load(root)
}
func projectFlag(args []string) string {
for i, a := range args {
switch {
case a == "--project" && i+1 < len(args):
return args[i+1]
case strings.HasPrefix(a, "--project="):
return strings.TrimPrefix(a, "--project=")
}
}
return ""
}
func cmdAgent(args []string) int {
p, err := openProject(projectFlag(args))
if err != nil {
return fail(err)
}
if bootstrap.NeedsNative(p) {
return scriptTool(p, append([]string{"agent"}, args...)...)
}
return agent.Main(p, args, os.Stdout, os.Stderr, agent.Options{Run: run.AgentRunner()})
}
func cmdMcp(args []string) int {
p, err := openProject(projectFlag(args))
if err != nil {
return fail(err)
}
if bootstrap.NeedsNative(p) {
return scriptTool(p, append([]string{"mcp"}, args...)...)
}
return mcp.Main(p, args, os.Stdin, os.Stdout, os.Stderr, agent.Options{Run: run.AgentRunner()})
}
func firstArg(args []string) (string, []string) {
if len(args) > 0 && len(args[0]) > 0 && args[0][0] != '-' {
return args[0], args[1:]
+1
View File
@@ -23,6 +23,7 @@ func scriptTool(p *project.Project, args ...string) int {
}
cmd := exec.Command(binary, append([]string{"--tool"}, args...)...)
cmd.Dir = p.Root
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return exitCode(cmd.Run())
+22 -3
View File
@@ -3,7 +3,7 @@
`aego export` builds a game that runs on a machine without Go, without the engine sources and without the project.
```
aego export [dir] --target host|linux|windows|darwin[/amd64|/arm64] --development|--release --out dir
aego export [dir] --target host|linux|windows|darwin[/amd64|/arm64]|web --development|--release --out dir
aego export --check --release pre-export checks only
aego export --json report as JSON
```
@@ -77,13 +77,32 @@ The export needs Go and the SDL build for the target in `.cache/native/<os>_<arc
| host | `make bootstrap` |
| linux → windows | `CC=x86_64-w64-mingw32-gcc GOOS=windows make bootstrap`, then `aego export --target windows --cc x86_64-w64-mingw32-gcc`, or `zig cc -target x86_64-windows-gnu` |
| linux → darwin | osxcross: `CC=o64-clang GOOS=darwin make bootstrap`, `--cc o64-clang` |
| any → web | Stage 9.5c |
| any → web | nothing beyond Go: the build is GOOS=js GOARCH=wasm without cgo |
`CC_<OS>_<ARCH>` (for example `CC_WINDOWS_AMD64`) replaces `--cc`.
## Not in v1
No `.aepak`, no pruning of unreferenced assets (every imported asset except presets is exported), no stripping of embedded engine resources, no web target, no installers or notarization.
No `.aepak`, no pruning of unreferenced assets (every imported asset except presets is exported), no stripping of embedded engine resources, no installers or notarization.
## Web (D295)
`aego export --target web` builds the same runtime for `js/wasm` and writes a folder that any static server can host:
| File | Content |
|---|---|
| `index.html` | a full-window canvas, the loader with progress and errors, a fullscreen button |
| `loader.js` | fetches `data/files.json`, then `game.wasm`, the manifest and every artifact (six at a time), and starts the game |
| `wasm_exec.js` | the Go WebAssembly support script of the toolchain that built the game, its license in `licenses/Go.txt` |
| `game.wasm` | `cmd/aego-runtime` with `-tags aego_runtime`; release adds `-trimpath -s -w` |
| `data/` | `manifest.aetext`, `artifacts/` and `files.json` with every file and its size |
| `export.aetext` | as for desktop targets, plus `binaryGzip` |
Every file that compresses by more than 10% gets a `.gz` copy next to it, and a `.br` copy when `brotli` is in PATH; serve them with `gzip_static`/`brotli_static` or the equivalent, and serve `.wasm` as `application/wasm`. Artifacts are named by content hash and can be cached forever; `index.html`, `game.wasm`, the manifest and `files.json` carry the export hash in their query strings.
The game reads its data from memory: the loader hands the bytes to the runtime, which mounts them with `vfs/memfs` after checking every file against the manifest. Settings and saves go to `localStorage` through `platform.UserFiles()` (a few MB per site, writes past the quota fail with an error); there are no log files and no crash bundles, the log goes to the browser console. Fullscreen and pointer lock need a click or key press first. URL parameters `?scene=`, `?seed=`, `?profile=` and `?debug=1` replace the command-line flags.
Checks: EXP010 warns about an asset kind the web runtime cannot load (none yet), EXP011 warns that a native module must build without cgo. The target budget is 8 MB gzip for `game.wasm`; `make site` exports the platformer to `site/demo/` and fails above it.
## Editor app on macOS
+6
View File
@@ -162,3 +162,9 @@ func (q *questPanel) Draw(f *sdk.Frame, r mathx.Rect) {
| Settings page, profiler section, vector layer | `SettingsPage`, `Profiler`, `Vectors` |
A panel gets a fresh `Frame` every draw (`UI`, drag and drop, time, focus). Anything else it needs it captures at registration through `e.Editor()`. Built-in panels follow a stricter rule and never see the editor (D216). `PanelDesc.Slot` puts a panel into a column of the default layout; `Near` opens it next to a panel on demand; without `Order` extension panels follow the built-ins in the Window menu (D217).
## Try it scenes
A module can give any of its components a live example on the documentation site: put a scene in `res/docs/try/<Component>.aescene`, named after the last part of the component ID (`try/Body2D.aescene` for `physics.Body2D`). The page of that component gets a Try it block that runs the scene in the browser with the Inspector, selecting the first node that has the component; a guide page can show a scene with `try: try/name.aescene` and `try_select: module.Component` in its front matter.
Try scenes run on the sandbox catalog (core, input, render2d, scripting, physics, debug) with builtin assets only and no scripts. `aego docs check` loads each one, runs it for 60 frames and fails on a load error, a warning or a fault, and on a scene of a module outside the sandbox. Keep the scene to what the component needs, put the documented node first, and add a camera.
+4
View File
@@ -43,3 +43,7 @@ What the browser build (`GOOS=js GOARCH=wasm`, `platform/web` + `gpu/webgl2`) su
| Debug output, object labels | missing | `Options.Debug` checks `getError` after creation calls |
| Context loss | works | objects are recreated from CPU copies; textures cost twice the memory |
| Shaders | works | GLSL ES 3.00 copies generated by `make shaders` |
## Exported games
`aego export --target web` (D295) runs the same runtime as the desktop export on this platform and GPU backend. Differences from desktop: data is loaded into memory before the game starts, there are no log files or crash bundles, saves and settings share the site's `localStorage` quota.
+6 -11
View File
@@ -3,7 +3,6 @@ package assetops
import (
"os"
"path"
"regexp"
"sort"
"strconv"
"strings"
@@ -12,6 +11,8 @@ import (
"aego/core/ids"
"aego/editor/core/context"
"aego/editor/core/document"
"aego/project/refactor"
"aego/project/rename"
)
const (
@@ -19,8 +20,6 @@ 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
@@ -180,8 +179,8 @@ func setPackage(ctx *context.Editor, rel, pkg string) error {
return err
}
c := d.Code
if m := packageDecl.FindSubmatchIndex(c.Bytes()); m != nil {
c.ReplaceRanges([][2]int{{m[2], m[3]}}, [][]byte{[]byte(pkg)})
if a, b, ok := refactor.PackageSpan(c.Bytes()); ok {
c.ReplaceRanges([][2]int{{a, b}}, [][]byte{[]byte(pkg)})
} else {
c.ReplaceRanges([][2]int{{0, 0}}, [][]byte{[]byte("package " + pkg + "\n\n")})
}
@@ -207,7 +206,7 @@ func Stem(p string) string {
}
func ValidName(s string) bool {
return s != "" && s != "." && s != ".." && !strings.HasPrefix(s, ".") && !strings.ContainsAny(s, "/\\")
return rename.ValidName(s)
}
func ValidIdent(s string) bool {
@@ -270,11 +269,7 @@ func ScriptsRel(dir string) (string, bool) {
}
func PackageOf(ctx *context.Editor, dir string) string {
if rel, ok := ScriptsRel(dir); ok && rel != "" {
return path.Base(rel)
}
m := moduleOf(ctx)
return m[strings.LastIndexByte(m, '/')+1:]
return refactor.PackageOf(ctx.Project, dir)
}
func FileStem(name string) string {

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