Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6a24ba393 | ||
|
|
7dca06e4b1 | ||
|
|
6dfdb824e2 | ||
|
|
de49a53172 | ||
|
|
1030dc9a43 | ||
|
|
3d25734dfa | ||
|
|
12f628c469 | ||
|
|
1529213434 | ||
|
|
f8bb865920 | ||
|
|
f5fb32565e | ||
|
|
f1a8f538d3 | ||
|
|
963b01a011 | ||
|
|
6aec9dc544 | ||
|
|
70a0111deb | ||
|
|
0da2549d91 | ||
|
|
810984ae7a | ||
|
|
62c8129364 | ||
|
|
674af03a65 | ||
|
|
6b0d006467 | ||
|
|
ff7e95afe4 | ||
|
|
67b16d3da5 | ||
|
|
56ca1c638e |
@@ -5,3 +5,4 @@ site
|
|||||||
exports
|
exports
|
||||||
**/.aego
|
**/.aego
|
||||||
*.test
|
*.test
|
||||||
|
.claude
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ name: ci
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
|
tags: ["v*"]
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -36,6 +37,10 @@ jobs:
|
|||||||
- run: go vet ./platform/sdl
|
- run: go vet ./platform/sdl
|
||||||
- run: make lint
|
- run: make lint
|
||||||
- run: make depcheck
|
- run: make depcheck
|
||||||
|
- name: packaging tools
|
||||||
|
run: sudo apt-get install -y nsis squashfs-tools
|
||||||
|
- name: installer dry run
|
||||||
|
run: make nsis-check
|
||||||
|
|
||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
@@ -111,4 +116,39 @@ jobs:
|
|||||||
- name: example project
|
- name: example project
|
||||||
run: |
|
run: |
|
||||||
/tmp/aego check examples/native-rotator
|
/tmp/aego check examples/native-rotator
|
||||||
/tmp/aego run examples/native-rotator --headless --frames 60
|
/tmp/aego run examples/native-rotator --headless --frames 60
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: [checks, build]
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: "1"
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
- name: linux packages
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y $LINUX_PKGS
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
path: .cache/native
|
||||||
|
key: native-linux_amd64-${{ hashFiles('third_party/sdl/version.txt', 'tools/bootstrap-native.sh') }}
|
||||||
|
- run: bash tools/bootstrap-native.sh
|
||||||
|
- name: windows tools
|
||||||
|
run: sudo apt-get install -y mingw-w64 nsis zip
|
||||||
|
- name: publish
|
||||||
|
env:
|
||||||
|
REQUIRE_WINDOWS: "1"
|
||||||
|
GITEA_RESOLVE: git.akarpov.ru:443:79.142.92.250
|
||||||
|
run: |
|
||||||
|
case "$GITHUB_REF" in
|
||||||
|
refs/tags/v*) sh tools/release/release.sh "$GITHUB_REF_NAME" ;;
|
||||||
|
refs/heads/master) sh tools/release/release.sh nightly ;;
|
||||||
|
esac
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -65,9 +66,8 @@ func sceneCommands() []*Command {
|
|||||||
{Name: "scene.rename", Doc: "rename a node", Write: true, Args: []Arg{argFile, argNode, {Name: "name", Type: "string", Doc: "new name", Required: true, Pos: true}}, Run: func(s *Session, a Args) (*Output, error) {
|
{Name: "scene.rename", Doc: "rename a node", Write: true, Args: []Arg{argFile, argNode, {Name: "name", Type: "string", Doc: "new name", Required: true, Pos: true}}, Run: func(s *Session, a Args) (*Output, error) {
|
||||||
return s.sceneTx(a, "Rename", mutate.Rename(a.Str("node"), a.Str("name")))
|
return s.sceneTx(a, "Rename", mutate.Rename(a.Str("node"), a.Str("name")))
|
||||||
}},
|
}},
|
||||||
{Name: "scene.add-comp", Doc: "add a component with its defaults", Write: true, Args: []Arg{argFile, argNode, {Name: "comp", Type: "string", Doc: "component name, full or a unique suffix (Body2D)", Required: true, Pos: true}}, Run: func(s *Session, a Args) (*Output, error) {
|
{Name: "scene.add-comp", Doc: "add a component with its defaults and the components it requires", Write: true, Args: []Arg{argFile, argNode, {Name: "comp", Type: "string", Doc: "component name, full or a unique suffix (Body2D)", Required: true, Pos: true},
|
||||||
return s.sceneTx(a, "Add Component", mutate.AddComp(a.Str("node"), a.Str("comp")))
|
{Name: "no-required", Type: "boolean", Doc: "add only this component; missing requirements stay as problems"}}, Run: (*Session).sceneAddComp},
|
||||||
}},
|
|
||||||
{Name: "scene.remove-comp", Doc: "remove a component", Write: true, Args: []Arg{argFile, argNode, {Name: "comp", Type: "string", Doc: "component name", Required: true, Pos: true}}, Run: func(s *Session, a Args) (*Output, error) {
|
{Name: "scene.remove-comp", Doc: "remove a component", Write: true, Args: []Arg{argFile, argNode, {Name: "comp", Type: "string", Doc: "component name", Required: true, Pos: true}}, Run: func(s *Session, a Args) (*Output, error) {
|
||||||
return s.sceneTx(a, "Remove Component", mutate.RemoveComp(a.Str("node"), a.Str("comp")))
|
return s.sceneTx(a, "Remove Component", mutate.RemoveComp(a.Str("node"), a.Str("comp")))
|
||||||
}},
|
}},
|
||||||
@@ -93,6 +93,49 @@ func (s *Session) scene(a Args) (string, []byte, *mutate.Document, error) {
|
|||||||
return rel, src, doc, err
|
return rel, src, doc, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sceneAddComp adds a component and, unless no-required is set, the components it requires first, in one transaction (D144).
|
||||||
|
func (s *Session) sceneAddComp(a Args) (*Output, error) {
|
||||||
|
rel, src, doc, err := s.scene(a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
node, comp := a.Str("node"), a.Str("comp")
|
||||||
|
ops := []mutate.Op{mutate.AddComp(node, comp)}
|
||||||
|
var notes []string
|
||||||
|
if !a.Bool("no-required") {
|
||||||
|
td, err := doc.Component(comp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h, err := doc.Node(node)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req, err := mutate.Required(doc.Scene, td, h)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ops = ops[:0]
|
||||||
|
for _, r := range req {
|
||||||
|
ops = append(ops, mutate.AddComp(node, r.FullName))
|
||||||
|
notes = append(notes, "added "+label(r)+" (required by "+label(td)+")")
|
||||||
|
}
|
||||||
|
ops = append(ops, mutate.AddComp(node, td.FullName))
|
||||||
|
}
|
||||||
|
out, err := s.applyDoc(a, rel, src, doc, mutate.Tx{Label: "Add Component", Ops: ops})
|
||||||
|
if out != nil {
|
||||||
|
out.Notes = append(notes, out.Notes...)
|
||||||
|
}
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func label(td *schema.TypeDescriptor) string {
|
||||||
|
if td.DisplayName != "" {
|
||||||
|
return td.DisplayName
|
||||||
|
}
|
||||||
|
return td.FullName
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Session) sceneTx(a Args, label string, ops ...mutate.Op) (*Output, error) {
|
func (s *Session) sceneTx(a Args, label string, ops ...mutate.Op) (*Output, error) {
|
||||||
rel, src, doc, err := s.scene(a)
|
rel, src, doc, err := s.scene(a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -245,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 {
|
||||||
@@ -90,6 +99,7 @@ func (s *Session) lookupType(name string) (*schema.TypeDescriptor, error) {
|
|||||||
found = append(found, td)
|
found = append(found, td)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
found = schema.PreferProject(found)
|
||||||
switch len(found) {
|
switch len(found) {
|
||||||
case 1:
|
case 1:
|
||||||
return found[0], nil
|
return found[0], nil
|
||||||
@@ -162,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"
|
||||||
@@ -46,10 +47,11 @@ 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"`
|
||||||
Log []string `json:"log,omitempty"`
|
Counters map[string]uint64 `json:"counters,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Log []string `json:"log,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Runner func(p *project.Project, r RunRequest) (RunReport, error)
|
type Runner func(p *project.Project, r RunRequest) (RunReport, error)
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import (
|
|||||||
"aego/engine/module"
|
"aego/engine/module"
|
||||||
"aego/modules/accessibility"
|
"aego/modules/accessibility"
|
||||||
"aego/modules/audio"
|
"aego/modules/audio"
|
||||||
|
"aego/modules/camera"
|
||||||
"aego/modules/dataasset"
|
"aego/modules/dataasset"
|
||||||
"aego/modules/debug"
|
"aego/modules/debug"
|
||||||
"aego/modules/display"
|
"aego/modules/display"
|
||||||
|
"aego/modules/gameplay"
|
||||||
inputmod "aego/modules/input"
|
inputmod "aego/modules/input"
|
||||||
"aego/modules/physics"
|
"aego/modules/physics"
|
||||||
"aego/modules/render2d"
|
"aego/modules/render2d"
|
||||||
@@ -71,6 +73,14 @@ func tweenEntry() assembly.Entry {
|
|||||||
return assembly.Entry{ID: tween.ID, Requires: []string{scripting.ID}, New: func(assembly.Options) module.Module { return tween.New() }}
|
return assembly.Entry{ID: tween.ID, Requires: []string{scripting.ID}, New: func(assembly.Options) module.Module { return tween.New() }}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cameraEntry() assembly.Entry {
|
||||||
|
return assembly.Entry{ID: camera.ID, Requires: []string{scripting.ID}, New: func(assembly.Options) module.Module { return camera.New() }}
|
||||||
|
}
|
||||||
|
|
||||||
|
func gameplayEntry() assembly.Entry {
|
||||||
|
return assembly.Entry{ID: gameplay.ID, Requires: []string{physics.ID, scripting.ID}, New: func(assembly.Options) module.Module { return gameplay.New() }}
|
||||||
|
}
|
||||||
|
|
||||||
func dataEntry() assembly.Entry {
|
func dataEntry() assembly.Entry {
|
||||||
return assembly.Entry{ID: dataasset.ID, New: func(assembly.Options) module.Module { return dataasset.New() }}
|
return assembly.Entry{ID: dataasset.ID, New: func(assembly.Options) module.Module { return dataasset.New() }}
|
||||||
}
|
}
|
||||||
@@ -85,5 +95,5 @@ func savegameEntry() assembly.Entry {
|
|||||||
|
|
||||||
func Default() assembly.Catalog {
|
func Default() assembly.Catalog {
|
||||||
return assembly.Catalog{core(), dataEntry(), inputEntry(), render2dEntry(), render3dEntry(), scriptEntry(), physicsEntry(), debugEntry(),
|
return assembly.Catalog{core(), dataEntry(), inputEntry(), render2dEntry(), render3dEntry(), scriptEntry(), physicsEntry(), debugEntry(),
|
||||||
displayEntry(), audioEntry(), accessibilityEntry(), settingsEntry(), tweenEntry(), spawnEntry(), savegameEntry()}
|
displayEntry(), audioEntry(), accessibilityEntry(), settingsEntry(), tweenEntry(), cameraEntry(), gameplayEntry(), spawnEntry(), savegameEntry()}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ package builtin
|
|||||||
import "aego/app/assembly"
|
import "aego/app/assembly"
|
||||||
|
|
||||||
func Headless() assembly.Catalog {
|
func Headless() assembly.Catalog {
|
||||||
return assembly.Catalog{core(), dataEntry(), inputEntry(), scriptEntry(), physicsEntry(), debugEntry(), displayEntry(), audioEntry(), accessibilityEntry(), settingsEntry(), tweenEntry(), spawnEntry(), savegameEntry()}
|
return assembly.Catalog{core(), dataEntry(), inputEntry(), scriptEntry(), physicsEntry(), debugEntry(), displayEntry(), audioEntry(), accessibilityEntry(), settingsEntry(), tweenEntry(), cameraEntry(), spawnEntry(), savegameEntry()}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,14 @@ package editors
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"aego/app/assembly"
|
"aego/app/assembly"
|
||||||
|
"aego/modules/audio"
|
||||||
|
audioeditor "aego/modules/audio/editor"
|
||||||
|
"aego/modules/camera"
|
||||||
|
cameraeditor "aego/modules/camera/editor"
|
||||||
"aego/modules/debug"
|
"aego/modules/debug"
|
||||||
debugeditor "aego/modules/debug/editor"
|
debugeditor "aego/modules/debug/editor"
|
||||||
|
"aego/modules/gameplay"
|
||||||
|
gameplayeditor "aego/modules/gameplay/editor"
|
||||||
inputmod "aego/modules/input"
|
inputmod "aego/modules/input"
|
||||||
inputeditor "aego/modules/input/editor"
|
inputeditor "aego/modules/input/editor"
|
||||||
"aego/modules/physics"
|
"aego/modules/physics"
|
||||||
@@ -20,5 +26,8 @@ func With(c assembly.Catalog) assembly.Catalog {
|
|||||||
With(render2d.ID, render2deditor.Hook).
|
With(render2d.ID, render2deditor.Hook).
|
||||||
With(render3d.ID, render3deditor.Hook).
|
With(render3d.ID, render3deditor.Hook).
|
||||||
With(physics.ID, physicseditor.Hook).
|
With(physics.ID, physicseditor.Hook).
|
||||||
|
With(camera.ID, cameraeditor.Hook).
|
||||||
|
With(gameplay.ID, gameplayeditor.Hook).
|
||||||
|
With(audio.ID, audioeditor.Hook).
|
||||||
With(debug.ID, debugeditor.Hook)
|
With(debug.ID, debugeditor.Hook)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,18 @@ import (
|
|||||||
"aego/app/assembly"
|
"aego/app/assembly"
|
||||||
"aego/engine/base"
|
"aego/engine/base"
|
||||||
"aego/engine/module"
|
"aego/engine/module"
|
||||||
|
"aego/modules/audio"
|
||||||
|
"aego/modules/camera"
|
||||||
"aego/modules/debug"
|
"aego/modules/debug"
|
||||||
|
"aego/modules/gameplay"
|
||||||
inputmod "aego/modules/input"
|
inputmod "aego/modules/input"
|
||||||
"aego/modules/physics"
|
"aego/modules/physics"
|
||||||
"aego/modules/render2d"
|
"aego/modules/render2d"
|
||||||
|
"aego/modules/render3d"
|
||||||
|
"aego/modules/savegame"
|
||||||
"aego/modules/scripting"
|
"aego/modules/scripting"
|
||||||
|
"aego/modules/spawn"
|
||||||
|
"aego/modules/tween"
|
||||||
"aego/script/stdlib"
|
"aego/script/stdlib"
|
||||||
"aego/script/vm"
|
"aego/script/vm"
|
||||||
)
|
)
|
||||||
@@ -23,6 +30,13 @@ func Catalog() assembly.Catalog {
|
|||||||
}},
|
}},
|
||||||
{ID: physics.ID, New: func(assembly.Options) module.Module { return physics.New() }},
|
{ID: physics.ID, New: func(assembly.Options) module.Module { return physics.New() }},
|
||||||
{ID: debug.ID, New: func(assembly.Options) module.Module { return debug.New() }},
|
{ID: debug.ID, New: func(assembly.Options) module.Module { return debug.New() }},
|
||||||
|
{ID: tween.ID, Requires: []string{scripting.ID}, New: func(assembly.Options) module.Module { return tween.New() }},
|
||||||
|
{ID: camera.ID, Requires: []string{scripting.ID}, New: func(assembly.Options) module.Module { return camera.New() }},
|
||||||
|
{ID: gameplay.ID, Requires: []string{physics.ID, scripting.ID}, New: func(assembly.Options) module.Module { return gameplay.New() }},
|
||||||
|
{ID: render3d.ID, New: func(assembly.Options) module.Module { return render3d.New() }},
|
||||||
|
{ID: spawn.ID, New: func(assembly.Options) module.Module { return spawn.New() }},
|
||||||
|
{ID: savegame.ID, Requires: []string{scripting.ID, spawn.ID}, New: func(assembly.Options) module.Module { return savegame.New() }},
|
||||||
|
{ID: audio.ID, New: func(assembly.Options) module.Module { return audio.New() }},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package playground
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"aego/contracts/persistence"
|
||||||
|
"aego/engine"
|
||||||
|
"aego/platform"
|
||||||
|
"aego/save"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UseMemorySaves gives e a save store kept in memory, for runs in a browser page that must not touch the visitor's storage.
|
||||||
|
func UseMemorySaves(e *engine.Engine) {
|
||||||
|
e.SetService(persistence.StoreID, save.NewStoreFS("saves", &memUser{files: map[string][]byte{}}))
|
||||||
|
}
|
||||||
|
|
||||||
|
type memUser struct {
|
||||||
|
files map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memUser) ReadFile(p string) ([]byte, error) {
|
||||||
|
data, ok := m.files[path.Clean(p)]
|
||||||
|
if !ok {
|
||||||
|
return nil, fs.ErrNotExist
|
||||||
|
}
|
||||||
|
return append([]byte(nil), data...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memUser) WriteFile(p string, data []byte) error {
|
||||||
|
m.files[path.Clean(p)] = append([]byte(nil), data...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memUser) Remove(p string) error {
|
||||||
|
delete(m.files, path.Clean(p))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memUser) Stat(p string) (platform.UserEntry, error) {
|
||||||
|
data, ok := m.files[path.Clean(p)]
|
||||||
|
if !ok {
|
||||||
|
return platform.UserEntry{}, fs.ErrNotExist
|
||||||
|
}
|
||||||
|
return platform.UserEntry{Name: path.Base(p), Size: int64(len(data)), Mod: time.Time{}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memUser) List(dir string) ([]platform.UserEntry, error) {
|
||||||
|
prefix := path.Clean(dir) + "/"
|
||||||
|
var out []platform.UserEntry
|
||||||
|
for p, data := range m.files {
|
||||||
|
if rest, ok := strings.CutPrefix(p, prefix); ok && !strings.Contains(rest, "/") {
|
||||||
|
out = append(out, platform.UserEntry{Name: rest, Size: int64(len(data))})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package playground
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"aego/asset"
|
||||||
|
"aego/asset/loader"
|
||||||
|
"aego/core/ids"
|
||||||
|
"aego/script/bytecode"
|
||||||
|
"aego/vfs"
|
||||||
|
"aego/vfs/memfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EncodeTry packs the compiled script of a try scene for a docs page: base64 modules joined by commas.
|
||||||
|
func EncodeTry(mods []*bytecode.Module) string {
|
||||||
|
parts := make([]string, len(mods))
|
||||||
|
for i, m := range mods {
|
||||||
|
parts[i] = base64.StdEncoding.EncodeToString(bytecode.Encode(m))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeTry(code string) ([]*bytecode.Module, error) {
|
||||||
|
if code == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var mods []*bytecode.Module
|
||||||
|
for _, part := range strings.Split(code, ",") {
|
||||||
|
b, err := base64.StdEncoding.DecodeString(part)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m, err := bytecode.Decode(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mods = append(mods, m)
|
||||||
|
}
|
||||||
|
return mods, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MountTry gives the loader the prefabs of a try scene, keyed by asset id; none unmounts them.
|
||||||
|
func MountTry(l *loader.Loader, prefabs map[string]string) error {
|
||||||
|
idx := asset.NewIndex()
|
||||||
|
files := map[string][]byte{}
|
||||||
|
for key, text := range prefabs {
|
||||||
|
id, err := ids.ParseAssetID(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p := id.String() + ".aeprefab"
|
||||||
|
files[p] = []byte(text)
|
||||||
|
idx.Put(asset.Entry{ID: id, Kind: asset.ArtPrefab, Name: p, Source: p,
|
||||||
|
Artifacts: []asset.Artifact{{Kind: asset.ArtPrefab, Path: "try://" + p}}})
|
||||||
|
}
|
||||||
|
fs, err := vfs.New(vfs.Mount{Scheme: "try", ReadOnly: true, Source: memfs.New(files)})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return l.Mount(idx, fs)
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"aego/app/proc"
|
||||||
"aego/core/errs"
|
"aego/core/errs"
|
||||||
"aego/core/log"
|
"aego/core/log"
|
||||||
"aego/project"
|
"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 := exec.Command("go", args...)
|
||||||
cmd.Dir = p.Root
|
cmd.Dir = p.Root
|
||||||
cmd.Env = append(os.Environ(), "GOOS="+t.OS, "GOARCH="+t.Arch)
|
cmd.Env = append(os.Environ(), "GOOS="+t.OS, "GOARCH="+t.Arch)
|
||||||
|
proc.Hide(cmd)
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.RemoveAll(outDir)
|
os.RemoveAll(outDir)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"aego/app/proc"
|
||||||
"aego/core/errs"
|
"aego/core/errs"
|
||||||
"aego/core/log"
|
"aego/core/log"
|
||||||
"aego/project"
|
"aego/project"
|
||||||
@@ -32,12 +33,14 @@ func RunIcons(p *project.Project, engineVersion string, out io.Writer) (int, err
|
|||||||
defer os.Remove(bin)
|
defer os.Remove(bin)
|
||||||
build := exec.Command("go", "build", "-tags", strings.Join(t.Tags, ","), "-o", bin, "./.aego/generated/icons")
|
build := exec.Command("go", "build", "-tags", strings.Join(t.Tags, ","), "-o", bin, "./.aego/generated/icons")
|
||||||
build.Dir = p.Root
|
build.Dir = p.Root
|
||||||
|
proc.Hide(build)
|
||||||
if output, err := build.CombinedOutput(); err != nil {
|
if output, err := build.CombinedOutput(); err != nil {
|
||||||
return 2, errs.New(errs.BuildFailed, "native icons helper does not compile",
|
return 2, errs.New(errs.BuildFailed, "native icons helper does not compile",
|
||||||
log.F("output", strings.TrimSpace(string(output))))
|
log.F("output", strings.TrimSpace(string(output))))
|
||||||
}
|
}
|
||||||
run := exec.Command(bin)
|
run := exec.Command(bin)
|
||||||
run.Dir, run.Stdout, run.Stderr = p.Root, out, out
|
run.Dir, run.Stdout, run.Stderr = p.Root, out, out
|
||||||
|
proc.Hide(run)
|
||||||
err = run.Run()
|
err = run.Run()
|
||||||
var ee *exec.ExitError
|
var ee *exec.ExitError
|
||||||
if errors.As(err, &ee) {
|
if errors.As(err, &ee) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"aego/app/proc"
|
||||||
"aego/core/errs"
|
"aego/core/errs"
|
||||||
"aego/core/log"
|
"aego/core/log"
|
||||||
"aego/project"
|
"aego/project"
|
||||||
@@ -52,6 +53,7 @@ func BuildManifest(p *project.Project, engineVersion string, lg *log.Logger) (st
|
|||||||
defer os.Remove(bin)
|
defer os.Remove(bin)
|
||||||
build := exec.Command("go", "build", "-tags", strings.Join(t.Tags, ","), "-o", bin, "./.aego/generated/manifest")
|
build := exec.Command("go", "build", "-tags", strings.Join(t.Tags, ","), "-o", bin, "./.aego/generated/manifest")
|
||||||
build.Dir = p.Root
|
build.Dir = p.Root
|
||||||
|
proc.Hide(build)
|
||||||
if output, err := build.CombinedOutput(); err != nil {
|
if output, err := build.CombinedOutput(); err != nil {
|
||||||
return "", errs.New(errs.BuildFailed, "native bindings helper does not compile",
|
return "", errs.New(errs.BuildFailed, "native bindings helper does not compile",
|
||||||
log.F("output", strings.TrimSpace(string(output))))
|
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"
|
tmp := out + ".tmp"
|
||||||
run := exec.Command(bin, tmp)
|
run := exec.Command(bin, tmp)
|
||||||
run.Dir = p.Root
|
run.Dir = p.Root
|
||||||
|
proc.Hide(run)
|
||||||
if output, err := run.CombinedOutput(); err != nil {
|
if output, err := run.CombinedOutput(); err != nil {
|
||||||
os.Remove(tmp)
|
os.Remove(tmp)
|
||||||
return "", errs.New(errs.BuildFailed, "native module Register failed",
|
return "", errs.New(errs.BuildFailed, "native module Register failed",
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 563 B |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 878 B |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
@@ -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,33 @@
|
|||||||
|
package docstool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"aego/app/playground"
|
||||||
|
"aego/engine/docs"
|
||||||
|
"aego/script/diag"
|
||||||
|
)
|
||||||
|
|
||||||
|
func diagnoser(pg *playground.Playground) func(string, *docs.DiagInfo) {
|
||||||
|
return func(code string, d *docs.DiagInfo) {
|
||||||
|
got, err := pg.Reproduce(code)
|
||||||
|
if err != nil || got.Code == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.Severity, d.At, d.Message = got.Severity, got.File, got.Message
|
||||||
|
d.Line, d.Col, d.EndLine, d.EndCol = got.Line, got.Col, got.EndLine, got.EndCol
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiagnosticProblems lists codes whose example is missing or no longer produces the code.
|
||||||
|
func DiagnosticProblems() []docs.Problem {
|
||||||
|
pg, err := playground.New()
|
||||||
|
if err != nil {
|
||||||
|
return []docs.Problem{{ID: docs.DiagnosticsID, Error: true, Message: "cannot start the docs runtime: " + err.Error()}}
|
||||||
|
}
|
||||||
|
var out []docs.Problem
|
||||||
|
for _, c := range diag.Codes() {
|
||||||
|
if _, err := pg.Reproduce(c); err != nil {
|
||||||
|
out = append(out, docs.Problem{ID: c, Error: true, Message: err.Error()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -138,7 +138,7 @@ func TestF1Refs(t *testing.T) {
|
|||||||
if err := os.WriteFile(path, []byte(probe), 0o644); err != nil {
|
if err := os.WriteFile(path, []byte(probe), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
reg, ws, err := build(p, nil)
|
reg, ws, err := build(p, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -169,3 +169,17 @@ func TestF1Refs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDiagnosticExamplesExported(t *testing.T) {
|
||||||
|
if ps := DiagnosticProblems(); len(ps) > 0 {
|
||||||
|
t.Fatalf("diagnostics examples: %v", ps)
|
||||||
|
}
|
||||||
|
reg, err := ExportRegistry(nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
e, ok := reg.Get("AES2001")
|
||||||
|
if !ok || e.Diag == nil || e.Diag.Line == 0 || e.Diag.Severity != "error" || !strings.Contains(e.Diag.Message, "heigth") {
|
||||||
|
t.Fatalf("AES2001 example was not diagnosed: %+v", e.Diag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"aego/app/assembly"
|
"aego/app/assembly"
|
||||||
"aego/app/assembly/builtin"
|
"aego/app/assembly/builtin"
|
||||||
|
"aego/app/playground"
|
||||||
"aego/engine/docs"
|
"aego/engine/docs"
|
||||||
"aego/engine/module"
|
"aego/engine/module"
|
||||||
scriptsystems "aego/modules/scripting/systems"
|
scriptsystems "aego/modules/scripting/systems"
|
||||||
@@ -20,7 +21,8 @@ import (
|
|||||||
"aego/script/workspace"
|
"aego/script/workspace"
|
||||||
)
|
)
|
||||||
|
|
||||||
func build(p *project.Project, mods []module.Module) (*docs.Registry, *workspace.Workspace, error) {
|
// build assembles the registry; with pg it also compiles the diagnostics examples and the try scripts.
|
||||||
|
func build(p *project.Project, mods []module.Module, pg *playground.Playground) (*docs.Registry, *workspace.Workspace, error) {
|
||||||
types, err := assembly.NewTypes()
|
types, err := assembly.NewTypes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@@ -41,6 +43,10 @@ func build(p *project.Project, mods []module.Module) (*docs.Registry, *workspace
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
src := docs.SourcesOf(bm, types.Schema(), man)
|
src := docs.SourcesOf(bm, types.Schema(), man)
|
||||||
|
if pg != nil {
|
||||||
|
src.Diagnose = diagnoser(pg)
|
||||||
|
src.CompileTry = pg.TryCode
|
||||||
|
}
|
||||||
var ws *workspace.Workspace
|
var ws *workspace.Workspace
|
||||||
if p != nil {
|
if p != nil {
|
||||||
ws, err = workspace.Open(workspace.Config{Root: p.Root, Module: p.Module, Scripts: p.ScriptsDir(), Bindings: reg.Table(), Docs: bm})
|
ws, err = workspace.Open(workspace.Config{Root: p.Root, Module: p.Module, Scripts: p.ScriptsDir(), Bindings: reg.Table(), Docs: bm})
|
||||||
@@ -61,7 +67,17 @@ func build(p *project.Project, mods []module.Module) (*docs.Registry, *workspace
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Registry(p *project.Project, mods []module.Module) (*docs.Registry, error) {
|
func Registry(p *project.Project, mods []module.Module) (*docs.Registry, error) {
|
||||||
r, _, err := build(p, mods)
|
r, _, err := build(p, mods, nil)
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportRegistry is Registry with the diagnostics examples compiled for their messages and positions, and the try scripts compiled.
|
||||||
|
func ExportRegistry(p *project.Project, mods []module.Module) (*docs.Registry, error) {
|
||||||
|
pg, err := playground.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r, _, err := build(p, mods, pg)
|
||||||
return r, err
|
return r, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,9 +100,9 @@ func WriteExport(reg *docs.Registry, title, path string, w io.Writer) error {
|
|||||||
return os.WriteFile(path, data, 0o644)
|
return os.WriteFile(path, data, 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Check(reg *docs.Registry, strict bool, w io.Writer) int {
|
func Check(reg *docs.Registry, strict bool, w io.Writer, extra ...docs.Problem) int {
|
||||||
errs, warns := 0, 0
|
errs, warns := 0, 0
|
||||||
for _, p := range reg.Check(strict) {
|
for _, p := range append(reg.Check(strict), extra...) {
|
||||||
level := "warn "
|
level := "warn "
|
||||||
if p.Error {
|
if p.Error {
|
||||||
level = "error"
|
level = "error"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"aego/app/assembly/builtin"
|
||||||
"aego/engine"
|
"aego/engine"
|
||||||
"aego/engine/module"
|
"aego/engine/module"
|
||||||
"aego/project"
|
"aego/project"
|
||||||
@@ -40,7 +41,7 @@ func (e *Env) export(path string) error {
|
|||||||
}
|
}
|
||||||
return e.Native(args...)
|
return e.Native(args...)
|
||||||
}
|
}
|
||||||
reg, err := Registry(e.Project, e.Modules)
|
reg, err := ExportRegistry(e.Project, e.Modules)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -71,10 +72,13 @@ func (e *Env) Check(strict bool) int {
|
|||||||
fmt.Fprintln(e.Stderr, "aego:", err)
|
fmt.Fprintln(e.Stderr, "aego:", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
code := Check(reg, strict, e.Stdout)
|
code := Check(reg, strict, e.Stdout, DiagnosticProblems()...)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +102,10 @@ func (e *Env) git(args ...string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Env) commit() string {
|
func (e *Env) commit() string {
|
||||||
return e.git("rev-parse", "--short", "HEAD")
|
if c := e.git("rev-parse", "--short", "HEAD"); c != "" {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return os.Getenv("AEGO_COMMIT")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Env) branch() string {
|
func (e *Env) branch() string {
|
||||||
@@ -150,6 +157,14 @@ func (e *Env) Build(out string, play bool) error {
|
|||||||
if c := e.commit(); c != "" {
|
if c := e.commit(); c != "" {
|
||||||
args = append(args, "-commit", c)
|
args = append(args, "-commit", c)
|
||||||
}
|
}
|
||||||
|
var icons []string
|
||||||
|
for _, d := range builtin.IconSources() {
|
||||||
|
icons = append(icons, filepath.Join(e.Engine, filepath.FromSlash(d)))
|
||||||
|
}
|
||||||
|
args = append(args, "-icons", strings.Join(icons, ","))
|
||||||
|
if shots := filepath.Join(e.Engine, "docs", "shots"); fileExists(filepath.Join(shots, "demo.png")) {
|
||||||
|
args = append(args, "-poster", filepath.Join(shots, "demo.png"), "-shots", shots)
|
||||||
|
}
|
||||||
if hero := filepath.Join(e.Engine, "docs", "hero.png"); fileExists(hero) {
|
if hero := filepath.Join(e.Engine, "docs", "hero.png"); fileExists(hero) {
|
||||||
args = append(args, "-hero", hero)
|
args = append(args, "-hero", hero)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ func Tool(p *project.Project, args []string, stdout, stderr io.Writer, mods ...m
|
|||||||
if err := fs.Parse(args[1:]); err != nil {
|
if err := fs.Parse(args[1:]); err != nil {
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
reg, err := Registry(p, mods)
|
build := Registry
|
||||||
|
if args[0] == "export" {
|
||||||
|
build = ExportRegistry
|
||||||
|
}
|
||||||
|
reg, err := build(p, mods)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(stderr, "aego:", err)
|
fmt.Fprintln(stderr, "aego:", err)
|
||||||
return 1
|
return 1
|
||||||
@@ -34,7 +38,7 @@ func Tool(p *project.Project, args []string, stdout, stderr io.Writer, mods ...m
|
|||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
case "check":
|
case "check":
|
||||||
return Check(reg, *strict, stdout)
|
return Check(reg, *strict, stdout, DiagnosticProblems()...)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(stderr, "unknown docs command %q\n", args[0])
|
fmt.Fprintf(stderr, "unknown docs command %q\n", args[0])
|
||||||
return 2
|
return 2
|
||||||
|
|||||||
@@ -41,9 +41,16 @@ func CheckTry(reg *docs.Registry, w io.Writer) int {
|
|||||||
}
|
}
|
||||||
msgs = append(msgs, msg)
|
msgs = append(msgs, msg)
|
||||||
}
|
}
|
||||||
switch res := pg.PlayScene(e.ID, []byte(e.Try)); {
|
var code string
|
||||||
|
var err error
|
||||||
|
if e.TryScript != "" {
|
||||||
|
code, err = pg.TryCode(e.TryScript)
|
||||||
|
}
|
||||||
|
switch res := pg.PlayScene(e.ID, []byte(e.Try), code, e.TryPrefabs); {
|
||||||
case !sandbox[e.ID]:
|
case !sandbox[e.ID]:
|
||||||
add("belongs to a module the docs runtime does not include")
|
add("belongs to a module the docs runtime does not include")
|
||||||
|
case err != nil:
|
||||||
|
add("script does not compile: " + err.Error())
|
||||||
case !res.Started:
|
case !res.Started:
|
||||||
add("does not load: " + res.Error)
|
add("does not load: " + res.Error)
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
playmods "aego/app/assembly/playground"
|
playmods "aego/app/assembly/playground"
|
||||||
"aego/asset"
|
"aego/asset"
|
||||||
"aego/asset/loader"
|
"aego/asset/loader"
|
||||||
|
"aego/contracts/debugdraw"
|
||||||
"aego/core/log"
|
"aego/core/log"
|
||||||
"aego/core/mathx"
|
"aego/core/mathx"
|
||||||
edclip "aego/editor/core/clipboard"
|
edclip "aego/editor/core/clipboard"
|
||||||
@@ -20,6 +21,7 @@ import (
|
|||||||
"aego/engine/base"
|
"aego/engine/base"
|
||||||
"aego/engine/module"
|
"aego/engine/module"
|
||||||
"aego/format/aetext"
|
"aego/format/aetext"
|
||||||
|
"aego/modules/physics"
|
||||||
scriptsystems "aego/modules/scripting/systems"
|
scriptsystems "aego/modules/scripting/systems"
|
||||||
"aego/platform"
|
"aego/platform"
|
||||||
"aego/render"
|
"aego/render"
|
||||||
@@ -41,9 +43,11 @@ const (
|
|||||||
maxSprites = 16384
|
maxSprites = 16384
|
||||||
gameShare = 0.6
|
gameShare = 0.6
|
||||||
stackBelow = 640
|
stackBelow = 640
|
||||||
maxLines = 200
|
// inspectorMin keeps field names and values readable in narrow embeds.
|
||||||
defaultW = 960
|
inspectorMin = 400
|
||||||
defaultH = 440
|
maxLines = 200
|
||||||
|
defaultW = 960
|
||||||
|
defaultH = 440
|
||||||
)
|
)
|
||||||
|
|
||||||
var coreBlocks = []string{"window", "defaults"}
|
var coreBlocks = []string{"window", "defaults"}
|
||||||
@@ -71,12 +75,14 @@ type App struct {
|
|||||||
ui *core.Context
|
ui *core.Context
|
||||||
clip *docsession.Clipboard
|
clip *docsession.Clipboard
|
||||||
session *docsession.Session
|
session *docsession.Session
|
||||||
|
scripts *scriptHost
|
||||||
insp *inspector.Panel
|
insp *inspector.Panel
|
||||||
game *gameview.Panel
|
game *gameview.Panel
|
||||||
list *draw.List
|
list *draw.List
|
||||||
log *log.Logger
|
log *log.Logger
|
||||||
lines []Line
|
lines []Line
|
||||||
textOn bool
|
textOn bool
|
||||||
|
shown *engine.World
|
||||||
closed bool
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,15 +144,17 @@ func New(opt Options) (*App, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
a.e.SetService(loader.ServiceID, a.loader)
|
a.e.SetService(loader.ServiceID, a.loader)
|
||||||
|
playmods.UseMemorySaves(a.e)
|
||||||
a.e.SetService(renderhost.ServiceID, a.renderer)
|
a.e.SetService(renderhost.ServiceID, a.renderer)
|
||||||
if err := a.man.Init(&module.InitContext{Engine: a.e, Assets: a.loader, Log: a.log, Editor: true, Sections: sections}); err != nil {
|
if err := a.man.Init(&module.InitContext{Engine: a.e, Assets: a.loader, Log: a.log, Editor: true, Sections: sections}); err != nil {
|
||||||
a.Close()
|
a.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
a.initUI()
|
a.initUI()
|
||||||
|
a.scripts = &scriptHost{reg: reg, types: a.types, man: a.man, log: a.log}
|
||||||
a.session = docsession.New(docsession.Options{
|
a.session = docsession.New(docsession.Options{
|
||||||
Engine: a.e, Renderer: a.renderer, Loader: a.loader, Types: a.types, Sections: sections,
|
Engine: a.e, Renderer: a.renderer, Loader: a.loader, Types: a.types, Sections: sections,
|
||||||
Scripts: &scriptHost{reg: reg, types: a.types, man: a.man, log: a.log},
|
Scripts: a.scripts,
|
||||||
UI: a.ui,
|
UI: a.ui,
|
||||||
Clipboard: a.clip,
|
Clipboard: a.clip,
|
||||||
Log: a.log,
|
Log: a.log,
|
||||||
@@ -270,7 +278,7 @@ func layout(body mathx.Rect, gap float32) (game, insp mathx.Rect) {
|
|||||||
return mathx.Rect{X: body.X, Y: body.Y, W: body.W, H: h},
|
return mathx.Rect{X: body.X, Y: body.Y, W: body.W, H: h},
|
||||||
mathx.Rect{X: body.X, Y: body.Y + h + gap, W: body.W, H: max(0, body.H-h-gap)}
|
mathx.Rect{X: body.X, Y: body.Y + h + gap, W: body.W, H: max(0, body.H-h-gap)}
|
||||||
}
|
}
|
||||||
w := body.W * gameShare
|
w := min(body.W*gameShare, body.W-inspectorMin)
|
||||||
return mathx.Rect{X: body.X, Y: body.Y, W: w, H: body.H},
|
return mathx.Rect{X: body.X, Y: body.Y, W: w, H: body.H},
|
||||||
mathx.Rect{X: body.X + w + gap, Y: body.Y, W: max(0, body.W-w-gap), H: body.H}
|
mathx.Rect{X: body.X + w + gap, Y: body.Y, W: max(0, body.W-w-gap), H: body.H}
|
||||||
}
|
}
|
||||||
@@ -308,10 +316,24 @@ func (a *App) toolbar(r mathx.Rect) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// showPhysics draws collider and joint outlines in the game view, since docs scenes have no art for them.
|
||||||
|
func (a *App) showPhysics() {
|
||||||
|
w := a.session.Controller().World()
|
||||||
|
if w == nil || w == a.shown {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if d, ok := engine.ServiceOf[debugdraw.Draw](w, debugdraw.ServiceID); ok {
|
||||||
|
d.Hide(physics.ChannelShapes, false)
|
||||||
|
d.Hide(physics.ChannelJoints, false)
|
||||||
|
a.shown = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) Update(dt float64) {
|
func (a *App) Update(dt float64) {
|
||||||
e := a.e
|
e := a.e
|
||||||
now := e.Platform().Now()
|
now := e.Platform().Now()
|
||||||
a.session.Tick(dt)
|
a.session.Tick(dt)
|
||||||
|
a.showPhysics()
|
||||||
ui := a.ui
|
ui := a.ui
|
||||||
ui.Time = now
|
ui.Time = now
|
||||||
m := e.Metrics()
|
m := e.Metrics()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
package docsview
|
package docsview
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ func tries(t *testing.T) []docs.Entry {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
out := p.TryScenes()
|
out := p.TryScenes()
|
||||||
if len(out) < 4 {
|
if len(out) < 10 {
|
||||||
t.Fatalf("only %d try scenes; does the module embed res/docs/try?", len(out))
|
t.Fatalf("only %d try scenes; does the module embed res/docs/try?", len(out))
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
@@ -46,11 +47,22 @@ func errorsOf(a *App) []string {
|
|||||||
|
|
||||||
func TestTryScenesRun(t *testing.T) {
|
func TestTryScenesRun(t *testing.T) {
|
||||||
a := newHeadless(t)
|
a := newHeadless(t)
|
||||||
|
p, err := playground.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
for _, e := range tries(t) {
|
for _, e := range tries(t) {
|
||||||
if err := a.session.Load([]byte(e.Try), e.TrySelect); err != nil {
|
var code string
|
||||||
t.Fatalf("%s: %v", e.ID, err)
|
if e.TryScript != "" {
|
||||||
|
if code, err = p.TryCode(e.TryScript); err != nil {
|
||||||
|
t.Fatalf("%s: %v", e.ID, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := a.Step(60); err != nil {
|
req, _ := json.Marshal(map[string]any{"scene": e.Try, "select": e.TrySelect, "code": code, "prefabs": e.TryPrefabs})
|
||||||
|
if r := a.Call("load", req); r.Error != "" {
|
||||||
|
t.Fatalf("%s: %s", e.ID, r.Error)
|
||||||
|
}
|
||||||
|
if err := a.Step(400); err != nil {
|
||||||
t.Fatalf("%s: %v", e.ID, err)
|
t.Fatalf("%s: %v", e.ID, err)
|
||||||
}
|
}
|
||||||
st := a.session.State()
|
st := a.session.State()
|
||||||
|
|||||||
@@ -6,22 +6,29 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
playmods "aego/app/assembly/playground"
|
||||||
"aego/editor/core/docsession"
|
"aego/editor/core/docsession"
|
||||||
|
"aego/scene/sceneio"
|
||||||
)
|
)
|
||||||
|
|
||||||
type request struct {
|
type request struct {
|
||||||
Scene string `json:"scene"`
|
Scene string `json:"scene"`
|
||||||
Select string `json:"select"`
|
Select string `json:"select"`
|
||||||
Component string `json:"component"`
|
Code string `json:"code"`
|
||||||
Field string `json:"field"`
|
Prefabs map[string]string `json:"prefabs"`
|
||||||
Value float64 `json:"value"`
|
Component string `json:"component"`
|
||||||
|
Field string `json:"field"`
|
||||||
|
Value float64 `json:"value"`
|
||||||
|
Node string `json:"node"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Reply struct {
|
type Reply struct {
|
||||||
Text string `json:"text,omitempty"`
|
Text string `json:"text,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Values map[string]string `json:"values,omitempty"`
|
||||||
State *docsession.State `json:"state,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
Log []Line `json:"log,omitempty"`
|
State *docsession.State `json:"state,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 {
|
||||||
@@ -35,7 +42,16 @@ func (a *App) Call(method string, payload []byte) Reply {
|
|||||||
var err error
|
var err error
|
||||||
switch method {
|
switch method {
|
||||||
case "load":
|
case "load":
|
||||||
err = s.Load([]byte(req.Scene), req.Select)
|
mods, derr := playmods.DecodeTry(req.Code)
|
||||||
|
if err = derr; err == nil {
|
||||||
|
err = a.scripts.use(mods)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = playmods.MountTry(a.loader, req.Prefabs)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = s.Load([]byte(req.Scene), req.Select)
|
||||||
|
}
|
||||||
case "reset":
|
case "reset":
|
||||||
err = s.Reset()
|
err = s.Reset()
|
||||||
case "text":
|
case "text":
|
||||||
@@ -50,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
|
||||||
@@ -61,7 +88,16 @@ func (a *App) Call(method string, payload []byte) Reply {
|
|||||||
return Reply{Error: err.Error()}
|
return Reply{Error: err.Error()}
|
||||||
}
|
}
|
||||||
st := s.State()
|
st := s.State()
|
||||||
return Reply{Text: string(s.Text()), State: &st}
|
return Reply{Text: string(s.Text()), Values: Values(s.Text()), State: &st}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values flattens a scene file into the field keys the docs scene tree uses.
|
||||||
|
func Values(text []byte) map[string]string {
|
||||||
|
nodes, err := sceneio.Outline(text)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return sceneio.OutlineValues(nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Reply(method, payload string) string {
|
func (a *App) Reply(method, payload string) string {
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ type scriptHost struct {
|
|||||||
mods []*bytecode.Module
|
mods []*bytecode.Module
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// use makes mods the scripts of the next scene and publishes their types, so the scene keeps their fields.
|
||||||
|
func (h *scriptHost) use(mods []*bytecode.Module) error {
|
||||||
|
if err := scriptruntime.Publish(h.types.Schema(), mods, h.reg.Surface()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.mods = mods
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *scriptHost) Errors() int {
|
func (h *scriptHost) Errors() int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package edit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"aego/engine"
|
||||||
|
"aego/platform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func startBoot(verb string) {
|
||||||
|
wd, _ := os.Getwd()
|
||||||
|
exe, _ := os.Executable()
|
||||||
|
gobin, err := exec.LookPath("go")
|
||||||
|
if err != nil {
|
||||||
|
gobin = "not on PATH"
|
||||||
|
}
|
||||||
|
_ = platform.StartBoot(platform.DefaultBootPath(),
|
||||||
|
"aego "+engine.Version+" "+runtime.GOOS+"/"+runtime.GOARCH+" "+verb,
|
||||||
|
"exe "+exe,
|
||||||
|
"args "+strings.Join(os.Args[1:], " "),
|
||||||
|
"cwd "+wd,
|
||||||
|
"go "+gobin,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
package edit
|
package edit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"aego/app/bootstrap"
|
"aego/app/bootstrap"
|
||||||
"aego/app/export"
|
"aego/app/export"
|
||||||
"aego/app/projects"
|
"aego/app/projects"
|
||||||
"aego/asset/pipeline"
|
"aego/asset/pipeline"
|
||||||
"aego/editor/core/services"
|
"aego/editor/core/services"
|
||||||
|
"aego/editor/core/sysopen"
|
||||||
"aego/engine/problems"
|
"aego/engine/problems"
|
||||||
"aego/project"
|
"aego/project"
|
||||||
)
|
)
|
||||||
@@ -87,3 +92,47 @@ func (x *exporter) Reveal(path string) error {
|
|||||||
}
|
}
|
||||||
return cmd.Start()
|
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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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]))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||