Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d8e3b2fd3 | ||
|
|
c6a24ba393 | ||
|
|
7dca06e4b1 | ||
|
|
6dfdb824e2 | ||
|
|
de49a53172 | ||
|
|
1030dc9a43 | ||
|
|
3d25734dfa | ||
|
|
12f628c469 | ||
|
|
1529213434 | ||
|
|
f8bb865920 | ||
|
|
f5fb32565e | ||
|
|
f1a8f538d3 | ||
|
|
963b01a011 | ||
|
|
6aec9dc544 | ||
|
|
70a0111deb | ||
|
|
0da2549d91 | ||
|
|
810984ae7a | ||
|
|
62c8129364 | ||
|
|
674af03a65 | ||
|
|
6b0d006467 | ||
|
|
ff7e95afe4 | ||
|
|
67b16d3da5 | ||
|
|
56ca1c638e |
@@ -40,14 +40,14 @@ BENCH_PHYSICS_PKGS :=
|
|||||||
.DEFAULT_GOAL := bootstrap
|
.DEFAULT_GOAL := bootstrap
|
||||||
-include modules/*/module.mk
|
-include modules/*/module.mk
|
||||||
|
|
||||||
.PHONY: dmg nsis-check appimage 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
|
.PHONY: dmg nsis-check appimage 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 affected quick check-camera check-audio check-gameplay check-mutate check-editor check-web check-docs pkgsize
|
||||||
|
|
||||||
XVFB := $(shell command -v xvfb-run 2>/dev/null)
|
XVFB := $(shell command -v xvfb-run 2>/dev/null)
|
||||||
GL_OVERRIDE = MESA_GL_VERSION_OVERRIDE=3.3 MESA_GLSL_VERSION_OVERRIDE=330
|
GL_OVERRIDE = MESA_GL_VERSION_OVERRIDE=3.3 MESA_GLSL_VERSION_OVERRIDE=330
|
||||||
ifneq ($(XVFB),)
|
ifneq ($(XVFB),)
|
||||||
GL_ENV = $(XVFB) -a -s "-screen 0 1280x720x24" env SDL_VIDEODRIVER=x11 LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe $(GL_OVERRIDE)
|
GL_ENV = $(XVFB) -a -s "-screen 0 1280x720x24" env SDL_VIDEODRIVER=x11 SDL_AUDIO_DRIVER=dummy LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe $(GL_OVERRIDE)
|
||||||
else
|
else
|
||||||
GL_ENV = env SDL_VIDEODRIVER=offscreen LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe $(GL_OVERRIDE)
|
GL_ENV = env SDL_VIDEODRIVER=offscreen SDL_AUDIO_DRIVER=dummy LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe $(GL_OVERRIDE)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
NONATIVE_PKGS = $$(go list ./... | grep -v -e '/platform/sdl$$' -e '/gpu/gl33$$' -e '/gpu/gl33/gl$$')
|
NONATIVE_PKGS = $$(go list ./... | grep -v -e '/platform/sdl$$' -e '/gpu/gl33$$' -e '/gpu/gl33/gl$$')
|
||||||
@@ -247,6 +247,7 @@ try-size:
|
|||||||
go run ./tools/wasmsize -top 15 -depth 2 .cache/aego-try.wasm
|
go run ./tools/wasmsize -top 15 -depth 2 .cache/aego-try.wasm
|
||||||
|
|
||||||
web-smoke: site
|
web-smoke: site
|
||||||
|
sh tools/webbudget.sh site
|
||||||
$(WEB_SMOKE) --report=$(CURDIR)/.cache/web-bench.json
|
$(WEB_SMOKE) --report=$(CURDIR)/.cache/web-bench.json
|
||||||
$(WEB_DEMO)
|
$(WEB_DEMO)
|
||||||
$(WEB_TRY)
|
$(WEB_TRY)
|
||||||
@@ -255,6 +256,7 @@ web-smoke: site
|
|||||||
web-smoke-update: site
|
web-smoke-update: site
|
||||||
$(WEB_SMOKE) --update
|
$(WEB_SMOKE) --update
|
||||||
$(WEB_DEMO) --update
|
$(WEB_DEMO) --update
|
||||||
|
$(WEB_TRY) --update
|
||||||
|
|
||||||
cli: bootstrap
|
cli: bootstrap
|
||||||
go build -o .cache/bin/aego ./cmd/aego
|
go build -o .cache/bin/aego ./cmd/aego
|
||||||
@@ -290,13 +292,42 @@ fuzz:
|
|||||||
go test -run '^$$' -fuzz '^FuzzWAV$$' -fuzztime $(FUZZTIME) ./audio/codec
|
go test -run '^$$' -fuzz '^FuzzWAV$$' -fuzztime $(FUZZTIME) ./audio/codec
|
||||||
go test -run '^$$' -fuzz '^FuzzVorbis$$' -fuzztime $(FUZZTIME) ./audio/codec
|
go test -run '^$$' -fuzz '^FuzzVorbis$$' -fuzztime $(FUZZTIME) ./audio/codec
|
||||||
|
|
||||||
|
|
||||||
|
affected:
|
||||||
|
@pkgs=$$(sh tools/affected.sh $(BASE)); if [ -z "$$pkgs" ]; then echo "nothing affected"; else go vet $$pkgs && go test $$pkgs; fi
|
||||||
|
|
||||||
|
quick:
|
||||||
|
@pkgs=$$(sh tools/affected.sh $(BASE)); if [ -n "$$pkgs" ]; then go build $$pkgs && go test -short $$pkgs; fi
|
||||||
|
|
||||||
|
check-camera:
|
||||||
|
go test ./modules/camera/... ./tests/camera/...
|
||||||
|
|
||||||
|
check-audio:
|
||||||
|
go test ./audio/... ./modules/audio/... ./tests/audio/...
|
||||||
|
|
||||||
|
check-gameplay:
|
||||||
|
go test ./modules/gameplay/... ./tests/gameplay/... ./modules/physics/...
|
||||||
|
|
||||||
|
check-mutate:
|
||||||
|
go test ./scene/mutate/... ./tests/mutate/... ./app/agent/...
|
||||||
|
|
||||||
|
check-editor:
|
||||||
|
go test ./editor/... ./app/edit/...
|
||||||
|
|
||||||
|
check-web: test-wasm web-smoke
|
||||||
|
|
||||||
|
check-docs: site-check
|
||||||
|
|
||||||
|
pkgsize:
|
||||||
|
@sh tools/pkgsize.sh
|
||||||
|
|
||||||
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
|
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.
|
# Documentation screenshots of the real editor on the platformer example, under a clean user profile.
|
||||||
SHOTS_TMP = $(CURDIR)/.cache/shots
|
SHOTS_TMP = $(CURDIR)/.cache/shots
|
||||||
ifneq ($(XVFB),)
|
ifneq ($(XVFB),)
|
||||||
SHOTS_ENV = $(XVFB) -a -s "-screen 0 1600x900x24" env SDL_VIDEODRIVER=x11 LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe $(GL_OVERRIDE)
|
SHOTS_ENV = $(XVFB) -a -s "-screen 0 1600x900x24" env SDL_VIDEODRIVER=x11 SDL_AUDIO_DRIVER=dummy LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe $(GL_OVERRIDE)
|
||||||
else
|
else
|
||||||
SHOTS_ENV = $(GL_ENV)
|
SHOTS_ENV = $(GL_ENV)
|
||||||
endif
|
endif
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Aego
|
||||||
|
|
||||||
|
A 2D and 3D game engine with an editor, a scripting language and a web export, written in Go.
|
||||||
|
|
||||||
|
- Documentation and the playground: <https://aego.akarpov.ru>
|
||||||
|
- Installing and building from source: [engine/res/docs/install.md](engine/res/docs/install.md)
|
||||||
|
- Contributing, check levels and rules for engine code: [docs/contributing.md](docs/contributing.md)
|
||||||
|
- Architecture and decisions: [ARCHITECTURE.md](ARCHITECTURE.md)
|
||||||
@@ -2,6 +2,7 @@ package agent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -287,6 +288,9 @@ func (s *Session) sceneRun(a Args) (*Output, error) {
|
|||||||
}
|
}
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("frames: " + strconv.Itoa(rep.Frames) + "\nhash: " + rep.Hash + "\n")
|
b.WriteString("frames: " + strconv.Itoa(rep.Frames) + "\nhash: " + rep.Hash + "\n")
|
||||||
|
for _, k := range slices.Sorted(maps.Keys(rep.Counters)) {
|
||||||
|
b.WriteString(k + ": " + strconv.FormatUint(rep.Counters[k], 10) + "\n")
|
||||||
|
}
|
||||||
for _, l := range rep.Log {
|
for _, l := range rep.Log {
|
||||||
b.WriteString(l + "\n")
|
b.WriteString(l + "\n")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"aego/script/abi"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -34,6 +35,14 @@ type TypeView struct {
|
|||||||
Requires []string `json:"requires,omitempty"`
|
Requires []string `json:"requires,omitempty"`
|
||||||
Doc string `json:"doc,omitempty"`
|
Doc string `json:"doc,omitempty"`
|
||||||
Fields []FieldView `json:"fields,omitempty"`
|
Fields []FieldView `json:"fields,omitempty"`
|
||||||
|
Methods []CallView `json:"methods,omitempty"`
|
||||||
|
Signals []CallView `json:"signals,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CallView struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Sig string `json:"sig"`
|
||||||
|
Doc string `json:"doc,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func schemaCommands() []*Command {
|
func schemaCommands() []*Command {
|
||||||
@@ -163,5 +172,51 @@ func (s *Session) schemaShow(a Args) (*Output, error) {
|
|||||||
}
|
}
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
if s.kit != nil {
|
||||||
|
for _, cm := range s.kit.methods(td.FullName) {
|
||||||
|
v.Methods = append(v.Methods, CallView{Name: cm.Name, Sig: methodSig(cm.Sig, cm.Params), Doc: summary(cm.Doc)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, sg := range td.Signals {
|
||||||
|
params := make([]string, len(sg.Params))
|
||||||
|
for i, p := range sg.Params {
|
||||||
|
params[i] = p
|
||||||
|
if i < len(sg.Names) && sg.Names[i] != "" {
|
||||||
|
params[i] = sg.Names[i] + " " + p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.Signals = append(v.Signals, CallView{Name: sg.Name, Sig: "(" + strings.Join(params, ", ") + ")", Doc: summary(sg.Doc)})
|
||||||
|
}
|
||||||
|
for _, sec := range []struct {
|
||||||
|
title string
|
||||||
|
list []CallView
|
||||||
|
}{{"methods", v.Methods}, {"signals", v.Signals}} {
|
||||||
|
if len(sec.list) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.WriteString(sec.title + ":\n")
|
||||||
|
for _, c := range sec.list {
|
||||||
|
b.WriteString(" " + c.Name + c.Sig)
|
||||||
|
if c.Doc != "" {
|
||||||
|
b.WriteString(" — " + c.Doc)
|
||||||
|
}
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
return &Output{Value: v, Text: b.String()}, nil
|
return &Output{Value: v, Text: b.String()}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func methodSig(sig abi.Signature, names []string) string {
|
||||||
|
params := make([]string, len(sig.Params))
|
||||||
|
for i, p := range sig.Params {
|
||||||
|
params[i] = p.String()
|
||||||
|
if i < len(names) && names[i] != "" {
|
||||||
|
params[i] = names[i] + " " + params[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out := "(" + strings.Join(params, ", ") + ")"
|
||||||
|
if sig.Result.Kind != abi.Void {
|
||||||
|
out += " " + sig.Result.String()
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import (
|
|||||||
"aego/scene/sceneio"
|
"aego/scene/sceneio"
|
||||||
"aego/schema"
|
"aego/schema"
|
||||||
"aego/script/bind"
|
"aego/script/bind"
|
||||||
|
"aego/script/bind/manifest"
|
||||||
"aego/script/reload"
|
"aego/script/reload"
|
||||||
"aego/script/runtime"
|
"aego/script/runtime"
|
||||||
"aego/script/workspace"
|
"aego/script/workspace"
|
||||||
@@ -48,6 +49,7 @@ type RunRequest struct {
|
|||||||
type RunReport struct {
|
type RunReport struct {
|
||||||
Frames int `json:"frames"`
|
Frames int `json:"frames"`
|
||||||
Hash string `json:"hash"`
|
Hash string `json:"hash"`
|
||||||
|
Counters map[string]uint64 `json:"counters,omitempty"`
|
||||||
Log []string `json:"log,omitempty"`
|
Log []string `json:"log,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -79,6 +81,25 @@ type Kit struct {
|
|||||||
ws *workspace.Workspace
|
ws *workspace.Workspace
|
||||||
binds *bind.Registry
|
binds *bind.Registry
|
||||||
opt Options
|
opt Options
|
||||||
|
man *manifest.Manifest
|
||||||
|
}
|
||||||
|
|
||||||
|
// methods lists the script methods the bindings give a component type, built once per kit.
|
||||||
|
func (k *Kit) methods(comp string) []manifest.CompMethod {
|
||||||
|
if k.man == nil {
|
||||||
|
m, err := manifest.FromRegistry(k.binds, k.Types.Schema())
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
k.man = m
|
||||||
|
}
|
||||||
|
var out []manifest.CompMethod
|
||||||
|
for _, cm := range k.man.CompMethods {
|
||||||
|
if cm.Comp == comp {
|
||||||
|
out = append(out, cm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKit(p *project.Project, opt Options) (*Kit, error) {
|
func NewKit(p *project.Project, opt Options) (*Kit, error) {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type Config struct {
|
|||||||
Editor bool
|
Editor bool
|
||||||
FailOnScriptErrors bool
|
FailOnScriptErrors bool
|
||||||
Catalog assembly.Catalog
|
Catalog assembly.Catalog
|
||||||
|
Platform platform.Platform
|
||||||
Profile string
|
Profile string
|
||||||
NoProfile bool
|
NoProfile bool
|
||||||
}
|
}
|
||||||
@@ -98,6 +99,9 @@ func (h *Host) build(cfg Config, mods []module.Module) error {
|
|||||||
ecfg.LoopScope = cfg.LoopScope
|
ecfg.LoopScope = cfg.LoopScope
|
||||||
}
|
}
|
||||||
builder := engine.NewBuilder(ecfg).WithLogger(lg).Use(rmod)
|
builder := engine.NewBuilder(ecfg).WithLogger(lg).Use(rmod)
|
||||||
|
if cfg.Platform != nil {
|
||||||
|
builder.WithPlatform(cfg.Platform)
|
||||||
|
}
|
||||||
if h.Types, err = assembly.NewTypes(); err != nil {
|
if h.Types, err = assembly.NewTypes(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -187,6 +191,7 @@ func (h *Host) build(cfg Config, mods []module.Module) error {
|
|||||||
}
|
}
|
||||||
h.Settings.SetNames(h.Sections.Names)
|
h.Settings.SetNames(h.Sections.Names)
|
||||||
h.Engine.SetService(settings.ServiceID, h.Settings)
|
h.Engine.SetService(settings.ServiceID, h.Settings)
|
||||||
|
h.Engine.OnClose(h.Modules.Shutdown)
|
||||||
if err = h.Modules.Init(&module.InitContext{Engine: h.Engine, Assets: h.Loader, Log: lg, Editor: cfg.Editor, Sections: h.Sections, Problems: h.Problems, Settings: h.Settings}); err != nil {
|
if err = h.Modules.Init(&module.InitContext{Engine: h.Engine, Assets: h.Loader, Log: lg, Editor: cfg.Editor, Sections: h.Sections, Problems: h.Problems, Settings: h.Settings}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -227,9 +232,6 @@ func (h *Host) Close() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.closed = true
|
h.closed = true
|
||||||
if h.Modules != nil {
|
|
||||||
h.Modules.Shutdown()
|
|
||||||
}
|
|
||||||
if h.Engine != nil {
|
if h.Engine != nil {
|
||||||
h.Engine.Close()
|
h.Engine.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package docstool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"aego/app/playground"
|
||||||
|
"aego/engine/docs"
|
||||||
|
"aego/engine/module"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckExamples loads the example scene of every module code, validates it and runs it headless; the code must be among the problems it reports (D146).
|
||||||
|
func CheckExamples(reg *docs.Registry, w io.Writer) int {
|
||||||
|
pg, err := playground.New()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(w, "examples: 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()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
bad := 0
|
||||||
|
for _, e := range reg.All() {
|
||||||
|
if e.Kind != docs.Code || e.Diag == nil || e.Diag.Example == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
found, why := false, ""
|
||||||
|
code := ""
|
||||||
|
if e.Diag.ExampleScript != "" {
|
||||||
|
c, err := pg.TryCode(e.Diag.ExampleScript)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(w, "%s: error: example script does not compile: %v\n", e.ID, err)
|
||||||
|
bad++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
code = c
|
||||||
|
}
|
||||||
|
if res := pg.PlayScene("example-"+e.ID, []byte(e.Diag.Example), code, nil); !res.Started {
|
||||||
|
why = "does not load: " + res.Error
|
||||||
|
} else {
|
||||||
|
for _, d := range module.ValidateScene(pg.Game().Scene(), nil) {
|
||||||
|
found = found || d.Code == e.ID
|
||||||
|
}
|
||||||
|
if err := pg.Game().Step(tryFrames); err != nil {
|
||||||
|
why = "stops the engine: " + err.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
poll := pg.Poll()
|
||||||
|
for _, f := range poll.Faults {
|
||||||
|
found = found || f.Code == e.ID
|
||||||
|
}
|
||||||
|
for _, l := range poll.Log {
|
||||||
|
found = found || strings.Contains(l.Text, e.ID)
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case why != "":
|
||||||
|
fmt.Fprintf(w, "%s: error: example %s %s\n", e.ID, e.Diag.ExampleFile, why)
|
||||||
|
bad++
|
||||||
|
case !found:
|
||||||
|
fmt.Fprintf(w, "%s: error: example of %s does not produce %s\n", e.ID, e.ID, e.ID)
|
||||||
|
bad++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pg.Stop()
|
||||||
|
return bad
|
||||||
|
}
|
||||||
@@ -76,6 +76,9 @@ func (e *Env) Check(strict bool) int {
|
|||||||
if CheckTry(reg, e.Stdout) > 0 && code == 0 {
|
if CheckTry(reg, e.Stdout) > 0 && code == 0 {
|
||||||
code = 1
|
code = 1
|
||||||
}
|
}
|
||||||
|
if CheckExamples(reg, e.Stdout) > 0 && code == 0 {
|
||||||
|
code = 1
|
||||||
|
}
|
||||||
return code
|
return code
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type request struct {
|
|||||||
Component string `json:"component"`
|
Component string `json:"component"`
|
||||||
Field string `json:"field"`
|
Field string `json:"field"`
|
||||||
Value float64 `json:"value"`
|
Value float64 `json:"value"`
|
||||||
|
Node string `json:"node"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Reply struct {
|
type Reply struct {
|
||||||
@@ -27,6 +28,7 @@ type Reply struct {
|
|||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
State *docsession.State `json:"state,omitempty"`
|
State *docsession.State `json:"state,omitempty"`
|
||||||
Log []Line `json:"log,omitempty"`
|
Log []Line `json:"log,omitempty"`
|
||||||
|
Pose []float64 `json:"pose,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Call(method string, payload []byte) Reply {
|
func (a *App) Call(method string, payload []byte) Reply {
|
||||||
@@ -64,6 +66,17 @@ func (a *App) Call(method string, payload []byte) Reply {
|
|||||||
case "state":
|
case "state":
|
||||||
st := s.State()
|
st := s.State()
|
||||||
return Reply{State: &st}
|
return Reply{State: &st}
|
||||||
|
case "pose":
|
||||||
|
sc := s.Controller().Scene()
|
||||||
|
if sc == nil {
|
||||||
|
return Reply{Error: "the scene is not running"}
|
||||||
|
}
|
||||||
|
h := sc.Find(sc.Root(), req.Node)
|
||||||
|
if h.IsZero() || !sc.IsAlive(h) {
|
||||||
|
return Reply{Error: "no node " + strconv.Quote(req.Node)}
|
||||||
|
}
|
||||||
|
p := sc.WorldPosition(h)
|
||||||
|
return Reply{Pose: []float64{float64(p.X), float64(p.Y)}}
|
||||||
case "log":
|
case "log":
|
||||||
out := Reply{Log: a.lines}
|
out := Reply{Log: a.lines}
|
||||||
a.lines = nil
|
a.lines = nil
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ func TestProfilerSectionsFollowPlay(t *testing.T) {
|
|||||||
s.Step()
|
s.Step()
|
||||||
}
|
}
|
||||||
drawPanel(s, prof)
|
drawPanel(s, prof)
|
||||||
expectSections(t, prof.Sections(), "Frame", "Memory", "Game", "Scripts", "Physics", "Audio")
|
expectSections(t, prof.Sections(), "Frame", "Memory", "Game", "Scripts", "Physics", "Audio", "Camera", "Gameplay")
|
||||||
if _, err := ctx.Docs.Open("scenes/other.aescene"); err != nil {
|
if _, err := ctx.Docs.Open("scenes/other.aescene"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"aego/core/ids"
|
||||||
"aego/editor/core/command"
|
"aego/editor/core/command"
|
||||||
"aego/editor/core/context"
|
"aego/editor/core/context"
|
||||||
|
"aego/editor/core/vectors"
|
||||||
"aego/editor/core/workspace"
|
"aego/editor/core/workspace"
|
||||||
"aego/editor/panels/gameview"
|
"aego/editor/panels/gameview"
|
||||||
"aego/editor/shell"
|
"aego/editor/shell"
|
||||||
@@ -56,7 +58,60 @@ func (s *Session) shots(dir string) error {
|
|||||||
}
|
}
|
||||||
return selectNamed(ctx, "Player")
|
return selectNamed(ctx, "Player")
|
||||||
}},
|
}},
|
||||||
|
{name: "hitbox-gizmo", frames: 60, setup: func(s *Session) error {
|
||||||
|
sc := ctx.Scene()
|
||||||
|
var span []ids.NodeID
|
||||||
|
for _, n := range []string{"EnemyLeft", "PatrolA1", "PatrolB1"} {
|
||||||
|
span = append(span, sc.ID(sc.Find(sc.Root(), n)))
|
||||||
|
}
|
||||||
|
ctx.Select(span...)
|
||||||
|
if err := ctx.Commands.Run(command.SceneFrame); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := 0; i < 30; i++ {
|
||||||
|
s.Step()
|
||||||
|
}
|
||||||
|
return selectNamed(ctx, "EnemyLeft")
|
||||||
|
}},
|
||||||
|
{name: "camera-follow", frames: 60, setup: func(s *Session) error {
|
||||||
|
sc := ctx.Scene()
|
||||||
|
ctx.Select(sc.ID(sc.Find(sc.Root(), "Camera")), sc.ID(sc.Find(sc.Root(), "Player")))
|
||||||
|
if err := ctx.Commands.Run(command.SceneFrame); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := 0; i < 30; i++ {
|
||||||
|
s.Step()
|
||||||
|
}
|
||||||
|
return selectNamed(ctx, "Camera")
|
||||||
|
}},
|
||||||
|
{name: "camera-zone", frames: 60, setup: func(s *Session) error {
|
||||||
|
sc := ctx.Scene()
|
||||||
|
ctx.Select(sc.ID(sc.Find(sc.Root(), "ArenaZone")), sc.ID(sc.Find(sc.Root(), "PatrolA2")), sc.ID(sc.Find(sc.Root(), "PatrolB2")), sc.ID(sc.Find(sc.Root(), "GroundRight")))
|
||||||
|
if err := ctx.Commands.Run(command.SceneFrame); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := 0; i < 30; i++ {
|
||||||
|
s.Step()
|
||||||
|
}
|
||||||
|
return selectNamed(ctx, "ArenaZone")
|
||||||
|
}},
|
||||||
|
{name: "clip-inspector", frames: 60, setup: func(s *Session) error {
|
||||||
|
id, ok := s.Pipeline.Lookup("assets/audio/music.ogg")
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("no assets/audio/music.ogg")
|
||||||
|
}
|
||||||
|
s.App.Data().Inspect(id)
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
{name: "add-required", frames: 30, setup: func(s *Session) error {
|
||||||
|
if err := selectNamed(ctx, "PatrolA1"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.App.Inspector().OpenAdd("follow")
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
{name: "tags", frames: 60, setup: func(s *Session) error {
|
{name: "tags", frames: 60, setup: func(s *Session) error {
|
||||||
|
ctx.UI.CloseAllPopups()
|
||||||
return selectNamed(ctx, "Player")
|
return selectNamed(ctx, "Player")
|
||||||
}},
|
}},
|
||||||
{name: "prefab", frames: 60, setup: func(s *Session) error {
|
{name: "prefab", frames: 60, setup: func(s *Session) error {
|
||||||
@@ -83,12 +138,23 @@ func (s *Session) shots(dir string) error {
|
|||||||
scene()
|
scene()
|
||||||
return ctx.Commands.Run(command.PlayStart)
|
return ctx.Commands.Run(command.PlayStart)
|
||||||
}},
|
}},
|
||||||
|
{name: "motion-play", frames: 30, setup: func(s *Session) error {
|
||||||
|
s.App.Workspace().Show(workspace.Inspector)
|
||||||
|
s.App.Inspector().ScrollTo("gameplay.Motion2D")
|
||||||
|
return selectNamed(ctx, "Player")
|
||||||
|
}},
|
||||||
|
{name: "mixer", frames: 120, setup: func(s *Session) error {
|
||||||
|
s.App.Workspace().Switch(workspace.Audio)
|
||||||
|
s.App.Workspace().Show(workspace.Mixer)
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
{name: "physics-debug", frames: 30, setup: func(s *Session) error {
|
{name: "physics-debug", frames: 30, setup: func(s *Session) error {
|
||||||
|
s.App.Workspace().Switch(workspace.Design)
|
||||||
ctx.Overlay.Set("physics.debug.contacts", true)
|
ctx.Overlay.Set("physics.debug.contacts", true)
|
||||||
return ctx.Commands.Run(gameview.CommandOverlays)
|
return ctx.Commands.Run(gameview.CommandOverlays)
|
||||||
}},
|
}},
|
||||||
{name: "demo", frames: 60, inset: 2, bottom: 28, setup: func(s *Session) error {
|
{name: "demo", frames: 60, inset: 2, bottom: 28, setup: func(s *Session) error {
|
||||||
for _, f := range []string{"physics.debug", "physics.debug.shapes", "physics.debug.contacts", "physics.debug.joints"} {
|
for _, f := range []string{gameview.CommandOverlays, vectors.CommandToggle} {
|
||||||
ctx.Overlay.Set(f, false)
|
ctx.Overlay.Set(f, false)
|
||||||
}
|
}
|
||||||
return ctx.Commands.Run(shell.CommandFullscreenPlay)
|
return ctx.Commands.Run(shell.CommandFullscreenPlay)
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package export
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
"crypto/sha256"
|
||||||
"embed"
|
"embed"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
@@ -141,6 +143,21 @@ func (j *job) webTitle() string {
|
|||||||
return AppName(j.p)
|
return AppName(j.p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// webBuild keys the page's ?v= URLs on the data, the runtime and both scripts, so a new engine gets new URLs under immutable caching.
|
||||||
|
func (j *job) webBuild(scripts ...[]byte) (string, error) {
|
||||||
|
wasm, err := os.ReadFile(j.l.binary())
|
||||||
|
if err != nil {
|
||||||
|
return "", errs.Wrap(err, errs.VFSIO, "cannot read the web runtime")
|
||||||
|
}
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write([]byte(j.rep.Hash))
|
||||||
|
h.Write(wasm)
|
||||||
|
for _, s := range scripts {
|
||||||
|
h.Write(s)
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(h.Sum(nil))[:buildKeyLen], nil
|
||||||
|
}
|
||||||
|
|
||||||
func (j *job) packWeb() error {
|
func (j *job) packWeb() error {
|
||||||
src, err := wasmExecPath(j.goroot)
|
src, err := wasmExecPath(j.goroot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -176,9 +193,9 @@ func (j *job) packWeb() error {
|
|||||||
if err := writeFile(filepath.Join(j.l.data, WebFiles), list, WebFiles); err != nil {
|
if err := writeFile(filepath.Join(j.l.data, WebFiles), list, WebFiles); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
build := j.rep.Hash
|
build, err := j.webBuild(exec, loader)
|
||||||
if len(build) > buildKeyLen {
|
if err != nil {
|
||||||
build = build[:buildKeyLen]
|
return err
|
||||||
}
|
}
|
||||||
var page bytes.Buffer
|
var page bytes.Buffer
|
||||||
if err := webIndexTmpl.Execute(&page, webPage{Title: j.webTitle(), Build: build, Icon: j.icon != nil}); err != nil {
|
if err := webIndexTmpl.Execute(&page, webPage{Title: j.webTitle(), Build: build, Icon: j.icon != nil}); err != nil {
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ canvas { display: block; width: 100%; height: 100%; outline: none; touch-action:
|
|||||||
#mute .off, #mute.muted .on { display: none; }
|
#mute .off, #mute.muted .on { display: none; }
|
||||||
#mute.muted .off { display: inline; }
|
#mute.muted .off { display: inline; }
|
||||||
</style>
|
</style>
|
||||||
<link rel="preload" href="game.wasm?v={{.Build}}" as="fetch" type="application/wasm" crossorigin>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="game" data-aego tabindex="0" aria-label="{{.Title}}"></canvas>
|
<canvas id="game" data-aego tabindex="0" aria-label="{{.Title}}"></canvas>
|
||||||
|
|||||||
@@ -71,9 +71,22 @@
|
|||||||
status.textContent = `${mb(got)} of ${mb(total)}`;
|
status.textContent = `${mb(got)} of ${mb(total)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function read(url) {
|
// A request that delivers nothing for STALL ms is aborted and made again past the HTTP cache; a stuck cache entry must not leave the page on "Loading…".
|
||||||
const resp = await fetch(url);
|
const STALL = 8000;
|
||||||
|
const TRIES = 3;
|
||||||
|
|
||||||
|
async function readOnce(url, cache) {
|
||||||
|
const ctl = new AbortController();
|
||||||
|
let timer = setTimeout(() => ctl.abort(), STALL);
|
||||||
|
const kick = () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => ctl.abort(), STALL);
|
||||||
|
};
|
||||||
|
let counted = 0;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url, { signal: ctl.signal, cache });
|
||||||
if (!resp.ok) throw new Error(`${url}: HTTP ${resp.status}`);
|
if (!resp.ok) throw new Error(`${url}: HTTP ${resp.status}`);
|
||||||
|
kick();
|
||||||
if (!resp.body) {
|
if (!resp.body) {
|
||||||
const all = new Uint8Array(await resp.arrayBuffer());
|
const all = new Uint8Array(await resp.arrayBuffer());
|
||||||
progress(all.byteLength);
|
progress(all.byteLength);
|
||||||
@@ -85,8 +98,10 @@
|
|||||||
for (;;) {
|
for (;;) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
if (done) break;
|
if (done) break;
|
||||||
|
kick();
|
||||||
parts.push(value);
|
parts.push(value);
|
||||||
size += value.byteLength;
|
size += value.byteLength;
|
||||||
|
counted += value.byteLength;
|
||||||
progress(value.byteLength);
|
progress(value.byteLength);
|
||||||
}
|
}
|
||||||
const out = new Uint8Array(size);
|
const out = new Uint8Array(size);
|
||||||
@@ -96,6 +111,26 @@
|
|||||||
at += p.byteLength;
|
at += p.byteLength;
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
|
} catch (e) {
|
||||||
|
got -= counted;
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function read(url) {
|
||||||
|
for (let i = 0; ; i++) {
|
||||||
|
try {
|
||||||
|
return await readOnce(url, i === 0 ? "default" : "reload");
|
||||||
|
} catch (e) {
|
||||||
|
if (!(e && e.name === "AbortError") || i + 1 >= TRIES) throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readJSON(url) {
|
||||||
|
return JSON.parse(new TextDecoder().decode(await read(url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function each(items, fn) {
|
async function each(items, fn) {
|
||||||
@@ -107,9 +142,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
const resp = await fetch(`data/files.json?v=${build}`);
|
const list = await readJSON(`data/files.json?v=${build}`);
|
||||||
if (!resp.ok) throw new Error(`data/files.json: HTTP ${resp.status}`);
|
got = 0;
|
||||||
const list = await resp.json();
|
|
||||||
total = list.wasm + list.manifest.size + list.files.reduce((s, f) => s + f.size, 0);
|
total = list.wasm + list.manifest.size + list.files.reduce((s, f) => s + f.size, 0);
|
||||||
const wasm = read(`game.wasm?v=${build}`);
|
const wasm = read(`game.wasm?v=${build}`);
|
||||||
const manifest = read(`data/${list.manifest.path}?v=${build}`);
|
const manifest = read(`data/${list.manifest.path}?v=${build}`);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -60,9 +61,16 @@ func TestPackWeb(t *testing.T) {
|
|||||||
if !strings.Contains(string(page), "<Game &") || strings.Contains(string(page), "<Game") {
|
if !strings.Contains(string(page), "<Game &") || strings.Contains(string(page), "<Game") {
|
||||||
t.Fatal("the title is not escaped")
|
t.Fatal("the title is not escaped")
|
||||||
}
|
}
|
||||||
if !strings.Contains(string(page), `data-build="abababababab"`) {
|
key := regexp.MustCompile(`data-build="([0-9a-f]{12})"`).FindStringSubmatch(string(page))
|
||||||
|
if key == nil {
|
||||||
t.Fatal("the build key is missing")
|
t.Fatal("the build key is missing")
|
||||||
}
|
}
|
||||||
|
if err := os.WriteFile(j.l.binary(), append(wasm, '!'), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if again, err := j.webBuild(); err != nil || again == key[1] {
|
||||||
|
t.Fatalf("a new runtime kept the build key %s (%v)", again, err)
|
||||||
|
}
|
||||||
raw, err := os.ReadFile(filepath.Join(j.l.data, WebFiles))
|
raw, err := os.ReadFile(filepath.Join(j.l.data, WebFiles))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
@@ -198,6 +198,11 @@ func (g *Game) Engine() *engine.Engine {
|
|||||||
return g.e
|
return g.e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scene is the scene Play loaded, nil before the first Play or after Stop.
|
||||||
|
func (g *Game) Scene() *scene.Scene {
|
||||||
|
return g.scene
|
||||||
|
}
|
||||||
|
|
||||||
func (g *Game) Play(name string, src []byte, mods []*bytecode.Module) error {
|
func (g *Game) Play(name string, src []byte, mods []*bytecode.Module) error {
|
||||||
g.Stop()
|
g.Stop()
|
||||||
rt := scriptruntime.New(scriptruntime.Options{Bindings: g.reg, Schema: g.types.Schema(), Log: g.log, VM: playmods.SandboxVM()})
|
rt := scriptruntime.New(scriptruntime.Options{Bindings: g.reg, Schema: g.types.Schema(), Log: g.log, VM: playmods.SandboxVM()})
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import (
|
|||||||
"aego/app/agent"
|
"aego/app/agent"
|
||||||
"aego/core/log"
|
"aego/core/log"
|
||||||
"aego/core/rewind"
|
"aego/core/rewind"
|
||||||
|
"aego/engine/recorder"
|
||||||
|
"aego/modules/audio"
|
||||||
|
"aego/modules/camera"
|
||||||
|
"aego/modules/gameplay"
|
||||||
"aego/project"
|
"aego/project"
|
||||||
"aego/sdk"
|
"aego/sdk"
|
||||||
)
|
)
|
||||||
@@ -45,6 +49,7 @@ func AgentRunner(mods ...sdk.Module) agent.Runner {
|
|||||||
}
|
}
|
||||||
sum, _, _ := WorldHash(a.Engine.MainWorld(), rewind.NewHasher())
|
sum, _, _ := WorldHash(a.Engine.MainWorld(), rewind.NewHasher())
|
||||||
rep.Hash = hex.EncodeToString(sum[:])
|
rep.Hash = hex.EncodeToString(sum[:])
|
||||||
|
rep.Counters = counters(a)
|
||||||
a.Close()
|
a.Close()
|
||||||
rep.Log = logLines(buf.String())
|
rep.Log = logLines(buf.String())
|
||||||
return rep, nil
|
return rep, nil
|
||||||
@@ -63,3 +68,27 @@ func logLines(s string) []string {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// counters sums what the modules counted during the run, for scene run and run_headless.
|
||||||
|
func counters(a *App) map[string]uint64 {
|
||||||
|
out := map[string]uint64{}
|
||||||
|
w := a.Engine.MainWorld()
|
||||||
|
if s := gameplay.Of(w); s != nil {
|
||||||
|
st := s.Stats()
|
||||||
|
out["gameplay.steps"], out["gameplay.casts"], out["gameplay.hits"] = st.Steps, st.Casts, st.Hits
|
||||||
|
out["gameplay.deaths"], out["gameplay.respawns"], out["gameplay.pickups"] = st.Deaths, st.Respawns, st.Pickups
|
||||||
|
}
|
||||||
|
if c := camera.Of(w); c != nil {
|
||||||
|
c.Outputs(func(camera.OutputInfo) { out["camera.outputs"]++ })
|
||||||
|
}
|
||||||
|
if m := audio.ModuleOf(a.Engine); m != nil {
|
||||||
|
c, st := m.Counts(), m.Stats()
|
||||||
|
out["audio.finished"], out["audio.stolen"], out["audio.rejected"], out["audio.starved"] = c.Finished, c.Stolen, c.Rejected, c.Starved
|
||||||
|
out["audio.underruns"] = st.Underruns
|
||||||
|
}
|
||||||
|
if r := w.Recorder; r != nil {
|
||||||
|
counts := r.Counts()
|
||||||
|
out["events.audio"], out["events.gameplay"] = counts[recorder.Audio], counts[recorder.Gameplay]
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ type bus struct {
|
|||||||
target float32
|
target float32
|
||||||
delta float32
|
delta float32
|
||||||
ramp int
|
ramp int
|
||||||
|
live bool
|
||||||
acc []float32
|
acc []float32
|
||||||
peak atomic.Uint32
|
peak atomic.Uint32
|
||||||
rms atomic.Uint32
|
rms atomic.Uint32
|
||||||
|
|||||||
@@ -441,3 +441,87 @@ func TestVoicePositionAndKill(t *testing.T) {
|
|||||||
t.Fatalf("kill: %+v, live %v", ev, m.Live(id))
|
t.Fatalf("kill: %+v, live %v", ev, m.Live(id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func directPair(tb testing.TB, mem *Memory, loop bool) (*Mixer, voice, voice) {
|
||||||
|
m := newMixer(tb)
|
||||||
|
m.Play(PlayParams{Memory: mem, Gain: 0.7, Pan: 0.3, Loop: loop, FadeIn: 0.005})
|
||||||
|
buf := make([]float32, 64*2)
|
||||||
|
m.Mix(buf)
|
||||||
|
for i := range m.voices {
|
||||||
|
if m.voices[i].state == voicePlaying {
|
||||||
|
return m, m.voices[i], m.voices[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tb.Fatal("no playing voice")
|
||||||
|
return nil, voice{}, voice{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The direct path for pitch 1 gives the same samples, position, gain and end as Hermite, through fades, loops and the stop ramp.
|
||||||
|
func TestDirectPathMatchesHermite(t *testing.T) {
|
||||||
|
for _, ch := range []int{1, 2} {
|
||||||
|
for _, loop := range []bool{false, true} {
|
||||||
|
mem := sine(440, rate/20, ch, 0.5)
|
||||||
|
if loop {
|
||||||
|
mem.LoopStart, mem.LoopEnd = 100, mem.Frames()-50
|
||||||
|
}
|
||||||
|
m, a, b := directPair(t, mem, loop)
|
||||||
|
if b.step != 1 {
|
||||||
|
t.Fatalf("step %v, the direct path needs 1", b.step)
|
||||||
|
}
|
||||||
|
const n = 256
|
||||||
|
accA, accB := make([]float32, n*2), make([]float32, n*2)
|
||||||
|
for block := 0; block < 40; block++ {
|
||||||
|
if block == 20 {
|
||||||
|
a.rampTo(0, 300)
|
||||||
|
b.rampTo(0, 300)
|
||||||
|
a.stopping, b.stopping = true, true
|
||||||
|
}
|
||||||
|
clear(accA)
|
||||||
|
clear(accB)
|
||||||
|
da := m.renderMemory(&a, accA, n)
|
||||||
|
db := m.renderMemoryDirect(&b, accB, n)
|
||||||
|
if da != db || a.pos != b.pos || a.gain != b.gain {
|
||||||
|
t.Fatalf("ch=%d loop=%v block %d: done %v/%v pos %v/%v gain %v/%v", ch, loop, block, da, db, a.pos, b.pos, a.gain, b.gain)
|
||||||
|
}
|
||||||
|
for i := range accA {
|
||||||
|
if accA[i] != accB[i] {
|
||||||
|
t.Fatalf("ch=%d loop=%v block %d sample %d: %v != %v", ch, loop, block, i, accA[i], accB[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if da {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadedPitch1(tb testing.TB) (*Mixer, []float32) {
|
||||||
|
m := newMixer(tb)
|
||||||
|
mem := sine(440, rate/10, 1, 0.5)
|
||||||
|
for i := 0; i < 64; i++ {
|
||||||
|
m.Play(PlayParams{Memory: mem, Gain: 0.1, Pan: float32(i%3 - 1), Pitch: 1, Loop: true})
|
||||||
|
}
|
||||||
|
buf := make([]float32, 512*2)
|
||||||
|
m.Mix(buf)
|
||||||
|
if m.Active() != 64 {
|
||||||
|
tb.Fatalf("%d active voices", m.Active())
|
||||||
|
}
|
||||||
|
return m, buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMixPitch1Allocs(t *testing.T) {
|
||||||
|
m, buf := loadedPitch1(t)
|
||||||
|
if n := testing.AllocsPerRun(200, func() { m.Mix(buf) }); n != 0 {
|
||||||
|
t.Fatalf("Mix allocates %v times per call", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMix64Pitch1(b *testing.B) {
|
||||||
|
m, buf := loadedPitch1(b)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for b.Loop() {
|
||||||
|
m.Mix(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ func (p *Pump) Start() {
|
|||||||
|
|
||||||
func (p *Pump) run(stop, done chan struct{}) {
|
func (p *Pump) run(stop, done chan struct{}) {
|
||||||
defer close(done)
|
defer close(done)
|
||||||
period := time.Duration(float64(p.cfg.Block) / float64(p.cfg.SampleRate) / 2 * float64(time.Second))
|
period := time.Duration(float64(p.cfg.Block*p.cfg.Target) / float64(p.cfg.SampleRate) / 6 * float64(time.Second))
|
||||||
t := time.NewTicker(max(period, time.Millisecond))
|
t := time.NewTicker(max(period, time.Millisecond))
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
for {
|
for {
|
||||||
|
|||||||
@@ -102,3 +102,33 @@ func TestAgentSceneTreeJSON(t *testing.T) {
|
|||||||
t.Fatalf("output %v:\n%s", err, out)
|
t.Fatalf("output %v:\n%s", err, out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The top-down example needs no native module, so the CLI can run it from a copy.
|
||||||
|
func TestAgentSceneRunCounters(t *testing.T) {
|
||||||
|
dir := copyProject(t, "topdown")
|
||||||
|
r, w, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
stdout := os.Stdout
|
||||||
|
os.Stdout = w
|
||||||
|
code := dispatch([]string{"scene", "run", "scenes/main.aescene", "--frames", "300", "--json", "--project", dir})
|
||||||
|
os.Stdout = stdout
|
||||||
|
w.Close()
|
||||||
|
out, _ := io.ReadAll(r)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("exit %d:\n%s", code, out)
|
||||||
|
}
|
||||||
|
var v struct {
|
||||||
|
Value struct {
|
||||||
|
Counters map[string]uint64 `json:"counters"`
|
||||||
|
} `json:"value"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(out, &v); err != nil {
|
||||||
|
t.Fatalf("output %v:\n%s", err, out)
|
||||||
|
}
|
||||||
|
c := v.Value.Counters
|
||||||
|
if c["gameplay.steps"] == 0 || c["camera.outputs"] != 1 {
|
||||||
|
t.Fatalf("counters %v, want gameplay steps and one camera output", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
## Build and run
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bash tools/bootstrap-native.sh
|
||||||
|
make cli
|
||||||
|
.cache/bin/aego edit examples/platformer
|
||||||
|
```
|
||||||
|
|
||||||
|
`make res` regenerates icons, previews and ES shaders; `make golden-update` rewrites golden images after an intended UI change.
|
||||||
|
|
||||||
|
## Check levels
|
||||||
|
|
||||||
|
| Level | When | Command |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | after an edit | `make quick`: build and short tests of the packages your change touches and everything that imports them |
|
||||||
|
| 2 | before a commit | `make affected`, plus `make check-camera`, `check-audio`, `check-gameplay`, `check-mutate`, `check-editor`, `check-web` or `check-docs` for the areas you touched, and `make golden` after UI changes |
|
||||||
|
| 3 | before a push or a tag | `make ci` |
|
||||||
|
|
||||||
|
`tools/affected.sh [base]` lists the affected packages (default base `origin/master`, uncommitted files included). The CI server always runs the full `make ci`.
|
||||||
|
|
||||||
|
## Rules for engine code
|
||||||
|
|
||||||
|
- **Modules talk through contracts.** A module never imports another module: shared types and services live in `contracts/` (`physics2d`, `camera2d`, `scriptcall`, `signals`, `debugdraw`, …). `tools/depcheck` enforces the import rules; an exception goes into `tools/depcheck/exceptions.txt` with its file and reason.
|
||||||
|
- **Kernel stays neutral.** Kernel, editor and `sdk` never name a built-in module, its components or its diagnostic codes in string literals; names come from the module catalog.
|
||||||
|
- **Every component has a Try it scene.** It lives at `res/docs/try/<Component>.aescene` (with `<Component>.aes` and prefabs next to it when needed), shows the component's own effect and runs without warnings; `aego docs check` fails without it.
|
||||||
|
- **Internal state is not a component.** Controller state lives in unexported fields of the component it belongs to or in a world service. A component that exists only to carry state for a service is registered `Hidden`: it is not in Create, Add Component, Try it or the docs.
|
||||||
|
- **Requirements add, they do not block.** Declare `Requires`; Add Component, `scene add-comp`, Paste and presets add the missing components in the same undo step.
|
||||||
|
- **Every diagnostic code has a page with an example.** Component codes (`CAM`, `AUD`, `GMP`, `PHY`, …) also have an example scene that produces the code; `aego docs check` fails without one. Asset codes say `scope: asset`, codes that only an edit during Play produces say `scope: play`.
|
||||||
|
- **Nodes are labelled, not pathed.** UI shows nodes with `props.NodeLabel` (icon and name, the path in the tooltip).
|
||||||
|
- **Editor edits go through history.** Every edit is a history command, normally `commands.Mutation`, so Undo, the Play changes diff and agents see it.
|
||||||
|
- **Hot paths do not allocate.** Systems, mixers and controllers allocate nothing per frame; add `testing.AllocsPerRun` checks and a benchmark next to new hot code.
|
||||||
|
|
||||||
|
## Commits
|
||||||
|
|
||||||
|
One commit per sub-stage, named `Stage <n><x>: <what>`; decisions go into `ARCHITECTURE.md` as `D<number>` in the same commit.
|
||||||
@@ -97,6 +97,16 @@ sdk.Component[Wind](r, sdk.Name("weather.Wind")).
|
|||||||
|
|
||||||
`engine/problems` collects `sdk.Diagnostic{Owner, Code, Severity, Message, Subject, BlocksPlay}` from sources with a revision; the service rebuilds the list only when a revision changes. `sdk.Diagnostics(ctx)` in `Init` returns the service; a module adds runtime diagnostics as a `sdk.ProblemSource` from `AttachWorld` and removes it in `DetachWorld`. The Problems panel shows them with the owner as category; an error with `BlocksPlay` stops Play (D210).
|
`engine/problems` collects `sdk.Diagnostic{Owner, Code, Severity, Message, Subject, BlocksPlay}` from sources with a revision; the service rebuilds the list only when a revision changes. `sdk.Diagnostics(ctx)` in `Init` returns the service; a module adds runtime diagnostics as a `sdk.ProblemSource` from `AttachWorld` and removes it in `DetachWorld`. The Problems panel shows them with the owner as category; an error with `BlocksPlay` stops Play (D210).
|
||||||
|
|
||||||
|
Every code a module reports is listed in `Info.Codes` and has a page `res/docs/diagnostics/<CODE>.md` with front matter `code`, `title`, `severity` (`error`, `warning` or `info`) and either `example: <CODE>.aescene`, a scene next to the page that produces the code (with `<CODE>.aes` next to it when a script has to act, as for PHY007), `scope: asset` for codes that come from importing or checking an asset, or `scope: play` for codes that only an edit during Play produces (PHY006). `aego docs check` fails on a declared code without a page, a page whose code is not declared, a component code without an example, and an example that does not produce its code after `module.ValidateScene` and 60 headless frames. The pages appear under Reference → Diagnostics next to the script codes (D146, D316).
|
||||||
|
|
||||||
|
### Contracts between modules
|
||||||
|
|
||||||
|
A module never imports another module. What two modules share lives in `contracts/<name>` as plain types, a service ID and an interface; the provider registers the service in its world, the consumer looks it up with `engine.ServiceOf` and works without it.
|
||||||
|
|
||||||
|
- **Provisions.** A value handed over at load time, keyed by a kind. `camera2d.AccessibilityKind` (`"camera2d.accessibility"`) carries the IDs of the Screen Shake and Reduce Motion settings: the settings module provides them with `r.Provide(camera2d.AccessibilityKind, camera2d.Accessibility{...})`, the camera module reads `ctx.Provided(camera2d.AccessibilityKind)` in `Init` and scales shake by them every frame.
|
||||||
|
- **`physics2d.Queries`** (`physics2d.ServiceID`). Ray, point, circle and box queries; shape casts `CastCircle`, `CastBox`, `CastCapsule` and `CastBody`, which sweeps every non-trigger collider of a body and returns the first hit with its fraction, normal and, when the shapes already overlap, `Initial` and `Depth`; `Penetration` lists the overlaps of a body for depenetration; `BodyPose`, `BodyVelocity`, `MoveKinematic` (moves a kinematic body so contacts see its velocity), `Teleport`, `ApplyImpulse` and `Gravity`. `QueryFilter` selects layers and triggers; `SkipDynamic` ignores dynamic bodies, and `ExcludeNode` and `ExcludeBody` skip the caller's own collider or body. The gameplay controller is written only against this interface.
|
||||||
|
- **`scriptcall.Caller`** (`scriptcall.ServiceID`). `Method(node, name, params)` finds a plain method by name in the scripts on a node and returns a `Ref`; `Call(&ref, args)` runs it. A hot reload starts a new `Generation`, which makes every older `Ref` stale: look the method up again when `Ref.Gen` differs. The State Machine finds its hooks this way.
|
||||||
|
|
||||||
### Resources
|
### Resources
|
||||||
|
|
||||||
Module icons are named `<owner>.*` or `cat.<owner>`; a clash with the engine atlas or another module is a build error. Built-in modules list their icon sources through `IconDir()`. Docs registered with `Registry.Docs` carry the module as owner and are left out of runtime builds (D211).
|
Module icons are named `<owner>.*` or `cat.<owner>`; a clash with the engine atlas or another module is a build error. Built-in modules list their icon sources through `IconDir()`. Docs registered with `Registry.Docs` carry the module as owner and are left out of runtime builds (D211).
|
||||||
@@ -169,4 +179,6 @@ A panel gets a fresh `Frame` every draw (`UI`, drag and drop, time, focus). Anyt
|
|||||||
|
|
||||||
A module can give any of its components a live example on the documentation site: put a scene in `res/docs/try/<Component>.aescene`, named after the last part of the component ID (`try/Body2D.aescene` for `physics.Body2D`). The page of that component gets a Try it block that runs the scene in the browser with the Inspector, selecting the first node that has the component; a guide page can show a scene with `try: try/name.aescene` and `try_select: module.Component` in its front matter.
|
A module can give any of its components a live example on the documentation site: put a scene in `res/docs/try/<Component>.aescene`, named after the last part of the component ID (`try/Body2D.aescene` for `physics.Body2D`). The page of that component gets a Try it block that runs the scene in the browser with the Inspector, selecting the first node that has the component; a guide page can show a scene with `try: try/name.aescene` and `try_select: module.Component` in its front matter.
|
||||||
|
|
||||||
Try scenes run on the sandbox catalog (core, input, render2d, scripting, physics, debug) with builtin assets only and no scripts. `aego docs check` loads each one, runs it for 60 frames and fails on a load error, a warning or a fault, and on a scene of a module outside the sandbox. Keep the scene to what the component needs, put the documented node first, and add a camera.
|
Try scenes run on the docs runtime catalog (core, input, render2d, render3d, scripting, physics, debug, tween, cameras, gameplay, spawn, savegame and audio) with builtin assets only. A script `<Component>.aes` and prefabs `<Component>.<name>.aeprefab` (with their `.meta`) next to the scene are compiled and mounted with it. `aego docs check` loads each one, runs it for 60 frames and fails on a load error, a warning or a fault, and on a scene of a module outside the catalog. Keep the scene to what the component needs, put the documented node first, and add a camera.
|
||||||
|
|
||||||
|
Every visible component needs one. A component registered `Hidden`, which only carries state for a service, has none and is left out of Create, Add Component, Try it and the docs. The rules for engine code, including these, are in [contributing.md](contributing.md).
|
||||||
|
|||||||
@@ -61,8 +61,10 @@ Sources are `<id>.svg` files in `engine/res/icons/src` and `modules/<id>/res/ico
|
|||||||
| `edit.paste` | engine | 16 | Paste (nodes, assets, inspector field) | ok |
|
| `edit.paste` | engine | 16 | Paste (nodes, assets, inspector field) | ok |
|
||||||
| `edit.rename` | engine | 16 | Rename (nodes, assets, workspace) | ok |
|
| `edit.rename` | engine | 16 | Rename (nodes, assets, workspace) | ok |
|
||||||
| `edit.select_all` | engine | 16 | Edit → Select All | ok |
|
| `edit.select_all` | engine | 16 | Edit → Select All | ok |
|
||||||
|
| `file.audio` | engine | 16 | Documentation: sound clips page | ok |
|
||||||
| `file.autosave` | engine | 16 | File → Autosave Scripts | ok |
|
| `file.autosave` | engine | 16 | File → Autosave Scripts | ok |
|
||||||
| `file.data` | engine | 24 | Assets: data asset tiles | ok |
|
| `file.data` | engine | 24 | Assets: data asset tiles | ok |
|
||||||
|
| `file.download` | engine | 16 | Documentation: Install and run page | ok |
|
||||||
| `file.folder` | engine | 24 | Assets: folders | ok |
|
| `file.folder` | engine | 24 | Assets: folders | ok |
|
||||||
| `file.mesh` | engine | 24 | Assets: mesh tiles | ok |
|
| `file.mesh` | engine | 24 | Assets: mesh tiles | ok |
|
||||||
| `file.open` | engine | 16 | Assets menu: Open | ok |
|
| `file.open` | engine | 16 | Assets menu: Open | ok |
|
||||||
@@ -134,6 +136,7 @@ Sources are `<id>.svg` files in `engine/res/icons/src` and `modules/<id>/res/ico
|
|||||||
| `prefab.variant` | engine | 16 | Assets menu: Create Variant | ok |
|
| `prefab.variant` | engine | 16 | Assets menu: Create Variant | ok |
|
||||||
| `shape.cube` | engine | 16 | template render3d.cube | ok |
|
| `shape.cube` | engine | 16 | template render3d.cube | ok |
|
||||||
| `shape.cylinder` | engine | 16 | template render3d.cylinder | ok |
|
| `shape.cylinder` | engine | 16 | template render3d.cylinder | ok |
|
||||||
|
| `shape.dice` | engine | 16 | Documentation: random numbers module | ok |
|
||||||
| `shape.plane` | engine | 16 | template render3d.plane | ok |
|
| `shape.plane` | engine | 16 | template render3d.plane | ok |
|
||||||
| `shape.sphere` | engine | 16 | template render3d.sphere | ok |
|
| `shape.sphere` | engine | 16 | template render3d.sphere | ok |
|
||||||
| `status.busy` | engine | 14 | Task Center: running task | ok |
|
| `status.busy` | engine | 14 | Task Center: running task | ok |
|
||||||
@@ -151,6 +154,7 @@ Sources are `<id>.svg` files in `engine/res/icons/src` and `modules/<id>/res/ico
|
|||||||
| `ui.chevron_down` | engine | 16 | trees, combos and drop-downs | ok |
|
| `ui.chevron_down` | engine | 16 | trees, combos and drop-downs | ok |
|
||||||
| `ui.chevron_right` | engine | 16 | trees and submenus | ok |
|
| `ui.chevron_right` | engine | 16 | trees and submenus | ok |
|
||||||
| `ui.close` | engine | 16 | tabs, chips and dialogs | ok |
|
| `ui.close` | engine | 16 | tabs, chips and dialogs | ok |
|
||||||
|
| `ui.curve` | engine | 16 | Documentation: curves module | ok |
|
||||||
| `ui.lock` | engine | 16 | Inspector: locked | ok |
|
| `ui.lock` | engine | 16 | Inspector: locked | ok |
|
||||||
| `ui.minus` | engine | 16 | list editors: remove | ok |
|
| `ui.minus` | engine | 16 | list editors: remove | ok |
|
||||||
| `ui.more` | engine | 16 | card menus | ok |
|
| `ui.more` | engine | 16 | card menus | ok |
|
||||||
|
|||||||
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 123 KiB |
@@ -30,6 +30,9 @@ type ProfilerSection struct {
|
|||||||
Draw func()
|
Draw func()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OverlayGame is the flag of Window → Game View Overlays; module overlays and debug channels in the Game View follow it.
|
||||||
|
const OverlayGame = "view.game.overlays"
|
||||||
|
|
||||||
type Overlays struct {
|
type Overlays struct {
|
||||||
on map[string]bool
|
on map[string]bool
|
||||||
Scale float32
|
Scale float32
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ var Editor = []module.IconUse{
|
|||||||
use("edit.paste", 16, "Paste (nodes, assets, inspector field)"),
|
use("edit.paste", 16, "Paste (nodes, assets, inspector field)"),
|
||||||
use("edit.rename", 16, "Rename (nodes, assets, workspace)"),
|
use("edit.rename", 16, "Rename (nodes, assets, workspace)"),
|
||||||
use("edit.select_all", 16, "Edit → Select All"),
|
use("edit.select_all", 16, "Edit → Select All"),
|
||||||
|
use("file.audio", 16, "Documentation: sound clips page"),
|
||||||
use("file.autosave", 16, "File → Autosave Scripts"),
|
use("file.autosave", 16, "File → Autosave Scripts"),
|
||||||
|
use("file.download", 16, "Documentation: Install and run page"),
|
||||||
use("file.data", 24, "Assets: data asset tiles"),
|
use("file.data", 24, "Assets: data asset tiles"),
|
||||||
use("file.folder", 24, "Assets: folders"),
|
use("file.folder", 24, "Assets: folders"),
|
||||||
use("file.mesh", 24, "Assets: mesh tiles"),
|
use("file.mesh", 24, "Assets: mesh tiles"),
|
||||||
@@ -74,6 +76,8 @@ var Editor = []module.IconUse{
|
|||||||
use("file.texture", 24, "Assets: texture tiles"),
|
use("file.texture", 24, "Assets: texture tiles"),
|
||||||
use("file.unknown", 24, "Assets: tiles of other kinds"),
|
use("file.unknown", 24, "Assets: tiles of other kinds"),
|
||||||
use("help.about", 16, "Help → About Aego"),
|
use("help.about", 16, "Help → About Aego"),
|
||||||
|
use("shape.dice", 16, "Documentation: random numbers module"),
|
||||||
|
use("ui.curve", 16, "Documentation: curves module"),
|
||||||
use("help.book", 16, "Documentation tab"),
|
use("help.book", 16, "Documentation tab"),
|
||||||
use("log.error", 16, "Console: errors"),
|
use("log.error", 16, "Console: errors"),
|
||||||
use("log.info", 16, "Console: info"),
|
use("log.info", 16, "Console: info"),
|
||||||
|
|||||||
@@ -50,6 +50,27 @@ type Registry struct {
|
|||||||
owned map[string]*owned
|
owned map[string]*owned
|
||||||
disabled map[string]string
|
disabled map[string]string
|
||||||
pending []pending
|
pending []pending
|
||||||
|
ticks []tick
|
||||||
|
}
|
||||||
|
|
||||||
|
type tick struct {
|
||||||
|
ext string
|
||||||
|
fn func()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tick runs fn once per editor frame, before the panels draw, whether or not any view is visible.
|
||||||
|
func (r *Registry) Tick(fn func()) {
|
||||||
|
r.ticks = append(r.ticks, tick{ext: r.ext(), fn: fn})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunTicks calls the Tick hooks of the extensions that are not disabled.
|
||||||
|
func (r *Registry) RunTicks() {
|
||||||
|
for _, t := range r.ticks {
|
||||||
|
if _, off := r.disabled[t.ext]; off {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
Safe(t.ext, r, t.fn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(d Deps) *Registry {
|
func New(d Deps) *Registry {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
|
|
||||||
const ID = "events"
|
const ID = "events"
|
||||||
|
|
||||||
var shown = [...]recorder.Kind{recorder.Signal, recorder.Event, recorder.Fault, recorder.Spawn, recorder.Despawn, recorder.SceneChange, recorder.Input, recorder.Audio}
|
var shown = [...]recorder.Kind{recorder.Signal, recorder.Event, recorder.Fault, recorder.Spawn, recorder.Despawn, recorder.SceneChange, recorder.Input, recorder.Audio, recorder.Gameplay}
|
||||||
|
|
||||||
type Deps struct {
|
type Deps struct {
|
||||||
World func() *engine.World
|
World func() *engine.World
|
||||||
@@ -174,9 +174,11 @@ func (p *Panel) toolbar(ui *core.Context, s *scene.Scene) {
|
|||||||
widgets.ToggleButton(ui, core.IDSeq(ui.ID("kind"), i), mathx.Rect{X: x, Y: row.Y, W: w, H: row.H}, k.String(), &p.show[k])
|
widgets.ToggleButton(ui, core.IDSeq(ui.ID("kind"), i), mathx.Rect{X: x, Y: row.Y, W: w, H: row.H}, k.String(), &p.show[k])
|
||||||
x += w + 4
|
x += w + 4
|
||||||
}
|
}
|
||||||
if row.X+row.W-x > 80 {
|
if row.X+row.W-x < 120 {
|
||||||
widgets.SearchField(ui, ui.ID("search"), mathx.Rect{X: x, Y: row.Y, W: row.X + row.W - x, H: row.H}, &p.search)
|
row = ui.Cursor().Next(0)
|
||||||
|
x = row.X
|
||||||
}
|
}
|
||||||
|
widgets.SearchField(ui, ui.ID("search"), mathx.Rect{X: x, Y: row.Y, W: row.X + row.W - x, H: row.H}, &p.search)
|
||||||
row = ui.Cursor().Next(0)
|
row = ui.Cursor().Next(0)
|
||||||
x = row.X
|
x = row.X
|
||||||
place := func(w float32) mathx.Rect {
|
place := func(w float32) mathx.Rect {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
"aego/ui/widgets"
|
"aego/ui/widgets"
|
||||||
)
|
)
|
||||||
|
|
||||||
const CommandOverlays = "view.game.overlays"
|
const CommandOverlays = context.OverlayGame
|
||||||
|
|
||||||
type Panel struct {
|
type Panel struct {
|
||||||
play *play.Controller
|
play *play.Controller
|
||||||
@@ -77,6 +77,7 @@ func (p *Panel) Close() {
|
|||||||
|
|
||||||
func (p *Panel) Draw(f *services.Frame, r mathx.Rect) {
|
func (p *Panel) Draw(f *services.Frame, r mathx.Rect) {
|
||||||
ui := f.UI
|
ui := f.UI
|
||||||
|
p.tl.Apply()
|
||||||
th := &ui.Theme
|
th := &ui.Theme
|
||||||
if p.pool == nil {
|
if p.pool == nil {
|
||||||
p.hook = p.render
|
p.hook = p.render
|
||||||
|
|||||||
@@ -24,16 +24,51 @@ type TimelineBar struct {
|
|||||||
bar mathx.Rect
|
bar mathx.Rect
|
||||||
dragging bool
|
dragging bool
|
||||||
dragTo int
|
dragTo int
|
||||||
scrubbed int
|
pending int
|
||||||
|
sought int
|
||||||
|
release bool
|
||||||
seekErr string
|
seekErr string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTimelineBar(src Scrubber) *TimelineBar {
|
func NewTimelineBar(src Scrubber) *TimelineBar {
|
||||||
return &TimelineBar{src: src}
|
return &TimelineBar{src: src, pending: -1, sought: -1}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *TimelineBar) Reset() {
|
func (b *TimelineBar) Reset() {
|
||||||
b.bar, b.dragging = mathx.Rect{}, false
|
b.bar, b.dragging, b.pending, b.sought, b.release = mathx.Rect{}, false, -1, -1, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// hold records where the drag is; the seek waits for Apply, so any number of mouse moves in a frame cost one seek.
|
||||||
|
func (b *TimelineBar) hold(n int, down bool) {
|
||||||
|
b.dragTo, b.pending = n, n
|
||||||
|
if !down {
|
||||||
|
b.dragging, b.release = false, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply runs at most one seek per frame: a scrub to the latest drag position, or the final seek after release.
|
||||||
|
func (b *TimelineBar) Apply() {
|
||||||
|
if b.pending < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n := b.pending
|
||||||
|
if b.release {
|
||||||
|
b.release, b.pending = false, -1
|
||||||
|
if n != b.sought && b.seekErr == "" {
|
||||||
|
if err := b.src.Seek(n); err != nil {
|
||||||
|
b.seekErr = err.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.sought = -1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n == b.sought || b.seekErr != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.sought = n
|
||||||
|
if err := b.src.Scrub(n); err != nil {
|
||||||
|
b.seekErr = err.Error()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *TimelineBar) Clear() {
|
func (b *TimelineBar) Clear() {
|
||||||
@@ -123,27 +158,17 @@ func (b *TimelineBar) Draw(ui *core.Context, r mathx.Rect) {
|
|||||||
id := ui.ID("timeline")
|
id := ui.ID("timeline")
|
||||||
hit := mathx.Rect{X: track.X - 4, Y: bar.Y, W: track.W + 8, H: bar.H}
|
hit := mathx.Rect{X: track.X - 4, Y: bar.Y, W: track.W + 8, H: bar.H}
|
||||||
if ui.Pressed(id, hit) {
|
if ui.Pressed(id, hit) {
|
||||||
b.dragging, b.seekErr, b.scrubbed = true, "", -1
|
b.dragging, b.seekErr, b.sought, b.release = true, "", -1, false
|
||||||
}
|
}
|
||||||
cur := tl.Cursor
|
cur := tl.Cursor
|
||||||
if tl.Beyond {
|
if tl.Beyond {
|
||||||
cur = tl.Frames
|
cur = tl.Frames
|
||||||
}
|
}
|
||||||
if b.dragging {
|
if b.dragging {
|
||||||
b.dragTo = min(max(frameAt(ui.Mouse().X), tl.Floor), tl.Usable)
|
b.hold(min(max(frameAt(ui.Mouse().X), tl.Floor), tl.Usable), ui.MouseDown(input.MouseLeft))
|
||||||
|
}
|
||||||
|
if b.dragging || b.pending >= 0 {
|
||||||
cur = b.dragTo
|
cur = b.dragTo
|
||||||
if b.dragTo != b.scrubbed && b.seekErr == "" {
|
|
||||||
b.scrubbed = b.dragTo
|
|
||||||
if err := b.src.Scrub(b.dragTo); err != nil {
|
|
||||||
b.seekErr = err.Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !ui.MouseDown(input.MouseLeft) {
|
|
||||||
b.dragging = false
|
|
||||||
if err := b.src.Seek(b.dragTo); err != nil {
|
|
||||||
b.seekErr = err.Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
hx := xAt(cur)
|
hx := xAt(cur)
|
||||||
ui.Rect(mathx.Rect{X: hx - 2, Y: bar.Y + 3, W: 4, H: bar.H - 6}, th.Accent)
|
ui.Rect(mathx.Rect{X: hx - 2, Y: bar.Y + 3, W: 4, H: bar.H - 6}, th.Accent)
|
||||||
|
|||||||