Compare commits
53
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9abf5c75ab | ||
|
|
96fef1f6a8 | ||
|
|
6230d30620 | ||
|
|
91ea202e74 | ||
|
|
6128c86685 | ||
|
|
2be6ce1d64 | ||
|
|
2e664ae508 | ||
|
|
aab6e0b0c9 | ||
|
|
9ba3e1631f | ||
|
|
4068e00375 | ||
|
|
914cf39150 | ||
|
|
dede4d8af3 | ||
|
|
b6dd46f523 | ||
|
|
9d32052624 | ||
|
|
e636801578 | ||
|
|
01fdeef7a8 | ||
|
|
71354f6a60 | ||
|
|
8ae6832503 | ||
|
|
183d75ed50 | ||
|
|
19edcbf9b2 | ||
|
|
8ed77a7638 | ||
|
|
ca3c9bf2e7 | ||
|
|
db59f117a4 | ||
|
|
0d2c20da26 | ||
|
|
dd41ff15a5 | ||
|
|
599e09a35d | ||
|
|
60614938be | ||
|
|
a60ca47d31 | ||
|
|
00ae2418a0 | ||
|
|
8fc9b4c287 | ||
|
|
b29934b51d | ||
|
|
c475b42e5d | ||
|
|
b103816400 | ||
|
|
487170dd9c | ||
|
|
57579356d6 | ||
|
|
d5377410e4 | ||
|
|
f6c83a6e0c | ||
|
|
8d7712d671 | ||
|
|
63980faccb | ||
|
|
0041e7e639 | ||
|
|
e334be569f | ||
|
|
b488958444 | ||
|
|
5875a65193 | ||
|
|
6ab66cb8ae | ||
|
|
f8fe54e9c5 | ||
|
|
e4326e7e7c | ||
|
|
6a22396467 | ||
|
|
21e03128a8 | ||
|
|
e5b1964f3e | ||
|
|
cffa66514b | ||
|
|
2744cff481 | ||
|
|
91052a54db | ||
|
|
7f4d87ef2c |
@@ -5,3 +5,4 @@ site
|
||||
exports
|
||||
**/.aego
|
||||
*.test
|
||||
.claude
|
||||
|
||||
@@ -3,6 +3,7 @@ name: ci
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
tags: ["v*"]
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
@@ -111,4 +112,39 @@ jobs:
|
||||
- name: example project
|
||||
run: |
|
||||
/tmp/aego check examples/native-rotator
|
||||
/tmp/aego run examples/native-rotator --headless --frames 60
|
||||
/tmp/aego run examples/native-rotator --headless --frames 60
|
||||
|
||||
release:
|
||||
needs: [checks, build]
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
CGO_ENABLED: "1"
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: linux packages
|
||||
run: sudo apt-get update && sudo apt-get install -y $LINUX_PKGS
|
||||
- uses: actions/cache@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: .cache/native
|
||||
key: native-linux_amd64-${{ hashFiles('third_party/sdl/version.txt', 'tools/bootstrap-native.sh') }}
|
||||
- run: bash tools/bootstrap-native.sh
|
||||
- name: windows tools
|
||||
run: sudo apt-get install -y mingw-w64 nsis zip
|
||||
- name: publish
|
||||
env:
|
||||
REQUIRE_WINDOWS: "1"
|
||||
GITEA_RESOLVE: git.akarpov.ru:443:79.142.92.250
|
||||
run: |
|
||||
case "$GITHUB_REF" in
|
||||
refs/tags/v*) sh tools/release/release.sh "$GITHUB_REF_NAME" ;;
|
||||
refs/heads/master) sh tools/release/release.sh nightly ;;
|
||||
esac
|
||||
|
||||
+107
-4
File diff suppressed because one or more lines are too long
@@ -38,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 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 try-size shots shots-check
|
||||
|
||||
XVFB := $(shell command -v xvfb-run 2>/dev/null)
|
||||
GL_OVERRIDE = MESA_GL_VERSION_OVERRIDE=3.3 MESA_GLSL_VERSION_OVERRIDE=330
|
||||
@@ -98,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:
|
||||
@@ -211,7 +212,7 @@ site: cli
|
||||
@$(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
|
||||
@if GOOS=js GOARCH=wasm go list -deps ./cmd/aego-playground | grep -E '^aego/(platform/sdl|gpu/gl33|asset/pipeline|asset/importers)'; then echo "playground graph pulls a native or pipeline package"; exit 1; fi
|
||||
$(MAKE) site
|
||||
|
||||
site-headless:
|
||||
@@ -281,7 +282,29 @@ fuzz:
|
||||
go test -run '^$$' -fuzz '^FuzzDecodeVerify$$' -fuzztime $(FUZZTIME) ./script/verify
|
||||
go test -run '^$$' -fuzz '^FuzzDecodeVerifyV3$$' -fuzztime $(FUZZTIME) ./script/verify
|
||||
|
||||
ci: build vet lint depcheck test test-nonative test-editor test-runtime test-wasm runtime smoke fuzz golden test-gl cli playground web-smoke site-check example example-platformer check-all export-smoke $(MODULE_CI) bench-allocs
|
||||
ci: build vet lint depcheck test test-nonative test-editor test-runtime test-wasm runtime smoke fuzz golden shots-check test-gl cli playground web-smoke site-check example example-platformer check-all export-smoke $(MODULE_CI) bench-allocs
|
||||
|
||||
|
||||
# Documentation screenshots of the real editor on the platformer example, under a clean user profile.
|
||||
SHOTS_TMP = $(CURDIR)/.cache/shots
|
||||
ifneq ($(XVFB),)
|
||||
SHOTS_ENV = $(XVFB) -a -s "-screen 0 1600x900x24" env SDL_VIDEODRIVER=x11 LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe $(GL_OVERRIDE)
|
||||
else
|
||||
SHOTS_ENV = $(GL_ENV)
|
||||
endif
|
||||
SHOTS_RUN = rm -rf $(SHOTS_TMP)/project $(SHOTS_TMP)/home && mkdir -p $(SHOTS_TMP)/home && cp -R examples/platformer $(SHOTS_TMP)/project && \
|
||||
rm -rf $(SHOTS_TMP)/project/.aego $(SHOTS_TMP)/project/exports && \
|
||||
sed -i.bak 's|=> ../..|=> $(CURDIR)|' $(SHOTS_TMP)/project/go.mod && \
|
||||
XDG_CONFIG_HOME=$(SHOTS_TMP)/home XDG_DATA_HOME=$(SHOTS_TMP)/home $(SHOTS_ENV) env AEGO_SHOTS=$(1) .cache/bin/aego edit $(SHOTS_TMP)/project --hidden
|
||||
|
||||
shots: cli
|
||||
rm -rf docs/shots
|
||||
$(call SHOTS_RUN,$(CURDIR)/docs/shots)
|
||||
|
||||
shots-check: cli
|
||||
rm -rf $(SHOTS_TMP)/out
|
||||
$(call SHOTS_RUN,$(SHOTS_TMP)/out)
|
||||
go run ./tools/shotdiff -want docs/shots -got $(SHOTS_TMP)/out
|
||||
|
||||
runtime: bootstrap
|
||||
go build -tags aego_runtime -o .cache/bin/aego-runtime ./cmd/aego-runtime
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func mustFind(t *testing.T, name string) *Command {
|
||||
t.Helper()
|
||||
c, ok := Find(name)
|
||||
if !ok {
|
||||
t.Fatalf("no command %s", name)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func TestParseArgs(t *testing.T) {
|
||||
set := mustFind(t, "scene.set")
|
||||
a, f, err := parseArgs(set, []string{"s.aescene", "--json", "Player", "--dry-run", "Body2D.mass", "-3"})
|
||||
if err != nil || !f.json || a.Str("value") != "-3" || a.Str("node") != "Player" || !a.Bool("dry-run") {
|
||||
t.Fatalf("flags after positionals: %v %+v %v", a, f, err)
|
||||
}
|
||||
add := mustFind(t, "scene.add-node")
|
||||
for _, toks := range [][]string{{"s", "N", "--index=1"}, {"s", "N", "--index", "1"}} {
|
||||
a, _, err := parseArgs(add, toks)
|
||||
if n, _ := a.Int("index", -1); err != nil || n != 1 {
|
||||
t.Fatalf("%v: %v %v", toks, a, err)
|
||||
}
|
||||
}
|
||||
a, _, err = parseArgs(set, []string{"s", "--", "Player", "Body2D.mass", "--odd"})
|
||||
if err != nil || a.Str("value") != "--odd" {
|
||||
t.Fatalf("after --: %v %v", a, err)
|
||||
}
|
||||
if _, _, err := parseArgs(set, []string{"s", "--nope"}); err == nil {
|
||||
t.Fatal("an unknown flag was accepted")
|
||||
}
|
||||
if _, _, err := parseArgs(set, []string{"s", "a", "b", "c", "d"}); err == nil {
|
||||
t.Fatal("an extra positional was accepted")
|
||||
}
|
||||
var out, errw bytes.Buffer
|
||||
if code := Main(nil, []string{"scene", "set", "s", "--nope"}, &out, &errw, Options{}); code != 2 {
|
||||
t.Fatalf("unknown flag exit %d", code)
|
||||
}
|
||||
if code := Main(nil, []string{"scene", "set", "a", "b", "c", "d", "e"}, &out, &errw, Options{}); code != 2 {
|
||||
t.Fatalf("extra positional exit %d", code)
|
||||
}
|
||||
if code := Main(nil, []string{"scene", "help"}, &out, &errw, Options{}); code != 0 {
|
||||
t.Fatalf("help exit %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemasAndToolNames(t *testing.T) {
|
||||
seen := map[string]bool{}
|
||||
for _, c := range Commands() {
|
||||
if seen[c.Tool()] {
|
||||
t.Fatalf("tool name %s is used twice", c.Tool())
|
||||
}
|
||||
seen[c.Tool()] = true
|
||||
b, err := json.Marshal(c.Schema())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var sch struct {
|
||||
Type string `json:"type"`
|
||||
Properties map[string]map[string]any `json:"properties"`
|
||||
Required []string `json:"required"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &sch); err != nil || sch.Type != "object" {
|
||||
t.Fatalf("%s: %s", c.Name, b)
|
||||
}
|
||||
var want []string
|
||||
for _, x := range c.AllArgs() {
|
||||
if x.Required {
|
||||
want = append(want, x.Name)
|
||||
}
|
||||
if _, ok := sch.Properties[x.Name]; !ok {
|
||||
t.Fatalf("%s: no property %s", c.Name, x.Name)
|
||||
}
|
||||
}
|
||||
if len(want) != len(sch.Required) {
|
||||
t.Fatalf("%s: required %v, want %v", c.Name, sch.Required, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
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
@@ -0,0 +1,66 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"aego/asset"
|
||||
"aego/project/refs"
|
||||
"aego/scene/mutate"
|
||||
)
|
||||
|
||||
func projectCommands() []*Command {
|
||||
return []*Command{
|
||||
{Name: "project.validate", Doc: "missing assets and nodes, import errors, prefabs and scenes that do not load", Run: (*Session).projectValidate},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) projectValidate(Args) (*Output, error) {
|
||||
out := &Output{}
|
||||
add := func(err bool, node, msg string) {
|
||||
out.Problems = append(out.Problems, mutate.Problem{Error: err, Node: node, Message: msg})
|
||||
}
|
||||
for _, m := range refs.MissingAssets(s.Project, s.Pipeline) {
|
||||
add(true, "", "missing asset "+m.Label())
|
||||
}
|
||||
for _, m := range refs.MissingNodes(s.Pipeline) {
|
||||
add(true, m.Scene+" "+m.Path, m.Owner+"."+m.Field+" references a node that is not in the scene")
|
||||
}
|
||||
if s.scriptErr != nil {
|
||||
add(true, "", "script types: "+s.scriptErr.Error())
|
||||
}
|
||||
for _, e := range s.libErrs {
|
||||
add(true, "", e.Error())
|
||||
}
|
||||
for _, x := range s.Pipeline.Assets() {
|
||||
if x.Error != "" {
|
||||
add(true, x.Path, x.Error)
|
||||
continue
|
||||
}
|
||||
if x.Kind != asset.ArtScene && x.Kind != asset.ArtPrefab {
|
||||
continue
|
||||
}
|
||||
src, err := s.read(x.Path)
|
||||
if err != nil {
|
||||
add(true, x.Path, err.Error())
|
||||
continue
|
||||
}
|
||||
doc, err := mutate.Load(src, x.Path, s.env())
|
||||
if err != nil {
|
||||
add(true, x.Path, err.Error())
|
||||
continue
|
||||
}
|
||||
for _, p := range doc.Problems {
|
||||
add(p.Error, x.Path+" "+p.Node, p.Message)
|
||||
}
|
||||
}
|
||||
errs, warns := 0, 0
|
||||
for _, p := range out.Problems {
|
||||
if p.Error {
|
||||
errs++
|
||||
} else {
|
||||
warns++
|
||||
}
|
||||
}
|
||||
out.Notes = []string{"project: " + strconv.Itoa(errs) + " error(s), " + strconv.Itoa(warns) + " warning(s)"}
|
||||
return out, nil
|
||||
}
|
||||
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
@@ -6,6 +6,7 @@ import (
|
||||
"aego/engine/module"
|
||||
"aego/modules/accessibility"
|
||||
"aego/modules/audio"
|
||||
"aego/modules/camera"
|
||||
"aego/modules/dataasset"
|
||||
"aego/modules/debug"
|
||||
"aego/modules/display"
|
||||
@@ -71,6 +72,10 @@ func tweenEntry() assembly.Entry {
|
||||
return assembly.Entry{ID: tween.ID, Requires: []string{scripting.ID}, New: func(assembly.Options) module.Module { return tween.New() }}
|
||||
}
|
||||
|
||||
func cameraEntry() assembly.Entry {
|
||||
return assembly.Entry{ID: camera.ID, Requires: []string{scripting.ID}, New: func(assembly.Options) module.Module { return camera.New() }}
|
||||
}
|
||||
|
||||
func dataEntry() assembly.Entry {
|
||||
return assembly.Entry{ID: dataasset.ID, New: func(assembly.Options) module.Module { return dataasset.New() }}
|
||||
}
|
||||
@@ -85,5 +90,5 @@ func savegameEntry() assembly.Entry {
|
||||
|
||||
func Default() assembly.Catalog {
|
||||
return assembly.Catalog{core(), dataEntry(), inputEntry(), render2dEntry(), render3dEntry(), scriptEntry(), physicsEntry(), debugEntry(),
|
||||
displayEntry(), audioEntry(), accessibilityEntry(), settingsEntry(), tweenEntry(), spawnEntry(), savegameEntry()}
|
||||
displayEntry(), audioEntry(), accessibilityEntry(), settingsEntry(), tweenEntry(), cameraEntry(), spawnEntry(), savegameEntry()}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@ package builtin
|
||||
import "aego/app/assembly"
|
||||
|
||||
func Headless() assembly.Catalog {
|
||||
return assembly.Catalog{core(), dataEntry(), inputEntry(), scriptEntry(), physicsEntry(), debugEntry(), displayEntry(), audioEntry(), accessibilityEntry(), settingsEntry(), tweenEntry(), spawnEntry(), savegameEntry()}
|
||||
return assembly.Catalog{core(), dataEntry(), inputEntry(), scriptEntry(), physicsEntry(), debugEntry(), displayEntry(), audioEntry(), accessibilityEntry(), settingsEntry(), tweenEntry(), cameraEntry(), spawnEntry(), savegameEntry()}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package editors
|
||||
|
||||
import (
|
||||
"aego/app/assembly"
|
||||
"aego/modules/camera"
|
||||
cameraeditor "aego/modules/camera/editor"
|
||||
"aego/modules/debug"
|
||||
debugeditor "aego/modules/debug/editor"
|
||||
inputmod "aego/modules/input"
|
||||
@@ -20,5 +22,6 @@ func With(c assembly.Catalog) assembly.Catalog {
|
||||
With(render2d.ID, render2deditor.Hook).
|
||||
With(render3d.ID, render3deditor.Hook).
|
||||
With(physics.ID, physicseditor.Hook).
|
||||
With(camera.ID, cameraeditor.Hook).
|
||||
With(debug.ID, debugeditor.Hook)
|
||||
}
|
||||
|
||||
@@ -4,11 +4,16 @@ import (
|
||||
"aego/app/assembly"
|
||||
"aego/engine/base"
|
||||
"aego/engine/module"
|
||||
"aego/modules/camera"
|
||||
"aego/modules/debug"
|
||||
inputmod "aego/modules/input"
|
||||
"aego/modules/physics"
|
||||
"aego/modules/render2d"
|
||||
"aego/modules/render3d"
|
||||
"aego/modules/savegame"
|
||||
"aego/modules/scripting"
|
||||
"aego/modules/spawn"
|
||||
"aego/modules/tween"
|
||||
"aego/script/stdlib"
|
||||
"aego/script/vm"
|
||||
)
|
||||
@@ -23,6 +28,11 @@ func Catalog() assembly.Catalog {
|
||||
}},
|
||||
{ID: physics.ID, New: func(assembly.Options) module.Module { return physics.New() }},
|
||||
{ID: debug.ID, New: func(assembly.Options) module.Module { return debug.New() }},
|
||||
{ID: tween.ID, Requires: []string{scripting.ID}, New: func(assembly.Options) module.Module { return tween.New() }},
|
||||
{ID: camera.ID, Requires: []string{scripting.ID}, New: func(assembly.Options) module.Module { return camera.New() }},
|
||||
{ID: render3d.ID, New: func(assembly.Options) module.Module { return render3d.New() }},
|
||||
{ID: spawn.ID, New: func(assembly.Options) module.Module { return spawn.New() }},
|
||||
{ID: savegame.ID, Requires: []string{scripting.ID, spawn.ID}, New: func(assembly.Options) module.Module { return savegame.New() }},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package playground
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"aego/contracts/persistence"
|
||||
"aego/engine"
|
||||
"aego/platform"
|
||||
"aego/save"
|
||||
)
|
||||
|
||||
// UseMemorySaves gives e a save store kept in memory, for runs in a browser page that must not touch the visitor's storage.
|
||||
func UseMemorySaves(e *engine.Engine) {
|
||||
e.SetService(persistence.StoreID, save.NewStoreFS("saves", &memUser{files: map[string][]byte{}}))
|
||||
}
|
||||
|
||||
type memUser struct {
|
||||
files map[string][]byte
|
||||
}
|
||||
|
||||
func (m *memUser) ReadFile(p string) ([]byte, error) {
|
||||
data, ok := m.files[path.Clean(p)]
|
||||
if !ok {
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
return append([]byte(nil), data...), nil
|
||||
}
|
||||
|
||||
func (m *memUser) WriteFile(p string, data []byte) error {
|
||||
m.files[path.Clean(p)] = append([]byte(nil), data...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memUser) Remove(p string) error {
|
||||
delete(m.files, path.Clean(p))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memUser) Stat(p string) (platform.UserEntry, error) {
|
||||
data, ok := m.files[path.Clean(p)]
|
||||
if !ok {
|
||||
return platform.UserEntry{}, fs.ErrNotExist
|
||||
}
|
||||
return platform.UserEntry{Name: path.Base(p), Size: int64(len(data)), Mod: time.Time{}}, nil
|
||||
}
|
||||
|
||||
func (m *memUser) List(dir string) ([]platform.UserEntry, error) {
|
||||
prefix := path.Clean(dir) + "/"
|
||||
var out []platform.UserEntry
|
||||
for p, data := range m.files {
|
||||
if rest, ok := strings.CutPrefix(p, prefix); ok && !strings.Contains(rest, "/") {
|
||||
out = append(out, platform.UserEntry{Name: rest, Size: int64(len(data))})
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package playground
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
"aego/asset"
|
||||
"aego/asset/loader"
|
||||
"aego/core/ids"
|
||||
"aego/script/bytecode"
|
||||
"aego/vfs"
|
||||
"aego/vfs/memfs"
|
||||
)
|
||||
|
||||
// EncodeTry packs the compiled script of a try scene for a docs page: base64 modules joined by commas.
|
||||
func EncodeTry(mods []*bytecode.Module) string {
|
||||
parts := make([]string, len(mods))
|
||||
for i, m := range mods {
|
||||
parts[i] = base64.StdEncoding.EncodeToString(bytecode.Encode(m))
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func DecodeTry(code string) ([]*bytecode.Module, error) {
|
||||
if code == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var mods []*bytecode.Module
|
||||
for _, part := range strings.Split(code, ",") {
|
||||
b, err := base64.StdEncoding.DecodeString(part)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m, err := bytecode.Decode(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mods = append(mods, m)
|
||||
}
|
||||
return mods, nil
|
||||
}
|
||||
|
||||
// MountTry gives the loader the prefabs of a try scene, keyed by asset id; none unmounts them.
|
||||
func MountTry(l *loader.Loader, prefabs map[string]string) error {
|
||||
idx := asset.NewIndex()
|
||||
files := map[string][]byte{}
|
||||
for key, text := range prefabs {
|
||||
id, err := ids.ParseAssetID(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := id.String() + ".aeprefab"
|
||||
files[p] = []byte(text)
|
||||
idx.Put(asset.Entry{ID: id, Kind: asset.ArtPrefab, Name: p, Source: p,
|
||||
Artifacts: []asset.Artifact{{Kind: asset.ArtPrefab, Path: "try://" + p}}})
|
||||
}
|
||||
fs, err := vfs.New(vfs.Mount{Scheme: "try", ReadOnly: true, Source: memfs.New(files)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return l.Mount(idx, fs)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package docstool
|
||||
|
||||
import (
|
||||
"aego/app/playground"
|
||||
"aego/engine/docs"
|
||||
"aego/script/diag"
|
||||
)
|
||||
|
||||
func diagnoser(pg *playground.Playground) func(string, *docs.DiagInfo) {
|
||||
return func(code string, d *docs.DiagInfo) {
|
||||
got, err := pg.Reproduce(code)
|
||||
if err != nil || got.Code == "" {
|
||||
return
|
||||
}
|
||||
d.Severity, d.At, d.Message = got.Severity, got.File, got.Message
|
||||
d.Line, d.Col, d.EndLine, d.EndCol = got.Line, got.Col, got.EndLine, got.EndCol
|
||||
}
|
||||
}
|
||||
|
||||
// DiagnosticProblems lists codes whose example is missing or no longer produces the code.
|
||||
func DiagnosticProblems() []docs.Problem {
|
||||
pg, err := playground.New()
|
||||
if err != nil {
|
||||
return []docs.Problem{{ID: docs.DiagnosticsID, Error: true, Message: "cannot start the docs runtime: " + err.Error()}}
|
||||
}
|
||||
var out []docs.Problem
|
||||
for _, c := range diag.Codes() {
|
||||
if _, err := pg.Reproduce(c); err != nil {
|
||||
out = append(out, docs.Problem{ID: c, Error: true, Message: err.Error()})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -138,7 +138,7 @@ func TestF1Refs(t *testing.T) {
|
||||
if err := os.WriteFile(path, []byte(probe), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reg, ws, err := build(p, nil)
|
||||
reg, ws, err := build(p, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -169,3 +169,17 @@ func TestF1Refs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagnosticExamplesExported(t *testing.T) {
|
||||
if ps := DiagnosticProblems(); len(ps) > 0 {
|
||||
t.Fatalf("diagnostics examples: %v", ps)
|
||||
}
|
||||
reg, err := ExportRegistry(nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
e, ok := reg.Get("AES2001")
|
||||
if !ok || e.Diag == nil || e.Diag.Line == 0 || e.Diag.Severity != "error" || !strings.Contains(e.Diag.Message, "heigth") {
|
||||
t.Fatalf("AES2001 example was not diagnosed: %+v", e.Diag)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"aego/app/assembly"
|
||||
"aego/app/assembly/builtin"
|
||||
"aego/app/playground"
|
||||
"aego/engine/docs"
|
||||
"aego/engine/module"
|
||||
scriptsystems "aego/modules/scripting/systems"
|
||||
@@ -20,7 +21,8 @@ import (
|
||||
"aego/script/workspace"
|
||||
)
|
||||
|
||||
func build(p *project.Project, mods []module.Module) (*docs.Registry, *workspace.Workspace, error) {
|
||||
// build assembles the registry; with pg it also compiles the diagnostics examples and the try scripts.
|
||||
func build(p *project.Project, mods []module.Module, pg *playground.Playground) (*docs.Registry, *workspace.Workspace, error) {
|
||||
types, err := assembly.NewTypes()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -41,6 +43,10 @@ func build(p *project.Project, mods []module.Module) (*docs.Registry, *workspace
|
||||
return nil, nil, err
|
||||
}
|
||||
src := docs.SourcesOf(bm, types.Schema(), man)
|
||||
if pg != nil {
|
||||
src.Diagnose = diagnoser(pg)
|
||||
src.CompileTry = pg.TryCode
|
||||
}
|
||||
var ws *workspace.Workspace
|
||||
if p != nil {
|
||||
ws, err = workspace.Open(workspace.Config{Root: p.Root, Module: p.Module, Scripts: p.ScriptsDir(), Bindings: reg.Table(), Docs: bm})
|
||||
@@ -61,7 +67,17 @@ func build(p *project.Project, mods []module.Module) (*docs.Registry, *workspace
|
||||
}
|
||||
|
||||
func Registry(p *project.Project, mods []module.Module) (*docs.Registry, error) {
|
||||
r, _, err := build(p, mods)
|
||||
r, _, err := build(p, mods, nil)
|
||||
return r, err
|
||||
}
|
||||
|
||||
// ExportRegistry is Registry with the diagnostics examples compiled for their messages and positions, and the try scripts compiled.
|
||||
func ExportRegistry(p *project.Project, mods []module.Module) (*docs.Registry, error) {
|
||||
pg, err := playground.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r, _, err := build(p, mods, pg)
|
||||
return r, err
|
||||
}
|
||||
|
||||
@@ -84,9 +100,9 @@ func WriteExport(reg *docs.Registry, title, path string, w io.Writer) error {
|
||||
return os.WriteFile(path, data, 0o644)
|
||||
}
|
||||
|
||||
func Check(reg *docs.Registry, strict bool, w io.Writer) int {
|
||||
func Check(reg *docs.Registry, strict bool, w io.Writer, extra ...docs.Problem) int {
|
||||
errs, warns := 0, 0
|
||||
for _, p := range reg.Check(strict) {
|
||||
for _, p := range append(reg.Check(strict), extra...) {
|
||||
level := "warn "
|
||||
if p.Error {
|
||||
level = "error"
|
||||
|
||||
+15
-3
@@ -16,6 +16,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"aego/app/assembly/builtin"
|
||||
"aego/engine"
|
||||
"aego/engine/module"
|
||||
"aego/project"
|
||||
@@ -40,7 +41,7 @@ func (e *Env) export(path string) error {
|
||||
}
|
||||
return e.Native(args...)
|
||||
}
|
||||
reg, err := Registry(e.Project, e.Modules)
|
||||
reg, err := ExportRegistry(e.Project, e.Modules)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -71,7 +72,7 @@ func (e *Env) Check(strict bool) int {
|
||||
fmt.Fprintln(e.Stderr, "aego:", err)
|
||||
return 1
|
||||
}
|
||||
code := Check(reg, strict, e.Stdout)
|
||||
code := Check(reg, strict, e.Stdout, DiagnosticProblems()...)
|
||||
if CheckTry(reg, e.Stdout) > 0 && code == 0 {
|
||||
code = 1
|
||||
}
|
||||
@@ -98,7 +99,10 @@ func (e *Env) git(args ...string) string {
|
||||
}
|
||||
|
||||
func (e *Env) commit() string {
|
||||
return e.git("rev-parse", "--short", "HEAD")
|
||||
if c := e.git("rev-parse", "--short", "HEAD"); c != "" {
|
||||
return c
|
||||
}
|
||||
return os.Getenv("AEGO_COMMIT")
|
||||
}
|
||||
|
||||
func (e *Env) branch() string {
|
||||
@@ -150,6 +154,14 @@ func (e *Env) Build(out string, play bool) error {
|
||||
if c := e.commit(); c != "" {
|
||||
args = append(args, "-commit", c)
|
||||
}
|
||||
var icons []string
|
||||
for _, d := range builtin.IconSources() {
|
||||
icons = append(icons, filepath.Join(e.Engine, filepath.FromSlash(d)))
|
||||
}
|
||||
args = append(args, "-icons", strings.Join(icons, ","))
|
||||
if shots := filepath.Join(e.Engine, "docs", "shots"); fileExists(filepath.Join(shots, "demo.png")) {
|
||||
args = append(args, "-poster", filepath.Join(shots, "demo.png"), "-shots", shots)
|
||||
}
|
||||
if hero := filepath.Join(e.Engine, "docs", "hero.png"); fileExists(hero) {
|
||||
args = append(args, "-hero", hero)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,11 @@ func Tool(p *project.Project, args []string, stdout, stderr io.Writer, mods ...m
|
||||
if err := fs.Parse(args[1:]); err != nil {
|
||||
return 2
|
||||
}
|
||||
reg, err := Registry(p, mods)
|
||||
build := Registry
|
||||
if args[0] == "export" {
|
||||
build = ExportRegistry
|
||||
}
|
||||
reg, err := build(p, mods)
|
||||
if err != nil {
|
||||
fmt.Fprintln(stderr, "aego:", err)
|
||||
return 1
|
||||
@@ -34,7 +38,7 @@ func Tool(p *project.Project, args []string, stdout, stderr io.Writer, mods ...m
|
||||
}
|
||||
return 0
|
||||
case "check":
|
||||
return Check(reg, *strict, stdout)
|
||||
return Check(reg, *strict, stdout, DiagnosticProblems()...)
|
||||
}
|
||||
fmt.Fprintf(stderr, "unknown docs command %q\n", args[0])
|
||||
return 2
|
||||
|
||||
+8
-1
@@ -41,9 +41,16 @@ func CheckTry(reg *docs.Registry, w io.Writer) int {
|
||||
}
|
||||
msgs = append(msgs, msg)
|
||||
}
|
||||
switch res := pg.PlayScene(e.ID, []byte(e.Try)); {
|
||||
var code string
|
||||
var err error
|
||||
if e.TryScript != "" {
|
||||
code, err = pg.TryCode(e.TryScript)
|
||||
}
|
||||
switch res := pg.PlayScene(e.ID, []byte(e.Try), code, e.TryPrefabs); {
|
||||
case !sandbox[e.ID]:
|
||||
add("belongs to a module the docs runtime does not include")
|
||||
case err != nil:
|
||||
add("script does not compile: " + err.Error())
|
||||
case !res.Started:
|
||||
add("does not load: " + res.Error)
|
||||
default:
|
||||
|
||||
+27
-5
@@ -7,6 +7,7 @@ import (
|
||||
playmods "aego/app/assembly/playground"
|
||||
"aego/asset"
|
||||
"aego/asset/loader"
|
||||
"aego/contracts/debugdraw"
|
||||
"aego/core/log"
|
||||
"aego/core/mathx"
|
||||
edclip "aego/editor/core/clipboard"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"aego/engine/base"
|
||||
"aego/engine/module"
|
||||
"aego/format/aetext"
|
||||
"aego/modules/physics"
|
||||
scriptsystems "aego/modules/scripting/systems"
|
||||
"aego/platform"
|
||||
"aego/render"
|
||||
@@ -41,9 +43,11 @@ const (
|
||||
maxSprites = 16384
|
||||
gameShare = 0.6
|
||||
stackBelow = 640
|
||||
maxLines = 200
|
||||
defaultW = 960
|
||||
defaultH = 440
|
||||
// inspectorMin keeps field names and values readable in narrow embeds.
|
||||
inspectorMin = 400
|
||||
maxLines = 200
|
||||
defaultW = 960
|
||||
defaultH = 440
|
||||
)
|
||||
|
||||
var coreBlocks = []string{"window", "defaults"}
|
||||
@@ -71,12 +75,14 @@ type App struct {
|
||||
ui *core.Context
|
||||
clip *docsession.Clipboard
|
||||
session *docsession.Session
|
||||
scripts *scriptHost
|
||||
insp *inspector.Panel
|
||||
game *gameview.Panel
|
||||
list *draw.List
|
||||
log *log.Logger
|
||||
lines []Line
|
||||
textOn bool
|
||||
shown *engine.World
|
||||
closed bool
|
||||
}
|
||||
|
||||
@@ -138,15 +144,17 @@ func New(opt Options) (*App, error) {
|
||||
return nil, err
|
||||
}
|
||||
a.e.SetService(loader.ServiceID, a.loader)
|
||||
playmods.UseMemorySaves(a.e)
|
||||
a.e.SetService(renderhost.ServiceID, a.renderer)
|
||||
if err := a.man.Init(&module.InitContext{Engine: a.e, Assets: a.loader, Log: a.log, Editor: true, Sections: sections}); err != nil {
|
||||
a.Close()
|
||||
return nil, err
|
||||
}
|
||||
a.initUI()
|
||||
a.scripts = &scriptHost{reg: reg, types: a.types, man: a.man, log: a.log}
|
||||
a.session = docsession.New(docsession.Options{
|
||||
Engine: a.e, Renderer: a.renderer, Loader: a.loader, Types: a.types, Sections: sections,
|
||||
Scripts: &scriptHost{reg: reg, types: a.types, man: a.man, log: a.log},
|
||||
Scripts: a.scripts,
|
||||
UI: a.ui,
|
||||
Clipboard: a.clip,
|
||||
Log: a.log,
|
||||
@@ -270,7 +278,7 @@ func layout(body mathx.Rect, gap float32) (game, insp mathx.Rect) {
|
||||
return mathx.Rect{X: body.X, Y: body.Y, W: body.W, H: h},
|
||||
mathx.Rect{X: body.X, Y: body.Y + h + gap, W: body.W, H: max(0, body.H-h-gap)}
|
||||
}
|
||||
w := body.W * gameShare
|
||||
w := min(body.W*gameShare, body.W-inspectorMin)
|
||||
return mathx.Rect{X: body.X, Y: body.Y, W: w, H: body.H},
|
||||
mathx.Rect{X: body.X + w + gap, Y: body.Y, W: max(0, body.W-w-gap), H: body.H}
|
||||
}
|
||||
@@ -308,10 +316,24 @@ func (a *App) toolbar(r mathx.Rect) {
|
||||
}
|
||||
}
|
||||
|
||||
// showPhysics draws collider and joint outlines in the game view, since docs scenes have no art for them.
|
||||
func (a *App) showPhysics() {
|
||||
w := a.session.Controller().World()
|
||||
if w == nil || w == a.shown {
|
||||
return
|
||||
}
|
||||
if d, ok := engine.ServiceOf[debugdraw.Draw](w, debugdraw.ServiceID); ok {
|
||||
d.Hide(physics.ChannelShapes, false)
|
||||
d.Hide(physics.ChannelJoints, false)
|
||||
a.shown = w
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) Update(dt float64) {
|
||||
e := a.e
|
||||
now := e.Platform().Now()
|
||||
a.session.Tick(dt)
|
||||
a.showPhysics()
|
||||
ui := a.ui
|
||||
ui.Time = now
|
||||
m := e.Metrics()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package docsview
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -27,7 +28,7 @@ func tries(t *testing.T) []docs.Entry {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := p.TryScenes()
|
||||
if len(out) < 4 {
|
||||
if len(out) < 10 {
|
||||
t.Fatalf("only %d try scenes; does the module embed res/docs/try?", len(out))
|
||||
}
|
||||
return out
|
||||
@@ -46,11 +47,22 @@ func errorsOf(a *App) []string {
|
||||
|
||||
func TestTryScenesRun(t *testing.T) {
|
||||
a := newHeadless(t)
|
||||
p, err := playground.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, e := range tries(t) {
|
||||
if err := a.session.Load([]byte(e.Try), e.TrySelect); err != nil {
|
||||
t.Fatalf("%s: %v", e.ID, err)
|
||||
var code string
|
||||
if e.TryScript != "" {
|
||||
if code, err = p.TryCode(e.TryScript); err != nil {
|
||||
t.Fatalf("%s: %v", e.ID, err)
|
||||
}
|
||||
}
|
||||
if err := a.Step(60); err != nil {
|
||||
req, _ := json.Marshal(map[string]any{"scene": e.Try, "select": e.TrySelect, "code": code, "prefabs": e.TryPrefabs})
|
||||
if r := a.Call("load", req); r.Error != "" {
|
||||
t.Fatalf("%s: %s", e.ID, r.Error)
|
||||
}
|
||||
if err := a.Step(400); err != nil {
|
||||
t.Fatalf("%s: %v", e.ID, err)
|
||||
}
|
||||
st := a.session.State()
|
||||
|
||||
+34
-11
@@ -6,22 +6,27 @@ import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
playmods "aego/app/assembly/playground"
|
||||
"aego/editor/core/docsession"
|
||||
"aego/scene/sceneio"
|
||||
)
|
||||
|
||||
type request struct {
|
||||
Scene string `json:"scene"`
|
||||
Select string `json:"select"`
|
||||
Component string `json:"component"`
|
||||
Field string `json:"field"`
|
||||
Value float64 `json:"value"`
|
||||
Scene string `json:"scene"`
|
||||
Select string `json:"select"`
|
||||
Code string `json:"code"`
|
||||
Prefabs map[string]string `json:"prefabs"`
|
||||
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"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Values map[string]string `json:"values,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 {
|
||||
@@ -35,7 +40,16 @@ func (a *App) Call(method string, payload []byte) Reply {
|
||||
var err error
|
||||
switch method {
|
||||
case "load":
|
||||
err = s.Load([]byte(req.Scene), req.Select)
|
||||
mods, derr := playmods.DecodeTry(req.Code)
|
||||
if err = derr; err == nil {
|
||||
err = a.scripts.use(mods)
|
||||
}
|
||||
if err == nil {
|
||||
err = playmods.MountTry(a.loader, req.Prefabs)
|
||||
}
|
||||
if err == nil {
|
||||
err = s.Load([]byte(req.Scene), req.Select)
|
||||
}
|
||||
case "reset":
|
||||
err = s.Reset()
|
||||
case "text":
|
||||
@@ -61,7 +75,16 @@ func (a *App) Call(method string, payload []byte) Reply {
|
||||
return Reply{Error: err.Error()}
|
||||
}
|
||||
st := s.State()
|
||||
return Reply{Text: string(s.Text()), State: &st}
|
||||
return Reply{Text: string(s.Text()), Values: Values(s.Text()), State: &st}
|
||||
}
|
||||
|
||||
// Values flattens a scene file into the field keys the docs scene tree uses.
|
||||
func Values(text []byte) map[string]string {
|
||||
nodes, err := sceneio.Outline(text)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return sceneio.OutlineValues(nodes)
|
||||
}
|
||||
|
||||
func (a *App) Reply(method, payload string) string {
|
||||
|
||||
@@ -22,6 +22,15 @@ type scriptHost struct {
|
||||
mods []*bytecode.Module
|
||||
}
|
||||
|
||||
// use makes mods the scripts of the next scene and publishes their types, so the scene keeps their fields.
|
||||
func (h *scriptHost) use(mods []*bytecode.Module) error {
|
||||
if err := scriptruntime.Publish(h.types.Schema(), mods, h.reg.Surface()); err != nil {
|
||||
return err
|
||||
}
|
||||
h.mods = mods
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *scriptHost) Errors() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -154,6 +154,9 @@ func Run(p *project.Project, cfg Config, mods ...sdk.Module) error {
|
||||
panic(rec)
|
||||
}
|
||||
}()
|
||||
if dir := os.Getenv(EnvShots); dir != "" {
|
||||
return s.shots(dir)
|
||||
}
|
||||
if err := s.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ canvas { display: block; width: 100%; height: 100%; outline: none; touch-action:
|
||||
#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>
|
||||
<link rel="preload" href="game.wasm?v={{.Build}}" as="fetch" type="application/wasm" crossorigin>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="game" data-aego tabindex="0" aria-label="{{.Title}}"></canvas>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package playground
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"aego/app/assembly/builtin"
|
||||
)
|
||||
|
||||
func TestCheckSnippetsCompile(t *testing.T) {
|
||||
p, err := NewWith(builtin.Default())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n := 0
|
||||
for _, e := range p.docs.All() {
|
||||
for _, s := range e.Snippets {
|
||||
if !s.Check || s.Lang != "aes" {
|
||||
continue
|
||||
}
|
||||
n++
|
||||
for _, d := range p.Compile(map[string]string{"main.aes": s.Code}) {
|
||||
if d.Severity == "error" {
|
||||
t.Errorf("%s (line %d of its page): %d:%d %s %s", e.ID, s.Line, d.Line, d.Col, d.Code, d.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatal("no aes check blocks in the documentation")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package playground
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"aego/script/diag"
|
||||
)
|
||||
|
||||
const exampleFrames = 3
|
||||
|
||||
// Reproduce runs the diagnostics example of code and returns what its broken version reports.
|
||||
func (p *Playground) Reproduce(code string) (Diagnostic, error) {
|
||||
ex, ok := diag.Example(code)
|
||||
if !ok {
|
||||
return Diagnostic{}, errors.New("no example in script/diag/examples.go")
|
||||
}
|
||||
if ex.Why != "" {
|
||||
return Diagnostic{}, nil
|
||||
}
|
||||
if ex.Bad == "" || ex.Good == "" {
|
||||
return Diagnostic{}, errors.New("the example needs both a broken and a fixed version")
|
||||
}
|
||||
bad := reports(p.Run(ex.Sources(false), exampleFrames))
|
||||
var found *Diagnostic
|
||||
for i := range bad {
|
||||
if bad[i].Code == code {
|
||||
found = &bad[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
return Diagnostic{}, fmt.Errorf("the broken example does not report %s, it reports %s", code, list(bad))
|
||||
}
|
||||
var wrong []Diagnostic
|
||||
for _, d := range reports(p.Run(ex.Sources(true), exampleFrames)) {
|
||||
if d.Code == code || d.Severity == "error" {
|
||||
wrong = append(wrong, d)
|
||||
}
|
||||
}
|
||||
if len(wrong) > 0 {
|
||||
return *found, fmt.Errorf("the fixed example still reports %s", list(wrong))
|
||||
}
|
||||
return *found, nil
|
||||
}
|
||||
|
||||
func reports(res Result) []Diagnostic {
|
||||
out := append([]Diagnostic(nil), res.Diagnostics...)
|
||||
for _, f := range res.Faults {
|
||||
out = append(out, Diagnostic{File: f.File, Line: f.Line, Code: f.Code, Severity: "error", Message: f.Message})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func list(ds []Diagnostic) string {
|
||||
if len(ds) == 0 {
|
||||
return "nothing"
|
||||
}
|
||||
parts := make([]string, len(ds))
|
||||
for i, d := range ds {
|
||||
parts[i] = strings.TrimSpace(d.Code + " " + d.Message)
|
||||
}
|
||||
return strings.Join(parts, "; ")
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package playground
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"aego/script/diag"
|
||||
)
|
||||
|
||||
func TestDiagnosticExamples(t *testing.T) {
|
||||
p := open(t)
|
||||
var skipped []string
|
||||
for _, code := range diag.Codes() {
|
||||
d, err := p.Reproduce(code)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", code, err)
|
||||
continue
|
||||
}
|
||||
if d.Code == "" {
|
||||
skipped = append(skipped, code)
|
||||
continue
|
||||
}
|
||||
t.Logf("%s %s:%d:%d-%d:%d %s %s", code, d.File, d.Line, d.Col, d.EndLine, d.EndCol, d.Severity, d.Message)
|
||||
}
|
||||
if len(skipped) > 10 {
|
||||
t.Errorf("%d codes are not reproducible: %v", len(skipped), skipped)
|
||||
}
|
||||
}
|
||||
@@ -152,6 +152,7 @@ func newGame(cfg GameConfig, types *scene.Types, reg *bind.Registry, man *module
|
||||
return nil, err
|
||||
}
|
||||
e.SetService(loader.ServiceID, g.loader)
|
||||
playmods.UseMemorySaves(e)
|
||||
e.SetService(renderhost.ServiceID, g.renderer)
|
||||
e.SetView(frame.ServiceID, g.runner)
|
||||
e.MainWorld().Recorder = recorder.New(recordDepth)
|
||||
|
||||
+31
-2
@@ -1,10 +1,13 @@
|
||||
package playground
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
playmods "aego/app/assembly/playground"
|
||||
"aego/script/bytecode"
|
||||
"aego/script/diag"
|
||||
)
|
||||
@@ -110,13 +113,39 @@ func (p *Playground) LoseContext(lose bool) bool {
|
||||
return p.game != nil && p.game.LoseContext(lose)
|
||||
}
|
||||
|
||||
func (p *Playground) PlayScene(name string, src []byte) PlayResult {
|
||||
func (p *Playground) PlayScene(name string, src []byte, code string, prefabs map[string]string) PlayResult {
|
||||
mods, err := playmods.DecodeTry(code)
|
||||
if err != nil {
|
||||
return PlayResult{Error: err.Error()}
|
||||
}
|
||||
g, err := p.host()
|
||||
if err != nil {
|
||||
return PlayResult{Error: err.Error()}
|
||||
}
|
||||
if err := g.Play(name, src, nil); err != nil {
|
||||
if err := playmods.MountTry(g.loader, prefabs); err != nil {
|
||||
return PlayResult{Error: err.Error()}
|
||||
}
|
||||
if err := g.Play(name, src, mods); err != nil {
|
||||
return PlayResult{Error: err.Error()}
|
||||
}
|
||||
return PlayResult{Started: true}
|
||||
}
|
||||
|
||||
// TryCode compiles the script of a try scene into the form DecodeTry reads.
|
||||
func (p *Playground) TryCode(script string) (string, error) {
|
||||
placed, err := place(map[string]string{"main.aes": script})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
p.ws.Sync(placed)
|
||||
gen, ds := p.ws.Build()
|
||||
if len(gen.Failed) > 0 || diag.HasErrors(ds) {
|
||||
for _, d := range diagnostics(gen.Sources, ds) {
|
||||
if d.Severity == "error" {
|
||||
return "", fmt.Errorf("%s:%d:%d: %s", d.File, d.Line, d.Col, d.Message)
|
||||
}
|
||||
}
|
||||
return "", errors.New("the script does not build")
|
||||
}
|
||||
return playmods.EncodeTry(ordered(gen.Modules)), nil
|
||||
}
|
||||
|
||||
@@ -101,9 +101,16 @@ type Playground struct {
|
||||
proj *exampleProject
|
||||
gameCfg GameConfig
|
||||
game *Game
|
||||
|
||||
simEngine *engine.Engine
|
||||
}
|
||||
|
||||
func New() (*Playground, error) {
|
||||
return NewWith(playmods.Catalog())
|
||||
}
|
||||
|
||||
// NewWith builds a playground over another module catalog, for checks that need every module's types.
|
||||
func NewWith(cat assembly.Catalog) (*Playground, error) {
|
||||
proj, err := loadProject()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -113,7 +120,7 @@ func New() (*Playground, error) {
|
||||
return nil, err
|
||||
}
|
||||
reg := bind.New()
|
||||
man, err := playmods.Catalog().Load(types, reg, assembly.Options{Seed: 1, Testing: true})
|
||||
man, err := cat.Load(types, reg, assembly.Options{Seed: 1, Testing: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -205,6 +212,23 @@ func diagnostics(sm *source.Manager, ds []diag.Diagnostic) []Diagnostic {
|
||||
return out
|
||||
}
|
||||
|
||||
func blocked(failed map[string][]diag.Diagnostic) []Diagnostic {
|
||||
pkgs := make([]string, 0, len(failed))
|
||||
for pkg := range failed {
|
||||
pkgs = append(pkgs, pkg)
|
||||
}
|
||||
sort.Strings(pkgs)
|
||||
var out []Diagnostic
|
||||
for _, pkg := range pkgs {
|
||||
for _, d := range failed[pkg] {
|
||||
if d.Code == diag.CompileBlocked {
|
||||
out = append(out, Diagnostic{Code: d.Code, Message: d.Message, Severity: severity(d)})
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func failed(err error) []Diagnostic {
|
||||
return []Diagnostic{{Severity: "error", Message: err.Error()}}
|
||||
}
|
||||
@@ -263,6 +287,7 @@ func (p *Playground) Run(files map[string]string, frames int) Result {
|
||||
gen, ds := p.ws.Build()
|
||||
res := Result{Diagnostics: diagnostics(gen.Sources, ds)}
|
||||
if len(gen.Failed) > 0 || diag.HasErrors(ds) {
|
||||
res.Diagnostics = append(res.Diagnostics, blocked(gen.Failed)...)
|
||||
return res
|
||||
}
|
||||
pkgs := make([]string, 0, len(gen.Modules))
|
||||
@@ -295,16 +320,17 @@ func (p *Playground) Run(files map[string]string, frames int) Result {
|
||||
}
|
||||
w := engine.NewWorld("playground")
|
||||
w.Input = &input.State{}
|
||||
ctx := engine.Context{World: w, Dt: 1.0 / 60}
|
||||
w.SetService(scene.ServiceID, s)
|
||||
w.SetService(runtime.ServiceID, rt)
|
||||
e, detach, err := p.attach(w, s, lg)
|
||||
if err != nil {
|
||||
res.Diagnostics = append(res.Diagnostics, failed(err)...)
|
||||
return res
|
||||
}
|
||||
defer detach()
|
||||
for i := 0; i < frames; i++ {
|
||||
ctx.Frame++
|
||||
frame = ctx.Frame
|
||||
rt.RunDeferred()
|
||||
rt.FixedUpdate(&ctx)
|
||||
rt.Update(&ctx)
|
||||
rt.Coroutines(&ctx)
|
||||
s.Flush()
|
||||
s.UpdateTransforms()
|
||||
frame = uint64(i + 1)
|
||||
e.TickWorld(w, engine.ScopePlay, 1.0/60)
|
||||
}
|
||||
res.Frames = frames
|
||||
for _, f := range rt.Faults() {
|
||||
|
||||
@@ -75,12 +75,18 @@ func TestSandboxHeapLimit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSandboxHasNoSaves(t *testing.T) {
|
||||
// The sandbox has aego/save for the Persist demo; its saves stay in memory, never in the visitor's storage.
|
||||
func TestSandboxSavesStayInMemory(t *testing.T) {
|
||||
p := newHeadless(t)
|
||||
for _, d := range p.Compile(map[string]string{"main.aes": saveScript}) {
|
||||
res := p.Run(map[string]string{"main.aes": saveScript}, 10)
|
||||
for _, d := range res.Diagnostics {
|
||||
if d.Severity == "error" {
|
||||
return
|
||||
t.Fatalf("aego/save does not compile in the sandbox: %s", d.Message)
|
||||
}
|
||||
}
|
||||
for _, l := range res.Log {
|
||||
if strings.Contains(l.Text, "saves are not available") {
|
||||
t.Fatal("the sandbox has no save store")
|
||||
}
|
||||
}
|
||||
t.Fatal("aego/save compiled in the sandbox")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package playground
|
||||
|
||||
import (
|
||||
"aego/core/log"
|
||||
"aego/engine"
|
||||
"aego/engine/module"
|
||||
"aego/scene"
|
||||
"aego/scene/frame"
|
||||
)
|
||||
|
||||
// sim is a windowless engine that steps the module systems of a console run, so tweens, timers and physics work there as in Play.
|
||||
func (p *Playground) sim() (*engine.Engine, error) {
|
||||
if p.simEngine != nil {
|
||||
return p.simEngine, nil
|
||||
}
|
||||
b := engine.NewBuilder(engine.Config{Headless: true, HeadlessFrameTime: 1.0 / 60, Seed: 1, AppID: "aego-playground-console"}).WithLogger(log.Nop())
|
||||
if err := frame.Register(b.Registry()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e, err := b.Use(p.man.Engine()).Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p.man.Init(&module.InitContext{Engine: e, Log: log.Nop(), Sections: p.sections}); err != nil {
|
||||
e.Close()
|
||||
return nil, err
|
||||
}
|
||||
p.simEngine = e
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func (p *Playground) attach(w *engine.World, s *scene.Scene, lg *log.Logger) (*engine.Engine, func(), error) {
|
||||
e, err := p.sim()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
w.Time.Configure(e.TimeConfig())
|
||||
detach, err := p.man.Attach(&module.AttachContext{World: w, Scene: s, Log: lg, Play: true})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return e, detach, nil
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package run
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"aego/app/agent"
|
||||
"aego/core/log"
|
||||
"aego/core/rewind"
|
||||
"aego/project"
|
||||
"aego/sdk"
|
||||
)
|
||||
|
||||
const maxRunLog = 200
|
||||
|
||||
func AgentRunner(mods ...sdk.Module) agent.Runner {
|
||||
return func(p *project.Project, r agent.RunRequest) (agent.RunReport, error) {
|
||||
var buf bytes.Buffer
|
||||
lg := log.New(log.NewTextSink(&buf))
|
||||
lg.SetLevel(log.Warn)
|
||||
frames, seed := r.Frames, r.Seed
|
||||
if frames <= 0 {
|
||||
frames = 120
|
||||
}
|
||||
if seed == 0 {
|
||||
seed = 1
|
||||
}
|
||||
a, err := Open(p, Config{Headless: true, FrameTime: defaultDetStep, Seed: seed, ScenePath: r.Scene, Logger: lg, NoProfile: true}, mods...)
|
||||
if err != nil {
|
||||
return agent.RunReport{Log: logLines(buf.String())}, err
|
||||
}
|
||||
rep := agent.RunReport{}
|
||||
driver := a.Driver()
|
||||
for rep.Frames < frames {
|
||||
if !a.Engine.Step(driver) {
|
||||
rep.Error = "the game stopped at frame " + strconv.Itoa(rep.Frames+1)
|
||||
if err := a.Engine.Err(); err != nil {
|
||||
rep.Error += ": " + err.Error()
|
||||
}
|
||||
break
|
||||
}
|
||||
rep.Frames++
|
||||
}
|
||||
sum, _, _ := WorldHash(a.Engine.MainWorld(), rewind.NewHasher())
|
||||
rep.Hash = hex.EncodeToString(sum[:])
|
||||
a.Close()
|
||||
rep.Log = logLines(buf.String())
|
||||
return rep, nil
|
||||
}
|
||||
}
|
||||
|
||||
func logLines(s string) []string {
|
||||
var out []string
|
||||
for _, l := range strings.Split(strings.TrimSpace(s), "\n") {
|
||||
if l = strings.TrimSpace(l); l != "" {
|
||||
out = append(out, l)
|
||||
}
|
||||
}
|
||||
if len(out) > maxRunLog {
|
||||
out = append(out[:maxRunLog], "…")
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -78,7 +78,7 @@ func WorldHash(w *engine.World, h *rewind.Hasher) (sum [32]byte, covered, skippe
|
||||
}
|
||||
|
||||
func determinismPass(p *project.Project, o DeterminismOptions, mods []sdk.Module) ([]checkpoint, []string, []string, error) {
|
||||
a, err := Open(p, Config{Headless: true, FrameTime: o.FrameTime, Seed: o.Seed, ScenePath: o.ScenePath, Logger: log.Nop()}, mods...)
|
||||
a, err := Open(p, Config{Headless: true, FrameTime: o.FrameTime, Seed: o.Seed, ScenePath: o.ScenePath, Logger: log.Nop(), NoProfile: true}, mods...)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ type Config struct {
|
||||
Profile string
|
||||
Logger *log.Logger
|
||||
Seed uint64
|
||||
NoProfile bool
|
||||
}
|
||||
|
||||
type App struct {
|
||||
@@ -138,6 +139,7 @@ func Open(p *project.Project, cfg Config, mods ...sdk.Module) (*App, error) {
|
||||
Debug: cfg.Debug,
|
||||
FailOnScriptErrors: true,
|
||||
Profile: cfg.Profile,
|
||||
NoProfile: cfg.NoProfile,
|
||||
}, sdk.AdaptAll(mods)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package run
|
||||
|
||||
import (
|
||||
"aego/app/agent"
|
||||
"aego/app/agent/mcp"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -39,6 +42,10 @@ func Tool(p *project.Project, args []string, stdout, stderr io.Writer, mods ...s
|
||||
return toolTest(p, args[1:], stdout, stderr, mods)
|
||||
case "docs":
|
||||
return docstool.Tool(p, args[1:], stdout, stderr, sdk.AdaptAll(mods)...)
|
||||
case "agent":
|
||||
return agent.Main(p, args[1:], stdout, stderr, agent.Options{Mods: mods, Run: AgentRunner(mods...)})
|
||||
case "mcp":
|
||||
return mcp.Main(p, args[1:], os.Stdin, stdout, stderr, agent.Options{Mods: mods, Run: AgentRunner(mods...)})
|
||||
}
|
||||
fmt.Fprintf(stderr, "unknown tool %q\n", args[0])
|
||||
return 2
|
||||
|
||||
@@ -222,6 +222,7 @@ function run() {
|
||||
}
|
||||
|
||||
function startBench(count) {
|
||||
gamePane.hidden = false;
|
||||
const res = call("bench", { count, animate: true });
|
||||
if (!res.started) {
|
||||
setStatus(res.error || "bench did not start", true);
|
||||
@@ -283,6 +284,7 @@ function syncScroll() {
|
||||
function selectExample(ex) {
|
||||
current = ex;
|
||||
framesLabel.hidden = !!(ex && ex.scene);
|
||||
gamePane.hidden = !!(ex && !ex.scene);
|
||||
if (!pollTimer) {
|
||||
gameHint.hidden = false;
|
||||
gameHint.textContent = ex && ex.scene ? "Press Run to play this scene" : "Console example: output goes to Console";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user