Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91052a54db | ||
|
|
7f4d87ef2c |
+16
-6
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,8 @@ func (j *job) pack() error {
|
||||
return j.packLinux()
|
||||
case "darwin":
|
||||
return j.packDarwin()
|
||||
case "js":
|
||||
return j.packWeb()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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"})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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), "<Game &") || 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
@@ -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
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
+3
-1
@@ -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
@@ -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>
|
||||
|
||||
+22
-3
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"aego/editor/core/history/commands"
|
||||
"aego/format/aetext"
|
||||
"aego/scene"
|
||||
"aego/scene/mutate"
|
||||
"aego/scene/sceneio"
|
||||
)
|
||||
|
||||
@@ -237,12 +238,24 @@ 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)
|
||||
cmd := &commands.Create{Parent: parent, Index: index, Fragment: data, Name: "Paste"}
|
||||
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...)
|
||||
if err := d.History.Exec(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.Select(append([]ids.NodeID(nil), cmd.Roots()...)...)
|
||||
ctx.Select(cmd.Created()...)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -267,17 +280,13 @@ func Duplicate(ctx *context.Editor) error {
|
||||
tx.Abort()
|
||||
return err
|
||||
}
|
||||
c := &commands.Create{
|
||||
Parent: s.ID(s.Parent(h)),
|
||||
Index: commands.ChildIndex(s, h) + 1,
|
||||
Fragment: data,
|
||||
Name: s.Name(h),
|
||||
}
|
||||
c := commands.NewMutation("Create "+s.Name(h), commands.EnvFor(s),
|
||||
commands.Insert(s.ID(s.Parent(h)), commands.ChildIndex(s, h)+1, string(data)))
|
||||
if err := tx.Exec(c); err != nil {
|
||||
tx.Abort()
|
||||
return err
|
||||
}
|
||||
created = append(created, c.Roots()...)
|
||||
created = append(created, c.Created()...)
|
||||
}
|
||||
tx.Commit()
|
||||
ctx.Select(created...)
|
||||
@@ -294,18 +303,13 @@ func Delete(ctx *context.Editor) error {
|
||||
if len(tops) == 0 {
|
||||
return nil
|
||||
}
|
||||
targets := make([]ids.NodeID, len(tops))
|
||||
ops := make([]mutate.Op, len(tops))
|
||||
for i, h := range tops {
|
||||
targets[i] = s.ID(h)
|
||||
ops[i] = mutate.RemoveNode(commands.Ref(s.ID(h)))
|
||||
}
|
||||
tx := d.History.Begin("Delete")
|
||||
for _, id := range targets {
|
||||
if err := tx.Exec(&commands.Delete{Node: id}); err != nil {
|
||||
tx.Abort()
|
||||
return err
|
||||
}
|
||||
if err := d.History.Exec(commands.NewMutation("Delete", commands.EnvFor(s), ops...)); err != nil {
|
||||
return err
|
||||
}
|
||||
tx.Commit()
|
||||
ctx.Select()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -113,8 +113,7 @@ func (e Env) encodeComponent(td *schema.TypeDescriptor, values []commands.FieldV
|
||||
sc := e.scratch()
|
||||
h := sc.Create(sc.Root(), "component")
|
||||
sc.Flush()
|
||||
add := &commands.AddComponent{Node: sc.ID(h), Type: td.ID, Name: td.FullName, Values: values}
|
||||
if err := add.Do(target{sc}); err != nil {
|
||||
if err := putComponent(sc, h, td, values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sceneio.SaveSubtree(sc, h, true), nil
|
||||
@@ -122,18 +121,14 @@ func (e Env) encodeComponent(td *schema.TypeDescriptor, values []commands.FieldV
|
||||
|
||||
func (e Env) decodeComponent(data []byte) (*schema.TypeDescriptor, []commands.FieldValue, error) {
|
||||
sc := e.scratch()
|
||||
c := &commands.Create{Index: -1, Fragment: data}
|
||||
if err := c.Do(target{sc}); err != nil {
|
||||
root, err := loadScratch(sc, data)
|
||||
if 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(roots[0]), func(t schema.TypeID) {
|
||||
sc.ComponentsOf(sc.Handle(root), func(t schema.TypeID) {
|
||||
if td != nil || (hasUnknown && t == unknown.ID) {
|
||||
return
|
||||
}
|
||||
@@ -142,7 +137,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, roots[0], td)
|
||||
values, err := componentValues(sc, root, td)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -189,13 +184,6 @@ 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 {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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})
|
||||
}
|
||||
@@ -22,6 +22,7 @@ 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) {
|
||||
@@ -31,7 +32,7 @@ func (e Env) pasteInto(tx *history.Transaction, d *document.SceneDocument, nodes
|
||||
if !s.Accepts(h, td.ID) {
|
||||
continue
|
||||
}
|
||||
if err := tx.Exec(&commands.AddComponent{Node: n, Type: td.ID, Name: td.FullName, Values: values, OnAdd: e.onAdd(td)}); err != nil {
|
||||
if err := tx.Exec(commands.NewMutation("Add "+td.FullName, env, commands.AddComp(s, n, td, values))); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
@@ -94,8 +95,7 @@ func (e Env) CopyComponents(s *scene.Scene, node ids.NodeID, types []schema.Type
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
add := &commands.AddComponent{Node: sc.ID(h), Type: td.ID, Name: td.FullName, Values: values}
|
||||
if err := add.Do(target{sc}); err != nil {
|
||||
if err := putComponent(sc, h, td, values); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -118,11 +118,10 @@ func (e Env) readComponents() ([]compData, bool) {
|
||||
data = []byte(strings.TrimPrefix(t, componentsText))
|
||||
}
|
||||
sc := e.scratch()
|
||||
cr := &commands.Create{Index: -1, Fragment: data}
|
||||
if err := cr.Do(target{sc}); err != nil || len(cr.Roots()) == 0 {
|
||||
root, err := loadScratch(sc, data)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
root := cr.Roots()[0]
|
||||
reg := sc.Registry()
|
||||
unknown, hasUnknown := reg.ByName("engine.Unknown")
|
||||
var out []compData
|
||||
@@ -186,14 +185,11 @@ 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()
|
||||
cr := &commands.Create{Index: -1, Fragment: data}
|
||||
if err := cr.Do(target{sc}); err != nil {
|
||||
root, err := loadScratch(sc, data)
|
||||
if err != nil {
|
||||
return "", schema.PropertyBag{}, err
|
||||
}
|
||||
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]))
|
||||
scs, ok := scriptcomp.ScriptsOf(sc, sc.Handle(root))
|
||||
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
File diff suppressed because it is too large
Load Diff
@@ -4,9 +4,11 @@ import (
|
||||
"testing"
|
||||
|
||||
"aego/core/errs"
|
||||
"aego/core/ids"
|
||||
"aego/core/mathx"
|
||||
"aego/editor/core/history"
|
||||
"aego/scene"
|
||||
"aego/scene/mutate"
|
||||
"aego/scene/sceneio"
|
||||
"aego/schema"
|
||||
)
|
||||
@@ -45,6 +47,14 @@ func newScene(t *testing.T) *scene.Scene {
|
||||
return scene.New(types, nil, scene.Options{})
|
||||
}
|
||||
|
||||
func del(s *scene.Scene, id ids.NodeID) *Mutation {
|
||||
return NewMutation("Delete", EnvFor(s), mutate.RemoveNode(Ref(id)))
|
||||
}
|
||||
|
||||
func reparent(s *scene.Scene, n, p scene.NodeHandle) *Mutation {
|
||||
return NewMutation("Reparent", EnvFor(s), Move(s.ID(n), s.ID(p), -1, true))
|
||||
}
|
||||
|
||||
func TestDeleteUndoRestoresIDsAndRefs(t *testing.T) {
|
||||
s := newScene(t)
|
||||
first := s.Create(s.Root(), "First")
|
||||
@@ -57,7 +67,7 @@ func TestDeleteUndoRestoresIDsAndRefs(t *testing.T) {
|
||||
aID, childID := s.ID(a), s.ID(child)
|
||||
|
||||
h := history.New(target{s})
|
||||
if err := h.Exec(&Delete{Node: aID}); err != nil {
|
||||
if err := h.Exec(del(s, aID)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !s.Handle(aID).IsZero() || !s.Handle(childID).IsZero() {
|
||||
@@ -100,7 +110,7 @@ func TestReparentPreservesWorld(t *testing.T) {
|
||||
before := s.World(n)
|
||||
|
||||
h := history.New(target{s})
|
||||
if err := h.Exec(&Reparent{Node: s.ID(n), NewParent: s.ID(p), NewIndex: -1, KeepWorld: true}); err != nil {
|
||||
if err := h.Exec(reparent(s, n, p)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s.Parent(n) != p {
|
||||
@@ -126,7 +136,7 @@ func TestReparentUnderZeroScaleFails(t *testing.T) {
|
||||
s.SetLocal(p, scene.Transform2D{Scale: mathx.Vec2{X: 0, Y: 1e-9}})
|
||||
s.Flush()
|
||||
h := history.New(target{s})
|
||||
err := h.Exec(&Reparent{Node: s.ID(n), NewParent: s.ID(p), NewIndex: -1, KeepWorld: true})
|
||||
err := h.Exec(reparent(s, n, p))
|
||||
if !errs.Has(err, errs.SceneBadReparent) {
|
||||
t.Fatalf("err = %v", err)
|
||||
}
|
||||
@@ -170,16 +180,17 @@ func TestCreateAndComponentRoundTrip(t *testing.T) {
|
||||
|
||||
s := newScene(t)
|
||||
h := history.New(target{s})
|
||||
c := &Create{Index: -1, Fragment: fragment, Name: "Enemy"}
|
||||
c := NewMutation("Create Enemy", EnvFor(s), Insert(ids.NodeID{}, -1, string(fragment)))
|
||||
if err := h.Exec(c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(c.Roots()) != 1 {
|
||||
t.Fatalf("roots = %d", len(c.Roots()))
|
||||
roots := c.Created()
|
||||
if len(roots) != 1 {
|
||||
t.Fatalf("roots = %d", len(roots))
|
||||
}
|
||||
n := s.Handle(c.Roots()[0])
|
||||
typeID := scene.Store[Health](s).Type()
|
||||
if err := h.Exec(&RemoveComponent{Node: s.ID(n), Type: typeID, Name: "Health"}); err != nil {
|
||||
n := s.Handle(roots[0])
|
||||
td, _ := s.Registry().ByID(scene.Store[Health](s).Type())
|
||||
if err := h.Exec(NewMutation("Remove Health", EnvFor(s), RemoveComp(s.ID(n), td))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if scene.Has[Health](s, n) {
|
||||
@@ -190,11 +201,11 @@ func TestCreateAndComponentRoundTrip(t *testing.T) {
|
||||
t.Fatal("component data not restored")
|
||||
}
|
||||
h.Undo()
|
||||
if !s.Handle(c.Roots()[0]).IsZero() {
|
||||
if !s.Handle(roots[0]).IsZero() {
|
||||
t.Fatal("create not undone")
|
||||
}
|
||||
h.Redo()
|
||||
if s.Handle(c.Roots()[0]).IsZero() {
|
||||
if s.Handle(roots[0]).IsZero() {
|
||||
t.Fatal("redo must recreate the same node id")
|
||||
}
|
||||
}
|
||||
|
||||
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
@@ -3,61 +3,10 @@ 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)
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestReparent3DPreservesWorld(t *testing.T) {
|
||||
s.Flush()
|
||||
before := s.World4(n)
|
||||
h := history.New(target{s})
|
||||
if err := h.Exec(&Reparent{Node: s.ID(n), NewParent: s.ID(p), NewIndex: -1, KeepWorld: true}); err != nil {
|
||||
if err := h.Exec(reparent(s, n, p)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s.Parent(n) != p {
|
||||
@@ -39,7 +39,7 @@ func TestReparent3DUnderTwoDFails(t *testing.T) {
|
||||
n := s.Create3D(s.Root(), "N")
|
||||
s.Flush()
|
||||
h := history.New(target{s})
|
||||
err := h.Exec(&Reparent{Node: s.ID(n), NewParent: s.ID(flat), NewIndex: -1, KeepWorld: true})
|
||||
err := h.Exec(reparent(s, n, flat))
|
||||
if !errs.Has(err, errs.SceneSpaceMismatch) {
|
||||
t.Fatalf("err = %v", err)
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func TestReparent2DIntoThreeDKeepsWorld(t *testing.T) {
|
||||
s.Flush()
|
||||
before := s.World4(n)
|
||||
h := history.New(target{s})
|
||||
if err := h.Exec(&Reparent{Node: s.ID(n), NewParent: s.ID(p), NewIndex: -1, KeepWorld: true}); err != nil {
|
||||
if err := h.Exec(reparent(s, n, p)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !s.Under3D(n) || !s.World4(n).NearlyEqual(before, 1e-4) {
|
||||
@@ -97,7 +97,7 @@ func TestDeleteUndoKeepsSpace(t *testing.T) {
|
||||
s.Flush()
|
||||
id := s.ID(n)
|
||||
h := history.New(target{s})
|
||||
if err := h.Exec(&Delete{Node: id}); err != nil {
|
||||
if err := h.Exec(del(s, id)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h.Undo()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -480,6 +480,7 @@ func (c *Controller) CopyToEdit(nodes []ids.NodeID, t schema.TypeID) error {
|
||||
if !ok {
|
||||
return errs.New(errs.SchemaUnknownType, "component type is not registered")
|
||||
}
|
||||
env := commands.EnvFor(es)
|
||||
tx := real.History.Begin("Copy " + td.FullName + " from Play")
|
||||
for _, n := range nodes {
|
||||
if _, has := es.GetByType(es.Handle(n), t); !has {
|
||||
@@ -492,7 +493,7 @@ func (c *Controller) CopyToEdit(nodes []ids.NodeID, t schema.TypeID) error {
|
||||
}
|
||||
values = append(values, commands.FieldValue{Field: td.Fields[i].ID, Value: v})
|
||||
}
|
||||
if err := tx.Exec(&commands.AddComponent{Node: n, Type: t, Name: td.FullName, Values: values}); err != nil {
|
||||
if err := tx.Exec(commands.NewMutation("Add "+td.FullName, env, commands.AddComp(es, n, td, values))); err != nil {
|
||||
tx.Abort()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package playdiff
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"aego/core/curve"
|
||||
"aego/core/ids"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"aego/editor/core/history/commands"
|
||||
"aego/editor/core/props"
|
||||
"aego/scene"
|
||||
"aego/scene/mutate"
|
||||
"aego/scene/scriptcomp"
|
||||
"aego/schema"
|
||||
)
|
||||
@@ -96,6 +98,16 @@ func Touched(h *history.History) map[Key]bool {
|
||||
switch c := c.(type) {
|
||||
case *commands.SetProperty:
|
||||
out[Key{Node: c.Node, Type: c.Type, Field: c.Field}] = true
|
||||
case *commands.Mutation:
|
||||
for _, ch := range c.Changes() {
|
||||
switch {
|
||||
case ch.Op != mutate.KindSet && ch.Op != mutate.KindReset:
|
||||
case ch.Type != 0:
|
||||
out[Key{Node: ch.NodeID, Type: ch.Type, Field: ch.Field}] = true
|
||||
case strings.HasPrefix(ch.Path, "transform."):
|
||||
out[Key{Node: ch.NodeID}] = true
|
||||
}
|
||||
}
|
||||
case *commands.SetTransform:
|
||||
out[Key{Node: c.Node}] = true
|
||||
case *commands.SetTransform3D:
|
||||
|
||||
@@ -45,7 +45,7 @@ func pair(t *testing.T) (*document.SceneDocument, *document.SceneDocument, ids.N
|
||||
}
|
||||
s.Flush()
|
||||
d := m.New(rel, s)
|
||||
if err := d.History.Exec(&commands.AddComponent{Node: id, Type: td.ID, Name: td.FullName}); err != nil {
|
||||
if err := d.History.Exec(commands.NewMutation("Add "+td.FullName, commands.EnvFor(s), commands.AddComp(s, id, td, nil))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
docs[i] = d
|
||||
@@ -108,3 +108,14 @@ func TestApplyIsOneStep(t *testing.T) {
|
||||
t.Fatal("undo must restore the edit scene")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTouchedSeesInspectorEdits(t *testing.T) {
|
||||
_, play, id, td := pair(t)
|
||||
fd, _ := td.Field("tint")
|
||||
if err := play.Props.Set(nil, []ids.NodeID{id}, td.ID, fd.ID, schema.Color(mathx.Color{G: 1, A: 1})); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !playdiff.Touched(play.History)[playdiff.Key{Node: id, Type: td.ID, Field: fd.ID}] {
|
||||
t.Fatal("a field set through the Inspector is not touched")
|
||||
}
|
||||
}
|
||||
|
||||
+27
-112
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ import (
|
||||
"aego/editor/core/document"
|
||||
"aego/editor/core/history/commands"
|
||||
"aego/scene"
|
||||
"aego/scene/mutate"
|
||||
"aego/scene/prefab"
|
||||
"aego/scene/sceneio"
|
||||
)
|
||||
@@ -217,7 +218,7 @@ func (s *Service) CreateFrom(d *document.SceneDocument, nodes []ids.NodeID, dir
|
||||
tx.Abort()
|
||||
return err
|
||||
}
|
||||
if err := tx.Exec(&commands.Replace{Node: sc.ID(h), Before: before, After: after, Name: "Create Prefab"}); err != nil {
|
||||
if err := tx.Exec(commands.NewMutation("Create Prefab", s.env(sc), mutate.Replace(commands.Ref(sc.ID(h)), string(after)))); err != nil {
|
||||
tx.Abort()
|
||||
return err
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user