Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||
|
||||
+115
-8
File diff suppressed because one or more lines are too long
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,12 +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/scripting"
|
||||
"aego/modules/tween"
|
||||
"aego/script/stdlib"
|
||||
"aego/script/vm"
|
||||
)
|
||||
|
||||
func Catalog() assembly.Catalog {
|
||||
@@ -22,5 +26,18 @@ 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() }},
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
SandboxBudget = 2_000_000
|
||||
SandboxHeap = 64 << 20
|
||||
SandboxNodes = 10_000
|
||||
)
|
||||
|
||||
func SandboxVM() vm.Options {
|
||||
return vm.Options{Budget: SandboxBudget, MaxHeap: SandboxHeap}
|
||||
}
|
||||
|
||||
@@ -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,7 @@ import (
|
||||
"aego/script/workspace"
|
||||
)
|
||||
|
||||
func build(p *project.Project, mods []module.Module) (*docs.Registry, *workspace.Workspace, error) {
|
||||
func build(p *project.Project, mods []module.Module, diagnose func(string, *docs.DiagInfo)) (*docs.Registry, *workspace.Workspace, error) {
|
||||
types, err := assembly.NewTypes()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -41,6 +42,7 @@ func build(p *project.Project, mods []module.Module) (*docs.Registry, *workspace
|
||||
return nil, nil, err
|
||||
}
|
||||
src := docs.SourcesOf(bm, types.Schema(), man)
|
||||
src.Diagnose = diagnose
|
||||
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 +63,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.
|
||||
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, diagnoser(pg))
|
||||
return r, err
|
||||
}
|
||||
|
||||
@@ -84,9 +96,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"
|
||||
|
||||
+93
-26
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package docstool
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"aego/app/playground"
|
||||
"aego/engine/docs"
|
||||
)
|
||||
|
||||
const tryFrames = 60
|
||||
|
||||
func CheckTry(reg *docs.Registry, w io.Writer) int {
|
||||
pg, err := playground.New()
|
||||
if err != nil {
|
||||
fmt.Fprintln(w, "try: cannot start the docs runtime:", err)
|
||||
return 1
|
||||
}
|
||||
pg.SetGame(playground.GameConfig{Headless: true, FrameTime: 1.0 / 60})
|
||||
defer func() {
|
||||
if g := pg.Game(); g != nil {
|
||||
g.Close()
|
||||
}
|
||||
}()
|
||||
sandbox := map[string]bool{}
|
||||
for _, e := range pg.TryScenes() {
|
||||
sandbox[e.ID] = true
|
||||
}
|
||||
bad := 0
|
||||
for _, e := range reg.All() {
|
||||
if e.Try == "" {
|
||||
continue
|
||||
}
|
||||
var msgs []string
|
||||
add := func(msg string) {
|
||||
for _, m := range msgs {
|
||||
if m == msg || strings.Contains(m, msg) || strings.Contains(msg, m) {
|
||||
return
|
||||
}
|
||||
}
|
||||
msgs = append(msgs, msg)
|
||||
}
|
||||
switch res := pg.PlayScene(e.ID, []byte(e.Try)); {
|
||||
case !sandbox[e.ID]:
|
||||
add("belongs to a module the docs runtime does not include")
|
||||
case !res.Started:
|
||||
add("does not load: " + res.Error)
|
||||
default:
|
||||
if err := pg.Game().Step(tryFrames); err != nil {
|
||||
add("stops the engine: " + err.Error())
|
||||
}
|
||||
}
|
||||
poll := pg.Poll()
|
||||
for _, l := range poll.Log {
|
||||
if l.Level != "INFO" && l.Level != "DEBUG" {
|
||||
add(l.Text)
|
||||
}
|
||||
}
|
||||
for _, f := range poll.Faults {
|
||||
add(f.Code + " " + f.Message)
|
||||
}
|
||||
for _, m := range msgs {
|
||||
fmt.Fprintf(w, "%s: error: try scene %s\n", e.ID, m)
|
||||
}
|
||||
bad += len(msgs)
|
||||
}
|
||||
pg.Stop()
|
||||
return bad
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
||||
//go:build aego_editor
|
||||
|
||||
package docsview
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"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"`
|
||||
}
|
||||
|
||||
type Reply struct {
|
||||
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 {
|
||||
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()), 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 {
|
||||
out, err := json.Marshal(a.Call(method, []byte(payload)))
|
||||
if err != nil {
|
||||
return `{"error":"cannot encode the reply"}`
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
//go:build aego_editor
|
||||
|
||||
package docsview
|
||||
|
||||
import (
|
||||
playmods "aego/app/assembly/playground"
|
||||
"aego/core/log"
|
||||
"aego/editor/core/context"
|
||||
"aego/engine"
|
||||
"aego/engine/module"
|
||||
"aego/scene"
|
||||
"aego/script/bind"
|
||||
"aego/script/bytecode"
|
||||
scriptruntime "aego/script/runtime"
|
||||
)
|
||||
|
||||
type scriptHost struct {
|
||||
reg *bind.Registry
|
||||
types *scene.Types
|
||||
man *module.Manifest
|
||||
log *log.Logger
|
||||
mods []*bytecode.Module
|
||||
}
|
||||
|
||||
func (h *scriptHost) Errors() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (h *scriptHost) Attach(w *engine.World, s *scene.Scene) (func(), error) {
|
||||
rt := scriptruntime.New(scriptruntime.Options{Bindings: h.reg, Schema: h.types.Schema(), Log: h.log, VM: playmods.SandboxVM()})
|
||||
if err := rt.Link(h.mods); err != nil {
|
||||
rt.Close()
|
||||
return nil, err
|
||||
}
|
||||
w.SetService(scriptruntime.ServiceID, rt)
|
||||
if err := rt.Instantiate(s); err != nil {
|
||||
w.RemoveService(scriptruntime.ServiceID)
|
||||
rt.Close()
|
||||
return nil, err
|
||||
}
|
||||
detach, err := h.man.Attach(&module.AttachContext{World: w, Scene: s, Log: h.log, Editor: true, Play: true})
|
||||
if err != nil {
|
||||
w.RemoveService(scriptruntime.ServiceID)
|
||||
rt.Close()
|
||||
return nil, err
|
||||
}
|
||||
return func() {
|
||||
detach()
|
||||
w.RemoveService(scriptruntime.ServiceID)
|
||||
rt.Close()
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *scriptHost) Problems() ([]context.ScriptProblem, uint64) {
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
func (h *scriptHost) Stats() (context.ScriptStats, bool) {
|
||||
return context.ScriptStats{}, false
|
||||
}
|
||||
|
||||
func (h *scriptHost) Status() (uint64, bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (h *scriptHost) Watch(*engine.World, scene.NodeHandle, string, string, int) (string, float64, bool, bool) {
|
||||
return "", 0, false, false
|
||||
}
|
||||
|
||||
var _ context.ScriptHost = (*scriptHost)(nil)
|
||||
@@ -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
+17
-2
@@ -67,6 +67,17 @@ func (j *job) toolchain() error {
|
||||
if _, err := exec.LookPath("go"); err != nil {
|
||||
return errs.New(errs.BuildFailed, "export needs the Go toolchain on this machine; the exported game does not, see docs/export.md")
|
||||
}
|
||||
if j.t.Web() {
|
||||
root, err := goRoot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := wasmExecPath(root); err != nil {
|
||||
return err
|
||||
}
|
||||
j.goroot = root
|
||||
return nil
|
||||
}
|
||||
cc := j.cc()
|
||||
if !j.t.Host() && cc == "" {
|
||||
return errs.New(errs.BuildFailed, "exporting to "+j.t.String()+" from "+runtime.GOOS+"/"+runtime.GOARCH+
|
||||
@@ -164,8 +175,12 @@ func (j *job) build() error {
|
||||
args = append(args, pkg)
|
||||
cmd := exec.Command("go", args...)
|
||||
cmd.Dir = root
|
||||
cmd.Env = append(os.Environ(), "GOOS="+j.t.OS, "GOARCH="+j.t.Arch, "CGO_ENABLED=1")
|
||||
if cc := j.cc(); cc != "" {
|
||||
cgo := "CGO_ENABLED=1"
|
||||
if j.t.Web() {
|
||||
cgo = "CGO_ENABLED=0"
|
||||
}
|
||||
cmd.Env = append(os.Environ(), "GOOS="+j.t.OS, "GOARCH="+j.t.Arch, cgo)
|
||||
if cc := j.cc(); cc != "" && !j.t.Web() {
|
||||
cmd.Env = append(cmd.Env, "CC="+cc)
|
||||
}
|
||||
if j.t.OS == "darwin" {
|
||||
|
||||
+18
-9
@@ -70,6 +70,7 @@ func (j *job) data() error {
|
||||
var total int64
|
||||
count := 0
|
||||
var failed error
|
||||
j.files = j.files[:0]
|
||||
j.snap.idx.Each(func(e *asset.Entry) {
|
||||
if failed != nil || e.Missing || e.Error != "" || exportSkip(e.Kind) {
|
||||
return
|
||||
@@ -98,6 +99,7 @@ func (j *job) data() error {
|
||||
}
|
||||
written[name] = true
|
||||
names = append(names, name)
|
||||
j.files = append(j.files, webFile{Path: bundle.ArtifactsDir + "/" + name, Size: int64(len(raw))})
|
||||
s.Bytes += int64(len(raw))
|
||||
total += int64(len(raw))
|
||||
}
|
||||
@@ -120,7 +122,9 @@ func (j *job) data() error {
|
||||
if err := os.WriteFile(filepath.Join(j.l.data, bundle.ManifestFile), raw, 0o644); err != nil {
|
||||
return errs.Wrap(err, errs.VFSIO, "cannot write the data manifest")
|
||||
}
|
||||
j.manifestSize = int64(len(raw))
|
||||
slices.Sort(names)
|
||||
slices.SortFunc(j.files, func(a, b webFile) int { return strings.Compare(a.Path, b.Path) })
|
||||
h := sha256.New()
|
||||
h.Write([]byte(strings.Join(names, "\n")))
|
||||
h.Write(raw)
|
||||
@@ -147,7 +151,7 @@ func (j *job) manifest() error {
|
||||
p := j.p
|
||||
x := &bundle.Export{
|
||||
Engine: engine.Version, Game: p.Name, GameVersion: GameVersion(p), Module: p.Module,
|
||||
Target: j.t.String(), Profile: j.profile, Binary: j.rep.Binary, BinarySize: j.rep.BinarySize,
|
||||
Target: j.t.String(), Profile: j.profile, Binary: j.rep.Binary, BinarySize: j.rep.BinarySize, BinaryGzip: j.rep.BinaryGzip,
|
||||
StartScene: p.MainScene, Modules: j.rep.Modules, Libs: j.libs(), Data: j.rep.Data,
|
||||
DataSize: j.rep.DataSize, Artifacts: j.rep.Artifacts, Hash: j.rep.Hash,
|
||||
BuildMillis: time.Since(j.start).Milliseconds(), Time: time.Now().UTC().Format(time.RFC3339),
|
||||
@@ -169,19 +173,24 @@ func (j *job) sdlLicense() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (j *job) libs() []bundle.Lib {
|
||||
lib := bundle.Lib{Name: "SDL3", Link: "static"}
|
||||
src := j.sdlLicense()
|
||||
func (j *job) license(lib bundle.Lib, src, name string) bundle.Lib {
|
||||
if src == "" {
|
||||
return []bundle.Lib{lib}
|
||||
return lib
|
||||
}
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return []bundle.Lib{lib}
|
||||
return lib
|
||||
}
|
||||
dst := filepath.Join(j.out, "licenses", "SDL3.txt")
|
||||
dst := filepath.Join(j.out, "licenses", name)
|
||||
if os.MkdirAll(filepath.Dir(dst), 0o755) == nil && os.WriteFile(dst, data, 0o644) == nil {
|
||||
lib.License = "licenses/SDL3.txt"
|
||||
lib.License = "licenses/" + name
|
||||
}
|
||||
return []bundle.Lib{lib}
|
||||
return lib
|
||||
}
|
||||
|
||||
func (j *job) libs() []bundle.Lib {
|
||||
if j.t.Web() {
|
||||
return []bundle.Lib{j.license(bundle.Lib{Name: "Go wasm_exec.js", Link: "script", File: webExec}, filepath.Join(j.goroot, "LICENSE"), "Go.txt")}
|
||||
}
|
||||
return []bundle.Lib{j.license(bundle.Lib{Name: "SDL3", Link: "static"}, j.sdlLicense(), "SDL3.txt")}
|
||||
}
|
||||
|
||||
+36
-15
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,8 @@ func (j *job) pack() error {
|
||||
return j.packLinux()
|
||||
case "darwin":
|
||||
return j.packDarwin()
|
||||
case "js":
|
||||
return j.packWeb()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ type Report struct {
|
||||
Out string `json:"out"`
|
||||
Binary string `json:"binary,omitempty"`
|
||||
BinarySize int64 `json:"binarySize"`
|
||||
BinaryGzip int64 `json:"binaryGzip,omitempty"`
|
||||
Data []bundle.Size `json:"data"`
|
||||
DataSize int64 `json:"dataSize"`
|
||||
Artifacts int `json:"artifacts"`
|
||||
@@ -70,7 +71,11 @@ func Bytes(n int64) string {
|
||||
func (r *Report) Lines() [][2]string {
|
||||
out := [][2]string{{"Target", r.Target}, {"Profile", r.Profile}, {"Output", r.Out}}
|
||||
if r.Binary != "" {
|
||||
out = append(out, [2]string{"Binary", r.Binary + " (" + Bytes(r.BinarySize) + ")"})
|
||||
s := r.Binary + " (" + Bytes(r.BinarySize)
|
||||
if r.BinaryGzip > 0 {
|
||||
s += ", gzip " + Bytes(r.BinaryGzip)
|
||||
}
|
||||
out = append(out, [2]string{"Binary", s + ")"})
|
||||
}
|
||||
if r.Artifacts > 0 {
|
||||
out = append(out, [2]string{"Data", Bytes(r.DataSize) + " in " + strconv.Itoa(r.Artifacts) + " artifacts"})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
<!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>
|
||||
<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>
|
||||
<div id="loader" class="loader" role="status" aria-live="polite">
|
||||
<div class="title">{{.Title}}</div>
|
||||
<div class="progress"><div id="bar"></div></div>
|
||||
<div id="status">Loading…</div>
|
||||
<pre id="error" hidden></pre>
|
||||
</div>
|
||||
<button id="fullscreen" type="button" title="Fullscreen" aria-label="Fullscreen" hidden>⛶</button>
|
||||
<script src="wasm_exec.js?v={{.Build}}"></script>
|
||||
<script src="loader.js?v={{.Build}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
||||
package export
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"aego/project"
|
||||
"aego/runtime/bundle"
|
||||
)
|
||||
|
||||
var webTarget = Target{OS: "js", Arch: "wasm"}
|
||||
|
||||
func TestParseWeb(t *testing.T) {
|
||||
for _, s := range []string{"web", "WEB", "js/wasm", "wasm"} {
|
||||
tg, err := ParseTarget(s)
|
||||
if err != nil || !tg.Web() || tg.String() != "web" {
|
||||
t.Fatalf("%s: %+v %v", s, tg, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebLayout(t *testing.T) {
|
||||
p := &project.Project{Name: "My Game", Root: t.TempDir()}
|
||||
l := layoutFor(webTarget, "/out", p)
|
||||
if l.binary() != filepath.Join("/out", WebWasm) || l.data != filepath.Join("/out", bundle.DataDir) {
|
||||
t.Fatalf("layout %+v", l)
|
||||
}
|
||||
if !strings.HasSuffix(DefaultOut(p, webTarget, bundle.Release), "web-"+bundle.Release) {
|
||||
t.Fatal(DefaultOut(p, webTarget, bundle.Release))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackWeb(t *testing.T) {
|
||||
root, err := goRoot()
|
||||
if err != nil {
|
||||
t.Skip(err)
|
||||
}
|
||||
out := t.TempDir()
|
||||
wasm := bytes.Repeat([]byte("wasm"), 4096)
|
||||
j := &job{
|
||||
p: &project.Project{Name: `<Game & "Co">`, Module: "game"}, t: webTarget, out: out, goroot: root,
|
||||
rep: &Report{Hash: strings.Repeat("ab", 32), BinarySize: int64(len(wasm))},
|
||||
files: []webFile{{Path: "artifacts/a.tex", Size: 3}}, manifestSize: 8,
|
||||
}
|
||||
j.l = layoutFor(webTarget, out, j.p)
|
||||
if err := os.MkdirAll(j.l.data, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(j.l.binary(), wasm, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := j.packWeb(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
page, _ := os.ReadFile(filepath.Join(out, webIndex))
|
||||
if !strings.Contains(string(page), "<Game &") || strings.Contains(string(page), "<Game") {
|
||||
t.Fatal("the title is not escaped")
|
||||
}
|
||||
if !strings.Contains(string(page), `data-build="abababababab"`) {
|
||||
t.Fatal("the build key is missing")
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(j.l.data, WebFiles))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var list webList
|
||||
if err := json.Unmarshal(raw, &list); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if list.Wasm != int64(len(wasm)) || list.Manifest.Size != 8 || len(list.Files) != 1 || list.Files[0].Path != "artifacts/a.tex" {
|
||||
t.Fatalf("files.json %+v", list)
|
||||
}
|
||||
for _, f := range []string{webExec, webLoader, WebWasm + ".gz"} {
|
||||
if _, err := os.Stat(filepath.Join(out, f)); err != nil {
|
||||
t.Errorf("%s is missing", f)
|
||||
}
|
||||
}
|
||||
if j.rep.BinaryGzip <= 0 || j.rep.BinaryGzip >= j.rep.BinarySize {
|
||||
t.Fatalf("wasm gzip size %d of %d", j.rep.BinaryGzip, j.rep.BinarySize)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+14
-2
@@ -1,8 +1,11 @@
|
||||
package playground
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
playmods "aego/app/assembly/playground"
|
||||
|
||||
"aego/asset"
|
||||
"aego/asset/loader"
|
||||
"aego/core/log"
|
||||
@@ -99,6 +102,7 @@ type Game struct {
|
||||
bench bench
|
||||
running bool
|
||||
looping bool
|
||||
overflow bool
|
||||
}
|
||||
|
||||
type driver struct {
|
||||
@@ -195,7 +199,7 @@ func (g *Game) Engine() *engine.Engine {
|
||||
|
||||
func (g *Game) Play(name string, src []byte, mods []*bytecode.Module) error {
|
||||
g.Stop()
|
||||
rt := scriptruntime.New(scriptruntime.Options{Bindings: g.reg, Schema: g.types.Schema(), Log: g.log})
|
||||
rt := scriptruntime.New(scriptruntime.Options{Bindings: g.reg, Schema: g.types.Schema(), Log: g.log, VM: playmods.SandboxVM()})
|
||||
if err := rt.Link(mods); err != nil {
|
||||
rt.Close()
|
||||
return err
|
||||
@@ -225,7 +229,7 @@ func (g *Game) Play(name string, src []byte, mods []*bytecode.Module) error {
|
||||
return err
|
||||
}
|
||||
g.detach = d
|
||||
g.example, g.running, g.faults = name, true, 0
|
||||
g.example, g.running, g.faults, g.overflow = name, true, 0, false
|
||||
g.start()
|
||||
return nil
|
||||
}
|
||||
@@ -272,6 +276,10 @@ func (g *Game) update(dt float64) {
|
||||
g.frameMs += (ft - g.frameMs) * statsAlpha
|
||||
}
|
||||
g.bench.t += dt
|
||||
if g.scene != nil && !g.overflow && g.scene.Stats().Alive > playmods.SandboxNodes {
|
||||
g.overflow = true
|
||||
g.clog.Error(fmt.Sprintf("the scene grew past %d nodes, the game was stopped", playmods.SandboxNodes))
|
||||
}
|
||||
}
|
||||
|
||||
func hue(i, n int) mathx.Color {
|
||||
@@ -351,6 +359,10 @@ func (g *Game) newFaults() []Fault {
|
||||
}
|
||||
|
||||
func (g *Game) Poll() Poll {
|
||||
if g.overflow {
|
||||
g.overflow = false
|
||||
g.Stop()
|
||||
}
|
||||
p := Poll{Log: g.lines, Faults: g.newFaults()}
|
||||
g.lines = nil
|
||||
s := &p.Stats
|
||||
|
||||
@@ -109,3 +109,14 @@ func (p *Playground) Poll() Poll {
|
||||
func (p *Playground) LoseContext(lose bool) bool {
|
||||
return p.game != nil && p.game.LoseContext(lose)
|
||||
}
|
||||
|
||||
func (p *Playground) PlayScene(name string, src []byte) PlayResult {
|
||||
g, err := p.host()
|
||||
if err != nil {
|
||||
return PlayResult{Error: err.Error()}
|
||||
}
|
||||
if err := g.Play(name, src, nil); err != nil {
|
||||
return PlayResult{Error: err.Error()}
|
||||
}
|
||||
return PlayResult{Started: true}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,86 @@
|
||||
package playground
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"aego/script/vm"
|
||||
)
|
||||
|
||||
const loopKick = `package boxes
|
||||
|
||||
script Kick {
|
||||
func Update(dt float) {
|
||||
n := 0
|
||||
for n >= 0 {
|
||||
n++
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const heapKick = `package boxes
|
||||
|
||||
script Kick {
|
||||
func Update(dt float) {
|
||||
s := "ab"
|
||||
for s != "" {
|
||||
s = s + s
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const saveScript = `package demo
|
||||
|
||||
import "aego/save"
|
||||
|
||||
script S {
|
||||
func Ready() {
|
||||
save.Save("slot")
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func faultOf(t *testing.T, p *Playground, code string) Fault {
|
||||
t.Helper()
|
||||
res := p.Play(ScenePrefix+"boxes", map[string]string{"main.aes": code})
|
||||
if !res.Started {
|
||||
t.Fatalf("did not start: %s %+v", res.Error, res.Diagnostics)
|
||||
}
|
||||
if err := p.Game().Step(3); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, f := range p.Poll().Faults {
|
||||
if f.Code == vm.CodeBudget {
|
||||
return f
|
||||
}
|
||||
}
|
||||
t.Fatal("the script was not stopped by the sandbox")
|
||||
return Fault{}
|
||||
}
|
||||
|
||||
func TestSandboxInstructionBudget(t *testing.T) {
|
||||
p := newHeadless(t)
|
||||
faultOf(t, p, loopKick)
|
||||
if !p.Poll().Stats.Running {
|
||||
t.Fatal("the game stopped with the script")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSandboxHeapLimit(t *testing.T) {
|
||||
p := newHeadless(t)
|
||||
if f := faultOf(t, p, heapKick); !strings.Contains(f.Message, "heap") {
|
||||
t.Fatalf("fault %+v", f)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSandboxHasNoSaves(t *testing.T) {
|
||||
p := newHeadless(t)
|
||||
for _, d := range p.Compile(map[string]string{"main.aes": saveScript}) {
|
||||
if d.Severity == "error" {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("aego/save compiled in the sandbox")
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user