2 Commits
Author SHA1 Message Date
sanspie 8e717a4fd1 Stage 10b3–10b3b: desktop onboarding, app icon, clip inspector (D327)
ci / checks (push) Successful in 31m3s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 48m31s
ci / release (push) Successful in 1h28m27s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
2026-10-21 20:33:32 +03:00
sanspie a2c3a24381 Stage 10b: audio mixer, codecs, clips, events, sources, zones and mixer UI (D307–D311, D321–D326) 2026-10-21 20:33:25 +03:00
254 changed files with 16535 additions and 278 deletions
+4
View File
@@ -37,6 +37,10 @@ jobs:
- run: go vet ./platform/sdl
- run: make lint
- run: make depcheck
- name: packaging tools
run: sudo apt-get install -y nsis squashfs-tools
- name: installer dry run
run: make nsis-check
build:
strategy:
+61 -8
View File
File diff suppressed because it is too large Load Diff
+17 -3
View File
@@ -14,7 +14,7 @@ BENCH_COUNT ?= 10
BENCH_CPU ?= 2
BENCH_PIN = $(shell command -v taskset >/dev/null && echo taskset -c $(BENCH_CPU))
EDITOR_VERSION ?= 0.1.0
EDITOR_ICON ?= third_party/brand/aego-1024.png
EDITOR_ICON ?= app/brand/png
EXPORT_SMOKE = $(CURDIR)/.cache/export-smoke
EXPORT_HOME = $(CURDIR)/.cache/export-home
CHROMIUM ?= $(shell command -v chromium || command -v chromium-browser || command -v google-chrome)
@@ -26,6 +26,7 @@ WEB_DEMO_BUDGET = @size=$$(stat -c %s site/demo/game.wasm.gz); echo "demo game.w
if [ $$size -gt $(WASM_BUDGET) ]; then echo "the web demo is over the 8 MB gzip budget"; exit 1; fi
TRY_BUDGET = 10485760
WEB_TRY = cd tools/websmoke && CHROMIUM=$(CHROMIUM) node try.mjs --site=$(CURDIR)/site
WEB_AUDIO = cd tools/websmoke && CHROMIUM=$(CHROMIUM) node audio.mjs --site=$(CURDIR)/site
TRY_SIZE_CHECK = gzip -9 -k -f site/try/aego-docs.wasm; size=$$(stat -c %s site/try/aego-docs.wasm.gz); echo "aego-docs.wasm.gz: $$size bytes"; \
if [ $$size -gt $(TRY_BUDGET) ]; then echo "the docs runtime is over the 10 MB gzip budget, see make try-size"; exit 1; fi
# Module targets, appended by modules/*/module.mk.
@@ -38,7 +39,7 @@ BENCH_PHYSICS_PKGS :=
.DEFAULT_GOAL := bootstrap
-include modules/*/module.mk
.PHONY: bootstrap build test test-nonative test-editor test-runtime test-gl vet lint depcheck smoke run genfont genicons icons icons-doc res genpreviews golden golden-update site-headless bench bench-baseline bench-scripts bench-build bench-compare bench-profile bench-scripts-run bench-allocs playground playground-size site site-check cli example example-platformer check-all ci clean fuzz runtime export-smoke editor-app shaders test-wasm web-smoke web-smoke-update try-size 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
XVFB := $(shell command -v xvfb-run 2>/dev/null)
GL_OVERRIDE = MESA_GL_VERSION_OVERRIDE=3.3 MESA_GLSL_VERSION_OVERRIDE=330
@@ -49,7 +50,7 @@ GL_ENV = env SDL_VIDEODRIVER=offscreen LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=ll
endif
NONATIVE_PKGS = $$(go list ./... | grep -v -e '/platform/sdl$$' -e '/gpu/gl33$$' -e '/gpu/gl33/gl$$')
LINT_PKGS = $$(go list ./... | grep -v '/gpu/gl33/gl$$')
LINT_PKGS = $$(go list ./... | grep -v -e '/gpu/gl33/gl$$' -e '/audio/codec/vorbis/internal/')
bootstrap: $(STAMP)
@@ -246,6 +247,7 @@ web-smoke: site
$(WEB_SMOKE) --report=$(CURDIR)/.cache/web-bench.json
$(WEB_DEMO)
$(WEB_TRY)
$(WEB_AUDIO)
web-smoke-update: site
$(WEB_SMOKE) --update
@@ -281,6 +283,8 @@ fuzz:
go test -run '^$$' -fuzz '^FuzzParse$$' -fuzztime $(FUZZTIME) ./script/parser
go test -run '^$$' -fuzz '^FuzzDecodeVerify$$' -fuzztime $(FUZZTIME) ./script/verify
go test -run '^$$' -fuzz '^FuzzDecodeVerifyV3$$' -fuzztime $(FUZZTIME) ./script/verify
go test -run '^$$' -fuzz '^FuzzWAV$$' -fuzztime $(FUZZTIME) ./audio/codec
go test -run '^$$' -fuzz '^FuzzVorbis$$' -fuzztime $(FUZZTIME) ./audio/codec
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
@@ -321,5 +325,15 @@ editor-app: bootstrap
go build -o .cache/bin/aego-editor ./cmd/aego-editor
go run ./tools/appbundle -bin .cache/bin/aego-editor -out .cache -version $(EDITOR_VERSION) $(if $(wildcard $(EDITOR_ICON)),-icon $(EDITOR_ICON))
dmg: editor-app
tools/release/dmg.sh ".cache/Aego Editor.app" .cache/dist/Aego-$$(sed -n 's/^const Version = "\(.*\)"$$/\1/p' engine/config.go).dmg
nsis-check:
sh tools/release/nsis-check.sh
appimage: cli
mkdir -p .cache/stage/bin .cache/dist && cp .cache/bin/aego .cache/stage/bin/aego && printf 'dev\n' > .cache/stage/VERSION && cp tools/release/README.txt .cache/stage/
sh tools/release/appimage.sh .cache/stage .cache/dist/Aego-dev-x86_64.AppImage
clean:
rm -rf .cache/build
+3
View File
@@ -2,6 +2,8 @@ package editors
import (
"aego/app/assembly"
"aego/modules/audio"
audioeditor "aego/modules/audio/editor"
"aego/modules/camera"
cameraeditor "aego/modules/camera/editor"
"aego/modules/debug"
@@ -23,5 +25,6 @@ func With(c assembly.Catalog) assembly.Catalog {
With(render3d.ID, render3deditor.Hook).
With(physics.ID, physicseditor.Hook).
With(camera.ID, cameraeditor.Hook).
With(audio.ID, audioeditor.Hook).
With(debug.ID, debugeditor.Hook)
}
+2
View File
@@ -4,6 +4,7 @@ import (
"aego/app/assembly"
"aego/engine/base"
"aego/engine/module"
"aego/modules/audio"
"aego/modules/camera"
"aego/modules/debug"
inputmod "aego/modules/input"
@@ -33,6 +34,7 @@ func Catalog() assembly.Catalog {
{ID: render3d.ID, New: func(assembly.Options) module.Module { return render3d.New() }},
{ID: spawn.ID, New: func(assembly.Options) module.Module { return spawn.New() }},
{ID: savegame.ID, Requires: []string{scripting.ID, spawn.ID}, New: func(assembly.Options) module.Module { return savegame.New() }},
{ID: audio.ID, New: func(assembly.Options) module.Module { return audio.New() }},
}
}
+2
View File
@@ -9,6 +9,7 @@ import (
"sort"
"strings"
"aego/app/proc"
"aego/core/errs"
"aego/core/log"
"aego/project"
@@ -169,6 +170,7 @@ func Build(p *project.Project, t Target, mode Mode, engineVersion string, lg *lo
cmd := exec.Command("go", args...)
cmd.Dir = p.Root
cmd.Env = append(os.Environ(), "GOOS="+t.OS, "GOARCH="+t.Arch)
proc.Hide(cmd)
output, err := cmd.CombinedOutput()
if err != nil {
os.RemoveAll(outDir)
+3
View File
@@ -8,6 +8,7 @@ import (
"path/filepath"
"strings"
"aego/app/proc"
"aego/core/errs"
"aego/core/log"
"aego/project"
@@ -32,12 +33,14 @@ func RunIcons(p *project.Project, engineVersion string, out io.Writer) (int, err
defer os.Remove(bin)
build := exec.Command("go", "build", "-tags", strings.Join(t.Tags, ","), "-o", bin, "./.aego/generated/icons")
build.Dir = p.Root
proc.Hide(build)
if output, err := build.CombinedOutput(); err != nil {
return 2, errs.New(errs.BuildFailed, "native icons helper does not compile",
log.F("output", strings.TrimSpace(string(output))))
}
run := exec.Command(bin)
run.Dir, run.Stdout, run.Stderr = p.Root, out, out
proc.Hide(run)
err = run.Run()
var ee *exec.ExitError
if errors.As(err, &ee) {
+3
View File
@@ -6,6 +6,7 @@ import (
"path/filepath"
"strings"
"aego/app/proc"
"aego/core/errs"
"aego/core/log"
"aego/project"
@@ -52,6 +53,7 @@ func BuildManifest(p *project.Project, engineVersion string, lg *log.Logger) (st
defer os.Remove(bin)
build := exec.Command("go", "build", "-tags", strings.Join(t.Tags, ","), "-o", bin, "./.aego/generated/manifest")
build.Dir = p.Root
proc.Hide(build)
if output, err := build.CombinedOutput(); err != nil {
return "", errs.New(errs.BuildFailed, "native bindings helper does not compile",
log.F("output", strings.TrimSpace(string(output))))
@@ -59,6 +61,7 @@ func BuildManifest(p *project.Project, engineVersion string, lg *log.Logger) (st
tmp := out + ".tmp"
run := exec.Command(bin, tmp)
run.Dir = p.Root
proc.Hide(run)
if output, err := run.CombinedOutput(); err != nil {
os.Remove(tmp)
return "", errs.New(errs.BuildFailed, "native module Register failed",
+33
View File
@@ -0,0 +1,33 @@
package brand
import (
"bytes"
"embed"
"image"
"image/png"
"strconv"
)
//go:embed png/aego-*.png
var files embed.FS
var windowSizes = []int{16, 24, 32, 48, 64, 128, 256}
func PNG(n int) ([]byte, bool) {
data, err := files.ReadFile("png/aego-" + strconv.Itoa(n) + ".png")
return data, err == nil
}
func Window() []image.Image {
out := make([]image.Image, 0, len(windowSizes))
for _, n := range windowSizes {
data, ok := PNG(n)
if !ok {
continue
}
if img, err := png.Decode(bytes.NewReader(data)); err == nil {
out = append(out, img)
}
}
return out
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

+12 -48
View File
File diff suppressed because it is too large Load Diff
+158
View File
File diff suppressed because it is too large Load Diff
+49
View File
@@ -1,14 +1,19 @@
package edit
import (
"net"
"net/http"
"os/exec"
"path/filepath"
"runtime"
"sync"
"aego/app/bootstrap"
"aego/app/export"
"aego/app/projects"
"aego/asset/pipeline"
"aego/editor/core/services"
"aego/editor/core/sysopen"
"aego/engine/problems"
"aego/project"
)
@@ -87,3 +92,47 @@ func (x *exporter) Reveal(path string) error {
}
return cmd.Start()
}
var webServers sync.Map
func serveWeb(dir string) (string, error) {
if u, ok := webServers.Load(dir); ok {
return u.(string), nil
}
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return "", err
}
files := http.FileServer(http.Dir(dir))
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp")
w.Header().Set("Cache-Control", "no-store")
files.ServeHTTP(w, r)
})
go http.Serve(ln, h)
u := "http://" + ln.Addr().String() + "/"
webServers.Store(dir, u)
return u, nil
}
func (x *exporter) Run(out, target string) error {
path, err := export.Launchable(x.p, target, out)
if err != nil {
return err
}
t, _ := export.ParseTarget(target)
switch {
case t.Web():
u, err := serveWeb(path)
if err != nil {
return err
}
return sysopen.Open(u)
case t.OS == "darwin":
return exec.Command("open", path).Start()
}
cmd := exec.Command(path)
cmd.Dir = filepath.Dir(path)
return cmd.Start()
}
+83 -7
View File
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
package edit
import (
"path/filepath"
"testing"
"aego/app/projects"
"aego/editor/shell/launcher"
"aego/project"
)
func TestLauncherCreatesAndOpens(t *testing.T) {
recent := filepath.Join(t.TempDir(), "recent")
ls, err := OpenLauncher(Config{Headless: true, Recent: recent})
if err != nil {
t.Fatal(err)
}
ls.UI.SetTab(launcher.TabNew)
dir := filepath.Join(t.TempDir(), "Aego Projects", "game")
if _, err := ls.UI.Create(dir, projects.Template2D, "", ""); err != nil {
ls.Close()
t.Fatal(err)
}
for i := 0; i < 3 && ls.Step(); i++ {
}
ls.Close()
root, ok := ls.UI.Result()
if !ok {
t.Fatal("the launcher has no result")
}
p, err := project.Load(root)
if err != nil {
t.Fatal(err)
}
s, err := Open(p, Config{Headless: true, NoWatch: true, Recent: recent})
if err != nil {
t.Fatal(err)
}
defer s.Close()
for i := 0; i < 3; i++ {
s.Step()
}
if s.App.Context().Docs.Active() == nil {
t.Fatal("no scene is open after creating the project")
}
list, _ := launcher.LoadRecent(recent)
if len(list) == 0 || list[0].Path != root {
t.Fatalf("recent list %+v", list)
}
}
+78
View File
@@ -0,0 +1,78 @@
package edit
import (
"io"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"aego/core/log"
)
const keepLogs = 10
type logFile struct {
f *os.File
}
func LogsDir() string {
dir, err := os.UserCacheDir()
if err != nil {
return ""
}
return filepath.Join(dir, "aego", "logs")
}
func attachLogs(cfg *Config) *logFile {
if cfg.Logger == nil {
cfg.Logger = log.New(log.NewTextSink(os.Stderr))
if cfg.Debug {
cfg.Logger.SetLevel(log.Debug)
}
}
dir := LogsDir()
if dir == "" || os.MkdirAll(dir, 0o755) != nil {
return &logFile{}
}
name := "editor-" + time.Now().Format("20060102-150405") + "-" + strconv.Itoa(os.Getpid()) + ".log"
f, err := os.Create(filepath.Join(dir, name))
if err != nil {
return &logFile{}
}
cfg.Logger.AddSink(log.NewTextSink(f))
pruneLogs(dir)
return &logFile{f: f}
}
func (l *logFile) writer(w io.Writer) io.Writer {
if l.f == nil {
return w
}
return io.MultiWriter(w, l.f)
}
func (l *logFile) close() {
if l.f != nil {
l.f.Close()
}
}
func pruneLogs(dir string) {
entries, err := os.ReadDir(dir)
if err != nil {
return
}
var names []string
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".log") {
names = append(names, e.Name())
}
}
sort.Strings(names)
for i := 0; i+keepLogs < len(names); i++ {
os.Remove(filepath.Join(dir, names[i]))
}
}
+1 -1
View File
@@ -179,7 +179,7 @@ func TestProfilerSectionsFollowPlay(t *testing.T) {
s.Step()
}
drawPanel(s, prof)
expectSections(t, prof.Sections(), "Frame", "Memory", "Game", "Scripts", "Physics")
expectSections(t, prof.Sections(), "Frame", "Memory", "Game", "Scripts", "Physics", "Audio")
if _, err := ctx.Docs.Open("scenes/other.aescene"); err != nil {
t.Fatal(err)
}
+77
View File
@@ -0,0 +1,77 @@
package edit
import (
"os"
"os/exec"
"strings"
"aego/app/proc"
"aego/editor/core/session"
)
const (
EnvEditorExe = "AEGO_EDITOR_EXE"
EnvEditorVerb = "AEGO_EDITOR_VERB"
)
func markEntry(verb string) {
if os.Getenv(EnvEditorExe) != "" {
return
}
if img := os.Getenv("APPIMAGE"); img != "" {
os.Setenv(EnvEditorExe, img)
os.Setenv(EnvEditorVerb, "")
return
}
exe, err := os.Executable()
if err != nil {
return
}
os.Setenv(EnvEditorExe, exe)
os.Setenv(EnvEditorVerb, verb)
}
func relaunchArgs(verb, target, tab string) []string {
if verb != "" {
args := []string{verb}
if target != "" {
args = append(args, target)
}
if tab != "" {
args = append(args, "--launcher", tab)
}
return args
}
var args []string
if tab != "" {
args = append(args, "-launcher", tab)
}
if target != "" {
args = append(args, target)
}
return args
}
func withoutEnv(env []string, key string) []string {
out := env[:0:0]
for _, kv := range env {
if !strings.HasPrefix(kv, key+"=") {
out = append(out, kv)
}
}
return out
}
func Relaunch(target, tab string) error {
exe := os.Getenv(EnvEditorExe)
if exe == "" {
var err error
if exe, err = os.Executable(); err != nil {
return err
}
}
cmd := exec.Command(exe, relaunchArgs(os.Getenv(EnvEditorVerb), target, tab)...)
cmd.Env = withoutEnv(os.Environ(), session.EnvSupervised)
proc.Hide(cmd)
return cmd.Start()
}
+77
View File
@@ -0,0 +1,77 @@
package edit
import (
"os"
"path/filepath"
"slices"
"testing"
"aego/project"
)
func TestRelaunchArgs(t *testing.T) {
cases := []struct {
verb, target, tab string
want []string
}{
{"", "/p", "", []string{"/p"}},
{"", "", "recent", []string{"-launcher", "recent"}},
{"edit", "/p", "", []string{"edit", "/p"}},
{"edit", "", "new", []string{"edit", "--launcher", "new"}},
}
for _, c := range cases {
if got := relaunchArgs(c.verb, c.target, c.tab); !slices.Equal(got, c.want) {
t.Errorf("%q %q %q: %v, want %v", c.verb, c.target, c.tab, got, c.want)
}
}
env := withoutEnv([]string{"A=1", "AEGO_SUPERVISED=1", "B=2"}, "AEGO_SUPERVISED")
if !slices.Equal(env, []string{"A=1", "B=2"}) {
t.Fatal(env)
}
}
func TestResolveArg(t *testing.T) {
root := t.TempDir()
os.WriteFile(filepath.Join(root, project.FileName), []byte("project version=1 {\n}\n"), 0o644)
os.MkdirAll(filepath.Join(root, "scenes"), 0o755)
scene := filepath.Join(root, "scenes", "main.aescene")
os.WriteFile(scene, []byte("scene version=1 {\n}\n"), 0o644)
if dir, file := resolveArg(filepath.Join(root, project.FileName)); dir != root || file != "" {
t.Fatalf("project.aego: %q %q", dir, file)
}
dir, file := resolveArg(scene)
if dir != filepath.Join(root, "scenes") || file != scene {
t.Fatalf("scene: %q %q", dir, file)
}
if rel, ok := relInside(root, file); !ok || rel != "scenes/main.aescene" {
t.Fatalf("rel %q %v", rel, ok)
}
}
func TestNewer(t *testing.T) {
cases := []struct {
latest, current string
want bool
}{
{"v0.9.0", "0.9.0-beta.1", true},
{"v0.9.0-beta.1", "0.9.0-beta.1", false},
{"v0.9.0-beta.2", "0.9.0-beta.1", true},
{"v0.8.5", "0.9.0-beta.1", false},
{"v1.0.0", "0.9.0", true},
{"nightly", "0.9.0", false},
{"", "0.9.0", false},
}
for _, c := range cases {
if got := newer(c.latest, c.current); got != c.want {
t.Errorf("newer(%q, %q) = %v", c.latest, c.current, got)
}
}
file := filepath.Join(t.TempDir(), "update")
st := updateState{Enabled: false, Latest: "v1.2.3"}
if err := saveUpdate(file, st); err != nil {
t.Fatal(err)
}
if back := loadUpdate(file); back.Enabled || back.Latest != "v1.2.3" {
t.Fatalf("round trip %+v", back)
}
}
+17
View File
@@ -0,0 +1,17 @@
package edit
import (
"bytes"
"aego/app/run"
)
func (s *Session) runTests(done func(out string, failed bool)) error {
p, mods := s.Project, s.mods
go func() {
var buf bytes.Buffer
code := run.Tool(p, []string{"test", "-v"}, &buf, &buf, mods...)
s.Engine.Jobs().OnMain(func() { done(buf.String(), code != 0) })
}()
return nil
}
+194
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -12,6 +12,7 @@ import (
"aego/app/export/icon"
"aego/app/export/winres"
"aego/app/proc"
"aego/core/errs"
"aego/core/log"
"aego/runtime/bundle"
@@ -121,7 +122,7 @@ func (j *job) resources(dir string) error {
var icons []winres.Icon
if j.icon != nil {
for _, n := range winres.IconSizes {
data, err := icon.PNG(icon.Resize(j.icon, n, n))
data, err := icon.PNG(icon.Sized(j.icon, n))
if err != nil {
return err
}
@@ -175,6 +176,7 @@ func (j *job) build() error {
args = append(args, pkg)
cmd := exec.Command("go", args...)
cmd.Dir = root
proc.Hide(cmd)
cgo := "CGO_ENABLED=1"
if j.t.Web() {
cgo = "CGO_ENABLED=0"
+3 -2
View File
@@ -189,8 +189,9 @@ func (j *job) license(lib bundle.Lib, src, name string) bundle.Lib {
}
func (j *job) libs() []bundle.Lib {
vorbis := j.license(bundle.Lib{Name: "oggvorbis", Link: "static"}, filepath.Join(j.opt.Engine, "audio", "codec", "vorbis", "internal", "oggvorbis", "LICENSE"), "oggvorbis.txt")
if j.t.Web() {
return []bundle.Lib{j.license(bundle.Lib{Name: "Go wasm_exec.js", Link: "script", File: webExec}, filepath.Join(j.goroot, "LICENSE"), "Go.txt")}
return []bundle.Lib{j.license(bundle.Lib{Name: "Go wasm_exec.js", Link: "script", File: webExec}, filepath.Join(j.goroot, "LICENSE"), "Go.txt"), vorbis}
}
return []bundle.Lib{j.license(bundle.Lib{Name: "SDL3", Link: "static"}, j.sdlLicense(), "SDL3.txt")}
return []bundle.Lib{j.license(bundle.Lib{Name: "SDL3", Link: "static"}, j.sdlLicense(), "SDL3.txt"), vorbis}
}
+18
View File
@@ -307,3 +307,21 @@ func (j *job) prepareOut() error {
}
return nil
}
func Launchable(p *project.Project, target, out string) (string, error) {
t, err := ParseTarget(target)
if err != nil {
return "", err
}
if !t.Web() && !t.Host() {
return "", errs.New(errs.EngineConfig, "the build is for "+t.String()+"; copy it to that system to run it")
}
l := layoutFor(t, out, p)
switch {
case t.Web():
return l.root, nil
case t.OS == "darwin":
return l.app, nil
}
return l.binary(), nil
}
File diff suppressed because it is too large Load Diff
+25 -1
View File
@@ -13,6 +13,12 @@ import (
"aego/core/errs"
)
type DocType struct {
Name string
Extensions []string
Role string
}
type Info struct {
Name string
Executable string
@@ -22,6 +28,7 @@ type Info struct {
MinOS string
Category string
Copyright string
Documents []DocType
}
var icnsTypes = []struct {
@@ -80,6 +87,23 @@ func Plist(i Info, iconFile string) []byte {
str("LSMinimumSystemVersion", i.MinOS)
str("LSApplicationCategoryType", i.Category)
str("NSHumanReadableCopyright", i.Copyright)
if len(i.Documents) > 0 {
b.WriteString("\t<key>CFBundleDocumentTypes</key>\n\t<array>\n")
for _, d := range i.Documents {
role := d.Role
if role == "" {
role = "Editor"
}
b.WriteString("\t\t<dict>\n\t\t\t<key>CFBundleTypeName</key>\n\t\t\t<string>" + esc(d.Name) + "</string>\n")
b.WriteString("\t\t\t<key>CFBundleTypeRole</key>\n\t\t\t<string>" + esc(role) + "</string>\n")
b.WriteString("\t\t\t<key>CFBundleTypeExtensions</key>\n\t\t\t<array>\n")
for _, e := range d.Extensions {
b.WriteString("\t\t\t\t<string>" + esc(e) + "</string>\n")
}
b.WriteString("\t\t\t</array>\n\t\t</dict>\n")
}
b.WriteString("\t</array>\n")
}
b.WriteString("\t<key>NSHighResolutionCapable</key>\n\t<true/>\n")
b.WriteString("</dict>\n</plist>\n")
return []byte(b.String())
@@ -92,7 +116,7 @@ func ICNS(src image.Image) ([]byte, error) {
data, ok := cache[t.size]
if !ok {
var err error
if data, err = icon.PNG(icon.Resize(src, t.size, t.size)); err != nil {
if data, err = icon.PNG(icon.Sized(src, t.size)); err != nil {
return nil, err
}
cache[t.size] = data
+9
View File
@@ -23,3 +23,12 @@ func TestPlistEscapesAndBundleID(t *testing.T) {
t.Fatal(p)
}
}
func TestPlistDocumentTypes(t *testing.T) {
out := string(Plist(Info{Name: "Aego", Executable: "aego", ID: "ru.akarpov.aego", Documents: []DocType{{Name: "Aego Scene", Extensions: []string{"aescene"}}}}, ""))
for _, want := range []string{"<key>CFBundleDocumentTypes</key>", "<string>Aego Scene</string>", "<string>Editor</string>", "<string>aescene</string>"} {
if !strings.Contains(out, want) {
t.Fatalf("plist has no %s:\n%s", want, out)
}
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ func (j *job) pack() error {
func (j *job) packLinux() error {
if j.icon != nil {
data, err := icon.PNG(icon.Resize(j.icon, 256, 256))
data, err := icon.PNG(icon.Sized(j.icon, 256))
if err != nil {
return err
}
+6 -1
View File
@@ -26,6 +26,7 @@ var webFS embed.FS
const (
CodeWebAsset = "EXP010"
CodeWebNative = "EXP011"
CodeWebMP3 = "EXP012"
WebWasm = "game.wasm"
WebFiles = "files.json"
WebBudget = 8 << 20
@@ -68,7 +69,11 @@ func CheckWeb(s *Snapshot, native bool) []problems.Diagnostic {
out = append(out, diag(CodeWebNative, problems.Warning,
"the native Go module is compiled to WebAssembly: it must build without cgo and must not use files, processes or the network", "native/"))
}
_, ffmpegErr := exec.LookPath("ffmpeg")
s.idx.Each(func(e *asset.Entry) {
if ffmpegErr != nil && strings.EqualFold(filepath.Ext(e.Source), ".mp3") {
out = append(out, diag(CodeWebMP3, problems.Warning, e.Source+" is an mp3; it is converted to ogg on import, which needs ffmpeg in PATH", e.Source))
}
if e.Missing || exportSkip(e.Kind) || !webUnsupported(e.Kind) {
return
}
@@ -156,7 +161,7 @@ func (j *job) packWeb() error {
return err
}
if j.icon != nil {
data, err := icon.PNG(icon.Resize(j.icon, webIconSize, webIconSize))
data, err := icon.PNG(icon.Sized(j.icon, webIconSize))
if err != nil {
return err
}
+3
View File
@@ -20,6 +20,8 @@ canvas { display: block; width: 100%; height: 100%; outline: none; touch-action:
#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; }
#mute { position: fixed; top: 10px; right: 52px; width: 36px; height: 36px; border: 0; border-radius: 6px; background: rgba(0, 0, 0, 0.45); color: #fff; font-size: 16px; cursor: pointer; opacity: 0.5; }
#mute:hover { opacity: 1; }
</style>
<link rel="preload" href="game.wasm?v={{.Build}}" as="fetch" type="application/wasm" crossorigin>
</head>
@@ -32,6 +34,7 @@ canvas { display: block; width: 100%; height: 100%; outline: none; touch-action:
<pre id="error" hidden></pre>
</div>
<button id="fullscreen" type="button" title="Fullscreen" aria-label="Fullscreen" hidden>⛶</button>
<button id="mute" type="button" title="Mute" aria-label="Mute" hidden>🔊</button>
<script src="wasm_exec.js?v={{.Build}}"></script>
<script src="loader.js?v={{.Build}}"></script>
</body>
+17
View File
@@ -8,6 +8,14 @@
const status = document.getElementById("status");
const errorBox = document.getElementById("error");
const full = document.getElementById("fullscreen");
const mute = document.getElementById("mute");
function syncMute() {
const on = globalThis.aegoAudioMute();
mute.textContent = on ? "🔇" : "🔊";
mute.title = on ? "Unmute" : "Mute";
mute.setAttribute("aria-label", mute.title);
}
const PARALLEL = 6;
const started = performance.now();
let total = 0;
@@ -34,6 +42,10 @@
if (errorBox.hidden) loader.remove();
}, 300);
full.hidden = !canvas.requestFullscreen;
if (typeof globalThis.aegoAudioMute === "function") {
mute.hidden = false;
syncMute();
}
canvas.focus();
};
@@ -42,6 +54,11 @@
if (done && done.catch) done.catch(() => {});
canvas.focus();
});
mute.addEventListener("click", () => {
globalThis.aegoAudioMute(!globalThis.aegoAudioMute());
syncMute();
canvas.focus();
});
document.addEventListener("fullscreenchange", () => {
full.hidden = !!document.fullscreenElement;
});
+7
View File
@@ -0,0 +1,7 @@
package proc
import "os/exec"
func Hide(cmd *exec.Cmd) {
hide(cmd)
}
+7
View File
@@ -0,0 +1,7 @@
//go:build !windows
package proc
import "os/exec"
func hide(*exec.Cmd) {}
+18
View File
@@ -0,0 +1,18 @@
//go:build windows
package proc
import (
"os/exec"
"syscall"
)
const createNoWindow = 0x08000000
func hide(cmd *exec.Cmd) {
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}
cmd.SysProcAttr.HideWindow = true
cmd.SysProcAttr.CreationFlags |= createNoWindow
}
+11 -1
View File
@@ -15,6 +15,7 @@ import (
"strings"
"time"
"aego/app/proc"
"aego/editor/core/session"
"aego/project"
)
@@ -32,6 +33,7 @@ type Options struct {
Hidden bool
CPUProfile string
Profile string
Open string
Stdout io.Writer
Stderr io.Writer
}
@@ -144,12 +146,17 @@ func (s *supervisor) start(exited chan<- int) error {
if s.opt.Profile != "" {
args = append(args, "-profile", s.opt.Profile)
}
if s.opt.Open != "" {
args = append(args, "-open", s.opt.Open)
s.opt.Open = ""
}
args = append(args, s.p.Root)
cmd := exec.Command(s.current, args...)
cmd.Env = append(os.Environ(), session.EnvSupervised+"=1")
cmd.Stdin = os.Stdin
cmd.Stdout = s.opt.Stdout
cmd.Stderr = s.opt.Stderr
proc.Hide(cmd)
if err := cmd.Start(); err != nil {
return fmt.Errorf("cannot start the editor: %w", err)
}
@@ -200,15 +207,18 @@ func (s *supervisor) rebuild() {
func (s *supervisor) build() (string, string, error) {
s.gen++
name := "aego-editor-" + strconv.Itoa(s.gen)
args := []string{"build", "-tags", "aego_editor"}
if runtime.GOOS == "windows" {
name += ".exe"
args = append(args, "-ldflags", "-H=windowsgui")
}
out := filepath.Join(s.dir, name)
cmd := exec.Command("go", "build", "-tags", "aego_editor", "-o", out, filepath.Join(s.dir, "main.go"))
cmd := exec.Command("go", append(args, "-o", out, filepath.Join(s.dir, "main.go"))...)
cmd.Dir = s.p.Root
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = &buf
proc.Hide(cmd)
if err := cmd.Run(); err != nil {
os.Remove(out)
return "", buf.String(), err
+99
View File
@@ -0,0 +1,99 @@
package asset
import (
"encoding/binary"
"math"
"aego/core/errs"
"aego/core/ids"
"aego/core/log"
)
const (
magicAudio = "AEAU"
audioVersion = 1
audioHeaderSize = 46
audioStream = 1
audioLoop = 2
AudioWAV = uint8(1)
AudioOgg = uint8(2)
)
type AudioSource interface {
Audio(id ids.AssetID) ([]byte, bool)
}
type AudioArtifact struct {
Format uint8
Rate int
Channels int
Frames int64
LoopStart int64
LoopEnd int64
Gain float32
Bus string
Stream bool
Loop bool
Data []byte
}
func EncodeAudio(a *AudioArtifact) []byte {
bus := a.Bus
if len(bus) > 255 {
bus = bus[:255]
}
out := make([]byte, audioHeaderSize+len(bus)+len(a.Data))
le := binary.LittleEndian
copy(out[0:4], magicAudio)
le.PutUint32(out[4:8], audioVersion)
out[8] = a.Format
var flags byte
if a.Stream {
flags |= audioStream
}
if a.Loop {
flags |= audioLoop
}
out[9] = flags
le.PutUint16(out[10:12], uint16(a.Channels))
le.PutUint32(out[12:16], uint32(a.Rate))
le.PutUint64(out[16:24], uint64(a.Frames))
le.PutUint64(out[24:32], uint64(a.LoopStart))
le.PutUint64(out[32:40], uint64(a.LoopEnd))
le.PutUint32(out[40:44], math.Float32bits(a.Gain))
le.PutUint16(out[44:46], uint16(len(bus)))
copy(out[audioHeaderSize:], bus)
copy(out[audioHeaderSize+len(bus):], a.Data)
return out
}
func DecodeAudio(data []byte) (*AudioArtifact, error) {
if len(data) < audioHeaderSize || string(data[0:4]) != magicAudio {
return nil, errs.New(errs.AssetBadArtifact, "not an audio artifact")
}
le := binary.LittleEndian
if v := le.Uint32(data[4:8]); v != audioVersion {
return nil, errs.New(errs.AssetBadArtifact, "unsupported audio artifact version", log.F("version", v))
}
n := int(le.Uint16(data[44:46]))
if len(data) < audioHeaderSize+n {
return nil, errs.New(errs.AssetBadArtifact, "audio artifact is truncated")
}
a := &AudioArtifact{
Format: data[8],
Stream: data[9]&audioStream != 0,
Loop: data[9]&audioLoop != 0,
Channels: int(le.Uint16(data[10:12])),
Rate: int(le.Uint32(data[12:16])),
Frames: int64(le.Uint64(data[16:24])),
LoopStart: int64(le.Uint64(data[24:32])),
LoopEnd: int64(le.Uint64(data[32:40])),
Gain: math.Float32frombits(le.Uint32(data[40:44])),
Bus: string(data[audioHeaderSize : audioHeaderSize+n]),
Data: data[audioHeaderSize+n:],
}
if a.Channels < 1 || a.Channels > 2 || a.Rate <= 0 {
return nil, errs.New(errs.AssetBadArtifact, "audio artifact has an invalid layout", log.F("channels", a.Channels), log.F("rate", a.Rate))
}
return a, nil
}
+4 -2
View File
@@ -29,8 +29,10 @@ var builtins = [...]Builtin{
{ID: BuiltinWhite, Name: "engine/textures/white", Kind: ArtTexture},
}
var allBuiltins = append(builtins[:len(builtins):len(builtins)], builtinAudioEntries()...)
func Builtins() []Builtin {
return builtins[:]
return allBuiltins
}
func IsBuiltin(id ids.AssetID) bool {
@@ -38,7 +40,7 @@ func IsBuiltin(id ids.AssetID) bool {
}
func LookupBuiltin(id ids.AssetID) (Builtin, bool) {
for _, b := range builtins {
for _, b := range allBuiltins {
if b.ID == id {
return b, true
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+14 -4
View File
@@ -378,6 +378,13 @@ func (l *Loader) Table(id ids.AssetID) ([]byte, bool) {
return l.blob(id, asset.ArtTable)
}
func (l *Loader) Audio(id ids.AssetID) ([]byte, bool) {
if data, ok := asset.BuiltinAudio(id); ok {
return data, true
}
return l.blob(id, asset.ArtAudio)
}
func (l *Loader) Revision() uint64 {
return uint64(l.gen)
}
@@ -430,6 +437,8 @@ func (l *Loader) Preload(list ...ids.AssetID) error {
_, ok = l.Data(id)
case asset.ArtTable:
_, ok = l.Table(id)
case asset.ArtAudio:
_, ok = l.Audio(id)
}
if !ok {
return errs.New(errs.AssetLoadFailed, "asset failed to load", log.F("asset", id.String()), log.F("kind", string(e.Kind)))
@@ -476,7 +485,7 @@ func (l *Loader) prepare(e *asset.Entry) (staged, bool, error) {
return staged{id: e.ID, kind: e.Kind, geo: m}, true, nil
case asset.ArtScript:
return staged{id: e.ID, kind: e.Kind}, true, nil
case asset.ArtSpriteRegion, asset.ArtScene, asset.ArtPrefab, asset.ArtData, asset.ArtTable:
case asset.ArtSpriteRegion, asset.ArtScene, asset.ArtPrefab, asset.ArtData, asset.ArtTable, asset.ArtAudio:
art, ok := e.Artifact(e.Kind)
if !ok {
return staged{}, false, errs.New(errs.AssetBadArtifact, "entry has no artifact of its own kind", log.F("asset", e.ID.String()))
@@ -601,7 +610,8 @@ func (l *Loader) Close() {
}
var (
_ asset.Resolver = (*Loader)(nil)
_ asset.Decoder = (*Loader)(nil)
_ asset.DataSource = (*Loader)(nil)
_ asset.Resolver = (*Loader)(nil)
_ asset.Decoder = (*Loader)(nil)
_ asset.DataSource = (*Loader)(nil)
_ asset.AudioSource = (*Loader)(nil)
)
+2
View File
@@ -574,6 +574,8 @@ func artifactExt(kind asset.ArtifactKind) string {
return ".aedata"
case asset.ArtTable:
return ".aetable"
case asset.ArtAudio:
return ".aeau"
}
return ".bin"
}

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