18 Commits
Author SHA1 Message Date
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
145 changed files with 1784 additions and 9845 deletions
+6 -17
View File
File diff suppressed because one or more lines are too long
+3 -31
View File
@@ -20,14 +20,6 @@ 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 :=
@@ -38,7 +30,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 try-size
.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
XVFB := $(shell command -v xvfb-run 2>/dev/null)
GL_OVERRIDE = MESA_GL_VERSION_OVERRIDE=3.3 MESA_GLSL_VERSION_OVERRIDE=330
@@ -78,10 +70,6 @@ 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)
@@ -98,7 +86,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:
@@ -203,13 +190,10 @@ 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) --demo
.cache/bin/aego export $(WEB_DEMO_PROJECT) --target web --release --out site/demo
.cache/bin/aego docs build --out site --repo $(DOCS_REPO)
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
@@ -217,13 +201,10 @@ 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) --demo
.cache/bin/aego-docs export $(WEB_DEMO_PROJECT) --target web --release --engine $(CURDIR) --out site/demo
.cache/bin/aego-docs docs build --out site --engine $(CURDIR) --repo $(DOCS_REPO)
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
@@ -236,20 +217,11 @@ 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
-11
View File
@@ -10,7 +10,6 @@ import (
"aego/modules/render2d"
"aego/modules/scripting"
"aego/script/stdlib"
"aego/script/vm"
)
func Catalog() assembly.Catalog {
@@ -25,13 +24,3 @@ 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}
}
+24 -79
View File
File diff suppressed because it is too large Load Diff
-70
View File
@@ -1,70 +0,0 @@
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
@@ -1,73 +0,0 @@
//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
@@ -1,70 +0,0 @@
//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)
+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)
}
+2 -17
View File
@@ -67,17 +67,6 @@ 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+
@@ -175,12 +164,8 @@ func (j *job) build() error {
args = append(args, pkg)
cmd := exec.Command("go", args...)
cmd.Dir = root
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(os.Environ(), "GOOS="+j.t.OS, "GOARCH="+j.t.Arch, "CGO_ENABLED=1")
if cc := j.cc(); cc != "" {
cmd.Env = append(cmd.Env, "CC="+cc)
}
if j.t.OS == "darwin" {
+9 -18
View File
@@ -70,7 +70,6 @@ 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
@@ -99,7 +98,6 @@ 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))
}
@@ -122,9 +120,7 @@ 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)
@@ -151,7 +147,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, BinaryGzip: j.rep.BinaryGzip,
Target: j.t.String(), Profile: j.profile, Binary: j.rep.Binary, BinarySize: j.rep.BinarySize,
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),
@@ -173,24 +169,19 @@ func (j *job) sdlLicense() string {
return ""
}
func (j *job) license(lib bundle.Lib, src, name string) bundle.Lib {
func (j *job) libs() []bundle.Lib {
lib := bundle.Lib{Name: "SDL3", Link: "static"}
src := j.sdlLicense()
if src == "" {
return lib
return []bundle.Lib{lib}
}
data, err := os.ReadFile(src)
if err != nil {
return lib
return []bundle.Lib{lib}
}
dst := filepath.Join(j.out, "licenses", name)
dst := filepath.Join(j.out, "licenses", "SDL3.txt")
if os.MkdirAll(filepath.Dir(dst), 0o755) == nil && os.WriteFile(dst, data, 0o644) == nil {
lib.License = "licenses/" + name
lib.License = "licenses/SDL3.txt"
}
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")}
return []bundle.Lib{lib}
}
+15 -36
View File
File diff suppressed because it is too large Load Diff
-2
View File
@@ -21,8 +21,6 @@ func (j *job) pack() error {
return j.packLinux()
case "darwin":
return j.packDarwin()
case "js":
return j.packWeb()
}
return nil
}
+1 -6
View File
@@ -24,7 +24,6 @@ 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"`
@@ -71,11 +70,7 @@ 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 != "" {
s := r.Binary + " (" + Bytes(r.BinarySize)
if r.BinaryGzip > 0 {
s += ", gzip " + Bytes(r.BinaryGzip)
}
out = append(out, [2]string{"Binary", s + ")"})
out = append(out, [2]string{"Binary", r.Binary + " (" + Bytes(r.BinarySize) + ")"})
}
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
@@ -1,37 +0,0 @@
<!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
@@ -1,85 +0,0 @@
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)
}
}
+2 -14
View File
@@ -1,11 +1,8 @@
package playground
import (
"fmt"
"strings"
playmods "aego/app/assembly/playground"
"aego/asset"
"aego/asset/loader"
"aego/core/log"
@@ -102,7 +99,6 @@ type Game struct {
bench bench
running bool
looping bool
overflow bool
}
type driver struct {
@@ -199,7 +195,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, VM: playmods.SandboxVM()})
rt := scriptruntime.New(scriptruntime.Options{Bindings: g.reg, Schema: g.types.Schema(), Log: g.log})
if err := rt.Link(mods); err != nil {
rt.Close()
return err
@@ -229,7 +225,7 @@ func (g *Game) Play(name string, src []byte, mods []*bytecode.Module) error {
return err
}
g.detach = d
g.example, g.running, g.faults, g.overflow = name, true, 0, false
g.example, g.running, g.faults = name, true, 0
g.start()
return nil
}
@@ -276,10 +272,6 @@ 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 {
@@ -359,10 +351,6 @@ 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,14 +109,3 @@ 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}
}
+3 -11
View File
@@ -31,6 +31,7 @@ import (
"aego/script/lexer"
"aego/script/runtime"
"aego/script/source"
"aego/script/vm"
"aego/script/workspace"
)
@@ -42,6 +43,7 @@ const (
ScriptsDir = "scripts"
DefaultFrames = 120
MaxFrames = 3600
Budget = 2_000_000
)
type Diagnostic struct {
@@ -282,7 +284,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: playmods.SandboxVM()})
rt := runtime.New(runtime.Options{Bindings: p.reg, Schema: p.types.Schema(), Log: lg, VM: vm.Options{Budget: Budget}})
defer rt.Close()
if err := rt.Link(mods); err != nil {
res.Diagnostics = append(res.Diagnostics, failed(err)...)
@@ -493,13 +495,3 @@ 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
@@ -1,86 +0,0 @@
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")
}
-40
View File
@@ -1,40 +0,0 @@
//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 {}
}
+1 -3
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] [--demo] generate the static site with the playground
build [--out site] 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,7 +64,6 @@ 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
@@ -74,7 +73,6 @@ 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]|web default host
--target host|linux|windows|darwin[/amd64|/arm64] 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>
+3 -22
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]|web --development|--release --out dir
aego export [dir] --target host|linux|windows|darwin[/amd64|/arm64] --development|--release --out dir
aego export --check --release pre-export checks only
aego export --json report as JSON
```
@@ -77,32 +77,13 @@ 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 | nothing beyond Go: the build is GOOS=js GOARCH=wasm without cgo |
| any → web | Stage 9.5c |
`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 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.
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.
## Editor app on macOS
-6
View File
@@ -162,9 +162,3 @@ 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,7 +43,3 @@ 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.
+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")
}
File diff suppressed because it is too large Load Diff
+10 -15
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 {
@@ -138,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
}
@@ -160,7 +151,11 @@ func (s *Service) Table(id ids.AssetID) (*Table, error) {
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
}
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
+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)

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