58 Commits
Author SHA1 Message Date
sanspie 9da9851538 docs: description and Open Graph title on every page and the playground
ci / checks (push) Successful in 14m32s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m14s
ci / release (push) Successful in 19m26s
2026-10-21 19:20:52 +03:00
sanspie 4fd1e11741 docs: download buttons for the visitor's OS from the latest release
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 15m54s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m37s
ci / release (push) Successful in 19m37s
2026-10-21 19:05:12 +03:00
sanspie 8f9a0299dc depcheck: finish the wasm fixture rename to modules/accessibility
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 19m28s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m22s
ci / release (push) Successful in 20m16s
2026-10-21 18:36:14 +03:00
sanspie ca3eedf9b0 docs: tell Dark Reader the site has its own dark theme
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 13m37s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / release (push) Skipped
ci / checks (push) Failing after 12m24s
2026-10-21 17:17:03 +03:00
sanspie 4f0662eb3b docs: highlighted inline code looks like other inline code and wraps cleanly
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Failing after 5m55s
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 10m29s
ci / release (push) Skipped
2026-10-21 17:12:02 +03:00
sanspie 9abf5c75ab playground: hide the game view for console examples
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Failing after 10m20s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci / release (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 12m23s
2026-10-21 17:07:27 +03:00
sanspie 96fef1f6a8 docs: real-export nav golden without the unreleased audio module 2026-10-21 17:06:11 +03:00
sanspie 6230d30620 docs: light code theme, reference icons for script packages and settings, no code ligatures
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Failing after 10m39s
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 11m41s
ci / release (push) Skipped
2026-10-21 17:00:56 +03:00
sanspie 91ea202e74 docs: inline CSS so Firefox paints dark at once, header on the page grid, readable fields table
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m28s
ci / checks (push) Successful in 17m7s
ci / release (push) Successful in 27m25s
2026-10-21 16:34:01 +03:00
sanspie 6128c86685 docs: editor icons back, editor theme colors, no white flash or sidebar jump on load
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 16m46s
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 14m12s
ci / release (push) Skipped
2026-10-21 16:21:51 +03:00
sanspie 2be6ce1d64 docs: self-host IBM Plex Sans and JetBrains Mono
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 14m44s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 16m44s
ci / release (push) Successful in 19m19s
2026-10-21 16:12:07 +03:00
sanspie 2e664ae508 docs: skip image-only lines when picking a page summary
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 15m19s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m41s
ci / release (push) Successful in 19m50s
2026-10-21 16:05:34 +03:00
sanspie aab6e0b0c9 docs: quieter site design, flat nav for single-page groups, drawer on mobile, light theme toggle
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci / checks (push) Successful in 16m48s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 18m53s
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / release (push) Successful in 22m38s
2026-10-21 15:57:53 +03:00
sanspie 9ba3e1631f docs: Try it demos for every component, with scripts, prefabs and in-memory saves
ci / release (push) Successful in 36m23s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 21m16s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 23m8s
2026-10-21 13:11:16 +03:00
sanspie 4068e00375 install: read SHA256SUMS.txt first and fetch the archive by its checksum, past stale proxy caches
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 15m44s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m13s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / release (push) Successful in 17m39s
2026-10-21 03:56:04 +03:00
sanspie 914cf39150 release: Linux files and checksums first, keep going when a Windows upload fails
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / release (push) Successful in 20m29s
ci / checks (push) Successful in 14m57s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 16m18s
2026-10-21 02:37:55 +03:00
sanspie dede4d8af3 release: record upload failures in the release body
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 13m53s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 15m0s
ci / release (push) Failing after 15m2s
2026-10-21 02:32:02 +03:00
sanspie b6dd46f523 Stage 10a follow-up: camera scene tests, rewind check, value Adjust
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 14m14s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 16m1s
ci / release (push) Failing after 14m38s
- camera.NewService and SetAccessibility; AttachWorld wires the accessibility settings
- tests/camera: priorities, blends, cuts, zones, follow, bounds, outputs, shake, zero allocations, BrainStep benchmark
- integration: rewind restores the camera state across a zone change
- camera2d.Adjuster passes the view by value (no per-frame allocation), D304 updated
- modules/camera/module.mk: test-camera in CI, bench-camera
2026-10-21 01:32:28 +03:00
sanspie 9d32052624 release: one SHA256SUMS.txt and a zipped Windows installer, the attachment types Gitea accepts
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / release (push) Failing after 14m32s
ci / checks (push) Successful in 14m14s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 16m8s
2026-10-21 01:31:53 +03:00
sanspie e636801578 release: upload straight to the Gitea server with retries
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / release (push) Failing after 14m22s
ci / checks (push) Successful in 14m30s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m14s
2026-10-21 00:29:58 +03:00
sanspie 01fdeef7a8 Aego 0.9.0-beta.1
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 14m2s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 16m23s
ci / release (push) Failing after 14m1s
2026-10-20 23:57:45 +03:00
sanspie 71354f6a60 install scripts: fall back to nightly, explain when nothing is published 2026-10-20 23:55:52 +03:00
sanspie 8ae6832503 edge sync: keep the destination when resuming downloads 2026-10-20 20:11:47 +03:00
sanspie 183d75ed50 gendocs: nav golden without the editor-only tour 2026-10-20 20:07:22 +03:00
sanspie 19edcbf9b2 render3d in the web sandbox without the OBJ importer; 3D try scenes select a shape 2026-10-20 19:49:47 +03:00
sanspie 8ed77a7638 Docs: no favicon request 2026-10-20 19:48:24 +03:00
sanspie ca3c9bf2e7 Docs: 3D Try it scenes, render3d in the sandbox, smarter code language detection 2026-10-20 19:43:38 +03:00
sanspie db59f117a4 deploy: short cache for the root page; edge notes in D303 2026-10-20 19:35:50 +03:00
sanspie 0d2c20da26 Docs: revision in the header, content-versioned assets; deploy passes the commit to the image 2026-10-20 19:31:14 +03:00
sanspie dd41ff15a5 deploy: edge serves the site and wasm from its own copy 2026-10-20 19:12:22 +03:00
sanspie 599e09a35d Docs: Try it on Physics 2D guide pages 2026-10-20 18:26:35 +03:00
sanspie 60614938be Release: Windows installer, install.ps1, component page layout 2026-10-20 18:20:18 +03:00
sanspie a60ca47d31 wip release windows 2026-10-20 18:13:57 +03:00
sanspie 00ae2418a0 Docs: more editor screenshots for the editor, prefab, tags, input, time and physics guides 2026-10-20 17:57:58 +03:00
sanspie 8fc9b4c287 Docs: highlighting for every code block, editor-only tour off the site, page icons, prefetch, field docs for physics, wider Try it, demo camera 2026-10-20 17:50:21 +03:00
sanspie b29934b51d deploy: short HTML cache the edge proxy can store 2026-10-20 17:29:55 +03:00
sanspie c475b42e5d gendocs: Script API index links to type pages; nav golden 2026-10-20 17:01:11 +03:00
sanspie b103816400 websmoke: Try smoke checks the scene tree 2026-10-20 17:00:46 +03:00
sanspie 487170dd9c depcheck: tween in the web sandbox 2026-10-20 16:46:52 +03:00
sanspie 57579356d6 Docs: streamed compile with tee'd progress, tween in the sandbox, console runs step module systems 2026-10-20 16:46:03 +03:00
sanspie d5377410e4 Docs: HTTP cache for the docs runtime, autostart after a first start, cheaper warm loads 2026-10-20 16:29:16 +03:00
sanspie f6c83a6e0c Docs: compile-checked examples for modules outside the sandbox, HasScript fix 2026-10-20 16:03:58 +03:00
sanspie 8d7712d671 Docs: runnable Script API module examples and Run it blocks 2026-10-20 16:03:26 +03:00
sanspie 63980faccb Docs: diagnostics with checked examples 2026-10-20 16:00:13 +03:00
sanspie 0041e7e639 Docs: loader streams compile from the original response 2026-10-20 15:59:50 +03:00
sanspie e334be569f Architecture: D301–D303, amendments for the docs overhaul 2026-10-20 15:49:46 +03:00
sanspie b488958444 Docs panel goldens with short-name links 2026-10-20 15:48:31 +03:00
sanspie 5875a65193 Docs: Try it for every 2D physics component, script examples on component pages, Script API index, smooth wheel scrolling on the web 2026-10-20 15:46:33 +03:00
sanspie 6ab66cb8ae Docs: scene tree, loading progress and cache, release builds, install page, editor screenshots 2026-10-20 15:36:03 +03:00
sanspie f8fe54e9c5 Docs: short-name cross references in the site and the editor docs panel, links from code 2026-10-20 15:17:10 +03:00
sanspie e4326e7e7c Docs: editor icons, editor highlighter, redesigned site and navigation 2026-10-20 15:14:55 +03:00
sanspie 6a22396467 Stage 9 complete
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 22m2s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 23m5s
2026-10-20 14:06:22 +03:00
sanspie 21e03128a8 Stage 9.6d2: MCP server and external edits in the editor (D297, D300) 2026-10-20 13:56:45 +03:00
sanspie e5b1964f3e Stage 9.6d1: agent layer and CLI (D299) 2026-10-20 13:48:01 +03:00
sanspie cffa66514b Stage 9.6c2: prefab apply and data editing without the editor (D298)
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 21m2s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 22m8s
2026-10-20 13:05:10 +03:00
sanspie 2744cff481 Stage 9.6c1: project refs and refactorings without the editor (D298)
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 23m31s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 21m57s
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
2026-10-20 12:12:18 +03:00
sanspie 91052a54db make lint: staticcheck the editor build
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 16m26s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m48s
2026-10-20 12:06:56 +03:00
sanspie 7f4d87ef2c Stage 9: gameplay toolkit, export and web, agent tooling 2026-10-20 12:06:56 +03:00
350 changed files with 26343 additions and 2380 deletions
+1
View File
@@ -5,3 +5,4 @@ site
exports
**/.aego
*.test
.claude
+37 -1
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
+26 -3
View File
@@ -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
+146
View File
File diff suppressed because it is too large Load Diff
+85
View File
@@ -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
+194
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+180
View File
File diff suppressed because it is too large Load Diff
+66
View File
@@ -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
}
+237
View File
File diff suppressed because it is too large Load Diff
+393
View File
File diff suppressed because it is too large Load Diff
+166
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -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()}
}
+1 -1
View File
@@ -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()}
}
+3
View File
@@ -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)
}
+10
View File
@@ -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() }},
}
}
+61
View File
@@ -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
}
+62
View File
@@ -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)
}
+33
View File
@@ -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
}
+15 -1
View File
@@ -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)
}
}
+20 -4
View File
@@ -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
View File
@@ -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)
}
+6 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+16 -4
View File
@@ -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
View File
@@ -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 {
+9
View File
@@ -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
}
+3
View File
@@ -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
}
+2 -6
View File
@@ -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)
}
+1 -1
View File
@@ -42,7 +42,7 @@ func TestColliderFitsSprite(t *testing.T) {
}
for _, name := range []string{"Crate", "Bare"} {
id := es.ID(es.Find(es.Root(), name))
if err := d.History.Exec(&commands.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)
}
}
+2 -6
View File
@@ -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)
}
+155
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -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>
+31
View File
@@ -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")
}
}
+65
View File
@@ -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, "; ")
}
+27
View File
@@ -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)
}
}
+1
View File
@@ -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
View File
@@ -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
}
+36 -10
View File
@@ -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() {
+10 -4
View File
@@ -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")
}
+43
View File
@@ -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
}
+65
View File
@@ -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
}
+1 -1
View File
@@ -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
}
+2
View File
@@ -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
+7
View File
@@ -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
+5
View File
@@ -4,6 +4,11 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Aego Playground</title>
<meta name="description" content="Write Aego Script in the browser and run it: console examples and small scenes, with problems and output next to the code.">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Aego">
<meta property="og:title" content="Aego Playground">
<meta property="og:description" content="Write Aego Script in the browser and run it: console examples and small scenes, with problems and output next to the code.">
<link rel="icon" href="data:,">
<link rel="stylesheet" href="play.css?v=dev">
</head>

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