Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f5584d062 | ||
|
|
6ce72aa1ec | ||
|
|
9ee2b0b05c | ||
|
|
99dacd7a75 | ||
|
|
5447383f2b | ||
|
|
6745d027fb | ||
|
|
e7dd01371e | ||
|
|
68802195b6 | ||
|
|
7cba364aed | ||
|
|
35e9849cae | ||
|
|
f21d455d68 | ||
|
|
d1a16d8208 | ||
|
|
2a1826f876 | ||
|
|
3af2d5d977 | ||
|
|
b61a1656ad | ||
|
|
0fbf22a090 | ||
|
|
5e763aa73c | ||
|
|
6093ca9d8b |
+6
-100
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,85 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func mustFind(t *testing.T, name string) *Command {
|
||||
t.Helper()
|
||||
c, ok := Find(name)
|
||||
if !ok {
|
||||
t.Fatalf("no command %s", name)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func TestParseArgs(t *testing.T) {
|
||||
set := mustFind(t, "scene.set")
|
||||
a, f, err := parseArgs(set, []string{"s.aescene", "--json", "Player", "--dry-run", "Body2D.mass", "-3"})
|
||||
if err != nil || !f.json || a.Str("value") != "-3" || a.Str("node") != "Player" || !a.Bool("dry-run") {
|
||||
t.Fatalf("flags after positionals: %v %+v %v", a, f, err)
|
||||
}
|
||||
add := mustFind(t, "scene.add-node")
|
||||
for _, toks := range [][]string{{"s", "N", "--index=1"}, {"s", "N", "--index", "1"}} {
|
||||
a, _, err := parseArgs(add, toks)
|
||||
if n, _ := a.Int("index", -1); err != nil || n != 1 {
|
||||
t.Fatalf("%v: %v %v", toks, a, err)
|
||||
}
|
||||
}
|
||||
a, _, err = parseArgs(set, []string{"s", "--", "Player", "Body2D.mass", "--odd"})
|
||||
if err != nil || a.Str("value") != "--odd" {
|
||||
t.Fatalf("after --: %v %v", a, err)
|
||||
}
|
||||
if _, _, err := parseArgs(set, []string{"s", "--nope"}); err == nil {
|
||||
t.Fatal("an unknown flag was accepted")
|
||||
}
|
||||
if _, _, err := parseArgs(set, []string{"s", "a", "b", "c", "d"}); err == nil {
|
||||
t.Fatal("an extra positional was accepted")
|
||||
}
|
||||
var out, errw bytes.Buffer
|
||||
if code := Main(nil, []string{"scene", "set", "s", "--nope"}, &out, &errw, Options{}); code != 2 {
|
||||
t.Fatalf("unknown flag exit %d", code)
|
||||
}
|
||||
if code := Main(nil, []string{"scene", "set", "a", "b", "c", "d", "e"}, &out, &errw, Options{}); code != 2 {
|
||||
t.Fatalf("extra positional exit %d", code)
|
||||
}
|
||||
if code := Main(nil, []string{"scene", "help"}, &out, &errw, Options{}); code != 0 {
|
||||
t.Fatalf("help exit %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemasAndToolNames(t *testing.T) {
|
||||
seen := map[string]bool{}
|
||||
for _, c := range Commands() {
|
||||
if seen[c.Tool()] {
|
||||
t.Fatalf("tool name %s is used twice", c.Tool())
|
||||
}
|
||||
seen[c.Tool()] = true
|
||||
b, err := json.Marshal(c.Schema())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var sch struct {
|
||||
Type string `json:"type"`
|
||||
Properties map[string]map[string]any `json:"properties"`
|
||||
Required []string `json:"required"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &sch); err != nil || sch.Type != "object" {
|
||||
t.Fatalf("%s: %s", c.Name, b)
|
||||
}
|
||||
var want []string
|
||||
for _, x := range c.AllArgs() {
|
||||
if x.Required {
|
||||
want = append(want, x.Name)
|
||||
}
|
||||
if _, ok := sch.Properties[x.Name]; !ok {
|
||||
t.Fatalf("%s: no property %s", c.Name, x.Name)
|
||||
}
|
||||
}
|
||||
if len(want) != len(sch.Required) {
|
||||
t.Fatalf("%s: required %v, want %v", c.Name, sch.Required, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
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
@@ -1,66 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"aego/asset"
|
||||
"aego/project/refs"
|
||||
"aego/scene/mutate"
|
||||
)
|
||||
|
||||
func projectCommands() []*Command {
|
||||
return []*Command{
|
||||
{Name: "project.validate", Doc: "missing assets and nodes, import errors, prefabs and scenes that do not load", Run: (*Session).projectValidate},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) projectValidate(Args) (*Output, error) {
|
||||
out := &Output{}
|
||||
add := func(err bool, node, msg string) {
|
||||
out.Problems = append(out.Problems, mutate.Problem{Error: err, Node: node, Message: msg})
|
||||
}
|
||||
for _, m := range refs.MissingAssets(s.Project, s.Pipeline) {
|
||||
add(true, "", "missing asset "+m.Label())
|
||||
}
|
||||
for _, m := range refs.MissingNodes(s.Pipeline) {
|
||||
add(true, m.Scene+" "+m.Path, m.Owner+"."+m.Field+" references a node that is not in the scene")
|
||||
}
|
||||
if s.scriptErr != nil {
|
||||
add(true, "", "script types: "+s.scriptErr.Error())
|
||||
}
|
||||
for _, e := range s.libErrs {
|
||||
add(true, "", e.Error())
|
||||
}
|
||||
for _, x := range s.Pipeline.Assets() {
|
||||
if x.Error != "" {
|
||||
add(true, x.Path, x.Error)
|
||||
continue
|
||||
}
|
||||
if x.Kind != asset.ArtScene && x.Kind != asset.ArtPrefab {
|
||||
continue
|
||||
}
|
||||
src, err := s.read(x.Path)
|
||||
if err != nil {
|
||||
add(true, x.Path, err.Error())
|
||||
continue
|
||||
}
|
||||
doc, err := mutate.Load(src, x.Path, s.env())
|
||||
if err != nil {
|
||||
add(true, x.Path, err.Error())
|
||||
continue
|
||||
}
|
||||
for _, p := range doc.Problems {
|
||||
add(p.Error, x.Path+" "+p.Node, p.Message)
|
||||
}
|
||||
}
|
||||
errs, warns := 0, 0
|
||||
for _, p := range out.Problems {
|
||||
if p.Error {
|
||||
errs++
|
||||
} else {
|
||||
warns++
|
||||
}
|
||||
}
|
||||
out.Notes = []string{"project: " + strconv.Itoa(errs) + " error(s), " + strconv.Itoa(warns) + " warning(s)"}
|
||||
return out, nil
|
||||
}
|
||||
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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -21,8 +21,6 @@ func (j *job) pack() error {
|
||||
return j.packLinux()
|
||||
case "darwin":
|
||||
return j.packDarwin()
|
||||
case "js":
|
||||
return j.packWeb()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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"})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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), "<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)
|
||||
}
|
||||
}
|
||||
+2
-14
@@ -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
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package run
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"aego/app/agent"
|
||||
"aego/core/log"
|
||||
"aego/core/rewind"
|
||||
"aego/project"
|
||||
"aego/sdk"
|
||||
)
|
||||
|
||||
const maxRunLog = 200
|
||||
|
||||
func AgentRunner(mods ...sdk.Module) agent.Runner {
|
||||
return func(p *project.Project, r agent.RunRequest) (agent.RunReport, error) {
|
||||
var buf bytes.Buffer
|
||||
lg := log.New(log.NewTextSink(&buf))
|
||||
lg.SetLevel(log.Warn)
|
||||
frames, seed := r.Frames, r.Seed
|
||||
if frames <= 0 {
|
||||
frames = 120
|
||||
}
|
||||
if seed == 0 {
|
||||
seed = 1
|
||||
}
|
||||
a, err := Open(p, Config{Headless: true, FrameTime: defaultDetStep, Seed: seed, ScenePath: r.Scene, Logger: lg, NoProfile: true}, mods...)
|
||||
if err != nil {
|
||||
return agent.RunReport{Log: logLines(buf.String())}, err
|
||||
}
|
||||
rep := agent.RunReport{}
|
||||
driver := a.Driver()
|
||||
for rep.Frames < frames {
|
||||
if !a.Engine.Step(driver) {
|
||||
rep.Error = "the game stopped at frame " + strconv.Itoa(rep.Frames+1)
|
||||
if err := a.Engine.Err(); err != nil {
|
||||
rep.Error += ": " + err.Error()
|
||||
}
|
||||
break
|
||||
}
|
||||
rep.Frames++
|
||||
}
|
||||
sum, _, _ := WorldHash(a.Engine.MainWorld(), rewind.NewHasher())
|
||||
rep.Hash = hex.EncodeToString(sum[:])
|
||||
a.Close()
|
||||
rep.Log = logLines(buf.String())
|
||||
return rep, nil
|
||||
}
|
||||
}
|
||||
|
||||
func logLines(s string) []string {
|
||||
var out []string
|
||||
for _, l := range strings.Split(strings.TrimSpace(s), "\n") {
|
||||
if l = strings.TrimSpace(l); l != "" {
|
||||
out = append(out, l)
|
||||
}
|
||||
}
|
||||
if len(out) > maxRunLog {
|
||||
out = append(out[:maxRunLog], "…")
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -78,7 +78,7 @@ func WorldHash(w *engine.World, h *rewind.Hasher) (sum [32]byte, covered, skippe
|
||||
}
|
||||
|
||||
func determinismPass(p *project.Project, o DeterminismOptions, mods []sdk.Module) ([]checkpoint, []string, []string, error) {
|
||||
a, err := Open(p, Config{Headless: true, FrameTime: o.FrameTime, Seed: o.Seed, ScenePath: o.ScenePath, Logger: log.Nop(), NoProfile: true}, mods...)
|
||||
a, err := Open(p, Config{Headless: true, FrameTime: o.FrameTime, Seed: o.Seed, ScenePath: o.ScenePath, Logger: log.Nop()}, mods...)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ type Config struct {
|
||||
Profile string
|
||||
Logger *log.Logger
|
||||
Seed uint64
|
||||
NoProfile bool
|
||||
}
|
||||
|
||||
type App struct {
|
||||
@@ -139,7 +138,6 @@ func Open(p *project.Project, cfg Config, mods ...sdk.Module) (*App, error) {
|
||||
Debug: cfg.Debug,
|
||||
FailOnScriptErrors: true,
|
||||
Profile: cfg.Profile,
|
||||
NoProfile: cfg.NoProfile,
|
||||
}, sdk.AdaptAll(mods)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
package run
|
||||
|
||||
import (
|
||||
"aego/app/agent"
|
||||
"aego/app/agent/mcp"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -42,10 +39,6 @@ func Tool(p *project.Project, args []string, stdout, stderr io.Writer, mods ...s
|
||||
return toolTest(p, args[1:], stdout, stderr, mods)
|
||||
case "docs":
|
||||
return docstool.Tool(p, args[1:], stdout, stderr, sdk.AdaptAll(mods)...)
|
||||
case "agent":
|
||||
return agent.Main(p, args[1:], stdout, stderr, agent.Options{Mods: mods, Run: AgentRunner(mods...)})
|
||||
case "mcp":
|
||||
return mcp.Main(p, args[1:], os.Stdin, stdout, stderr, agent.Options{Mods: mods, Run: AgentRunner(mods...)})
|
||||
}
|
||||
fmt.Fprintf(stderr, "unknown tool %q\n", args[0])
|
||||
return 2
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+1
-3
@@ -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
@@ -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>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"aego/app/agent"
|
||||
"aego/app/agent/mcp"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"aego/app/bootstrap"
|
||||
@@ -53,8 +50,6 @@ func dispatch(args []string) int {
|
||||
return cmdCheck(args[1:])
|
||||
case "icons":
|
||||
return cmdIcons(args[1:])
|
||||
case "mcp":
|
||||
return cmdMcp(args[1:])
|
||||
case "docs":
|
||||
return cmdDocs(args[1:])
|
||||
case "test":
|
||||
@@ -65,9 +60,6 @@ func dispatch(args []string) int {
|
||||
usage()
|
||||
return 0
|
||||
}
|
||||
if slices.Contains(agent.Groups(), args[0]) {
|
||||
return cmdAgent(args)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "unknown command %q\n", args[0])
|
||||
usage()
|
||||
return 2
|
||||
@@ -91,8 +83,6 @@ func usage() {
|
||||
run *_test.aes script tests headless, or run the game twice and compare state hashes
|
||||
dump script <pkg|Type> [--func name] disassemble a script package with source lines
|
||||
docs export|check|build|serve documentation registry, site and playground
|
||||
mcp [--project dir] [--watch] serve the project to an AI agent over MCP (stdio)
|
||||
scene|prefab|data|refs|asset|schema|project <verb> ... edit project files without the editor, see aego scene help
|
||||
`)
|
||||
}
|
||||
|
||||
@@ -107,40 +97,6 @@ func openProject(dir string) (*project.Project, error) {
|
||||
return project.Load(root)
|
||||
}
|
||||
|
||||
func projectFlag(args []string) string {
|
||||
for i, a := range args {
|
||||
switch {
|
||||
case a == "--project" && i+1 < len(args):
|
||||
return args[i+1]
|
||||
case strings.HasPrefix(a, "--project="):
|
||||
return strings.TrimPrefix(a, "--project=")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func cmdAgent(args []string) int {
|
||||
p, err := openProject(projectFlag(args))
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if bootstrap.NeedsNative(p) {
|
||||
return scriptTool(p, append([]string{"agent"}, args...)...)
|
||||
}
|
||||
return agent.Main(p, args, os.Stdout, os.Stderr, agent.Options{Run: run.AgentRunner()})
|
||||
}
|
||||
|
||||
func cmdMcp(args []string) int {
|
||||
p, err := openProject(projectFlag(args))
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if bootstrap.NeedsNative(p) {
|
||||
return scriptTool(p, append([]string{"mcp"}, args...)...)
|
||||
}
|
||||
return mcp.Main(p, args, os.Stdin, os.Stdout, os.Stderr, agent.Options{Run: run.AgentRunner()})
|
||||
}
|
||||
|
||||
func firstArg(args []string) (string, []string) {
|
||||
if len(args) > 0 && len(args[0]) > 0 && args[0][0] != '-' {
|
||||
return args[0], args[1:]
|
||||
|
||||
@@ -23,7 +23,6 @@ func scriptTool(p *project.Project, args ...string) int {
|
||||
}
|
||||
cmd := exec.Command(binary, append([]string{"--tool"}, args...)...)
|
||||
cmd.Dir = p.Root
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return exitCode(cmd.Run())
|
||||
|
||||
+3
-22
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user