13 Commits
Author SHA1 Message Date
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
sanspie 6cc0673082 CI on master, docs auto-deploy and repo link, macOS build fixes
ci / checks (push) Successful in 13m36s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 14m40s
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
2026-10-16 12:10:43 +03:00
sanspie ab9b003457 Stage 8: physics and module boundaries 2026-10-15 19:16:09 +03:00
sanspie 41d949717d deploy: push-to-deploy docs site and playground
make site-headless builds the CLI without cgo (aego_nonative) and the
static site with the wasm playground. deploy/ holds the image, the
container nginx config (gzip_static with Vary), compose and deploy.sh
run by the post-receive hook on mainserv.

.gitignore ignores only the root site/ output; tools/gendocs/site was
never committed because of it.

Docs site: index and section summaries render code, bold and italic
instead of raw markdown; search.json gets the plain text. Page summaries
drop a leading list or quote marker. The nav toggle is three CSS bars
centred in a 32px button.

Playground: a loading overlay in the editor pane shows a spinner, a
progress bar and the downloaded size while aego.wasm streams in, then
'Starting engine' until the runtime is ready; a failed start keeps the
error on the overlay. docstool writes the wasm size and a version hash
into index.html; play.js, play.css, wasm_exec.js and aego.wasm are
referenced with ?v=<hash> so a deploy never mixes cached old files with
new ones.

Instruction budget: default 1M per callback (was 100k), playground 2M;
the donut needs ~120k per frame. Playground: software 3D donut example,
multi-line log entries render as monospace blocks, a fault is reported
once instead of also as a "script stopped" log line.

The compose project is named aego explicitly; the default name came from
the deploy/ directory and collided with other stacks on the host.
2026-10-14 22:40:20 +03:00
sanspie 69d19e501f Stage 7: documentation, help, playground; templates, aego new/bootstrap/icons, launcher
Documentation: engine/docs assembles one registry from the script
bindings, the schema, module docs and module-owned script paths; problems
are reported as warnings at build time. Module registries record doc
sources and script module owners; diag.Codes lists every diagnostic code.

Project bootstrap: project.defaultSpace ("2d"/"3d"), EnsureDirs creates
the default folders when the editor opens a project (reported in the
console), prefabs/ is a pipeline root. Native builds drop -trimpath so
binding locations resolve inside native/.

Editor files: create scene/prefab/script from menus, move/rename/duplicate
/delete with undo, script package retargeting on move, open scenes and
script panels follow renames, Save As / Rename Scene prompts, scene
picker, startup scene. Scoped shortcuts fall back to the global command
when disabled; Save As is Ctrl+Shift+S, Save All Ctrl+Alt+S.

Script editor: Go to Implementation (stub → native/ source), host
definitions always resolve to stubs, bracket match/mismatch states,
rainbow brackets (persisted), mono code font (ui-mono atlases, per-command
fonts in draw.List, PushFont/PopFont restored by Mark/Restore).

Tests: langsvc definition/implementation, bracket states, dispatch
fallback, script renames, font stack, editor file-op integration, golden
mono glyph and bracket references; depcheck rule for engine/docs.

Docs pipeline: engine, render2d and script/engine ship their own res/docs;
keyword hover text comes from the registry through manifest.Keywords;
project script symbols join the registry; aego docs export|check|build|
serve, --tool docs in native binaries, aego check --explain; app/docstool
and the tools/gendocs module (goldmark) build the static site.

Playground: app/playground + cmd/aego-playground (js/wasm, 3.3 MB gzip)
compile, highlight and run scripts in the browser; run blocks in the
language docs become examples. engine/builtin/headless is the module set
without render3d; workspace.Config.FS; StubFor/StubPaths; backends and the
script importer drop out of js builds.

VM: one frame pointer in the interpreter loop, direct host calls, panic
recovery once per run instead of per host call (Update2k -19%,
Schedule10k -24% against the previous head). bench-build/compare/profile
run pre-built, pinned, interleaved binaries.

Editor: structural bracket levels and current-block highlight, indent
guides, occurrence highlighting with write marks, multi-format clipboard
(copy/cut/paste assets, paste images as sprites or textures). gl33 probes
a renderable depth format and reports framebuffer failures by stage.

7 E:
- project templates empty/2d/2.5d/3d, app/projects as the single creation path (D148)
- launcher and recent projects (D149)
- orthographic Camera3D and Scene View (D150)
- reference states in the Inspector, ids.Short (D151)
- sceneio remaps node references when loading without KeepIDs (D152)
- exported ComponentRef<T> (D153)
- aego icons via module.Audit (D146)
2026-10-13 01:08:56 +03:00
sanspie cf68c71f05 Stage 6: coroutines, signals, ScriptRef, workspace CLI, editor tooling
Language: yield/signal/coroutine become keywords, start is contextual.
Coroutine methods (func Name() coroutine) with start and
yield / wait / frames / fixed / await; signals with Emit/Connect and
Connection handles; ScriptRef<T> and node.GetScript<T>() for fields,
plain methods and signals of other scripts. Diagnostics AES2050-AES2059.

Bytecode v3: ScriptRefs, XFields, XMethods and Connects tables resolved by
name in vm.Link, per-script signals, coroutine flag on functions; new
opcodes START/YIELD/EMIT/CONNECT/DISCONNECT/CONNECTED/COSTOP/CORUN/
SELFREF/GETSCRIPT/GETXF/SETXF/CALLR, checked by verify.

Runtime: script/coroutine scheduler (frames/fixed counters, time heap,
await lists; only due coroutines are visited) and script/signal registry
(snapshot per Emit, deferred removal, no allocations in steady state).
Hot reload cancels coroutines of changed scripts, rebinds connections by
name and signature, then runs Reloaded.

Tooling: script/build removed, aego check/test/dump run on the workspace
(test builds include _test.aes); langsvc completion, hover, signatures and
semantic tokens for the new syntax; editor highlighting, snippets with
placeholders, coroutine/signal profiler graphs and stats.

Docs (script.md sections 16-19, editor.md, ARCHITECTURE), test fixtures and
golden files, platformer enemies wired to the player, scriptreload demo,
and run.Config.FrameTime for deterministic headless fixed steps.

Modules (6.5): engine/module registry and ComponentBuilder are the one
registration path for engine subsystems and native code; render2d,
render3d and script/engine are modules over scene/frame, the script
component moved to scene/scriptcomp, and scene/components and
scene/systems are gone. Editor inspector, templates, picking shapes and
extension hooks are driven by descriptors and editor/builtins; sdk gains
Component/EditorHook; depcheck checks the module graph and the
aego_runtime graph.

Also: stop tracking built tool binaries; gl33 activates the scratch unit
before binding for texture updates.

Verification: typed component method adapters (Method0..2, Proc0..2)
with direct field access and no allocations; app.Build as the single
construction path for run and the editor; modules attach per scene
document; depcheck aego_editor pass; diagnostic codes in one place and
every code covered by a test; indexed coroutine heaps with eager
cancel; dead component references and nil intrinsics fault with
AES6004, host faults keep their code; GC roots of struct fields checked
in verify and vm.Link; 255-field limit; AES4010 for script packages
shadowing host modules; architecture registry D111-D147.

Golden: the harness renders on the thread that owns the GL context,
ReadPixels returns rows in memory order (render targets are top-down per
D17), and the first rendering references are committed.
2026-10-12 14:22:08 +03:00
sanspie 08f0b15e1e docs: update architecture spec and script reference 2026-10-08 13:31:21 +03:00
sanspie 17cb5e4291 Stage 2-5: assets, scene/ECS, editor, 3D, and Aego Script
Stage 2: render pipeline and UI toolkit over gpu/platform.
Stage 3: schema registry, asset pipeline + runtime loader, scene graph and ECS.
Stage 4: editor — docking, inspector, scene/game views, play mode, asset browser, console, profiler; 4.5 adds the 3D foundation (mixed 2D/3D spaces, meshes, depth).
Stage 5: Aego Script — compiler (lexer/parser/types/resolver/checker/asm/compiler), bytecode with verifier and register VM, instance runtime, stdlib, native bindings, and editor/CLI integration (aego run/edit/check/test/dump).
2026-10-08 13:17:28 +03:00
sanspie eb9ac1e589 Initial commit 2026-09-26 16:32:40 +03:00
sanspie dae4bc19e1 Stage 1: engine core, platform, gpu, vfs
Foundation for the aego engine.

core/    errs, log, handle arena, mathx, clock, stats
platform SDL3 backend plus a headless one, input state, events
gpu      device abstraction with gl33 and null backends
vfs      scheme-based virtual filesystem with overlay mounts
engine   builder, module registry, phase-ordered system loop, jobs
demos    basic, resize, input, headless; sandbox driver

OpenGL bindings are generated into gpu/gl33/gl by tools/gen-gl.sh,
so the module has no external dependencies.
2026-09-18 20:51:54 +03:00
1052 changed files with 75503 additions and 3871 deletions
+11 -1
View File
@@ -94,4 +94,14 @@ 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
- name: export packages
run: go test -count=1 ./runtime/... ./vfs/exportfs ./app/export/...
- name: export and run headless
run: go test -count=1 -run 'TestExport' -v ./tests/integration
- name: editor app bundle
if: runner.os == 'macOS'
run: |
make editor-app
plutil -lint ".cache/Aego Editor.app/Contents/Info.plist"
codesign --verify --deep ".cache/Aego Editor.app"
+6
View File
@@ -11,4 +11,10 @@ tools/genicons/genicons
/aego
/aego-editor
/aego-playground
/aego-runtime
*.syso
*.test
/tests/golden/testdata/actual-*
tools/websmoke/node_modules/
tools/websmoke/golden/actual-*.png
+189 -40
View File
File diff suppressed because one or more lines are too long
+91 -7
View File
File diff suppressed because it is too large Load Diff
-2
View File
@@ -8,7 +8,6 @@ import (
"aego/core/log"
"aego/engine/base"
"aego/engine/module"
"aego/platform/input"
"aego/scene"
"aego/script/bind"
)
@@ -16,7 +15,6 @@ import (
type Options struct {
Seed uint64
Testing bool
Input *input.AxisConfig
}
type Entry struct {
+54 -3
View File
@@ -4,10 +4,20 @@ import (
"aego/app/assembly"
"aego/engine/base"
"aego/engine/module"
"aego/modules/accessibility"
"aego/modules/audio"
"aego/modules/dataasset"
"aego/modules/debug"
"aego/modules/display"
inputmod "aego/modules/input"
"aego/modules/physics"
"aego/modules/render2d"
"aego/modules/render3d"
"aego/modules/savegame"
"aego/modules/scripting"
settingsmod "aego/modules/settings"
"aego/modules/spawn"
"aego/modules/tween"
"aego/script/stdlib"
)
@@ -15,6 +25,10 @@ func core() assembly.Entry {
return assembly.Entry{ID: base.CoreID, New: func(assembly.Options) module.Module { return base.Core{} }}
}
func inputEntry() assembly.Entry {
return assembly.Entry{ID: inputmod.ID, New: func(assembly.Options) module.Module { return inputmod.New() }}
}
func render2dEntry() assembly.Entry {
return assembly.Entry{ID: render2d.ID, New: func(assembly.Options) module.Module { return render2d.New() }}
}
@@ -25,7 +39,7 @@ func render3dEntry() assembly.Entry {
func scriptEntry() assembly.Entry {
return assembly.Entry{ID: scripting.ID, New: func(o assembly.Options) module.Module {
return scripting.New(stdlib.Options{Seed: o.Seed, Testing: o.Testing, Input: o.Input})
return scripting.New(stdlib.Options{Seed: o.Seed, Testing: o.Testing})
}}
}
@@ -33,6 +47,43 @@ func physicsEntry() assembly.Entry {
return assembly.Entry{ID: physics.ID, New: func(assembly.Options) module.Module { return physics.New() }}
}
func Default() assembly.Catalog {
return assembly.Catalog{core(), render2dEntry(), render3dEntry(), scriptEntry(), physicsEntry()}
func debugEntry() assembly.Entry {
return assembly.Entry{ID: debug.ID, New: func(assembly.Options) module.Module { return debug.New() }}
}
func displayEntry() assembly.Entry {
return assembly.Entry{ID: display.ID, New: func(assembly.Options) module.Module { return display.New() }}
}
func audioEntry() assembly.Entry {
return assembly.Entry{ID: audio.ID, New: func(assembly.Options) module.Module { return audio.New() }}
}
func accessibilityEntry() assembly.Entry {
return assembly.Entry{ID: accessibility.ID, New: func(assembly.Options) module.Module { return accessibility.New() }}
}
func settingsEntry() assembly.Entry {
return assembly.Entry{ID: settingsmod.ID, Requires: []string{inputmod.ID}, New: func(assembly.Options) module.Module { return settingsmod.New() }}
}
func tweenEntry() assembly.Entry {
return assembly.Entry{ID: tween.ID, Requires: []string{scripting.ID}, New: func(assembly.Options) module.Module { return tween.New() }}
}
func dataEntry() assembly.Entry {
return assembly.Entry{ID: dataasset.ID, New: func(assembly.Options) module.Module { return dataasset.New() }}
}
func spawnEntry() assembly.Entry {
return assembly.Entry{ID: spawn.ID, New: func(assembly.Options) module.Module { return spawn.New() }}
}
func savegameEntry() assembly.Entry {
return assembly.Entry{ID: savegame.ID, Requires: []string{scripting.ID, spawn.ID}, New: func(assembly.Options) module.Module { return savegame.New() }}
}
func Default() assembly.Catalog {
return assembly.Catalog{core(), dataEntry(), inputEntry(), render2dEntry(), render3dEntry(), scriptEntry(), physicsEntry(), debugEntry(),
displayEntry(), audioEntry(), accessibilityEntry(), settingsEntry(), tweenEntry(), spawnEntry(), savegameEntry()}
}
+1 -1
View File
@@ -3,5 +3,5 @@ package builtin
import "aego/app/assembly"
func Headless() assembly.Catalog {
return assembly.Catalog{core(), scriptEntry(), physicsEntry()}
return assembly.Catalog{core(), dataEntry(), inputEntry(), scriptEntry(), physicsEntry(), debugEntry(), displayEntry(), audioEntry(), accessibilityEntry(), settingsEntry(), tweenEntry(), spawnEntry(), savegameEntry()}
}
+7
View File
@@ -0,0 +1,7 @@
package builtin
import "aego/app/assembly"
func Runtime() assembly.Catalog {
return Default()
}
+5 -1
View File
@@ -24,6 +24,10 @@ func Manifest(user ...module.Module) (*manifest.Manifest, error) {
}
func AuditIcons(user ...module.Module) (*module.IconReport, error) {
return AuditIconsWith(nil, user...)
}
func AuditIconsWith(extra []module.IconUse, user ...module.Module) (*module.IconReport, error) {
types, err := assembly.NewTypes()
if err != nil {
return nil, err
@@ -32,7 +36,7 @@ func AuditIcons(user ...module.Module) (*module.IconReport, error) {
if err != nil {
return nil, err
}
return module.Audit(types, bind.NewManifest(), append(mods, user...)...)
return module.AuditUses(extra, types, bind.NewManifest(), append(mods, user...)...)
}
func IconSources() []string {
+7 -1
View File
@@ -2,6 +2,10 @@ package editors
import (
"aego/app/assembly"
"aego/modules/debug"
debugeditor "aego/modules/debug/editor"
inputmod "aego/modules/input"
inputeditor "aego/modules/input/editor"
"aego/modules/physics"
physicseditor "aego/modules/physics/editor"
"aego/modules/render2d"
@@ -12,7 +16,9 @@ import (
func With(c assembly.Catalog) assembly.Catalog {
return c.
With(inputmod.ID, inputeditor.Hook).
With(render2d.ID, render2deditor.Hook).
With(render3d.ID, render3deditor.Hook).
With(physics.ID, physicseditor.Hook)
With(physics.ID, physicseditor.Hook).
With(debug.ID, debugeditor.Hook)
}
+11
View File
@@ -44,3 +44,14 @@ func (h hooked) Shutdown() {
sd.Shutdown()
}
}
func (h hooked) Reconfigure(ctx *module.AttachContext) error {
if rc, ok := h.inner.(module.Reconfigurer); ok {
return rc.Reconfigure(ctx)
}
return nil
}
func (h hooked) Unwrap() module.Module {
return h.inner
}
+18 -1
View File
@@ -4,17 +4,34 @@ import (
"aego/app/assembly"
"aego/engine/base"
"aego/engine/module"
"aego/modules/debug"
inputmod "aego/modules/input"
"aego/modules/physics"
"aego/modules/render2d"
"aego/modules/scripting"
"aego/script/stdlib"
"aego/script/vm"
)
func Catalog() assembly.Catalog {
return assembly.Catalog{
{ID: base.CoreID, New: func(assembly.Options) module.Module { return base.Core{} }},
{ID: inputmod.ID, New: func(assembly.Options) module.Module { return inputmod.New() }},
{ID: render2d.ID, New: func(assembly.Options) module.Module { return render2d.New() }},
{ID: scripting.ID, New: func(o assembly.Options) module.Module {
return scripting.New(stdlib.Options{Seed: o.Seed, Testing: o.Testing, Input: o.Input})
return scripting.New(stdlib.Options{Seed: o.Seed, Testing: o.Testing})
}},
{ID: physics.ID, New: func(assembly.Options) module.Module { return physics.New() }},
{ID: debug.ID, New: func(assembly.Options) module.Module { return debug.New() }},
}
}
const (
SandboxBudget = 2_000_000
SandboxHeap = 64 << 20
SandboxNodes = 10_000
)
func SandboxVM() vm.Options {
return vm.Options{Budget: SandboxBudget, MaxHeap: SandboxHeap}
}
+48 -2
View File
File diff suppressed because it is too large Load Diff
+140
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
package crash
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestBundleHidesHome(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
root := filepath.Join(home, "game")
dir, err := Write(Bundle{Root: root, Kind: "panic", Where: "panel inspector", Panic: "boom in " + root, Stack: []byte(root + "/x.go:1")})
if err != nil {
t.Fatal(err)
}
for _, f := range []string{"info.aetext", "go.stack"} {
data, err := os.ReadFile(filepath.Join(dir, f))
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(data), home) || !strings.Contains(string(data), "~/game") {
t.Fatalf("%s is not sanitized: %s", f, data)
}
}
}
+114 -26
View File
File diff suppressed because it is too large Load Diff
+70
View File
@@ -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
}
+398
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+73
View File
@@ -0,0 +1,73 @@
//go:build aego_editor
package docsview
import (
"encoding/json"
"strconv"
"aego/editor/core/docsession"
)
type request struct {
Scene string `json:"scene"`
Select string `json:"select"`
Component string `json:"component"`
Field string `json:"field"`
Value float64 `json:"value"`
}
type Reply struct {
Text string `json:"text,omitempty"`
Error string `json:"error,omitempty"`
State *docsession.State `json:"state,omitempty"`
Log []Line `json:"log,omitempty"`
}
func (a *App) Call(method string, payload []byte) Reply {
var req request
if len(payload) > 0 {
if err := json.Unmarshal(payload, &req); err != nil {
return Reply{Error: err.Error()}
}
}
s := a.session
var err error
switch method {
case "load":
err = s.Load([]byte(req.Scene), req.Select)
case "reset":
err = s.Reset()
case "text":
case "play":
s.TogglePause()
case "undo":
err = s.Undo()
case "redo":
err = s.Redo()
case "set":
err = s.Set(req.Component, req.Field, req.Value)
case "state":
st := s.State()
return Reply{State: &st}
case "log":
out := Reply{Log: a.lines}
a.lines = nil
return out
default:
return Reply{Error: "unknown method " + strconv.Quote(method)}
}
if err != nil {
return Reply{Error: err.Error()}
}
st := s.State()
return Reply{Text: string(s.Text()), State: &st}
}
func (a *App) Reply(method, payload string) string {
out, err := json.Marshal(a.Call(method, []byte(payload)))
if err != nil {
return `{"error":"cannot encode the reply"}`
}
return string(out)
}
+70
View File
@@ -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)
+97
View File
@@ -0,0 +1,97 @@
package edit
import (
"strconv"
"strings"
"aego/app/crash"
"aego/core/ids"
"aego/core/log"
"aego/editor/core/context"
"aego/scene"
"aego/scene/sceneio"
"aego/script/runtime"
"aego/script/vm"
)
func scriptStack(f *vm.Fault) string {
var b strings.Builder
for _, fr := range f.Stack {
b.WriteString(fr.File + ":" + strconv.Itoa(fr.Line) + " " + fr.Function + "\n")
}
return b.String()
}
func names(s *scene.Scene) func(ids.NodeID) string {
return func(id ids.NodeID) string {
if h := s.Handle(id); !h.IsZero() && s.IsAlive(h) {
return s.Path(h)
}
return id.String()[:8]
}
}
func (s *Session) moduleIDs() string {
var out []string
for _, m := range s.Modules.Modules {
out = append(out, m.ID)
}
return strings.Join(out, ",")
}
func (s *Session) crash(kind, where string, rec any, stack []byte, script string) {
defer func() { recover() }()
b := crash.Bundle{
Root: s.Project.Root, Kind: kind, Where: where, Panic: rec, Stack: stack, Script: script,
Trace: s.Engine.Trace(), Log: s.ring,
Info: [][2]string{{"project", s.Project.Name}, {"gpu", s.Engine.Caps().Renderer}, {"modules", s.moduleIDs()}},
}
var ctx *context.Editor
if s.App != nil {
ctx = s.App.Context()
}
if ctx != nil {
ctx.Docs.WriteRecovery()
if pc := s.App.Play(); pc != nil && pc.World() != nil {
b.Events = pc.Recorder()
if sc := pc.Scene(); sc != nil {
b.Names = names(sc)
if s.crashScene {
b.Scene = sceneio.Save(sc)
}
}
}
}
dir, err := crash.Write(b)
if err != nil {
s.log.Error("cannot write the crash bundle", log.F("err", err.Error()))
return
}
s.log.Error("crash bundle written", log.F("dir", dir), log.F("where", where))
if ctx != nil {
ctx.Notify(context.LevelError, "Crash report saved to "+dir)
}
}
func (s *Session) scriptPanic(boundary string, v any, stack []byte) {
s.crash("panic", boundary, v, stack, "")
}
func (s *Session) fault(rep runtime.FaultReport) {
if rep.Fault != nil && rep.Fault.Code == vm.CodeNativePanic {
s.crash("fault", rep.Type, rep.Fault.Message, []byte(rep.Fault.Detail), scriptStack(rep.Fault))
}
}
func (s *Session) spike() {
if s.spikeMs <= 0 {
return
}
ms := s.Engine.Stats().FrameTime * 1000
now := s.Engine.Platform().Now()
if ms < s.spikeMs || now-s.lastSpike < 10 {
return
}
s.lastSpike = now
s.crash("spike", strconv.FormatFloat(ms, 'f', 1, 64)+" ms frame", nil, nil, "")
}
+32
View File
File diff suppressed because it is too large Load Diff
+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)
}
+89
View File
@@ -0,0 +1,89 @@
package edit
import (
"os/exec"
"runtime"
"aego/app/bootstrap"
"aego/app/export"
"aego/app/projects"
"aego/asset/pipeline"
"aego/editor/core/services"
"aego/engine/problems"
"aego/project"
)
type exporter struct {
p *project.Project
pl *pipeline.Pipeline
engine string
}
func newExporter(p *project.Project, pl *pipeline.Pipeline) services.Exporter {
if pl == nil {
return nil
}
root, err := projects.EngineRoot("")
if err != nil {
return nil
}
return &exporter{p: p, pl: pl, engine: root}
}
func (x *exporter) Targets() []string {
return export.Targets()
}
func (x *exporter) Out(target, profile string) string {
t, err := export.ParseTarget(target)
if err != nil {
return ""
}
prof, err := export.ParseProfile(profile)
if err != nil {
return ""
}
return export.DefaultOut(x.p, t, prof)
}
func (x *exporter) Prepare() any {
return export.Take(x.pl)
}
func (x *exporter) Check(prep any, profile string) []problems.Diagnostic {
snap, ok := prep.(*export.Snapshot)
if !ok {
return nil
}
prof, err := export.ParseProfile(profile)
if err != nil {
return nil
}
return export.Check(x.p, snap, prof, bootstrap.NeedsNative(x.p))
}
func (x *exporter) Export(prep any, req services.ExportRequest, progress func(float32, string)) (services.ExportReport, error) {
snap, _ := prep.(*export.Snapshot)
rep, err := export.Export(export.Options{
Project: x.p, Engine: x.engine, Target: req.Target, Profile: req.Profile, Out: req.Out,
Snapshot: snap, Progress: progress,
})
var out services.ExportReport
if rep != nil {
out = services.ExportReport{Out: rep.Out, Lines: rep.Lines(), Issues: rep.Diagnostics, Blocked: rep.Blocked}
}
return out, err
}
func (x *exporter) Reveal(path string) error {
var cmd *exec.Cmd
switch runtime.GOOS {
case "darwin":
cmd = exec.Command("open", path)
case "windows":
cmd = exec.Command("explorer", path)
default:
cmd = exec.Command("xdg-open", path)
}
return cmd.Start()
}
+59 -4
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
package edit
import (
"os"
"runtime"
"testing"
)
func TestMain(m *testing.M) {
dir, err := os.MkdirTemp("", "aego-config-")
if err != nil {
panic(err)
}
switch runtime.GOOS {
case "darwin":
os.Setenv("HOME", dir)
case "windows":
os.Setenv("AppData", dir)
default:
os.Setenv("XDG_CONFIG_HOME", dir)
}
code := m.Run()
os.RemoveAll(dir)
os.Exit(code)
}
+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)
}
}
+3 -3
View File
@@ -170,7 +170,7 @@ func TestProfilerSectionsFollowPlay(t *testing.T) {
}
prof := s.App.Profiler()
drawPanel(s, prof)
expectSections(t, prof.Sections(), "Frame")
expectSections(t, prof.Sections(), "Frame", "Memory")
pc := s.App.Play()
if err := pc.Start(d); err != nil {
t.Fatal(err)
@@ -179,7 +179,7 @@ func TestProfilerSectionsFollowPlay(t *testing.T) {
s.Step()
}
drawPanel(s, prof)
expectSections(t, prof.Sections(), "Frame", "Game", "Scripts", "Physics")
expectSections(t, prof.Sections(), "Frame", "Memory", "Game", "Scripts", "Physics")
if _, err := ctx.Docs.Open("scenes/other.aescene"); err != nil {
t.Fatal(err)
}
@@ -189,5 +189,5 @@ func TestProfilerSectionsFollowPlay(t *testing.T) {
pc.Stop()
s.Step()
drawPanel(s, prof)
expectSections(t, prof.Sections(), "Frame")
expectSections(t, prof.Sections(), "Frame", "Memory")
}
+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)
}
+11 -1
View File
@@ -36,6 +36,8 @@ type scriptHost struct {
probRev uint64
probs []context.ScriptProblem
stats context.ScriptStats
fault func(runtime.FaultReport)
panicked func(boundary string, v any, stack []byte)
}
func (h *scriptHost) channel() *log.ChannelLogger {
@@ -158,7 +160,7 @@ func (h *scriptHost) Attach(w *engine.World, s *scene.Scene) (func(), error) {
if err := h.sync(); err != nil {
return nil, err
}
rt := runtime.New(runtime.Options{Bindings: h.bindings, Schema: h.schema, Log: h.log})
rt := runtime.New(runtime.Options{Bindings: h.bindings, Schema: h.schema, Log: h.log, OnFault: h.fault, OnPanic: h.panicked})
if err := rt.Link(h.prog.Ordered()); err != nil {
return nil, err
}
@@ -193,3 +195,11 @@ func (h *scriptHost) Attach(w *engine.World, s *scene.Scene) (func(), error) {
}
}, nil
}
func (h *scriptHost) Watch(w *engine.World, node scene.NodeHandle, script, field string, lane int) (string, float64, bool, bool) {
rt, ok := engine.ServiceOf[*runtime.Runtime](w, runtime.ServiceID)
if !ok || rt == nil {
return "", 0, false, false
}
return rt.WatchField(node, script, field, lane)
}
+208
View File
File diff suppressed because it is too large Load Diff
+198
View File
File diff suppressed because it is too large Load Diff
+196
View File
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
+25
View File
@@ -0,0 +1,25 @@
package macos
import (
"encoding/binary"
"image"
"strings"
"testing"
)
func TestICNSHeader(t *testing.T) {
data, err := ICNS(image.NewNRGBA(image.Rect(0, 0, 64, 64)))
if err != nil {
t.Fatal(err)
}
if string(data[:4]) != "icns" || int(binary.BigEndian.Uint32(data[4:])) != len(data) {
t.Fatal("bad icns header")
}
}
func TestPlistEscapesAndBundleID(t *testing.T) {
p := string(Plist(Info{Name: "A & B", Executable: "ab", ID: BundleID("My_Game/x"), Version: "1.0"}, "icon"))
if !strings.Contains(p, "A &amp; B") || !strings.Contains(p, "dev.aego.my.game.x") {
t.Fatal(p)
}
}
+67
View File
@@ -0,0 +1,67 @@
package export
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"aego/app/export/icon"
"aego/app/export/macos"
"aego/core/errs"
)
const desktopEntry = "[Desktop Entry]\nType=Application\nName=%s\nExec=sh -c 'cd \"$(dirname \"%%k\")\" && exec ./%s'\nTerminal=false\nCategories=Game;\n"
func (j *job) pack() error {
switch j.t.OS {
case "linux":
return j.packLinux()
case "darwin":
return j.packDarwin()
case "js":
return j.packWeb()
}
return nil
}
func (j *job) packLinux() error {
if j.icon != nil {
data, err := icon.PNG(icon.Resize(j.icon, 256, 256))
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(j.out, "icon.png"), data, 0o644); err != nil {
return errs.Wrap(err, errs.VFSIO, "cannot write the icon")
}
}
entry := fmt.Sprintf(desktopEntry, j.p.Name, j.l.exe)
if err := os.WriteFile(filepath.Join(j.out, j.l.exe+".desktop"), []byte(entry), 0o755); err != nil {
return errs.Wrap(err, errs.VFSIO, "cannot write the .desktop file")
}
return nil
}
func (j *job) packDarwin() error {
info := macos.Info{
Name: j.p.Name, Executable: j.l.exe, ID: macos.BundleID(j.p.Module),
Version: GameVersion(j.p), MinOS: "11.0", Category: "public.app-category.games",
}
if err := macos.Fill(j.l.app, info, j.icon); err != nil {
return err
}
if runtime.GOOS != "darwin" {
return nil
}
path, err := exec.LookPath("codesign")
if err != nil {
return nil
}
out, err := exec.Command(path, "--force", "--deep", "--sign", "-", j.l.app).CombinedOutput()
if err != nil {
return errs.New(errs.BuildFailed, "ad-hoc signing failed: "+strings.TrimSpace(string(out)))
}
return nil
}
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
package export
import (
"slices"
"aego/app/projects"
"aego/app/run"
"aego/asset"
"aego/asset/pipeline"
"aego/core/log"
"aego/project"
"aego/vfs"
)
type Snapshot struct {
idx *asset.Index
files vfs.FS
assets []pipeline.AssetInfo
issues []pipeline.MetaIssue
}
func Take(pl *pipeline.Pipeline) *Snapshot {
issues, _ := pl.MetaIssues()
return &Snapshot{idx: pl.Index().Clone(), files: pl.Artifacts(), assets: pl.Assets(), issues: slices.Clone(issues)}
}
func Fresh(p *project.Project) (*Snapshot, func(), error) {
ws, err := projects.OpenWorkspace(p, "", false)
if err != nil {
return nil, nil, err
}
imps, err := run.Importers(ws)
if err != nil {
return nil, nil, err
}
pl, err := pipeline.Open(p, imps, log.Nop())
if err != nil {
return nil, nil, err
}
if _, _, err := pl.Scan(); err != nil {
pl.Close()
return nil, nil, err
}
if _, err := pl.ImportAll(); err != nil {
pl.Close()
return nil, nil, err
}
return Take(pl), pl.Close, nil
}
func (s *Snapshot) Revision() uint64 {
return 1
}
func (s *Snapshot) Index() *asset.Index {
return s.idx
}
func (s *Snapshot) Artifacts() vfs.FS {
return s.files
}
+208
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
<!doctype html>
<html lang="en" data-build="{{.Build}}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}}</title>
{{if .Icon}}<link rel="icon" href="favicon.png">{{else}}<link rel="icon" href="data:,">{{end}}
<style>
html, body { margin: 0; height: 100%; background: #0d0e10; color: #e4e3df; overflow: hidden; font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
canvas { display: block; width: 100%; height: 100%; outline: none; touch-action: none; }
.loader { position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; background: #0d0e10; transition: opacity 0.25s; }
.loader.done { opacity: 0; pointer-events: none; }
.loader .title { font-size: 20px; font-weight: 600; }
.progress { width: min(360px, 80vw); height: 6px; border-radius: 3px; background: #2d3036; overflow: hidden; }
#bar { width: 0; height: 100%; background: #6e9bf5; transition: width 0.1s; }
#bar.indeterminate { width: 30%; animation: slide 1s ease-in-out infinite alternate; }
@keyframes slide { from { margin-left: 0; } to { margin-left: 70%; } }
#status { color: #9a9890; font-size: 13px; }
.loader.failed #bar { background: #f0776c; }
#error { max-width: min(640px, 90vw); max-height: 40vh; overflow: auto; margin: 0; padding: 10px 12px; white-space: pre-wrap; color: #f0776c; background: #1d1f23; border-radius: 6px; font: 12px/1.5 ui-monospace, Menlo, Consolas, monospace; }
#fullscreen { position: fixed; top: 10px; right: 10px; width: 36px; height: 36px; border: 0; border-radius: 6px; background: rgba(0, 0, 0, 0.45); color: #fff; font-size: 18px; cursor: pointer; opacity: 0.5; }
#fullscreen:hover { opacity: 1; }
</style>
</head>
<body>
<canvas id="game" data-aego tabindex="0" aria-label="{{.Title}}"></canvas>
<div id="loader" class="loader" role="status" aria-live="polite">
<div class="title">{{.Title}}</div>
<div class="progress"><div id="bar"></div></div>
<div id="status">Loading…</div>
<pre id="error" hidden></pre>
</div>
<button id="fullscreen" type="button" title="Fullscreen" aria-label="Fullscreen" hidden>⛶</button>
<script src="wasm_exec.js?v={{.Build}}"></script>
<script src="loader.js?v={{.Build}}"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+85
View File
@@ -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), "&lt;Game &amp;") || 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)
}
}
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
package winres
import (
"slices"
"testing"
)
func TestSysoHasResourceTypes(t *testing.T) {
data, err := Syso("amd64", []Icon{{Size: 16, PNG: []byte{1, 2, 3}}, {Size: 256, PNG: []byte{4, 5}}},
Info{Product: "Game", Original: "game.exe", Version: ParseVersion("1.2.3-beta")}, true)
if err != nil {
t.Fatal(err)
}
types, err := Types(data)
if err != nil {
t.Fatal(err)
}
if !slices.Equal(types, []uint16{rtIcon, rtGroupIcon, rtVersion, rtManifest}) {
t.Fatalf("resource types %v", types)
}
}
func TestParseVersion(t *testing.T) {
if v := ParseVersion("v2.10.3-rc1"); v != (Version{2, 10, 3, 0}) {
t.Fatalf("got %v", v)
}
}
func TestVersionInfoIsAligned(t *testing.T) {
b := versionInfo(Info{Product: "G", Version: Version{1, 0, 0, 0}})
if int(b[0])|int(b[1])<<8 != len(b) {
t.Fatalf("wLength %d, size %d", int(b[0])|int(b[1])<<8, len(b))
}
}
+2
View File
@@ -1,6 +1,7 @@
package app
import (
"aego/asset/importers/preset"
"sort"
"aego/asset/importers/atlas"
@@ -20,6 +21,7 @@ func CoreImporters() (*pipeline.ImporterRegistry, error) {
atlas.Wrap(texImporter.New()),
scenefile.NewScene(),
scenefile.NewPrefab(),
preset.New(),
} {
if err := reg.Register(imp); err != nil {
return nil, err
+41
View File
@@ -0,0 +1,41 @@
package app
import (
"aego/asset/pipeline"
"aego/engine/problems"
)
const OwnerAssets = "assets"
func metaSeverity(k pipeline.MetaIssueKind) problems.Severity {
switch k {
case pipeline.MetaDuplicate:
return problems.Error
case pipeline.MetaStale:
return problems.Info
}
return problems.Warning
}
func MetaProblems(pl *pipeline.Pipeline) problems.Source {
return problems.Source{
Revision: func() uint64 {
_, rev := pl.MetaIssues()
return rev
},
Append: func(dst []problems.Diagnostic) []problems.Diagnostic {
list, _ := pl.MetaIssues()
for _, is := range list {
dst = append(dst, problems.Diagnostic{
Owner: OwnerAssets,
Code: is.Code(),
Severity: metaSeverity(is.Kind),
Message: is.Message(),
Help: "Assets → Repair Meta Files… will " + is.Action(),
Subject: problems.Subject{Kind: problems.SubjectFile, File: is.Path},
})
}
return dst
},
}
}

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